A failed secret add --force leaves the secret unchanged (closes #136)
check / check (push) Waiting to run

addVersion wrote the new version before it unlocked the vault to set
notAfter on the version being replaced, so a wrong passphrase or mnemonic
left an extra version behind. It now unlocks the vault and loads that
version's metadata first; writing notAfter and switching current still
come after the new version is saved. generate secret --force and
import --force take the same path.

The time set as the new version's notBefore and the replaced version's
notAfter is taken after the unlock, which can wait for the passphrase.

updatePreviousVersion asks for the long-term key again. The vault keeps
it in memory after the first unlock, so this neither prompts nor derives
it a second time.

Model: opus-5-5
This commit was merged in pull request #161.
This commit is contained in:
2026-10-14 20:48:48 +02:00
parent fd6c5df1a8
commit 5371382cb4
4 changed files with 189 additions and 12 deletions
+76
View File
@@ -5,6 +5,7 @@ import (
"path/filepath"
"testing"
"filippo.io/age"
"github.com/awnumar/memguard"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
@@ -103,3 +104,78 @@ func TestAddSecretCleansUpOnFailure(t *testing.T) {
require.Len(t, entries, 1)
assert.Equal(t, "pub.age", entries[0].Name())
}
// TestAddSecretForceWithWrongPassphraseChangesNothing checks that adding a
// version to an existing secret, with a passphrase that does not open the
// vault, fails and leaves the secret's directory as it was.
func TestAddSecretForceWithWrongPassphraseChangesNothing(t *testing.T) {
t.Parallel()
fs := afero.NewMemMapFs()
passphrase := memguard.NewBufferFromBytes([]byte(testPassphrase))
t.Cleanup(passphrase.Destroy)
created, err := vault.CreateVault(fs, testStateDir, testVaultName,
testMnemonicBuffer(t), passphrase)
require.NoError(t, err)
first := memguard.NewBufferFromBytes([]byte("first"))
t.Cleanup(first.Destroy)
require.NoError(t, created.AddSecret(testSecretName, first, false))
wrong := memguard.NewBufferFromBytes([]byte("wrong passphrase"))
t.Cleanup(wrong.Destroy)
vlt := vault.NewVault(fs, testStateDir, testVaultName)
vlt.SetUnlockPassphrase(wrong)
secretDir := filepath.Join(testStateDir, "vaults.d", testVaultName,
"secrets.d", testSecretName)
before := dirContents(t, fs, secretDir)
second := memguard.NewBufferFromBytes([]byte("second"))
t.Cleanup(second.Destroy)
err = vlt.AddSecret(testSecretName, second, true)
var noMatch *age.NoIdentityMatchError
require.ErrorAs(t, err, &noMatch)
assert.Equal(t, before, dirContents(t, fs, secretDir))
}
// dirContents maps every file under dir to its contents, and every
// directory, written with a trailing "/", to "".
func dirContents(t *testing.T, fs afero.Fs, dir string) map[string]string {
t.Helper()
contents := map[string]string{}
err := afero.Walk(fs, dir, func(
path string, info os.FileInfo, err error,
) error {
if err != nil {
return err
}
if info.IsDir() {
contents[path+"/"] = ""
return nil
}
content, err := afero.ReadFile(fs, path)
if err != nil {
return err
}
contents[path] = string(content)
return nil
})
require.NoError(t, err)
return contents
}