check / check (push) Failing after 24m25s
With a private key the backup was not encrypted to, restore and deep verify failed with age's "no identity matched any of the recipients" behind a doubled prefix. They now say in one sentence that the key from VAULTIK_AGE_SECRET_KEY or age_secret_key is not one of the keys the snapshot, or a blob, was encrypted to. A blob reused after a recipient change gets the same message. Model: opus-5-5
162 lines
4.8 KiB
Go
162 lines
4.8 KiB
Go
package vaultik_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"io"
|
|
"path"
|
|
"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/config"
|
|
"sneak.berlin/go/vaultik/internal/log"
|
|
"sneak.berlin/go/vaultik/internal/snapshot"
|
|
"sneak.berlin/go/vaultik/internal/ui"
|
|
"sneak.berlin/go/vaultik/internal/vaultik"
|
|
)
|
|
|
|
// TestWrongPrivateKeyError checks that restore and deep verify, given a
|
|
// valid private key the backup was not encrypted to, fail with one plain
|
|
// sentence that names where the key was configured.
|
|
func TestWrongPrivateKeyError(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
store := NewMockStorer()
|
|
|
|
const snapshotID = "test-host_2026-01-01T00:00:00Z"
|
|
|
|
remoteKey := snapshot.RemoteSnapshotKey(snapshotID)
|
|
|
|
manifest, err := snapshot.EncodeManifest(
|
|
&snapshot.Manifest{SnapshotID: remoteKey}, 3)
|
|
require.NoError(t, err)
|
|
require.NoError(t, store.Put(ctx,
|
|
"metadata/"+remoteKey+"/manifest.json.zst", bytes.NewReader(manifest)))
|
|
|
|
// The database is encrypted to the test recipient only.
|
|
var db bytes.Buffer
|
|
|
|
writer, err := blobgen.NewWriter(&db, 1, []string{testAgePublicKey})
|
|
require.NoError(t, err)
|
|
|
|
_, err = writer.Write([]byte("snapshot database"))
|
|
require.NoError(t, err)
|
|
require.NoError(t, writer.Close())
|
|
require.NoError(t, store.Put(ctx,
|
|
"metadata/"+remoteKey+"/db.zst.age", &db))
|
|
|
|
otherKey, err := age.GenerateX25519Identity()
|
|
require.NoError(t, err)
|
|
|
|
v := &vaultik.Vaultik{
|
|
Config: &config.Config{AgeSecretKey: otherKey.String()},
|
|
Storage: store,
|
|
Fs: afero.NewMemMapFs(),
|
|
Stdout: io.Discard,
|
|
Stderr: io.Discard,
|
|
UI: ui.NewWithColor(io.Discard, false),
|
|
}
|
|
v.SetContext(ctx)
|
|
|
|
// A Config built directly has no recorded source, so the error names
|
|
// the config-file field.
|
|
const want = "cannot decrypt this snapshot: the private key in " +
|
|
"age_secret_key is not one of the keys the backup was encrypted to"
|
|
|
|
err = v.Restore(&vaultik.RestoreOptions{
|
|
SnapshotID: snapshotID,
|
|
TargetDir: t.TempDir(),
|
|
})
|
|
require.EqualError(t, err, want)
|
|
|
|
err = v.RunDeepVerify(snapshotID, &vaultik.VerifyOptions{Deep: true})
|
|
require.EqualError(t, err, want)
|
|
}
|
|
|
|
// TestWrongPrivateKeyForBlobError backs up a file, then backs it up again
|
|
// from an empty local index with a second recipient added. The second
|
|
// snapshot reuses the stored blob, which stays encrypted to the first
|
|
// recipient only. With only the added key configured, restore and deep
|
|
// verify read the database but fail on the blob with one plain sentence
|
|
// that names where the key was configured.
|
|
func TestWrongPrivateKeyForBlobError(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
fs := afero.NewOsFs()
|
|
tempDir := t.TempDir()
|
|
dataDir := filepath.Join(tempDir, "source")
|
|
dataPath := filepath.Join(dataDir, "data.bin")
|
|
storeDir := t.TempDir()
|
|
secondDBPath := filepath.Join(tempDir, "second.sqlite")
|
|
|
|
chunkSize := int64(64 * 1024)
|
|
maxBlobSize := int64(512 * 1024)
|
|
|
|
require.NoError(t, fs.MkdirAll(dataDir, 0o755))
|
|
require.NoError(t, afero.WriteFile(fs, dataPath,
|
|
bytesPattern("key-", int(chunkSize)), 0o644))
|
|
|
|
ctx := context.Background()
|
|
|
|
runFileStorageBackup(ctx, t, fs, dataDir, storeDir,
|
|
filepath.Join(tempDir, "first.sqlite"), chunkSize, maxBlobSize)
|
|
|
|
env := setupDedupBackupEnv(
|
|
ctx, t, fs, storeDir, secondDBPath, chunkSize, maxBlobSize)
|
|
|
|
defer func() { _ = env.db.Close() }()
|
|
|
|
addedKey, err := age.GenerateX25519Identity()
|
|
require.NoError(t, err)
|
|
|
|
env.cfg.AgeRecipients = append(env.cfg.AgeRecipients,
|
|
addedKey.Recipient().String())
|
|
|
|
snapshotID, result := runDedupSnapshot(ctx, t, env.sm, env.makeScanner(),
|
|
env.cfg.Hostname, "second", dataDir, secondDBPath)
|
|
require.Zero(t, result.BlobsUploaded,
|
|
"the second backup should reuse the stored blob")
|
|
|
|
require.NoError(t, env.db.Close())
|
|
|
|
blobKeys, err := env.storer.List(ctx, "blobs/")
|
|
require.NoError(t, err)
|
|
require.Len(t, blobKeys, 1)
|
|
|
|
blobHash := path.Base(blobKeys[0])
|
|
|
|
env.cfg.AgeSecretKey = addedKey.String()
|
|
|
|
v := &vaultik.Vaultik{
|
|
Config: env.cfg,
|
|
Storage: env.storer,
|
|
Fs: fs,
|
|
Stdout: io.Discard,
|
|
Stderr: io.Discard,
|
|
UI: ui.NewWithColor(io.Discard, false),
|
|
}
|
|
v.SetContext(ctx)
|
|
|
|
const sentence = "cannot decrypt this blob: the private key in " +
|
|
"age_secret_key is not one of the keys the blob was encrypted to"
|
|
|
|
// Restore names the blob by the first 16 characters of its hash.
|
|
err = v.Restore(&vaultik.RestoreOptions{
|
|
SnapshotID: snapshotID,
|
|
TargetDir: t.TempDir(),
|
|
})
|
|
require.EqualError(t, err, "restoring "+dataPath+": downloading blob "+
|
|
blobHash[:16]+": "+sentence+
|
|
" (pass --skip-errors to continue past restore failures)")
|
|
|
|
err = v.RunDeepVerify(snapshotID, &vaultik.VerifyOptions{Deep: true})
|
|
require.EqualError(t, err,
|
|
"blob "+blobHash+" verification failed: "+sentence)
|
|
}
|