Files
vaultik/internal/vaultik/restore_snapshotdb_test.go
T
clawbot c3bec7d3aa
check / check (push) Successful in 2m5s
check / check (pull_request) Successful in 2m28s
Reject a metadata database truncated to the age header and nonce (closes #152)
An object holding just the age header and its 16-byte nonce decrypts without error: the truncated read surfaces as io.ErrUnexpectedEOF at the age layer, which the zstd decoder maps to a clean EOF at frame start. blobgen then reported zero bytes and no error, so a truncated stream was indistinguishable from a valid empty one, and the metadata database export slipped through -- restore built a fresh schema on the empty file and reported success.

blobgen.Reader.Read now, on EOF, reads once more from the age reader and surfaces io.ErrUnexpectedEOF unless that read is (0, io.EOF), the state a genuine end leaves. downloadSnapshotDB additionally rejects a zero-length decrypted database before any schema is built.

Model: opus-4-8
2026-09-22 18:11:57 +02:00

109 lines
3.3 KiB
Go

package vaultik //nolint:testpackage // inspects unexported snapshot-db materialization
import (
"bytes"
"context"
"os"
"path/filepath"
"testing"
"filippo.io/age"
"github.com/spf13/afero"
"github.com/stretchr/testify/require"
"sneak.berlin/go/vaultik/internal/blobgen"
"sneak.berlin/go/vaultik/internal/database"
)
// genuineSnapshotDBBytes returns the on-disk bytes of a real snapshot
// database (the full schema applied).
func genuineSnapshotDBBytes(t *testing.T) []byte {
t.Helper()
path := filepath.Join(t.TempDir(), "snapshot.db")
db, err := database.New(context.Background(), path)
require.NoError(t, err)
require.NoError(t, db.Close())
data, err := os.ReadFile(path) //nolint:gosec // G304: test-controlled temp path
require.NoError(t, err)
return data
}
// TestMaterializeSnapshotDBPrivateDir proves the decrypted database lands
// in a private (0700) directory and opens read-only.
func TestMaterializeSnapshotDBPrivateDir(t *testing.T) {
dbData := genuineSnapshotDBBytes(t)
t.Setenv("TMPDIR", t.TempDir())
v := &Vaultik{ctx: context.Background(), Fs: afero.NewOsFs()}
db, dir, err := v.materializeSnapshotDB(bytes.NewReader(dbData))
require.NoError(t, err)
t.Cleanup(func() {
_ = db.Close()
_ = os.RemoveAll(dir)
})
info, err := os.Stat(dir)
require.NoError(t, err)
require.Equal(t, os.FileMode(0o700), info.Mode().Perm(),
"snapshot database directory must not be world-readable")
_, err = db.Conn().ExecContext(context.Background(),
"CREATE TABLE probe_readonly (x)")
require.Error(t, err, "materialized snapshot database must be read-only")
}
// TestMaterializeSnapshotDBRejectsCompleteEmptyStream proves the written == 0
// guard rejects a genuinely empty but complete metadata object: a real age
// header, nonce, and final tag encrypting zero plaintext bytes. The truncation
// case is stopped earlier by the reader (io.ErrUnexpectedEOF) and never reaches
// this branch, so it needs its own input. This complete stream decrypts to zero
// bytes with a clean EOF, passes the reader, and must be refused as empty rather
// than accepted as a valid zero-table database. Reverting the guard lets the
// empty file open as a fresh schema and the test fails.
func TestMaterializeSnapshotDBRejectsCompleteEmptyStream(t *testing.T) {
identity, err := age.GenerateX25519Identity()
require.NoError(t, err)
var stream bytes.Buffer
w, err := age.Encrypt(&stream, identity.Recipient())
require.NoError(t, err)
require.NoError(t, w.Close())
blobReader, err := blobgen.NewReader(bytes.NewReader(stream.Bytes()), identity)
require.NoError(t, err)
t.Cleanup(func() { _ = blobReader.Close() })
t.Setenv("TMPDIR", t.TempDir())
v := &Vaultik{ctx: context.Background(), Fs: afero.NewOsFs()}
_, _, err = v.materializeSnapshotDB(blobReader)
require.ErrorIs(t, err, errEmptySnapshotDB)
}
// TestMaterializeSnapshotDBRemovesDirOnOpenFailure proves a failed open
// leaves no temp directory behind.
func TestMaterializeSnapshotDBRemovesDirOnOpenFailure(t *testing.T) {
base := t.TempDir()
t.Setenv("TMPDIR", base)
v := &Vaultik{ctx: context.Background(), Fs: afero.NewOsFs()}
_, _, err := v.materializeSnapshotDB(
bytes.NewReader([]byte("this is not a sqlite database")))
require.Error(t, err)
entries, rerr := os.ReadDir(base)
require.NoError(t, rerr)
require.Empty(t, entries, "temp directory left behind after open failure")
}