Skip to content

Phase 1.6: Add saved pools and secure reconnect - #3

Draft
Seckcey wants to merge 6 commits into
mainfrom
phase-1-6-saved-pools-secure-reconnect
Draft

Seckcey wants to merge 6 commits into
mainfrom
phase-1-6-saved-pools-secure-reconnect

Conversation

@Seckcey

@Seckcey Seckcey commented Jul 13, 2026 •

Copy link
Copy Markdown
Owner

Summary

Phase 1.6 + 1.6.1 + 1.6.2 + Keystore-generated-IV device fix for securely saved pools and one-tap reconnect.

Fable did not implement security or architecture.
This PR is draft and remains unmerged.


Physical validation status (2026-07-13) — partial pass

Reproduced failure → fix → device pass

  1. Original caller-IV failure reproduced on Frankie’s real Android phone: remote connect succeeded with Save enabled, but the app showed “Connected, but the pool could not be saved securely on this phone.” (Settings empty; no vault write under Keystore randomized encryption + caller IV).
  2. Fix commit (application code physically tested): 4d463e6bb59e3e3032c1b792b6d6330c69795e62 — Android Keystore encrypt uses platform-generated IV (cipher.init(ENCRYPT_MODE, key) then pack cipher.iv); no plaintext/software fallback; randomized encryption remains required.
  3. Secure-save path retested on Frankie’s physical phone with the APK built from that commit:
    • Remote ScreenLogic connection succeeded
    • Save this pool on; Remember password off (first pass)
    • Encrypted vault write succeeded; prior secure-save error did not appear
    • Pool saved on the phone successfully
    • No pool/spa/jets/light/heat/temperature or other equipment commands

Physical validation is therefore partially passed, not fully complete.

Passed on device (confirmed only)

  • Real-device Android Keystore key creation (secure save completed)
  • Keystore-generated AES-GCM encryption IV
  • Encrypted vault write
  • Successful saved remote-pool creation
  • No plaintext or software-encryption fallback
  • No equipment commands

Still pending on device / process

  • Force-stop and reopen persistence
  • Recent Pools after restart
  • Password-only reconnect
  • Remembered-password reconnect
  • Rename / forget-password / removal persistence
  • Local Wi-Fi connection and DHCP recovery
  • Full Android instrumentation suite
  • Remaining pre-merge correctness cleanup

Details: docs/PHASE_1_6_SAVED_POOLS_SECURE_RECONNECT.md §16.


Device fix: Android Keystore encryption IV

Root cause

AesGcmVaultEnvelope.encrypt() generated a caller IV and called:

cipher.init(Cipher.ENCRYPT_MODE, key, GCMParameterSpec(TAG_BITS, iv))

Android Keystore keys use setRandomizedEncryptionRequired(true), which rejects caller-provided IVs on encrypt. JVM unit tests did not catch this because software AES keys accept caller IVs.

Corrected design

Platform-neutral envelope (AesGcmVaultEnvelope): pack / parse only.

Android Keystore encrypt: init(ENCRYPT_MODE, key) without caller IV → AAD → doFinal → validate cipher.iv (12 bytes) → pack WCAE.

Decrypt: stored IV via GCMParameterSpec only.

JVM / tests: encryptWithCallerIv may inject SecureRandom.

Tests and build (code head 4d463e6)

.\gradlew.bat clean testDebugUnitTest assembleDebug
BUILD SUCCESSFUL

Phase 1.6.2 corrections

Password replacement context

  • PasswordPromptReason: MISSING_PASSWORD | REPLACE_REJECTED_STORED_PASSWORD
  • Carried through prompt → submit → successful vault update
  • Not inferred from Ready.usedStoredPassword on the override attempt
  • REPLACE + Remember off → REMOVE_EXISTING after success
  • REPLACE + Remember on → STORE_NEW
  • Failed replacement leaves vault unchanged

Password preservation on merges

  • Normal connect / Save current: Remember on → STORE_NEW; Remember off → PRESERVE_EXISTING
  • Repository translates preserve to omit only when creating a brand-new profile

Reconnect identity enforcement

  • Expected identity stored with active target/factory for the session
  • Cleared on unrelated target, disconnect, close
  • Enforced on install, automatic reconnect, and explicit retry
  • Saved reconnect uses connectWithSessionFactory (same injectable factory)

Missing-candidate-identity policy

  • Expected known + candidate null → Rejected(CONTROLLER_IDENTITY_UNAVAILABLE) before publish
  • No address update; local recovery may be offered

Provisional merge rules

  1. Adapter + known identity
  2. New identity upgrades exact-route null-identity profile
  3. Null identity only merges with null-identity exact route
  4. Else separate profile

