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
182 lines
5.3 KiB
Go
182 lines
5.3 KiB
Go
package vault_test
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"filippo.io/age"
|
|
"github.com/awnumar/memguard"
|
|
"github.com/spf13/afero"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"sneak.berlin/go/secret/internal/secret"
|
|
"sneak.berlin/go/secret/internal/vault"
|
|
)
|
|
|
|
func TestAddSecretFailsWithMissingPublicKey(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Create in-memory filesystem
|
|
fs := afero.NewMemMapFs()
|
|
|
|
// Create a vault directory without a public key (simulating the error
|
|
// condition)
|
|
vaultDir := filepath.Join(testStateDir, "vaults.d", "broken")
|
|
require.NoError(t, fs.MkdirAll(vaultDir, secret.DirPerms))
|
|
|
|
// Create currentvault symlink
|
|
currentVaultPath := filepath.Join(testStateDir, "currentvault")
|
|
require.NoError(t,
|
|
afero.WriteFile(fs, currentVaultPath, []byte(vaultDir), secret.FilePerms))
|
|
|
|
// Create vault instance
|
|
vlt := vault.NewVault(fs, testStateDir, "broken")
|
|
|
|
// Try to add a secret - this should fail
|
|
value := memguard.NewBufferFromBytes([]byte("test-value"))
|
|
defer value.Destroy()
|
|
|
|
err := vlt.AddSecret(testSecretName, value, false)
|
|
|
|
var cause *os.PathError
|
|
|
|
require.ErrorAs(t, err, &cause)
|
|
require.ErrorIs(t, err, os.ErrNotExist,
|
|
"AddSecret should fail when public key is missing")
|
|
assert.Equal(t, filepath.Join(vaultDir, "pub.age"), cause.Path)
|
|
|
|
// Verify that the secret directory was NOT created
|
|
secretDir := filepath.Join(vaultDir, "secrets.d", testSecretName)
|
|
exists, _ := afero.DirExists(fs, secretDir)
|
|
assert.False(t, exists, "Secret directory should not exist after failed AddSecret")
|
|
|
|
// Verify the secrets.d directory is empty or doesn't exist
|
|
secretsDir := filepath.Join(vaultDir, "secrets.d")
|
|
if exists, _ := afero.DirExists(fs, secretsDir); exists {
|
|
entries, err := afero.ReadDir(fs, secretsDir)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, entries,
|
|
"secrets.d directory should be empty after failed AddSecret")
|
|
}
|
|
}
|
|
|
|
func TestAddSecretCleansUpOnFailure(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Create in-memory filesystem
|
|
fs := afero.NewMemMapFs()
|
|
|
|
// Create a vault directory with public key
|
|
vaultDir := filepath.Join(testStateDir, "vaults.d", "test")
|
|
require.NoError(t, fs.MkdirAll(vaultDir, secret.DirPerms))
|
|
|
|
// Create a mock public key that will cause encryption to fail
|
|
// by using an invalid age public key format
|
|
pubKeyPath := filepath.Join(vaultDir, "pub.age")
|
|
require.NoError(t,
|
|
afero.WriteFile(fs, pubKeyPath, []byte("invalid-public-key"),
|
|
secret.FilePerms))
|
|
|
|
// Create currentvault symlink
|
|
currentVaultPath := filepath.Join(testStateDir, "currentvault")
|
|
require.NoError(t,
|
|
afero.WriteFile(fs, currentVaultPath, []byte(vaultDir), secret.FilePerms))
|
|
|
|
// Create vault instance
|
|
vlt := vault.NewVault(fs, testStateDir, "test")
|
|
|
|
// Try to add a secret - this should fail during encryption
|
|
value := memguard.NewBufferFromBytes([]byte("test-value"))
|
|
defer value.Destroy()
|
|
|
|
err := vlt.AddSecret(testSecretName, value, false)
|
|
require.Error(t, err, "AddSecret should fail with invalid public key")
|
|
|
|
// Verify that the secret directory was NOT created
|
|
secretDir := filepath.Join(vaultDir, "secrets.d", testSecretName)
|
|
exists, _ := afero.DirExists(fs, secretDir)
|
|
assert.False(t, exists, "Secret directory should not exist after failed AddSecret")
|
|
|
|
// Nor is the temporary directory the secret was assembled in left behind
|
|
entries, err := afero.ReadDir(fs, vaultDir)
|
|
require.NoError(t, err)
|
|
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
|
|
}
|