Provide Android-native protected storage for HTTPS Git credentials and optional GPG-passphrase enrollment while leaving identity, protection policy, and secret lifecycle decisions in crates/storage.
Scope
Implement a narrow Kotlin platform adapter backed by Android Keystore AES-GCM keys and app-private ciphertext records; expose it to Rust through the reviewed UniFFI callback boundary.
Map storage protection classes to Android behavior: device-unlocked storage for Git application credentials, explicit user-presence/current-biometric-set protection for enrolled OpenPGP passphrases, and unsupported-policy errors when the device cannot meet the contract.
Use KeyGenParameterSpec, KeyProperties, KeyguardManager, and authentication-bound cipher operations. Prefer StrongBox only when available and tested; absence must not make supported devices unusable.
Bind protected GPG-passphrase keys to the current biometric enrollment and secure lock state so enrollment changes or lock-screen removal invalidate them and require manual recovery.
Store no secret in DataStore, SharedPreferences, Room, config TOML, logs, saved state, Bundle, clipboard, or generated Kotlin record longer than the operation requires.
Define create/retrieve/replace/delete, alias derivation, corruption, duplicate, cancellation, denial, invalidation, device migration/restore, and orphan cleanup semantics.
Zero/minimize transient byte arrays on both sides where APIs permit and ensure errors/debug strings are redacted.
Do not use deprecated AndroidX Security Crypto EncryptedSharedPreferences or MasterKey.
Acceptance criteria
Git credentials can be retrieved while the device meets the device-unlocked policy without exposing them to UI code.
Enrolled passphrase retrieval requires the configured biometric/user-presence flow and cannot silently fall back to weaker authentication.
Adding/removing biometrics, removing the secure lock screen, backup restore without the Keystore key, cancellation, lockout, and ciphertext tampering fail closed with typed Rust-visible errors.
Disabling biometric unlock deletes its protected record; replacing a token never creates a readable ordinary-settings copy.
Rust mock tests and Android instrumented Keystore tests cover all operations and policy mappings.
Depends on #88, the platform-neutral contract issue, and Android private-path integration. Part of #87.
Android support update (2026-10-08)
User-directed scope: Android release and CI targets are arm64-v8a and armeabi-v7a. x86_64 Android is excluded for now. Validation for every Android milestone issue uses emulator devices only; physical-device testing and physical-device checklists are excluded. Record the limits of available ARM emulator/API images without claiming physical-device coverage.
## Goal
Provide Android-native protected storage for HTTPS Git credentials and optional GPG-passphrase enrollment while leaving identity, protection policy, and secret lifecycle decisions in `crates/storage`.
## Scope
- Implement a narrow Kotlin platform adapter backed by Android Keystore AES-GCM keys and app-private ciphertext records; expose it to Rust through the reviewed UniFFI callback boundary.
- Map storage protection classes to Android behavior: device-unlocked storage for Git application credentials, explicit user-presence/current-biometric-set protection for enrolled OpenPGP passphrases, and unsupported-policy errors when the device cannot meet the contract.
- Use `KeyGenParameterSpec`, `KeyProperties`, `KeyguardManager`, and authentication-bound cipher operations. Prefer StrongBox only when available and tested; absence must not make supported devices unusable.
- Bind protected GPG-passphrase keys to the current biometric enrollment and secure lock state so enrollment changes or lock-screen removal invalidate them and require manual recovery.
- Store no secret in DataStore, SharedPreferences, Room, config TOML, logs, saved state, Bundle, clipboard, or generated Kotlin record longer than the operation requires.
- Define create/retrieve/replace/delete, alias derivation, corruption, duplicate, cancellation, denial, invalidation, device migration/restore, and orphan cleanup semantics.
- Zero/minimize transient byte arrays on both sides where APIs permit and ensure errors/debug strings are redacted.
- Do not use deprecated AndroidX Security Crypto `EncryptedSharedPreferences` or `MasterKey`.
## Acceptance criteria
- Git credentials can be retrieved while the device meets the device-unlocked policy without exposing them to UI code.
- Enrolled passphrase retrieval requires the configured biometric/user-presence flow and cannot silently fall back to weaker authentication.
- Adding/removing biometrics, removing the secure lock screen, backup restore without the Keystore key, cancellation, lockout, and ciphertext tampering fail closed with typed Rust-visible errors.
- Disabling biometric unlock deletes its protected record; replacing a token never creates a readable ordinary-settings copy.
- Rust mock tests and Android instrumented Keystore tests cover all operations and policy mappings.
Depends on #88, the platform-neutral contract issue, and Android private-path integration. Part of #87.
## Android support update (2026-10-08)
User-directed scope: Android release and CI targets are **arm64-v8a** and **armeabi-v7a**. x86_64 Android is excluded for now. Validation for every Android milestone issue uses emulator devices only; physical-device testing and physical-device checklists are excluded. Record the limits of available ARM emulator/API images without claiming physical-device coverage.
hugo
added this to the 06 - Implement an Android version of the software milestone 2026-08-17 19:10:20 +00:00
Rust storage now owns the Android secure-secret backend: canonical hashed identities, protection-class validation, versioned authenticated ciphertext records in private no-backup storage, generation-specific aliases, atomic replacement, duplicate/missing/corruption/restore handling, cancellation and lock races, and bounded orphan cleanup. Failed replacement retains the previous record; disable-biometry deletes the sealed passphrase and OS key. Git credentials/device binding use DeviceUnlocked, and OpenPGP locators cannot request that weaker protection.
The Kotlin adapter only performs OS calls: non-exportable AES-256-GCM keys, Keyguard facts, exact KeyGenParameterSpec flags, StrongBox availability/fallback, and per-operation BiometricPrompt CryptoObjects. Current-biometric protection permits only strong biometrics; explicit user-presence protection permits its configured credential flow. Authorization precedes AAD processing. Rust supplies startup recovery state. Callback buffers are erased after serialization, diagnostics are redacted, and no plaintext is persisted in ordinary settings. JVM/OS/UniFFI transient copies remain an API limitation; complete memory erasure is not claimed.
Validation and final review completed:
All four required Rust gates passed, including 442 workspace tests. Ten new storage tests cover real fixture passphrase enrollment/disable, encoded Git accounts, all operations, protection mappings, StrongBox fallback, tampering/restore, orphan ownership, and concurrent lock/cancellation.
Android ktlint, lint, and unit gates passed. Debug/release APKs and release AAB passed native-library audits for arm64-v8a, armeabi-v7a, and x86_64.
Persistent Android 37 ARM64 emulators with 4 KB and strict 16 KB pages passed nine Rust/JNI instrumented tests and two app tests each, including all 25 Rust fixture/tab combinations. Native callbacks verified typed Rust errors, policy rejection, factory recovery mapping, and returned-array erasure.
Real Android Keystore instrumentation passed device-only AES roundtrip/tamper/delete, no-host rejection, biometric encrypt/decrypt/cancel, explicit PIN user-presence encrypt/decrypt, enrollment addition, removal of all fingerprints, and secure-lock removal. Every enrollment/lock change also preserved the device-only Git-style credential key. Missing-key, invalidation and corruption remain distinct.
An isolated Android 35 ARM64 emulator passed actual sensor lockout (Lockout) and biometric encrypt/decrypt/cancel. Android 37's OS recovery PIN was tested separately: after entering it, the cipher still required a matching fingerprint; cancellation returned Cancelled with no result. The PIN never authorized the biometric-only key.
Release and debug install-over updates/cold launches passed on both persistent Android emulators; the sealed device-binding record remained unchanged and all five tabs rendered. Emulator UI was controlled autonomously with ADB using observed screens after native computer-use clicks proved unreliable.
Regenerated Apple bindings passed the 631-entry shared contract comparison and native Swift probe. iPhone/Watch and AutoFill simulator builds passed, and the iPhone app was installed over its existing container and launched with the existing store intact.
Android enrollment invalidation follows its documented OS contract: adding a biometric or removing all biometrics invalidates the protected key; removing one of several original biometrics does not admit a new biometric. API 28–34 unlocked keys require a secure lock because of documented platform bugs; API 35+ supports device-only keys without one. Older API branches and StrongBox fallback have Rust mock coverage. No physical-device validation was included, as requested.
Reviewed the implementation against this issue after validation; no remaining stubs or production subprocesses. Implementation details and platform guarantees are documented in docs/android/keystore.md.
Implemented and pushed in [1abd752](https://git.rfc1437.de/hugo/IronStorage/commit/1abd75282ea5f08af0aa469dc5527842979a744a).
Rust storage now owns the Android secure-secret backend: canonical hashed identities, protection-class validation, versioned authenticated ciphertext records in private no-backup storage, generation-specific aliases, atomic replacement, duplicate/missing/corruption/restore handling, cancellation and lock races, and bounded orphan cleanup. Failed replacement retains the previous record; disable-biometry deletes the sealed passphrase and OS key. Git credentials/device binding use DeviceUnlocked, and OpenPGP locators cannot request that weaker protection.
The Kotlin adapter only performs OS calls: non-exportable AES-256-GCM keys, Keyguard facts, exact KeyGenParameterSpec flags, StrongBox availability/fallback, and per-operation BiometricPrompt CryptoObjects. Current-biometric protection permits only strong biometrics; explicit user-presence protection permits its configured credential flow. Authorization precedes AAD processing. Rust supplies startup recovery state. Callback buffers are erased after serialization, diagnostics are redacted, and no plaintext is persisted in ordinary settings. JVM/OS/UniFFI transient copies remain an API limitation; complete memory erasure is not claimed.
Validation and final review completed:
- All four required Rust gates passed, including 442 workspace tests. Ten new storage tests cover real fixture passphrase enrollment/disable, encoded Git accounts, all operations, protection mappings, StrongBox fallback, tampering/restore, orphan ownership, and concurrent lock/cancellation.
- Android ktlint, lint, and unit gates passed. Debug/release APKs and release AAB passed native-library audits for arm64-v8a, armeabi-v7a, and x86_64.
- Persistent Android 37 ARM64 emulators with 4 KB and strict 16 KB pages passed nine Rust/JNI instrumented tests and two app tests each, including all 25 Rust fixture/tab combinations. Native callbacks verified typed Rust errors, policy rejection, factory recovery mapping, and returned-array erasure.
- Real Android Keystore instrumentation passed device-only AES roundtrip/tamper/delete, no-host rejection, biometric encrypt/decrypt/cancel, explicit PIN user-presence encrypt/decrypt, enrollment addition, removal of all fingerprints, and secure-lock removal. Every enrollment/lock change also preserved the device-only Git-style credential key. Missing-key, invalidation and corruption remain distinct.
- An isolated Android 35 ARM64 emulator passed actual sensor lockout (`Lockout`) and biometric encrypt/decrypt/cancel. Android 37's OS recovery PIN was tested separately: after entering it, the cipher still required a matching fingerprint; cancellation returned `Cancelled` with no result. The PIN never authorized the biometric-only key.
- Release and debug install-over updates/cold launches passed on both persistent Android emulators; the sealed device-binding record remained unchanged and all five tabs rendered. Emulator UI was controlled autonomously with ADB using observed screens after native computer-use clicks proved unreliable.
- Regenerated Apple bindings passed the 631-entry shared contract comparison and native Swift probe. iPhone/Watch and AutoFill simulator builds passed, and the iPhone app was installed over its existing container and launched with the existing store intact.
Android enrollment invalidation follows its documented OS contract: adding a biometric or removing all biometrics invalidates the protected key; removing one of several original biometrics does not admit a new biometric. API 28–34 unlocked keys require a secure lock because of documented platform bugs; API 35+ supports device-only keys without one. Older API branches and StrongBox fallback have Rust mock coverage. No physical-device validation was included, as requested.
Reviewed the implementation against this issue after validation; no remaining stubs or production subprocesses. Implementation details and platform guarantees are documented in `docs/android/keystore.md`.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Goal
Provide Android-native protected storage for HTTPS Git credentials and optional GPG-passphrase enrollment while leaving identity, protection policy, and secret lifecycle decisions in
crates/storage.Scope
KeyGenParameterSpec,KeyProperties,KeyguardManager, and authentication-bound cipher operations. Prefer StrongBox only when available and tested; absence must not make supported devices unusable.EncryptedSharedPreferencesorMasterKey.Acceptance criteria
Depends on #88, the platform-neutral contract issue, and Android private-path integration. Part of #87.
Android support update (2026-10-08)
User-directed scope: Android release and CI targets are arm64-v8a and armeabi-v7a. x86_64 Android is excluded for now. Validation for every Android milestone issue uses emulator devices only; physical-device testing and physical-device checklists are excluded. Record the limits of available ARM emulator/API images without claiming physical-device coverage.
Implemented and pushed in 1abd752.
Rust storage now owns the Android secure-secret backend: canonical hashed identities, protection-class validation, versioned authenticated ciphertext records in private no-backup storage, generation-specific aliases, atomic replacement, duplicate/missing/corruption/restore handling, cancellation and lock races, and bounded orphan cleanup. Failed replacement retains the previous record; disable-biometry deletes the sealed passphrase and OS key. Git credentials/device binding use DeviceUnlocked, and OpenPGP locators cannot request that weaker protection.
The Kotlin adapter only performs OS calls: non-exportable AES-256-GCM keys, Keyguard facts, exact KeyGenParameterSpec flags, StrongBox availability/fallback, and per-operation BiometricPrompt CryptoObjects. Current-biometric protection permits only strong biometrics; explicit user-presence protection permits its configured credential flow. Authorization precedes AAD processing. Rust supplies startup recovery state. Callback buffers are erased after serialization, diagnostics are redacted, and no plaintext is persisted in ordinary settings. JVM/OS/UniFFI transient copies remain an API limitation; complete memory erasure is not claimed.
Validation and final review completed:
Lockout) and biometric encrypt/decrypt/cancel. Android 37's OS recovery PIN was tested separately: after entering it, the cipher still required a matching fingerprint; cancellation returnedCancelledwith no result. The PIN never authorized the biometric-only key.Android enrollment invalidation follows its documented OS contract: adding a biometric or removing all biometrics invalidates the protected key; removing one of several original biometrics does not admit a new biometric. API 28–34 unlocked keys require a secure lock because of documented platform bugs; API 35+ supports device-only keys without one. Older API branches and StrongBox fallback have Rust mock coverage. No physical-device validation was included, as requested.
Reviewed the implementation against this issue after validation; no remaining stubs or production subprocesses. Implementation details and platform guarantees are documented in
docs/android/keystore.md.