diff --git a/app/build.gradle b/app/build.gradle
index dbc8e99..d0dc6bf 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -32,6 +32,7 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
+ implementation 'androidx.security:security-crypto:1.1.0-alpha06'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 98142e6..703c6c7 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -31,6 +31,15 @@
+
+
+
+
users = loadUsers();
+ String passwordHash = hashPassword(password, username);
+ users.add(new FTPUser(username, passwordHash));
+ saveUsers(users);
+
+ Log.i(TAG, "User added: " + username);
+ return true;
+ }
+
+ public synchronized boolean updateUser(String username, String newPassword) {
+ if (!isValidPassword(newPassword)) {
+ return false;
+ }
+
+ List users = loadUsers();
+ for (FTPUser user : users) {
+ if (user.getUsername().equals(username)) {
+ String newPasswordHash = hashPassword(newPassword, username);
+ user.setPasswordHash(newPasswordHash);
+ saveUsers(users);
+ Log.i(TAG, "User updated: " + username);
+ return true;
+ }
+ }
+
+ Log.w(TAG, "User not found for update: " + username);
+ return false;
+ }
+
+ public synchronized boolean deleteUser(String username) {
+ List users = loadUsers();
+
+ // Prevent deleting the last user
+ if (users.size() <= 1) {
+ Log.w(TAG, "Cannot delete last user");
+ return false;
+ }
+
+ boolean removed = users.removeIf(user -> user.getUsername().equals(username));
+ if (removed) {
+ saveUsers(users);
+ Log.i(TAG, "User deleted: " + username);
+ }
+ return removed;
+ }
+
+ public synchronized List getAllUsers() {
+ return new ArrayList<>(loadUsers());
+ }
+
+ public synchronized FTPUser getUser(String username) {
+ List users = loadUsers();
+ for (FTPUser user : users) {
+ if (user.getUsername().equals(username)) {
+ return user;
+ }
+ }
+ return null;
+ }
+
+ public synchronized boolean authenticate(String username, String password) {
+ FTPUser user = getUser(username);
+ if (user == null) {
+ Log.w(TAG, "Authentication failed - user not found: " + username);
+ return false;
+ }
+
+ String passwordHash = hashPassword(password, username);
+ boolean authenticated = passwordHash.equals(user.getPasswordHash());
+
+ if (authenticated) {
+ Log.i(TAG, "User authenticated: " + username);
+ } else {
+ Log.w(TAG, "Authentication failed - wrong password: " + username);
+ }
+
+ return authenticated;
+ }
+
+ public boolean userExists(String username) {
+ return getUser(username) != null;
+ }
+
+ public boolean isValidUsername(String username) {
+ if (username == null || username.trim().isEmpty()) {
+ return false;
+ }
+ username = username.trim();
+ return username.length() >= 3 &&
+ username.length() <= 20 &&
+ username.matches("^[a-zA-Z0-9_]+$");
+ }
+
+ public boolean isValidPassword(String password) {
+ return password != null && password.length() >= 4;
+ }
+
+ private String hashPassword(String password, String salt) {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ String saltedPassword = salt + password;
+ byte[] hash = digest.digest(saltedPassword.getBytes(StandardCharsets.UTF_8));
+
+ // Convert to hex string
+ StringBuilder hexString = new StringBuilder();
+ for (byte b : hash) {
+ String hex = Integer.toHexString(0xff & b);
+ if (hex.length() == 1) hexString.append('0');
+ hexString.append(hex);
+ }
+ return hexString.toString();
+ } catch (NoSuchAlgorithmException e) {
+ Log.e(TAG, "SHA-256 not available: " + e.getMessage());
+ return "";
+ }
+ }
+
+ private List loadUsers() {
+ List users = new ArrayList<>();
+ String usersJson = encryptedPrefs.getString(KEY_USERS, "[]");
+
+ try {
+ JSONArray jsonArray = new JSONArray(usersJson);
+ for (int i = 0; i < jsonArray.length(); i++) {
+ JSONObject userJson = jsonArray.getJSONObject(i);
+ users.add(FTPUser.fromJson(userJson));
+ }
+ } catch (JSONException e) {
+ Log.e(TAG, "Error loading users: " + e.getMessage());
+ }
+
+ return users;
+ }
+
+ private void saveUsers(List users) {
+ try {
+ JSONArray jsonArray = new JSONArray();
+ for (FTPUser user : users) {
+ jsonArray.put(user.toJson());
+ }
+
+ encryptedPrefs.edit()
+ .putString(KEY_USERS, jsonArray.toString())
+ .apply();
+ } catch (JSONException e) {
+ Log.e(TAG, "Error saving users: " + e.getMessage());
+ }
+ }
+}
diff --git a/app/src/main/java/be/gyu/android/server/ftp/MainActivity.java b/app/src/main/java/be/gyu/android/server/ftp/MainActivity.java
index 9d15e25..d8b05a1 100644
--- a/app/src/main/java/be/gyu/android/server/ftp/MainActivity.java
+++ b/app/src/main/java/be/gyu/android/server/ftp/MainActivity.java
@@ -37,6 +37,7 @@ public class MainActivity extends AppCompatActivity {
private Button saveSettingsButton;
private EditText minDataPortEditText;
private EditText maxDataPortEditText;
+ private Button manageUsersButton;
private FTPConfig config;
private ActivityResultLauncher directoryPickerLauncher;
@@ -54,6 +55,7 @@ public class MainActivity extends AppCompatActivity {
loadSettings();
setupListeners();
requestPermissions();
+ initializeDefaultUser();
}
private void setupDirectoryPicker() {
@@ -95,6 +97,7 @@ public class MainActivity extends AppCompatActivity {
saveSettingsButton = findViewById(R.id.saveSettingsButton);
minDataPortEditText = findViewById(R.id.minDataPortEditText);
maxDataPortEditText = findViewById(R.id.maxDataPortEditText);
+ manageUsersButton = findViewById(R.id.manageUsersButton);
}
private void loadSettings() {
@@ -121,6 +124,7 @@ public class MainActivity extends AppCompatActivity {
stopButton.setOnClickListener(v -> stopFTPServer());
selectDirButton.setOnClickListener(v -> selectDirectory());
saveSettingsButton.setOnClickListener(v -> saveSettings());
+ manageUsersButton.setOnClickListener(v -> openUserManagement());
}
private void selectDirectory() {
@@ -313,4 +317,17 @@ public class MainActivity extends AppCompatActivity {
isServerRunning = isServiceRunning();
updateUI();
}
+
+ private void openUserManagement() {
+ Intent intent = new Intent(this, UserManagementActivity.class);
+ startActivity(intent);
+ }
+
+ private void initializeDefaultUser() {
+ FTPUserManager userManager = FTPUserManager.getInstance(this);
+ if (userManager.getAllUsers().isEmpty()) {
+ userManager.addUser("admin", "admin");
+ Toast.makeText(this, "Default user created: admin/admin\nPlease change the password in User Management", Toast.LENGTH_LONG).show();
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/be/gyu/android/server/ftp/UserManagementActivity.java b/app/src/main/java/be/gyu/android/server/ftp/UserManagementActivity.java
new file mode 100644
index 0000000..b46daa1
--- /dev/null
+++ b/app/src/main/java/be/gyu/android/server/ftp/UserManagementActivity.java
@@ -0,0 +1,252 @@
+package be.gyu.android.server.ftp;
+
+import android.app.AlertDialog;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class UserManagementActivity extends AppCompatActivity {
+ private FTPUserManager userManager;
+ private ListView userListView;
+ private UserListAdapter userAdapter;
+ private List userList;
+ private TextView emptyView;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_user_management);
+
+ userManager = FTPUserManager.getInstance(this);
+
+ initViews();
+ setupListeners();
+ refreshUserList();
+ }
+
+ private void initViews() {
+ userListView = findViewById(R.id.userListView);
+ emptyView = findViewById(R.id.emptyView);
+ Button addUserButton = findViewById(R.id.addUserButton);
+
+ userList = new ArrayList<>();
+ userAdapter = new UserListAdapter();
+ userListView.setAdapter(userAdapter);
+ userListView.setEmptyView(emptyView);
+ }
+
+ private void setupListeners() {
+ findViewById(R.id.addUserButton).setOnClickListener(v -> showAddUserDialog());
+ }
+
+ private void showAddUserDialog() {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ View dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_add_user, null);
+ builder.setView(dialogView);
+
+ TextView dialogTitle = dialogView.findViewById(R.id.dialogTitle);
+ EditText usernameEditText = dialogView.findViewById(R.id.usernameEditText);
+ EditText passwordEditText = dialogView.findViewById(R.id.passwordEditText);
+ EditText confirmPasswordEditText = dialogView.findViewById(R.id.confirmPasswordEditText);
+ TextView errorTextView = dialogView.findViewById(R.id.errorTextView);
+ Button saveButton = dialogView.findViewById(R.id.saveButton);
+ Button cancelButton = dialogView.findViewById(R.id.cancelButton);
+
+ dialogTitle.setText("Add User");
+
+ AlertDialog dialog = builder.create();
+
+ saveButton.setOnClickListener(v -> {
+ String username = usernameEditText.getText().toString().trim();
+ String password = passwordEditText.getText().toString();
+ String confirmPassword = confirmPasswordEditText.getText().toString();
+
+ // Validate username
+ if (username.isEmpty()) {
+ errorTextView.setText("Username is required");
+ errorTextView.setVisibility(View.VISIBLE);
+ return;
+ }
+
+ if (!userManager.isValidUsername(username)) {
+ errorTextView.setText("Username must be 3-20 characters, alphanumeric + underscore");
+ errorTextView.setVisibility(View.VISIBLE);
+ return;
+ }
+
+ if (userManager.userExists(username)) {
+ errorTextView.setText("Username already exists");
+ errorTextView.setVisibility(View.VISIBLE);
+ return;
+ }
+
+ // Validate password
+ if (password.isEmpty()) {
+ errorTextView.setText("Password is required");
+ errorTextView.setVisibility(View.VISIBLE);
+ return;
+ }
+
+ if (!userManager.isValidPassword(password)) {
+ errorTextView.setText("Password must be at least 4 characters");
+ errorTextView.setVisibility(View.VISIBLE);
+ return;
+ }
+
+ if (!password.equals(confirmPassword)) {
+ errorTextView.setText("Passwords do not match");
+ errorTextView.setVisibility(View.VISIBLE);
+ return;
+ }
+
+ // Add user
+ if (userManager.addUser(username, password)) {
+ Toast.makeText(this, "User added successfully", Toast.LENGTH_SHORT).show();
+ refreshUserList();
+ dialog.dismiss();
+ } else {
+ errorTextView.setText("Failed to add user");
+ errorTextView.setVisibility(View.VISIBLE);
+ }
+ });
+
+ cancelButton.setOnClickListener(v -> dialog.dismiss());
+
+ dialog.show();
+ }
+
+ private void showEditUserDialog(FTPUser user) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ View dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_add_user, null);
+ builder.setView(dialogView);
+
+ TextView dialogTitle = dialogView.findViewById(R.id.dialogTitle);
+ EditText usernameEditText = dialogView.findViewById(R.id.usernameEditText);
+ EditText passwordEditText = dialogView.findViewById(R.id.passwordEditText);
+ EditText confirmPasswordEditText = dialogView.findViewById(R.id.confirmPasswordEditText);
+ TextView errorTextView = dialogView.findViewById(R.id.errorTextView);
+ Button saveButton = dialogView.findViewById(R.id.saveButton);
+ Button cancelButton = dialogView.findViewById(R.id.cancelButton);
+
+ dialogTitle.setText("Edit User");
+ usernameEditText.setText(user.getUsername());
+ usernameEditText.setEnabled(false);
+
+ AlertDialog dialog = builder.create();
+
+ saveButton.setOnClickListener(v -> {
+ String password = passwordEditText.getText().toString();
+ String confirmPassword = confirmPasswordEditText.getText().toString();
+
+ // Validate password
+ if (password.isEmpty()) {
+ errorTextView.setText("Password is required");
+ errorTextView.setVisibility(View.VISIBLE);
+ return;
+ }
+
+ if (!userManager.isValidPassword(password)) {
+ errorTextView.setText("Password must be at least 4 characters");
+ errorTextView.setVisibility(View.VISIBLE);
+ return;
+ }
+
+ if (!password.equals(confirmPassword)) {
+ errorTextView.setText("Passwords do not match");
+ errorTextView.setVisibility(View.VISIBLE);
+ return;
+ }
+
+ // Update user
+ if (userManager.updateUser(user.getUsername(), password)) {
+ Toast.makeText(this, "User updated successfully", Toast.LENGTH_SHORT).show();
+ refreshUserList();
+ dialog.dismiss();
+ } else {
+ errorTextView.setText("Failed to update user");
+ errorTextView.setVisibility(View.VISIBLE);
+ }
+ });
+
+ cancelButton.setOnClickListener(v -> dialog.dismiss());
+
+ dialog.show();
+ }
+
+ private void showDeleteConfirmation(FTPUser user) {
+ // Check if this is the last user
+ if (userList.size() <= 1) {
+ Toast.makeText(this, "Cannot delete the last user", Toast.LENGTH_LONG).show();
+ return;
+ }
+
+ new AlertDialog.Builder(this)
+ .setTitle("Delete User")
+ .setMessage("Are you sure you want to delete user '" + user.getUsername() + "'?")
+ .setPositiveButton("Delete", (dialog, which) -> {
+ if (userManager.deleteUser(user.getUsername())) {
+ Toast.makeText(this, "User deleted successfully", Toast.LENGTH_SHORT).show();
+ refreshUserList();
+ } else {
+ Toast.makeText(this, "Failed to delete user", Toast.LENGTH_SHORT).show();
+ }
+ })
+ .setNegativeButton("Cancel", null)
+ .show();
+ }
+
+ private void refreshUserList() {
+ userList.clear();
+ userList.addAll(userManager.getAllUsers());
+ userAdapter.notifyDataSetChanged();
+ }
+
+ private class UserListAdapter extends BaseAdapter {
+ @Override
+ public int getCount() {
+ return userList.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return userList.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = LayoutInflater.from(UserManagementActivity.this)
+ .inflate(R.layout.item_user, parent, false);
+ }
+
+ FTPUser user = userList.get(position);
+
+ TextView usernameView = convertView.findViewById(R.id.usernameTextView);
+ Button editButton = convertView.findViewById(R.id.editButton);
+ Button deleteButton = convertView.findViewById(R.id.deleteButton);
+
+ usernameView.setText(user.getUsername());
+ editButton.setOnClickListener(v -> showEditUserDialog(user));
+ deleteButton.setOnClickListener(v -> showDeleteConfirmation(user));
+
+ return convertView;
+ }
+ }
+}
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 97f9a68..bcc7fd5 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -68,13 +68,24 @@
app:layout_constraintTop_toBottomOf="@id/startButton"
android:layout_marginTop="16dp" />
+
+
diff --git a/app/src/main/res/layout/activity_user_management.xml b/app/src/main/res/layout/activity_user_management.xml
new file mode 100644
index 0000000..9bcc6d4
--- /dev/null
+++ b/app/src/main/res/layout/activity_user_management.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/dialog_add_user.xml b/app/src/main/res/layout/dialog_add_user.xml
new file mode 100644
index 0000000..472c851
--- /dev/null
+++ b/app/src/main/res/layout/dialog_add_user.xml
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_user.xml b/app/src/main/res/layout/item_user.xml
new file mode 100644
index 0000000..e95fe99
--- /dev/null
+++ b/app/src/main/res/layout/item_user.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+