Skip to content

feat: encrypted device token storage (AndroidKeyStore AES-GCM) - #7

Merged
mikahatdichlieb merged 3 commits into
mainfrom
feat/token-storage
Oct 26, 2026
Merged

mikahatdichlieb merged 3 commits into
mainfrom
feat/token-storage

Conversation

@mikahatdichlieb

Copy link
Copy Markdown
Contributor

Closes #4

What

Stores the dcd_... device token encrypted, as agreed in #4 (variant A).

  • KeystoreTokenCipher: AES-256-GCM key in AndroidKeyStore, with a 12-byte IV generated randomly by the Keystore. No user-authentication or unlocked-device requirement, so the background sync can read it. StrongBox is tried first; otherwise the key goes to the TEE or software. That fallback also covers devices that report a broken StrongBox via ProviderException.
  • DeviceTokenStore: writes noBackupFilesDir/device_token.bin via AtomicFile. The file holds a format version byte, then IV ‖ ciphertext. The associated data is org.daycompass.android/device-token plus the format version.
  • load() returns None, Present(token) or Unreadable. Unreadable covers a missing key, failed authentication, a truncated or empty file and an unknown version; handle it like a 401 (keep the buffer, re-pair, no retry).
  • If save() fails it throws and writes nothing.
  • clear() deletes the file and keeps the key.

Security-relevant

  • DeviceToken gains internal fun plaintextForEncryptedStorage(), the second and only other way to the plaintext besides the Authorization header. The returned bytes are wiped after encryption.
  • The format version byte in front of IV ‖ ciphertext is an addition to the original sketch, so a future format can pick the right associated data.
  • androidx.core 1.18.0 is now declared directly, for AtomicFile. It was already a transitive dependency. Verification metadata gains only POM/module files of versions that lose conflict resolution, and no new JARs.
  • Nothing is logged. Networking and the wire format are untouched.

Tests

The JVM tests use a software AES-GCM cipher with a key generated at runtime in place of the Keystore. They cover:

  • that a saved token loads back
  • that the plaintext does not appear in the file
  • tampering, unknown version, truncated or empty file, missing key, foreign associated data
  • a decrypted value that is not a token
  • that a failed encryption stores nothing
  • that toString does not reveal the token

KeystoreTokenCipher itself has not been exercised on a device or emulator yet.

AES-256-GCM key in AndroidKeyStore (StrongBox first, then TEE or software),
no user authentication or unlocked-device requirement so background sync
can read it. File in noBackupFilesDir via AtomicFile: version byte, IV,
ciphertext; associated data binds context string and format version.
A missing key or failed decryption yields StoredToken.Unreadable, to be
handled like a 401.
@mikahatdichlieb
mikahatdichlieb merged commit 8e22aa7 into main Oct 26, 2026
6 checks passed
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.

Encrypted device token storage (Keystore AES-GCM)

1 participant