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" /> +