check / check (push) Waiting to run
init, vault create, unlocker add, and add, import and generate secret with --force asked for the mnemonic or passphrase, or unlocked the vault, while holding the state directory lock, so every other command that changes anything waited at the prompt. They now ask and unlock first and lock after. What they checked before asking is checked again under the lock: unlocker add fails if the vault was removed meanwhile and refuses a GPG key added meanwhile. A forced add of a secret that another command created meanwhile lets go of the lock, gets the key, and takes the lock again. secret.CreatePGPUnlocker now takes the vault, already unlocked; SetMnemonic and SetUnlockPassphrase, used only by it, are gone. Not done: secret move between vaults still unlocks both under the lock; the issue does not name it. Model: opus-5-5
179 lines
5.2 KiB
Go
179 lines
5.2 KiB
Go
package vault_test
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"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.UnlockPassphrase = 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)
|
|
|
|
require.ErrorIs(t, err, secret.ErrWrongPassphrase)
|
|
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
|
|
}
|