97 lines
6.2 KiB
Markdown
97 lines
6.2 KiB
Markdown
# Android GPG QR transfer
|
||
|
||
Issue [#106](https://git.rfc1437.de/hugo/IronStorage/issues/106) uses the shared
|
||
ASCII-armored OpenPGP and `ISKT1` multipart format. `crates/storage` owns frame
|
||
bounds/orientation, QR decoding, checksum/reassembly, identity, passphrase
|
||
validation, duplicate handling and transactional key installation. Kotlin only
|
||
supplies camera facts and renders storage-provided QR matrices.
|
||
|
||
## Import and export
|
||
|
||
Open Preferences → Import or export GPG QR codes. Scanning requests camera
|
||
permission, binds CameraX Preview and ImageAnalysis to the Activity lifecycle,
|
||
and uses only the latest Y plane. Pause closes the camera while retaining the
|
||
in-memory multipart session; resume opens a fresh camera. Frames can arrive in
|
||
any order. Duplicate frames leave the received count unchanged. The complete
|
||
key's Rust-derived identity and fingerprint appear before import. Private import
|
||
requires a separate fingerprint/security confirmation. Default-key selection
|
||
and replacement are explicit controls; incorrect passphrases, incomplete,
|
||
conflicting or mismatched transfers cannot replace valid keys/configuration.
|
||
|
||
Onboarding → Import existing GPG key → Scan GPG QR codes uses the same scanner
|
||
and review, then stages the key through Android's existing onboarding transaction.
|
||
It does not create a store or bypass the later local/HTTPS setup choice. Public
|
||
keys cannot become onboarding/default decryption keys; Rust rejects them.
|
||
|
||
Export selects a storage-provided key. Private export requires a GPG passphrase
|
||
and a separate fingerprint/security confirmation. Every displayed export remains
|
||
labeled with the identity that produced it, even if selection changes. Previous
|
||
matrices are erased before a new export. Multipart exports support previous/next
|
||
and optional automatic frame advancement. Brightness and keep-screen-on apply
|
||
only while QR display is composed and are restored on disposal. Frame ticks do
|
||
not renew the storage-owned visibility deadline (the configured authentication
|
||
timeout).
|
||
|
||
## Privacy and interruption
|
||
|
||
The Activity and transfer dialog use `FLAG_SECURE`. Lock/background/focus loss,
|
||
process recreation and dismissal remove transfer state, clear Kotlin matrices
|
||
and inputs, and drop the Rust importer/exporter. Rust multipart chunks and
|
||
reassembly buffers zeroize on drop. No camera frame, armor or QR payload is
|
||
written to Android persistence, photos, clipboard, saved instance state or logs.
|
||
Only an explicitly confirmed successful import persists GPG-compatible keys.
|
||
|
||
Camera permission prompts, an external image picker, device rotation and app
|
||
backgrounding follow the existing fail-closed lifecycle policy: an interrupted
|
||
transfer is discarded; reopen and rescan rather than retaining private bytes
|
||
across an OS interruption. A camera pause within the active dialog retains
|
||
progress. Denial (including permanent denial) presents a settings link and image/
|
||
manual alternatives. Missing/busy camera reports an actionable recovery message.
|
||
|
||
Camera decoding accepts at most 2048 × 2048 pixels and a 4 MiB Y plane, validates
|
||
row/pixel strides and the four quarter-turn rotations before accessing pixels,
|
||
and allows one native decode in flight. Kotlin clears the input plane before
|
||
publishing a completed key to state observers, with a final cleanup on every
|
||
error/cancellation path. CameraX drops obsolete frames rather
|
||
than queueing them. Imported `ISKT1` envelopes retain the existing 11,000-frame/
|
||
17.6 MB armor bound and 1,600-byte chunk bound. Export rejects more than 256
|
||
frames before allocating matrices, keeping the matrix set below 8 MiB.
|
||
|
||
## Verification
|
||
|
||
Rust tests use upstream-compatible Alice public/private armor to prove byte-exact
|
||
single/multipart reconstruction, rotated/padded camera planes, out-of-order and
|
||
duplicate delivery, incomplete/corrupt/mismatched rejection, wrong-passphrase
|
||
retention, import rollback, export size bounds and deadline polling without renewal.
|
||
Native Android settings tests also exercise camera planes through generated UniFFI
|
||
and verify Kotlin frame bytes and QR matrices are cleared. Compose tests check
|
||
private import/export confirmation and hidden-input saved-state exclusions.
|
||
`AndroidQrCameraTest` exercises actual emulator CameraX frame delivery and repeated
|
||
close/reopen; the preview is clipped in both Android View and Compose layout.
|
||
|
||
Build gates include both ARM ABIs, debug/release/AAB, JVM tests, lint/ktlint,
|
||
architecture/source checks, exact dependency locks/checksums, the license/SBOM
|
||
gate, every native ELF/APK 16 KB alignment check, and matching FULL release symbols.
|
||
CameraX 1.6.2 and its exact transitives are reviewed against official Maven POM
|
||
licenses; camera-core includes Apache-2.0 and BSD-3-Clause portions. The merged
|
||
manifest removes Media3's unused ACCESS_NETWORK_STATE permission; only CAMERA
|
||
is added for this feature.
|
||
|
||
Validation is emulator-only under the milestone's ARM support policy. The
|
||
Keystore enrollment/invalidation tests require separate interactive device setup;
|
||
their setup failures are not camera test results and do not establish biometric
|
||
coverage for this change.
|
||
|
||
Recorded for #106 on 2026-10-12:
|
||
|
||
| Environment | Evidence |
|
||
| --- | --- |
|
||
| API 26 ARM, 4 KB | Native settings/viewer fixture including private export and relock erasure; 14 bridge tests; app runner OK (32 reported, including four opt-in/biometric methods not enabled); final onboarding/settings regression group 4 passed; CameraX close/reopen passed |
|
||
| API 37 ARM, strict 16 KB | Native fixture including private export and relock erasure; 14 bridge tests; CameraX close/reopen passed; original app run included both passing QR settings tests |
|
||
| API 37 ARM, 4 KB, computer use | Update retained existing store/preferences; camera permission interruption discarded transfer; live CameraX preview and clipped scrolling controls inspected; private export stayed disabled until fingerprint confirmation |
|
||
| Rust 1.97.1 and Android packages | Required workspace gates, androidVerification and release lint, license/SBOM, debug/release manifests/backup exclusions, ARM native alignment and release FULL-symbol checks passed |
|
||
|
||
Private export from the retained simulator profile awaits optional user input.
|
||
Successful private export/import and erasure evidence above uses isolated,
|
||
encrypted compatibility fixtures rather than a user's passphrase.
|