Repository navigation
Encrypt stored WireGuard configs with a Keystore key - #16
Merged
Merged
Conversation
Profile configs, including WireGuard private keys, were stored in plain text in the Room database. VpnProfileDao now encrypts configContent with AES-256-GCM on the way in and decrypts it on the way out, so callers are unchanged and the database only holds ciphertext. The key is generated in the Android Keystore and can't be exported. Configs saved by earlier versions are encrypted before Room opens the database: with no other connection open the WAL is checkpointed away, rows are overwritten with secure_delete on, and the file is vacuumed, so no plaintext key is left on disk. Unencrypted rows remain readable in case that step fails. Fixes #13 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # univpn/src/main/kotlin/com/univpn/app/data/db/AppDatabase.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #13. Stacked on #14 (it needs
VpnProfileDao.update()from that branch); GitHub will retarget this PR tomainonce #14 is merged.What changed
ConfigCipher: AES-256-GCM with a non-exportable Android Keystore key. Stored formatenc:v1:+ Base64(IV + ciphertext); a fresh IV per write.VpnProfileDaois now an abstract class that encrypts on insert/update and decrypts on read. Every caller keeps using the same methods, so no call site can forget to encrypt.LegacyConfigEncryptionruns once insideAppDatabase.get()before Room opens the file: it opens the database without WAL (which checkpoints and removes the old WAL), rewrites plaintext rows withsecure_deleteon, and runsVACUUM.Why not SQLCipher: it adds ~7 MB of native libraries and still needs its passphrase protected by a Keystore key.
Testing
Instrumented (Android TV emulator, API 30),
ConfigEncryptionTest: round trip with fresh IVs; the database holds only ciphertext (checked with raw SQL) while the DAO returns the plain config; legacy plaintext rows stay readable; the legacy conversion leaves noPrivateKeybytes in the db, WAL or journal file; corrupt ciphertext reads as empty. Full suite: 18 instrumented + 14 unit tests pass.Real upgrade on the emulator: pre-encryption build running with three plaintext profiles (
PrivateKey6 times acrossunivpn.dband its live WAL) → install this build →Encrypted 3 stored configs, the database file and WAL containPrivateKey0 times, and tunnels start from the decrypted configs.Note: the file-level test also passes without
secure_delete/VACUUM; closing the test's connection already removes its WAL. The real upgrade above is the case that left plaintext behind before this approach (an earlier version converted rows through Room and left the old pages in the WAL).🤖 Generated with Claude Code