Restore and deep verify downloaded metadata/<key>/db.zst.age, decrypted it, and then trusted it by object name alone. age decryption only proves a database is readable, not that it is the snapshot requested: an attacker could swap in another valid db.zst.age (no key material needed) to redirect restore to a different snapshot's files, and deep verify with a swapped database plus an empty manifest reported success with "Blobs verified: 0".
Both paths now check identity immediately after opening the database, before any row is trusted. An exported per-snapshot database holds exactly one snapshot row, and a snapshot's remote key is derived from that row's ID, so the database is the requested one exactly when its sole snapshot hashes back to the remote key it was fetched under. This is stronger than comparing the requested identifier directly — and necessary, because a recovery host may pass a remote-key prefix in place of the human ID it cannot know (see TestRestoreOnAnotherMachine).
New SnapshotRepository.GetOnlySnapshot returns that sole row (or reports zero / multiple); the shared verifySnapshotDBIdentity does the key comparison, and restore and deep verify both call it. internal/database cannot import internal/snapshot, so the key derivation stays on the vaultik side.
Judgement call: loadVerificationData would exceed the length limit, so its database-open block moved into a new downloadVerifiedSnapshotDB — the only structural change to verify.go, worth care when rebasing over #193.
Model: opus-4-8
Restore and deep verify downloaded `metadata/<key>/db.zst.age`, decrypted it, and then trusted it by object name alone. age decryption only proves a database is readable, not that it is the snapshot requested: an attacker could swap in another valid `db.zst.age` (no key material needed) to redirect restore to a different snapshot's files, and deep verify with a swapped database plus an empty manifest reported success with "Blobs verified: 0".
Both paths now check identity immediately after opening the database, before any row is trusted. An exported per-snapshot database holds exactly one snapshot row, and a snapshot's remote key is derived from that row's ID, so the database is the requested one exactly when its sole snapshot hashes back to the remote key it was fetched under. This is stronger than comparing the requested identifier directly — and necessary, because a recovery host may pass a remote-key prefix in place of the human ID it cannot know (see `TestRestoreOnAnotherMachine`).
New `SnapshotRepository.GetOnlySnapshot` returns that sole row (or reports zero / multiple); the shared `verifySnapshotDBIdentity` does the key comparison, and restore and deep verify both call it. `internal/database` cannot import `internal/snapshot`, so the key derivation stays on the vaultik side.
Judgement call: `loadVerificationData` would exceed the length limit, so its database-open block moved into a new `downloadVerifiedSnapshotDB` — the only structural change to `verify.go`, worth care when rebasing over https://git.eeqj.de/sneak/vaultik/pulls/193.
Model: opus-4-8
Restore and deep verify downloaded and decrypted metadata/<key>/db.zst.age
by object name alone. age decryption proves the database is readable, not
that it is the snapshot that was asked for: an attacker who swaps in another
valid db.zst.age (no key material needed) could redirect the operation to a
different snapshot, and deep verify with a swapped database plus an empty
manifest reported success with zero blobs verified.
After the database is opened, both paths now confirm its identity: an
exported per-snapshot database holds exactly one snapshot row, and a
snapshot's remote key is derived from that row's ID, so the database is the
requested one exactly when its sole snapshot hashes back to the remote key
fetched. Comparing the requested identifier directly would wrongly reject a
recovery host that supplies a remote-key prefix in place of the human ID it
cannot know.
The shared check lives in verifySnapshotDBIdentity, backed by a new
SnapshotRepository.GetOnlySnapshot; both restore and deep verify call it.
Model: opus-4-8
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Restore and deep verify downloaded
metadata/<key>/db.zst.age, decrypted it, and then trusted it by object name alone. age decryption only proves a database is readable, not that it is the snapshot requested: an attacker could swap in another validdb.zst.age(no key material needed) to redirect restore to a different snapshot's files, and deep verify with a swapped database plus an empty manifest reported success with "Blobs verified: 0".Both paths now check identity immediately after opening the database, before any row is trusted. An exported per-snapshot database holds exactly one snapshot row, and a snapshot's remote key is derived from that row's ID, so the database is the requested one exactly when its sole snapshot hashes back to the remote key it was fetched under. This is stronger than comparing the requested identifier directly — and necessary, because a recovery host may pass a remote-key prefix in place of the human ID it cannot know (see
TestRestoreOnAnotherMachine).New
SnapshotRepository.GetOnlySnapshotreturns that sole row (or reports zero / multiple); the sharedverifySnapshotDBIdentitydoes the key comparison, and restore and deep verify both call it.internal/databasecannot importinternal/snapshot, so the key derivation stays on the vaultik side.Judgement call:
loadVerificationDatawould exceed the length limit, so its database-open block moved into a newdownloadVerifiedSnapshotDB— the only structural change toverify.go, worth care when rebasing over #193.Model: opus-4-8
PASS
Model: opus-4-8
clawbot referenced this pull request2026-09-22 15:01:45 +02:00