From b42c4bb325509ad8b2dcb9c31b2d59e61575f7c5 Mon Sep 17 00:00:00 2001 From: sneak Date: Sun, 8 Nov 2026 14:14:05 +0000 Subject: [PATCH] Fix missing-key errors for restore and deep verify to name both key sources (closes #401) The missing-key error for restore and deep verify is one value, errDecryptionKeyRequired, whose text names both places the key can be set: the VAULTIK_AGE_SECRET_KEY environment variable first, then the age_secret_key config file field. Model: deepseek-v4-pro --- internal/vaultik/restore.go | 7 +++---- internal/vaultik/restore_identity_test.go | 20 ++++++++++++++++++++ internal/vaultik/vaultik.go | 5 ----- internal/vaultik/verify.go | 12 +++--------- 4 files changed, 26 insertions(+), 18 deletions(-) diff --git a/internal/vaultik/restore.go b/internal/vaultik/restore.go index f02caeb..d30862a 100644 --- a/internal/vaultik/restore.go +++ b/internal/vaultik/restore.go @@ -31,10 +31,9 @@ var ( errPathsNotInSnapshot = errors.New("requested path(s) not in the snapshot") errFilesFailedVerify = errors.New("files failed verification") errDecryptionKeyRequired = errors.New( - "decryption key required for restore\n\n" + - "Set the VAULTIK_AGE_SECRET_KEY environment variable to your " + - "age private key file:\n" + - " export VAULTIK_AGE_SECRET_KEY=\"$(cat vaultik_backup_private_key.txt)\"") + "age secret key is missing; set it in the " + + "VAULTIK_AGE_SECRET_KEY environment variable or the " + + "age_secret_key config file field") // errInvalidAgeSecretKey is returned when the configured key does not // parse as any age identity. It names the source but never the value, // which is secret, so the message is safe to print and log. diff --git a/internal/vaultik/restore_identity_test.go b/internal/vaultik/restore_identity_test.go index 50fd70d..1d7e0de 100644 --- a/internal/vaultik/restore_identity_test.go +++ b/internal/vaultik/restore_identity_test.go @@ -106,3 +106,23 @@ func TestRestoreIdentitiesMissingKey(t *testing.T) { _, err := v.restoreIdentities() require.ErrorIs(t, err, errDecryptionKeyRequired) } + +// TestMissingKeyErrorsNameBothSources asserts restore and deep verify +// return the same missing-key error, whose text names both places the key +// can be set: the VAULTIK_AGE_SECRET_KEY environment variable first since +// it is preferred, then the age_secret_key config file field. +func TestMissingKeyErrorsNameBothSources(t *testing.T) { + t.Parallel() + + const want = "age secret key is missing; set it in " + + "the VAULTIK_AGE_SECRET_KEY environment variable or the " + + "age_secret_key config file field" + + v := &Vaultik{Config: &config.Config{}} + + _, err := v.restoreIdentities() + require.EqualError(t, err, want) + + err = v.RunDeepVerify("any-snapshot", &VerifyOptions{}) + require.EqualError(t, err, want) +} diff --git a/internal/vaultik/vaultik.go b/internal/vaultik/vaultik.go index 3105147..188695e 100644 --- a/internal/vaultik/vaultik.go +++ b/internal/vaultik/vaultik.go @@ -162,11 +162,6 @@ func (v *Vaultik) StartOperation(fn func()) func(context.Context) bool { } } -// CanDecrypt returns true if this Vaultik instance has decryption capabilities -func (v *Vaultik) CanDecrypt() bool { - return v.Config.AgeSecretKey != "" -} - // GetFilesystem returns the filesystem instance used by Vaultik // //nolint:ireturn // afero.Fs is the filesystem abstraction by design diff --git a/internal/vaultik/verify.go b/internal/vaultik/verify.go index 6c88d93..4d13c38 100644 --- a/internal/vaultik/verify.go +++ b/internal/vaultik/verify.go @@ -22,9 +22,7 @@ import ( // Sentinel errors for snapshot verification failures. var ( - errVerificationFailed = errors.New("verification failed") - errSecretKeyRequired = errors.New( - "VAULTIK_AGE_SECRET_KEY not set; required for deep verification") + errVerificationFailed = errors.New("verification failed") errChunksOutOfOrder = errors.New("chunks out of order") errChunkHashMismatch = errors.New("chunk hash mismatch") errNegativeChunkLength = errors.New("chunk length is negative") @@ -106,13 +104,9 @@ func (v *Vaultik) RunDeepVerify(snapshotID string, opts *VerifyOptions) error { Mode: "deep", } - if !v.CanDecrypt() { - return v.deepVerifyFailure(result, opts, - errSecretKeyRequired.Error(), errSecretKeyRequired) - } - // Parse the age secret key once, the same way restore does, and reuse - // the identities for the database and every blob. + // the identities for the database and every blob. A missing key fails + // here, before anything is downloaded. identities, err := v.restoreIdentities() if err != nil { return v.deepVerifyFailure(result, opts, err.Error(), err)