You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
CBL-8891: Rekey is not atomic between database and attachments - #2544
rekey() now provides fail-atomicity: after rekey() returns, completes, or is interrupted by a crash at any point, the database is guaranteed to be openable with either the pre-rekey key or the target rekey key.
Caveat: it's fail-atomic under process crash; under power loss, durability is limited by LiteCore's existing lack of fsync for blob files and renames.
DatabaseImpl::rekey() rekeys the SQLite database and the attachments BlobStore as two separate operations with no atomicity between them. A crash (or any exception) between the database rekey succeeding and the attachments swap finishing left the two permanently out of sync: the database durably on the new key, the attachments still on the old one, with no way to tell afterward that anything was wrong.
We fix it by rekeying the attachments to a staging directory before rekeying the db, and swap it to right place after the SQLite db has successfully rekeyed. If the process faulted before the swapping, the staged directory will be left on the disk and swapped next time the db is opened.
rekey() now provides fail-atomicity: after rekey() returns, completes, or is interrupted by a crash at any point, the database is guaranteed to be openable with either the pre-rekey key or the target rekey key.
DatabaseImpl::rekey() rekeys the SQLite database and the attachments
BlobStore as two separate operations with no atomicity between them.
A crash (or any exception) between the database rekey succeeding and
the attachments swap finishing left the two permanently out of sync:
the database durably on the new key, the attachments still on the old
one, with no way to tell afterward that anything was wrong.
We fix it by rekeying the attachments to a staging directory before rekeying the db, and swap it to right place after the SQLite db has successfully rekeyed. If the process faulted before the swapping, the staged directory will be left on the disk and swapped next time the db is opened.
Previously rekey() renamed the rekeyed attachments into their
"Attachments_staged-<hash>" directory after the database rekey had
committed. A crash between the two steps left the database on the new
key with the attachments still on the old one, and nothing on disk for
the next open to recover from.
Now the rename happens first, so the database rekey is the single commit
point:
- crash before it: the database is still on the old key, the staged
directory's name doesn't match, and the next open discards it;
- crash after it: the database is on the new key, the name matches,
and the next open installs it.
The "Attachments_rekeying" scratch directory is kept so a partial
copyBlobsTo never carries the staged name (it would otherwise be
installed if the new key equals the current one). If the rename or the
database rekey throws, both directories are removed.
Update the comments and the finishPendingBlobStoreSwap() documentation
to match.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The reason will be displayed to describe this comment to others. Learn more.
Even with SHA-256, I think deriving the staged directory name from the encryption key is a security concern.
I think a fixed directory name can be used, and to check that the blobs were written with the current key, we can decrypt one blob and compare its SHA-1 to its filename?
The reason will be displayed to describe this comment to others. Learn more.
I think the attachments should be re-encrypted before rekeying the database. Something like:
Take exclusive db lock
Create a new attachments dir "Attachments_rekeyed" and copy the blobs into it encrypted with the new key
Rekey the SQLite database
Move "Attachments" to a temporary directory
Rename "Attachments_rekeyed" to "Attachments"
Release database lock
Asynchronously delete the old attachments directory
...Hm, according to the Jira ticket, those steps are how it's currently done.
So what needs to change is to check for the rekeyed attachments dir when opening the database. If it exists, try opening some attachment in it; if it succeeds continue from step 4 or 5, else delete it (there must have been a crash before step 3 finished.)
- Name the staged directory "Attachments_staged" instead of a key-derived
hash. On open, check that a blob in it decrypts with the database's
current key: if so, install it (the rekey committed); if not, discard it
(the rekey was interrupted before the database rekey committed).
C4BlobStore::validateEncryption() does the check, by comparing a blob's
SHA-1 against the digest in its filename.
- Never modify files when the database is read-only. A leftover staged
directory that is not needed is ignored; one that must be installed
logs a warning and throws NotWriteable.
- Add the "Database Rekey Atomicity" test, which recreates the on-disk
state left by a crash at each point of rekey() and checks that the
database opens only with the key it is really on, with its attachments
readable.
Test interrupted AES-to-none and none-to-AES rekey recovery
C/tests/c4DatabaseEncryptionTest.cc:167
The existing Database Rekey test only covers uninterrupted encryption/decryption, so it does not cover this recovery protocol for AES -> none or none -> AES. Those paths are materially different because validateEncryption interprets plaintext as encrypted data (and vice versa), especially for empty or short blobs. Add interrupted-before-commit and interrupted-after-commit reopen cases for both transitions rather than skipping the unencrypted variant.
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
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.
rekey() now provides fail-atomicity: after rekey() returns, completes, or is interrupted by a crash at any point, the database is guaranteed to be openable with either the pre-rekey key or the target rekey key.
Caveat: it's fail-atomic under process crash; under power loss, durability is limited by LiteCore's existing lack of fsync for blob files and renames.
DatabaseImpl::rekey() rekeys the SQLite database and the attachments BlobStore as two separate operations with no atomicity between them. A crash (or any exception) between the database rekey succeeding and the attachments swap finishing left the two permanently out of sync: the database durably on the new key, the attachments still on the old one, with no way to tell afterward that anything was wrong.
We fix it by rekeying the attachments to a staging directory before rekeying the db, and swap it to right place after the SQLite db has successfully rekeyed. If the process faulted before the swapping, the staged directory will be left on the disk and swapped next time the db is opened.