From 4f5465dfff37ba42c68cbe12a412be81f8873b87 Mon Sep 17 00:00:00 2001 From: Joe Julian Date: Mon, 6 Apr 2026 16:54:14 -0700 Subject: [PATCH] Receive shared Android vault imports --- android/application_snippets.xml | 18 +++ android/keepassgo-android.jar | Bin 16101 -> 19055 bytes .../keepassgo/SharedVaultImportActivity.java | 122 ++++++++++++++++++ main.go | 63 +++++++-- main_test.go | 66 ++++++++++ 5 files changed, 257 insertions(+), 12 deletions(-) create mode 100644 androidsrc/org/julianfamily/keepassgo/SharedVaultImportActivity.java diff --git a/android/application_snippets.xml b/android/application_snippets.xml index 0990299..a938cb3 100644 --- a/android/application_snippets.xml +++ b/android/application_snippets.xml @@ -22,3 +22,21 @@ android:name="android.accessibilityservice" android:resource="@xml/keepassgo_accessibility_service" /> + + + + + + + + + + + + + + + diff --git a/android/keepassgo-android.jar b/android/keepassgo-android.jar index 89509e5c6aea7fbffb3d10992116b5d3d13bdf95..e0a50fff888226dd4c069f69d75d8ef8cb5ae505 100644 GIT binary patch delta 16472 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 JuK3@({{^e93yA;# delta 13560 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 M3Jk29=kL@10s>Fh0ssI2 diff --git a/androidsrc/org/julianfamily/keepassgo/SharedVaultImportActivity.java b/androidsrc/org/julianfamily/keepassgo/SharedVaultImportActivity.java new file mode 100644 index 0000000..27268f6 --- /dev/null +++ b/androidsrc/org/julianfamily/keepassgo/SharedVaultImportActivity.java @@ -0,0 +1,122 @@ +package org.julianfamily.keepassgo; + +import android.app.Activity; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.provider.OpenableColumns; +import android.util.Log; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +public final class SharedVaultImportActivity extends Activity { + private static final String TAG = "KeePassGOImport"; + private static final String DEFAULT_NAME = "shared-vault.kdbx"; + + @Override + protected void onCreate(Bundle state) { + super.onCreate(state); + handleIntent(getIntent()); + launchMainActivity(); + finish(); + } + + private void handleIntent(Intent intent) { + Uri uri = resolveSharedUri(intent); + if (uri == null) { + Log.i(TAG, "no shared vault URI on intent"); + return; + } + try { + persistPendingImport(uri); + Log.i(TAG, "queued shared vault import from " + uri); + } catch (IOException err) { + Log.e(TAG, "failed to queue shared vault import", err); + } + } + + private Uri resolveSharedUri(Intent intent) { + if (intent == null) { + return null; + } + String action = intent.getAction(); + if (Intent.ACTION_SEND.equals(action)) { + return intent.getParcelableExtra(Intent.EXTRA_STREAM); + } + if (Intent.ACTION_VIEW.equals(action)) { + return intent.getData(); + } + return null; + } + + private void persistPendingImport(Uri uri) throws IOException { + File dir = new File(getFilesDir(), "keepassgo"); + if (!dir.exists() && !dir.mkdirs()) { + throw new IOException("failed to create " + dir.getAbsolutePath()); + } + File pendingFile = new File(dir, "pending-shared-vault.kdbx"); + try (InputStream in = getContentResolver().openInputStream(uri)) { + if (in == null) { + throw new IOException("failed to open shared vault stream"); + } + try (FileOutputStream out = new FileOutputStream(pendingFile, false)) { + byte[] buffer = new byte[8192]; + int count; + while ((count = in.read(buffer)) >= 0) { + out.write(buffer, 0, count); + } + } + } + + File nameFile = new File(dir, "pending-shared-vault-name.txt"); + try (FileOutputStream out = new FileOutputStream(nameFile, false)) { + out.write(resolveDisplayName(uri).getBytes(StandardCharsets.UTF_8)); + } + } + + private String resolveDisplayName(Uri uri) { + String displayName = queryDisplayName(uri); + if (!displayName.isEmpty()) { + return displayName; + } + String lastSegment = uri.getLastPathSegment(); + if (lastSegment != null && !lastSegment.trim().isEmpty()) { + return lastSegment.trim(); + } + return DEFAULT_NAME; + } + + private String queryDisplayName(Uri uri) { + Cursor cursor = null; + try { + cursor = getContentResolver().query(uri, new String[]{OpenableColumns.DISPLAY_NAME}, null, null, null); + if (cursor != null && cursor.moveToFirst()) { + int index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); + if (index >= 0) { + String value = cursor.getString(index); + if (value != null) { + return value.trim(); + } + } + } + } catch (RuntimeException err) { + Log.w(TAG, "failed to query display name", err); + } finally { + if (cursor != null) { + cursor.close(); + } + } + return ""; + } + + private void launchMainActivity() { + Intent launch = new Intent(); + launch.setClassName(this, "org.gioui.GioActivity"); + startActivity(launch); + } +} diff --git a/main.go b/main.go index 4c29a42..7b5a1e2 100644 --- a/main.go +++ b/main.go @@ -137,12 +137,14 @@ type attachmentItem struct { } type statePaths struct { - DefaultSaveAsPath string - RecentVaultsPath string - RecentRemotesPath string - SettingsPath string - UIPreferencesPath string - AutofillCachePath string + DefaultSaveAsPath string + RecentVaultsPath string + RecentRemotesPath string + SettingsPath string + UIPreferencesPath string + AutofillCachePath string + PendingSharedVaultPath string + PendingSharedVaultNamePath string } type recentVaultRecord struct { @@ -452,6 +454,8 @@ type ui struct { uiPreferencesPath string recentRemotesPath string autofillCachePath string + pendingSharedVaultPath string + pendingSharedVaultNamePath string editingEntry bool syncDefaultSourceMode syncSourceMode syncDefaultDirection syncDirection @@ -615,6 +619,8 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths) uiPreferencesPath: paths.UIPreferencesPath, recentRemotesPath: paths.RecentRemotesPath, autofillCachePath: paths.AutofillCachePath, + pendingSharedVaultPath: paths.PendingSharedVaultPath, + pendingSharedVaultNamePath: paths.PendingSharedVaultNamePath, recentVaultGroups: map[string][]string{}, recentVaultUsedAt: map[string]time.Time{}, lifecycleAdvancedHidden: true, @@ -655,6 +661,7 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths) u.setCustomFieldRows(nil) u.loadRecentVaults() u.loadRecentRemotes() + u.consumePendingSharedVaultImport() u.restoreStartupLifecycleTarget() u.requestMasterPassFocus = u.hasSelectedLifecycleTarget() u.loadUIPreferences() @@ -725,12 +732,14 @@ func defaultStatePaths(stateDir string) statePaths { baseDir = filepath.Join(configDir, "keepassgo") } return statePaths{ - DefaultSaveAsPath: filepath.Join(baseDir, "vault.kdbx"), - RecentVaultsPath: filepath.Join(baseDir, "recent-vaults.json"), - RecentRemotesPath: filepath.Join(baseDir, "recent-remotes.json"), - SettingsPath: filepath.Join(baseDir, "settings.json"), - UIPreferencesPath: filepath.Join(baseDir, "ui-prefs.json"), - AutofillCachePath: filepath.Join(baseDir, "autofill-cache.json"), + DefaultSaveAsPath: filepath.Join(baseDir, "vault.kdbx"), + RecentVaultsPath: filepath.Join(baseDir, "recent-vaults.json"), + RecentRemotesPath: filepath.Join(baseDir, "recent-remotes.json"), + SettingsPath: filepath.Join(baseDir, "settings.json"), + UIPreferencesPath: filepath.Join(baseDir, "ui-prefs.json"), + AutofillCachePath: filepath.Join(baseDir, "autofill-cache.json"), + PendingSharedVaultPath: filepath.Join(baseDir, "pending-shared-vault.kdbx"), + PendingSharedVaultNamePath: filepath.Join(baseDir, "pending-shared-vault-name.txt"), } } @@ -1494,6 +1503,36 @@ func (u *ui) importedVaultDestination(name string) string { } } +func (u *ui) consumePendingSharedVaultImport() { + path := strings.TrimSpace(u.pendingSharedVaultPath) + if path == "" { + return + } + content, err := os.ReadFile(path) + if err != nil { + if !errors.Is(err, os.ErrNotExist) { + u.state.ErrorMessage = fmt.Sprintf("import shared vault: %v", err) + } + return + } + name := "shared-vault.kdbx" + if namePath := strings.TrimSpace(u.pendingSharedVaultNamePath); namePath != "" { + if rawName, err := os.ReadFile(namePath); err == nil { + if trimmed := strings.TrimSpace(string(rawName)); trimmed != "" { + name = trimmed + } + } + } + if err := u.importSharedVaultBytesAction(name, content); err != nil { + u.state.ErrorMessage = fmt.Sprintf("import shared vault: %v", err) + return + } + _ = os.Remove(path) + if namePath := strings.TrimSpace(u.pendingSharedVaultNamePath); namePath != "" { + _ = os.Remove(namePath) + } +} + func (u *ui) importSharedVaultBytesAction(name string, content []byte) error { target := u.importedVaultDestination(name) if err := os.MkdirAll(filepath.Dir(target), 0o700); err != nil { diff --git a/main_test.go b/main_test.go index 2ad555b..8d2919c 100644 --- a/main_test.go +++ b/main_test.go @@ -5651,6 +5651,12 @@ func TestDefaultStatePathsUsesProvidedStateDir(t *testing.T) { if got := paths.AutofillCachePath; got != filepath.Join(base, "autofill-cache.json") { t.Fatalf("AutofillCachePath = %q, want %q", got, filepath.Join(base, "autofill-cache.json")) } + if got := paths.PendingSharedVaultPath; got != filepath.Join(base, "pending-shared-vault.kdbx") { + t.Fatalf("PendingSharedVaultPath = %q, want %q", got, filepath.Join(base, "pending-shared-vault.kdbx")) + } + if got := paths.PendingSharedVaultNamePath; got != filepath.Join(base, "pending-shared-vault-name.txt") { + t.Fatalf("PendingSharedVaultNamePath = %q, want %q", got, filepath.Join(base, "pending-shared-vault-name.txt")) + } } func TestImportedVaultDestinationUsesIncomingFilenameInsideDefaultDirectory(t *testing.T) { @@ -5721,6 +5727,66 @@ func TestUIImportSharedVaultBytesActionCopiesVaultAndSelectsIt(t *testing.T) { } } +func TestUIConsumesPendingSharedVaultImportOnStartup(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + paths := statePaths{ + DefaultSaveAsPath: filepath.Join(dir, "vault.kdbx"), + RecentVaultsPath: filepath.Join(dir, "recent-vaults.json"), + RecentRemotesPath: filepath.Join(dir, "recent-remotes.json"), + UIPreferencesPath: filepath.Join(dir, "ui-prefs.json"), + PendingSharedVaultPath: filepath.Join(dir, "pending-shared-vault.kdbx"), + PendingSharedVaultNamePath: filepath.Join(dir, "pending-shared-vault-name.txt"), + } + key := vault.MasterKey{Password: "correct horse battery staple"} + var encoded bytes.Buffer + if err := vault.SaveKDBXWithKey(&encoded, vault.Model{ + Entries: []vault.Entry{ + {ID: "entry-1", Title: "Bellagio", Path: []string{"Crew", "Internet"}}, + }, + }, key); err != nil { + t.Fatalf("SaveKDBXWithKey() error = %v", err) + } + if err := os.WriteFile(paths.PendingSharedVaultPath, encoded.Bytes(), 0o600); err != nil { + t.Fatalf("WriteFile(PendingSharedVaultPath) error = %v", err) + } + if err := os.WriteFile(paths.PendingSharedVaultNamePath, []byte("crew-shared.kdbx\n"), 0o600); err != nil { + t.Fatalf("WriteFile(PendingSharedVaultNamePath) error = %v", err) + } + + u := newUIWithState("phone", &session.Manager{}, paths) + + wantPath := filepath.Join(dir, "crew-shared.kdbx") + if got := u.vaultPath.Text(); got != wantPath { + t.Fatalf("vaultPath = %q, want %q", got, wantPath) + } + if got := u.lifecycleMode; got != "local" { + t.Fatalf("lifecycleMode = %q, want local", got) + } + if !u.hasSelectedLifecycleTarget() { + t.Fatal("hasSelectedLifecycleTarget() = false, want true after pending import") + } + if _, err := os.Stat(paths.PendingSharedVaultPath); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("Stat(PendingSharedVaultPath) error = %v, want not exist", err) + } + if _, err := os.Stat(paths.PendingSharedVaultNamePath); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("Stat(PendingSharedVaultNamePath) error = %v, want not exist", err) + } + + reopened := newUIWithState("phone", &session.Manager{}, paths) + reopened.vaultPath.SetText(wantPath) + reopened.masterPassword.SetText(key.Password) + if err := reopened.openVaultAction(); err != nil { + t.Fatalf("openVaultAction(imported) error = %v", err) + } + reopened.state.NavigateToPath([]string{"Crew", "Internet"}) + reopened.filter() + if got := reopened.filteredTitles(); !slices.Equal(got, []string{"Bellagio"}) { + t.Fatalf("filteredTitles() = %v, want [Bellagio]", got) + } +} + func TestDefaultStatePathsUsesEnvironmentStateDirWhenFlagUnset(t *testing.T) { base := filepath.Join(t.TempDir(), "keepassgo-state-env") t.Setenv("KEEPASSGO_STATE_DIR", base)