Refuse to open a journal on a build without SQLCipher - #139
Open
Jberma23 wants to merge 1 commit into
Open
Conversation
`PRAGMA key` cannot tell you whether SQLCipher is there. SQLite ignores pragmas it does not recognise rather than erroring, so on a plain build the key statement is accepted and does nothing, the schema read after it succeeds against a plaintext file, and the app writes an unencrypted medical journal with no error, no warning, and nothing that distinguishes it from the encrypted case. That build is easy to be running. `useSQLCipher` is applied by a config plugin at prebuild, so Expo Go has never had it, and neither has a stale ios/ or android/ directory generated before the flag was set. Until #129 lands the README still tells contributors to use Expo Go. `PRAGMA cipher_version` returns a version string on a SQLCipher build and no row at all on stock SQLite, which makes it the one cheap way to tell them apart. It asks the library rather than the database, so it is safe ahead of the key - and asking first means a build that cannot encrypt fails against an empty file instead of filling one with plaintext. 0017 refuses an unencrypted fallback on web for exactly this reason: a silent downgrade makes the privacy promise untrue in the way nobody notices until it matters. This applies the same rule to a build that lost SQLCipher, and reports it as its own error rather than a generic open failure, because the problem is the binary and not the database. Addresses #130. The other half of that issue - whether a SQLCipher build can read an unkeyed plaintext catalog.db - still needs a device and is untouched here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Base automatically changed from
feature/address-encrypted-storage-unlock-foundation
to
feature/encrypted-storage-unlock-foundation
August 18, 2026 01:43
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.
What this does
Adds the runtime guard half of #130: the app now refuses to open the journal on a build that has no SQLCipher, instead of silently writing a plaintext one.
PRAGMA keycannot tell you whether SQLCipher is present. SQLite ignores pragmas it does not recognise rather than erroring, so on a stock build the key statement is accepted and does nothing, the schema read after it succeeds against a plaintext file, and the app writes an unencrypted medical journal with no error, no warning, and nothing observable that distinguishes it from the encrypted case. Verified against stock SQLite:That build is easy to be running.
useSQLCipheris applied by a config plugin at prebuild, so Expo Go has never had it, and neither has a staleios/orandroid/directory generated before the flag was set — and until #129 lands the README still tells contributors to use Expo Go.PRAGMA cipher_versionreturns a version string on a SQLCipher build and no row on stock SQLite, which makes it the one cheap way to tell them apart. It asks the library rather than the database, so it is safe ahead of the key — and asking first means a build that cannot encrypt fails against an empty file rather than filling one with plaintext. The error names the remedy (npx expo prebuild+ a development build), since whoever hits it is the person who most needs to read it.0017 refuses an unencrypted fallback on web for exactly this reason: a silent downgrade makes the privacy promise untrue in the way nobody notices until it matters. This applies the same rule to a build that lost SQLCipher, and reports it as
SQLCipherUnavailableErrorrather than a generic open failure — the problem is the binary, not the database, and "could not open the journal database" would send someone looking in the wrong place.Issue
Addresses #130
Not closing it. #130 has two halves and this is only one: whether a SQLCipher build can read an unkeyed plaintext
catalog.dbstill needs a device, and is untouched here.Testing
cd mobile && npm test→ 11 suites, 114 tests, all pass (109 before).npx tsc --noEmitandnpm run lintclean, on Node 22.20.0.Five new tests, mutation-checked: commenting out the
assertSQLCiphercall fails all five and nothing else, so they are genuinely load-bearing rather than passing by coincidence. They cover the refusal itself, that the check happens beforePRAGMA keyso nothing is written to the file, that the message names the remedy, that the handle is closed, and that it is not reported as a genericDatabaseUnavailableError.Two existing ordering assertions became index-based casualties of inserting a statement — they now locate
PRAGMA keyrather than assuming position 0, which is what they actually meant.Cannot be tested here: that a real SQLCipher build returns a version string. That is the device half of #130.
Notes for review
feature/encrypted-storage-unlock-foundationdirectly, and targeted at Address #128 review: unopenable journals, repository binding, and the node:sqlite guard #138's branch. Both PRs edit the same region ofopen(), so basing this on Paul's branch would have guaranteed a conflict. Merge Address #128 review: unopenable journals, repository binding, and the node:sqlite guard #138 first and this retargets cleanly; the diff here shows only its own commit.getOrCreateDatabaseKey()still runs beforeopenDatabaseAsync, so on a broken build a key is minted and stored before the guard fires. That is harmless — we throw before anything writes, so the file stays zero-length and a later correct build keys it normally — but the alternative was reordering, which breaks the existing "does not open the database if the key cannot be read" test. Happy to swap if reviewers prefer.