This document captures the implementation work done on KeeShare support for Android, the challenges encountered, and what remains to be done.
KeeShare allows secure password group synchronization between KeePass databases. The goal was to implement the ability for Android-only users to create KeeShare import configurations directly from the app, rather than requiring a desktop KeePass client.
Files Modified:
src/keepass2android-app/ConfigureKeeShareActivity.cssrc/keepass2android-app/Resources/layout/config_keeshare.xml(added FAB)src/keepass2android-app/Resources/layout/dialog_add_keeshare.xml(new dialog)src/keepass2android-app/Resources/values/strings.xml(new strings)
Features Added:
- Floating Action Button (FAB) to add new KeeShare configurations
- "Add KeeShare" dialog with:
- Group selection (create new or use existing)
- Share type selection (Import/Synchronize/Export)
- Password field for the shared file
- Browse button to select the KeeShare file
Files Modified:
src/keepass2android-app/Resources/layout/keeshare_config_row.xml(added Edit button)src/keepass2android-app/Resources/layout/dialog_edit_keeshare.xml(new dialog)
Features Added:
- Edit button on each KeeShare configuration row
- Edit dialog allowing:
- Password updates
- Share type changes
- Password status indicator (shows if password is configured)
Files Modified:
src/keepass2android-app/KeeShare.cs
Changes:
- User-friendly error messages for wrong password
- Clear guidance to use Edit button when password is needed
Problem: When using FileSelectActivity to browse for the KeeShare file, the app would launch PasswordActivity to open the database, instead of just returning the selected file path.
Solution: Changed the Browse button to use Android's Storage Access Framework directly:
Intent intent = new Intent(Intent.ActionOpenDocument);
intent.AddCategory(Intent.CategoryOpenable);
intent.SetType("*/*");
StartActivityForResult(intent, ReqCodeSelectFileForNewConfig);Problem: ConfigureKeeShareActivity had LaunchMode = LaunchMode.SingleInstance, which caused the file picker result to not be delivered properly because SingleInstance activities run in their own task.
Solution: Changed to LaunchMode = LaunchMode.SingleTop:
[Activity(Label = "@string/keeshare_title", ... LaunchMode = LaunchMode.SingleTop, ...)]Problem: When creating a new group in the Browse button handler before launching the file picker, the group would not be properly recognized by FindDatabaseForElement after the activity was recreated.
Solution: Deferred group creation to OnActivityResult:
- Store
_pendingCreateNewGroupand_pendingNewGroupNameflags - Create the group only when file selection completes in OnActivityResult
- Use
SaveDatabase(db)directly instead ofSaveGroup(group)to avoid FindDatabaseForElement lookup
Problem: The Update() call in OnSyncNow's completion callback was being called from a background thread, causing CalledFromWrongThreadException.
Solution: Wrapped in RunOnUiThread:
activity?.RunOnUiThread(() => activity.Update());Problem: After a successful KeeShare sync/import, when trying to view the entries in the imported group, the app crashes with:
System.Exception: Database element KeePassLib.PwUuid not found in any of 1 databases!
at keepass2android.Kp2aApp.FindDatabaseForElement(IStructureItem element)
at keepass2android.view.PwEntryView..ctor(...)
Root Cause: Imported/cloned entries were not registered with Kp2a's database tracking system (Elements, EntriesById, GroupsById collections).
Solution: Added UpdateGlobals() and MarkAllGroupsAsDirty() after MergeIn in KeeShare.cs:
// In SyncGroups method, after MergeIn:
_app.CurrentDb.UpdateGlobals();
_app.MarkAllGroupsAsDirty();Status: FIXED. Imported entries can now be viewed without crashing.
The KeeShare functionality has comprehensive unit tests in src/KeeShare.Tests/.
Due to framework incompatibility (the app targets net9.0-android, tests target net8.0), tests use a pattern of copying pure logic into test helpers to avoid Android dependencies.
Test Helpers (TestHelpers/):
| File | Purpose |
|---|---|
PwGroupStub.cs |
Stub for PwGroup with CustomData, Groups, Entries |
PwEntryStub.cs |
Stub for PwEntry with ParentGroup |
PwDatabaseStub.cs |
Stub for PwDatabase with IsOpen, RootGroup |
KeeShareConfigLogic.cs |
Copy of configuration methods from KeeShare.cs |
KeeShareCompatibilityLogic.cs |
Copy of KeePassXC compatibility logic |
KeeShareStateLogic.cs |
Copy of state checking methods |
KeeShareTestHelpers.cs |
Signature verification logic |
cd src/KeeShare.Tests
dotnet test --verbosity normal| Test Class | Tests | Coverage |
|---|---|---|
ConfigurationTests.cs |
37 | EnableKeeShare, DisableKeeShare, GetEffectiveFilePath, etc. |
KeePassXCCompatibilityTests.cs |
30 | HasKeePassXCFormat, TryImportKeePassXCConfig |
StateCheckingTests.cs |
39 | IsReadOnlyBecauseKeeShareImport, HasExportableKeeShareGroups |
KeeShareItemTests.cs |
15 | KeeShareItem properties |
SignatureVerificationTests.cs |
14 | VerifySignatureCore with various inputs |
HasKeeShareGroupsTests.cs |
6 | HasKeeShareGroups recursive detection |
To validate that tests are meaningful (actually catch bugs), we use manual mutation testing.
What is Mutation Testing? Mutation testing introduces deliberate bugs (mutations) into the code and verifies that tests fail. If tests still pass after a mutation, the tests aren't catching that class of bug.
Running Manual Mutation Tests:
- Introduce a mutation in a test helper file
- Run tests
- Verify tests fail (mutation "killed")
- Revert the mutation
Example Mutations and Results:
| Mutation | Description | Result |
|---|---|---|
Change "true" to "false" in EnableKeeShare |
Sets Active to wrong value | KILLED (4 tests failed) |
| Remove type validation | Accept invalid types | KILLED (1 test failed) |
| Check "Export" instead of "Import" in IsReadOnlyBecauseKeeShareImport | Wrong type check | KILLED (7 tests failed) |
| HasKeePassXCFormat always returns false | Skip format detection | KILLED (21 tests failed) |
| GetEffectiveFilePath returns original path first | Wrong priority | KILLED (1 test failed) |
All mutations were killed, demonstrating the tests validate:
- Correct values being set
- Validation logic working
- Business logic (Import vs Export distinctions)
- Priority/ordering logic
- Detection algorithms
Automated Mutation Testing with Stryker.NET:
Stryker.NET is the standard tool for .NET mutation testing, but requires separate source and test projects. Since our logic is in test helpers (same project), Stryker can't directly mutate it.
If you restructure to have logic in a separate library:
# Install Stryker
dotnet tool install --global dotnet-stryker
# Run mutation testing
cd src/KeeShare.Tests
dotnet-strykerWhen modifying KeeShare.cs, update the corresponding test helper:
| Production Code | Test Helper |
|---|---|
KeeShare.cs lines 46-195 (config) |
KeeShareConfigLogic.cs |
KeeShare.cs lines 197-308 (KeePassXC) |
KeeShareCompatibilityLogic.cs |
KeeShare.cs lines 358-542 (state) |
KeeShareStateLogic.cs |
KeeShareCheckOperation.VerifySignatureCore |
KeeShareTestHelpers.cs |
Each helper includes a comment with the sync date - update this when syncing.
Test database files in e2e-tests/test-data/:
keeshare-test-main.kdbx- Password:test123- Main database to openkeeshare-test-export.kdbx- Password:share123- Contains "Test Service Account" entry to import
Important: The main test database can accumulate KeeShare groups from previous test runs. If tests start failing with "wrong password" errors on multiple groups, recreate the main database:
# Recreate fresh main database
rm e2e-tests/test-data/keeshare-test-main.kdbx
echo -e "test123\ntest123" | keepassxc-cli db-create -p e2e-tests/test-data/keeshare-test-main.kdbx
echo "test123" | keepassxc-cli add -u testuser -p e2e-tests/test-data/keeshare-test-main.kdbx "Sample Entry" <<< "testpass"
# Push to emulator
adb push e2e-tests/test-data/keeshare-test-main.kdbx /sdcard/Download/
adb push e2e-tests/test-data/keeshare-test-export.kdbx /sdcard/Download/Location: e2e-tests/.maestro/
Tests the complete flow:
- Open main test database
- Navigate to Settings -> Database -> Configure KeeShare
- Tap FAB to add new KeeShare
- Enter group name
- Browse and select export file
- Edit to set password
- Tap Sync now
- Verify sync completes
Current Status: Test passes completely, including verifying imported entries can be viewed.
keeshare_edit_password.yaml- Tests editing passwordkeeshare_wrong_password.yaml- Tests error handling
- KeeShare Configuration Screen - Shows existing configurations
- Add KeeShare Button (FAB) - Opens add dialog
- Add KeeShare Dialog - Group selection, type selection, password, browse
- File Selection - Uses direct Android file picker (ACTION_OPEN_DOCUMENT)
- Edit KeeShare - Can update password and type
- Sync - Actually imports entries from the KeeShare file
- Error Messages - Shows user-friendly password errors
- Viewing Imported Entries - FIXED! The display bug has been resolved
- Root cause was:
FindDatabaseForElementdidn't recognize imported entries because they weren't in the tracking collections - Fix: Added
UpdateGlobals()andMarkAllGroupsAsDirty()after MergeIn in KeeShare.cs
- Root cause was:
Added state preservation for activity recreation during file selection:
_pendingConfigItem(existing)_pendingNewConfigGroup_pendingNewConfigType_pendingNewConfigPassword_pendingCreateNewGroup(new)_pendingNewGroupName(new)
src/keepass2android-app/ConfigureKeeShareActivity.cs- Main configuration UIsrc/keepass2android-app/KeeShare.cs- Core sync logicsrc/keepass2android-app/KeeShareCheckOperation.cs- Sync operationsrc/keepass2android-app/Kp2aApp.cs- Database tracking (FindDatabaseForElement)
-
Display Bug - FIXED:
- Added
UpdateGlobals()andMarkAllGroupsAsDirty()after MergeIn inKeeShare.cs(SyncGroups method) - This registers imported entries in the tracking collections (Elements, EntriesById, GroupsById)
- Imported entries can now be viewed without crashing
- Added
-
UI Refresh Bug - FIXED:
- Added synchronous
db.UpdateGlobals()call inSaveDatabasecallback inConfigureKeeShareActivity.cs - Newly created KeeShare groups are now visible immediately in the UI
- Added synchronous
-
Test on Real Device:
- Verify functionality on physical Android device
- Test with real KeeShare files from KeePass desktop
-
Consider Additional Tests:
- Test Synchronize mode (bidirectional sync)
- Test Export mode
- Test with signed containers
# Build Release APK
cd src
dotnet build keepass2android-app/keepass2android-app.csproj -c Release -f net9.0-android
# Install
adb install -r keepass2android-app/bin/Release/net9.0-android/keepass2android.keepass2android_nonet-Signed.apk
# Run Maestro test
cd /path/to/keepass2android
~/.maestro/bin/maestro test e2e-tests/.maestro/keeshare_import_flow.yaml
# Check logs
adb logcat -d | grep "KP2A\|KeeShare"This work is part of the keeshare-support branch. The core "Add KeeShare" feature is fully functional:
- Add, Edit, and Sync KeeShare configurations work correctly
- Imported entries can be viewed without crashing (display bug fixed)
- E2E tests pass for the complete flow including viewing imported entries