From 301e408bd3aacf7b77e5bbe6e88c9f6108f75cd0 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/verify.go | 12 +++--------- 3 files changed, 26 insertions(+), 13 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..21a24e5 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.Equal(t, want, err.Error()) + + err = v.RunDeepVerify("any-snapshot", &VerifyOptions{}) + require.Equal(t, want, err.Error()) +} 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)