Say plainly when the private key cannot decrypt a snapshot (closes #427)
check / check (push) Failing after 24m25s
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
This commit was merged in pull request #448.
This commit is contained in:
1 parent
d8d13d1c8b
commit
1a478ef275
4 files changed
+234
-6
No files matched your search
@@ -0,0 +1,161 @@
|
||||
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)
|
||||
}
|
||||
Reference in new issue
Block a user