package vaultik_test import ( "bytes" "context" "io" "path/filepath" "testing" "filippo.io/age" "github.com/spf13/afero" "github.com/stretchr/testify/require" "sneak.berlin/go/vaultik/internal/log" "sneak.berlin/go/vaultik/internal/snapshot" "sneak.berlin/go/vaultik/internal/ui" "sneak.berlin/go/vaultik/internal/vaultik" ) // TestRestoreRejectsTruncatedMetadataDB backs up a real tree, then replaces // the snapshot's db.zst.age with a stream cut right after the age header and // its 16-byte nonce. age.Decrypt still accepts such an object and the zstd // decoder turns the truncated read into a clean EOF, so before the fix restore // built a fresh empty schema and reported success. Restore must now fail with // io.ErrUnexpectedEOF, the error the reader raises for a truncated object. // Asserting that specific error pins the reader fix: without it the truncation // yields an empty database, which the identity check rejects for an unrelated // reason, and this test would pass anyway. func TestRestoreRejectsTruncatedMetadataDB(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") restoreDir := filepath.Join(tempDir, "restored") dbPath := filepath.Join(tempDir, "index.sqlite") chunkSize := int64(64 * 1024) maxBlobSize := int64(512 * 1024) setupE2ESourceTree(t, fs, dataDir, chunkSize) ctx := context.Background() cfg, storer, snapshotID := runFileStorageBackup( ctx, t, fs, dataDir, storeDir, dbPath, chunkSize, maxBlobSize) // Encrypting empty plaintext to the snapshot recipient yields // header + nonce(16) + a single 16-byte final chunk tag. Dropping the // trailing tag leaves exactly the age header plus its nonce — the // truncation an attacker can write over metadata without any key. recipient, err := age.ParseX25519Recipient(testAgePublicKey) require.NoError(t, err) var full bytes.Buffer w, err := age.Encrypt(&full, recipient) require.NoError(t, err) require.NoError(t, w.Close()) truncated := full.Bytes()[:full.Len()-16] dbKeyPath := filepath.Join(storeDir, "metadata", snapshot.RemoteSnapshotKey(snapshotID), "db.zst.age") require.NoError(t, afero.WriteFile(fs, dbKeyPath, truncated, 0o644)) restoreVaultik := &vaultik.Vaultik{ Config: cfg, Storage: storer, Fs: fs, Stdout: io.Discard, Stderr: io.Discard, UI: ui.NewWithColor(io.Discard, false), } restoreVaultik.SetContext(ctx) err = restoreVaultik.Restore(&vaultik.RestoreOptions{ SnapshotID: snapshotID, TargetDir: restoreDir, Verify: true, }) require.ErrorIs(t, err, io.ErrUnexpectedEOF) }