Hash the plaintext, not the encrypted bytes, in verify --deep (closes #131) #138

Merged
clawbot merged 1 commits from issue-131-deep-verify-blob-hash into next 2026-09-21 20:24:37 +02:00
Collaborator

verify --deep's final blob-integrity step hashed the encrypted
downloaded bytes with a single SHA256 and compared that to the blob's
remote name. That name is the double SHA256 of the blob's plaintext
(internal/blobgen/writer.go, Sum256), so the two values can never be
equal and deep verification failed on every healthy blob with blob hash mismatch. The earlier per-chunk hash checks and blob-existence check
were correct; only this final comparison was wrong.

The fix hashes the decompressed plaintext instead: a TeeReader feeds
the plaintext into a hasher as chunk verification streams it, and
verifyBlobFinalIntegrity compares the double SHA256 of that plaintext
to the blob name — the same derivation the writer uses.

Test: TestDeepVerifyAcceptsHealthyAndRejectsCorruptBlob backs up a real
snapshot through the on-disk storage backend, runs RunDeepVerify (which
fails before this fix), then flips one byte inside a stored blob and
confirms deep verification then fails.

Reader's note: the pre-existing TestTeeReaderWithDecryption still hashes
encrypted bytes, but it only exercises TeeReader + age + zstd mechanics
and never compares against a blob name, so it stays valid and untouched.

Local make check (test + Docker lint + fmt-check) is green; CI on this
runner is known-unreliable (issue
#133).

Closes #131

Model: opus-4-8

`verify --deep`'s final blob-integrity step hashed the encrypted downloaded bytes with a single SHA256 and compared that to the blob's remote name. That name is the double SHA256 of the blob's plaintext (`internal/blobgen/writer.go`, `Sum256`), so the two values can never be equal and deep verification failed on every healthy blob with `blob hash mismatch`. The earlier per-chunk hash checks and blob-existence check were correct; only this final comparison was wrong. The fix hashes the decompressed plaintext instead: a `TeeReader` feeds the plaintext into a hasher as chunk verification streams it, and `verifyBlobFinalIntegrity` compares the double SHA256 of that plaintext to the blob name — the same derivation the writer uses. Test: `TestDeepVerifyAcceptsHealthyAndRejectsCorruptBlob` backs up a real snapshot through the on-disk storage backend, runs `RunDeepVerify` (which fails before this fix), then flips one byte inside a stored blob and confirms deep verification then fails. Reader's note: the pre-existing `TestTeeReaderWithDecryption` still hashes encrypted bytes, but it only exercises `TeeReader` + age + zstd mechanics and never compares against a blob name, so it stays valid and untouched. Local `make check` (test + Docker lint + fmt-check) is green; CI on this runner is known-unreliable (issue https://git.eeqj.de/sneak/vaultik/issues/133). Closes https://git.eeqj.de/sneak/vaultik/issues/131 Model: opus-4-8
clawbot self-assigned this 2026-09-21 19:55:54 +02:00
clawbot added 1 commit 2026-09-21 19:55:54 +02:00
Deep verification's final blob-integrity check hashed the encrypted
downloaded bytes with a single SHA256 and compared that to the blob's
remote name, which is the double SHA256 of the plaintext
(blobgen.Writer.Sum256). The two can never be equal, so verify --deep
reported every healthy blob as "blob hash mismatch". The per-chunk and
blob-existence checks were correct; only this final comparison was wrong.

It now hashes the decompressed plaintext as chunk verification streams it
and compares its double SHA256 to the blob name.

Added a test that backs up a real snapshot, deep-verifies it (which fails
before this fix), then flips a byte in one stored blob and confirms deep
verification then fails.

Model: opus-4-8
clawbot added the needs-review label 2026-09-21 19:56:00 +02:00
Author
Collaborator

PASS

model: claude-opus-4-8

PASS model: claude-opus-4-8
clawbot merged commit c423d13191 into next 2026-09-21 20:24:37 +02:00
clawbot deleted branch issue-131-deep-verify-blob-hash 2026-09-21 20:24:37 +02:00
Sign in to join this conversation.