Skip to content

fix: encrypt stored server credentials at rest - #917

Open
sinful1992 wants to merge 3 commits into
eddyizm:developmentfrom
sinful1992:fix/encrypt-credentials
Open

fix: encrypt stored server credentials at rest#917
sinful1992 wants to merge 3 commits into
eddyizm:developmentfrom
sinful1992:fix/encrypt-credentials

Conversation

@sinful1992

Copy link
Copy Markdown
Contributor

Closes #911 (split out from #858).

Problem

Server credentials are persisted in plaintext in two places:

  • The default SharedPreferencespassword, token, salt (util/Preferences.kt).
  • The Room server table password column (ServerSignupDialogServerRepository.insert), which survives even in secure/token mode because LoginFragment.saveServerPreference copies the raw password across.

Anything able to read app storage (rooted device, backup extraction) recovers the user's Navidrome/Subsonic password.

Change

Encrypt at rest without restructuring the storage model:

  • New util/CryptoUtil.kt — a Keystore-backed AES/GCM helper. Credentials are encrypted on write, decrypted on read.
  • Preferences password/token/salt go through CryptoUtil.
  • ServerRepository.insert() is the single choke point that encrypts the DB password; LoginFragment decrypts at the one read site.

Why Keystore and not EncryptedSharedPreferences

The issue suggested androidx.security EncryptedSharedPreferences or a Keystore-wrapped key. I went with the Keystore key: the androidx Security-Crypto library is deprecated by Google, and a small self-contained helper covers both the prefs values and the Room column with one mechanism and no new dependency. The key stays in secure hardware where available (min SDK 24).

Migration & safety

  • Reads tolerate values written before this change (unrecognised = legacy plaintext, returned as-is).
  • A one-time, idempotent migration on startup re-encrypts existing plaintext in both stores, so the raw values stop sitting on disk. Rows/keys already encrypted are skipped (no redundant writes).
  • Decryption failure (e.g. an invalidated Keystore key after a device/lock-screen change) returns null, so the app prompts a fresh login instead of crashing.

Deliberately out of scope

The storage layout is unchanged — same columns, same pref keys, password still kept. Dropping the raw password / adding per-server token+salt columns is a credential-lifecycle change that belongs to the Login milestone (#829), where LoginActivity will own persistence; doing it here would mean migrating storage twice.

Milestone placement

Per the #858 thread, this fits under #829 — encryption is best done at the same time LoginActivity takes over credential persistence. Flagging for @eddyizm / Tom to slot as they see fit.

Testing

Not build-verified locally (I don't have the signing/build env set up here) — CI/maintainer build appreciated. Logic is a straight encrypt-on-write / decrypt-on-read with legacy-tolerant reads; happy to adjust.

Server credentials were persisted in plaintext in two places: the default
SharedPreferences (password/token/salt) and the Room `server` table's
password column. Anything able to read app storage (rooted device, backup
extraction) could recover the user's Navidrome/Subsonic password.

Add a Keystore-backed CryptoUtil (AES/GCM) and route both stores through it:

- Preferences password/token/salt are encrypted on write and decrypted on
  read. A Keystore-wrapped key is used instead of the now-deprecated
  androidx.security EncryptedSharedPreferences, keeping the key in secure
  hardware where available and avoiding a deprecated dependency.
- ServerRepository.insert() is the single choke point that encrypts the
  password before it reaches the database; LoginFragment decrypts at the one
  read site.

Reads tolerate values written before this change (returned as-is), and a
one-time, idempotent migration on startup re-encrypts existing plaintext in
both stores so the raw values no longer sit on disk. Decryption failure
(e.g. an invalidated Keystore key) returns null so the app prompts a
re-login rather than crashing.

Storage layout is unchanged (same columns, same keys, password still kept):
credential-lifecycle changes belong to the Login milestone (eddyizm#829).

Closes eddyizm#911
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 42d1a52b-575c-4e6b-9d95-1adbe65e4976

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

encryptStoredPasswords runs on every startup after the update; if the
Keystore key is unavailable, CryptoUtil.encrypt returns null and feeding
that into the non-null Server.password column (or overwriting a working
preference value) would crash on the background thread or drop the
credential. Fall back to leaving the existing value untouched — reads
already tolerate plaintext — so a transient Keystore failure can't turn
into a launch crash or a lost login.
@sinful1992

Copy link
Copy Markdown
Contributor Author

Note: this and #918 (the #912 split) both touch LoginFragment.onServerClick a couple of lines apart, so they're conceptually stacked even though each is branched independently off development for a clean per-PR diff. Whichever merges first, I'll rebase the other — no action needed on your side.

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.

2 participants