Owned local-recovery discovery

  • Cancellable job + request epoch
  • Quiet discovery (discoverLocalQuiet) does not publish global Connecting
  • Stale epoch exits without publishing gateways

Local-only DHCP recovery

  • Find on Wi-Fi only for SCREENLOGIC_LOCAL reachability failures
  • Never for remote failures or pure auth failures
  • Sanitized UI messages (no raw addresses)

Route-specific password management

  • Summary exposes remotePasswordStatus / localPasswordStatus
  • Separate Forget remote / Forget home Wi-Fi actions

AtomicFile recovery-safe reading

  • Read only via openRead(); FileNotFound → empty
  • Size from opened stream; no baseFile trust before open

Keystore mapping

  • Encrypt wraps getOrCreateKey in mapped failures
  • Distinct KEYSTORE_MISSING / KEY_INVALIDATED / LOCKED_OR_UNAVAILABLE / AUTH_TAG_FAILURE
  • Key delete returns typed Keystore-related failures (not generic IO only)

Intermediate plaintext zeroing

  • Codec writeString / readString zero UTF-8 temps
  • ZeroingByteArrayOutputStream for encode buffer

SHAs

Role SHA
Physically tested application code 4d463e6bb59e3e3032c1b792b6d6330c69795e62
Branch head (docs: physical validation record) 5b0c4b91768aee25586dbb1bebca0fe0eb705597
Base 148707688e78662e454cf4bbf11f08dc26ad9924

Documentation

docs/PHASE_1_6_SAVED_POOLS_SECURE_RECONNECT.md (§4 Keystore IV note + §16 physical validation log)

Explicit statements

  • Fable did not implement security or architecture.
  • PR Phase 1.6: Add saved pools and secure reconnect #3 remains draft and unmerged.
  • No auto-connect on launch. No pool-equipment features.
  • No plaintext vault fallback. Randomized encryption remains required.
  • Physical validation: partial pass (secure-save only); remaining checklist items pending.

Seckcey added 6 commits July 12, 2026 21:45
Encrypt successful ScreenLogic connections in an AES-GCM vault under
noBackupFilesDir using an Android Keystore key (alias
westcurrent.saved_pools.aes.v1). Recent pools, one-tap reconnect,
password-only prompts, rename/remove, and fail-closed vault recovery.

Consent defaults are off. Passwords are optional and never land in
ordinary preferences, logs, or UI state. Existing SiteSession remains
the only connection engine. No auto-connect on launch. No equipment
features added. Fable did not implement security or architecture.
Remove long-lived decrypted profile caching; zero temporary plaintext
byte arrays after each repository call. Replace homemade file writes
with Android AtomicFile (SimulatedAtomicVaultFileStore for JVM tests).

Introduce ConnectionAttemptResult so save/password/last-success updates
bind only to the exact installed attempt. ViewModel request epochs
invalidate stale decrypt and password prompts. Pre-install controller
identity validation rejects mismatches without publishing site data.

Add PasswordSaveDecision, provisional identity upgrade, profile-limit
rejection, explicit preferred flag, local Find-on-Wi-Fi recovery, precise
Keystore health mapping, and non-saveable system code/address form state.

Fable did not implement security or architecture. PR remains draft.
Carry PasswordPromptReason through replace flows so remember-off removes
a known-bad stored password after successful override. Use
PRESERVE_EXISTING on normal merges and Save current when Remember is off.

Persist expected controller identity for the session and enforce it on
install, automatic reconnect, and explicit retry. Reject when expected
identity is known but the candidate reports none.

Fix provisional merge so identity-null connections never merge into
known-identity profiles. Own local-recovery discovery with epoch cancel;
restrict Find on Wi-Fi to local reachability failures only.

Expose route-specific password status and forget actions. Read AtomicFile
only via openRead recovery. Complete Keystore exception mapping. Zero
codec intermediate UTF-8 buffers. Add Phase 1.6.2 tests and androidTest
Keystore suite.

Fable did not implement security or architecture. PR remains draft.
Real-device save failed because encrypt passed a caller IV via
GCMParameterSpec while setRandomizedEncryptionRequired(true).
Android Keystore rejects that path.

Refactor: platform-neutral AesGcmVaultEnvelope pack/parse;
JVM SecretKeyVaultCipher keeps encryptWithCallerIv; Android
encrypt init without IV, pack cipher.iv into WCAE envelope.
Decrypt still supplies the stored IV. No plaintext fallback;
randomized encryption remains required.

Unit tests: 324 green. Instrumentation suite updated for the
Keystore-generated-IV path. Physical retest pending Frankie.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant