The last step of verify --deep hashed the encrypted bytes it downloaded once with SHA256 and compared the result to the blob name. The name is the double SHA256 of the blob plaintext, so the two could never match and deep verification failed on every healthy blob with "blob hash mismatch". It now hashes the decompressed plaintext as chunk verification streams it and compares the double SHA256 of that to the blob name, the same derivation the writer uses. A new test backs up a real snapshot, runs deep verify on it, then flips one byte in a stored blob and expects failure. model: claude-opus-4-8 (implementation, review); claude-fable-5-1 (merge)
109 lines
3.1 KiB
Go
109 lines
3.1 KiB
Go
package vaultik_test
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/spf13/afero"
|
|
"github.com/stretchr/testify/require"
|
|
"sneak.berlin/go/vaultik/internal/log"
|
|
"sneak.berlin/go/vaultik/internal/ui"
|
|
"sneak.berlin/go/vaultik/internal/vaultik"
|
|
)
|
|
|
|
// TestDeepVerifyAcceptsHealthyAndRejectsCorruptBlob backs up a real
|
|
// snapshot with the on-disk storage backend, runs deep verification on
|
|
// it, then flips a byte inside one stored blob and runs deep
|
|
// verification again. A healthy snapshot must pass; a corrupted blob
|
|
// must fail. The healthy case is the regression guard: deep
|
|
// verification used to hash the encrypted blob bytes and compare them
|
|
// to the blob's ID (the double SHA256 of the plaintext), so it reported
|
|
// every healthy blob as corrupt.
|
|
func TestDeepVerifyAcceptsHealthyAndRejectsCorruptBlob(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
fs := afero.NewOsFs()
|
|
tempDir := t.TempDir()
|
|
|
|
dataDir := filepath.Join(tempDir, "source")
|
|
storeDir := filepath.Join(tempDir, "remote")
|
|
dbPath := filepath.Join(tempDir, "index.sqlite")
|
|
|
|
chunkSize := int64(64 * 1024)
|
|
maxBlobSize := int64(512 * 1024)
|
|
|
|
// One file large enough to span several chunks within a single blob.
|
|
require.NoError(t, fs.MkdirAll(dataDir, 0o755))
|
|
require.NoError(t, afero.WriteFile(fs,
|
|
filepath.Join(dataDir, "data.bin"),
|
|
bytesPattern("deep-", int(chunkSize*3)), 0o644))
|
|
|
|
ctx := context.Background()
|
|
|
|
// runFileStorageBackup writes a real snapshot to storeDir and closes
|
|
// the source index, so verification runs from remote bytes only.
|
|
cfg, storer, snapshotID := runFileStorageBackup(
|
|
ctx, t, fs, dataDir, storeDir, dbPath, chunkSize, maxBlobSize)
|
|
|
|
newVerifier := func() *vaultik.Vaultik {
|
|
v := &vaultik.Vaultik{
|
|
Config: cfg,
|
|
Storage: storer,
|
|
Fs: fs,
|
|
Stdout: io.Discard,
|
|
Stderr: io.Discard,
|
|
UI: ui.NewWithColor(io.Discard, false),
|
|
}
|
|
v.SetContext(ctx)
|
|
|
|
return v
|
|
}
|
|
|
|
require.NoError(t,
|
|
newVerifier().RunDeepVerify(snapshotID, &vaultik.VerifyOptions{Deep: true}),
|
|
"deep verify should pass on a healthy snapshot")
|
|
|
|
// Flip a byte inside one blob without changing its length, so the
|
|
// blob-existence and size checks still pass and verification reaches
|
|
// the blob-content stage.
|
|
corruptOneBlob(t, fs, filepath.Join(storeDir, "blobs"))
|
|
|
|
require.Error(t,
|
|
newVerifier().RunDeepVerify(snapshotID, &vaultik.VerifyOptions{Deep: true}),
|
|
"deep verify should fail on a corrupted blob")
|
|
}
|
|
|
|
// corruptOneBlob flips a middle byte of the first blob file found under
|
|
// blobsDir, leaving the file length unchanged.
|
|
func corruptOneBlob(t *testing.T, fs afero.Fs, blobsDir string) {
|
|
t.Helper()
|
|
|
|
var blobPath string
|
|
|
|
err := afero.Walk(fs, blobsDir,
|
|
func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if blobPath == "" && !info.IsDir() {
|
|
blobPath = path
|
|
}
|
|
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, blobPath, "expected at least one blob on disk")
|
|
|
|
data, err := afero.ReadFile(fs, blobPath)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, data)
|
|
|
|
data[len(data)/2] ^= 0xff
|
|
require.NoError(t, afero.WriteFile(fs, blobPath, data, 0o644))
|
|
}
|