check / check (push) Waiting to run
A passphrase that did not decrypt the passphrase unlocker failed with age's "no identity matched any of the recipients" and advice to give the vault a new unlocker from its mnemonic, which would replace an unlocker that works. GetIdentity now turns that age error into "wrong passphrase for unlocker <id>", wrapping the new secret.ErrWrongPassphrase, and withMnemonicAdvice leaves it alone, as it does when no passphrase was read. A passphrase from SB_UNLOCK_PASSPHRASE and a typed one both reach this code. A priv.age that is not an age file keeps the advice. Judgement call: age cannot tell a wrong passphrase from a damaged key inside a priv.age whose header reads, so that case now reads as a wrong passphrase. 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.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)
|
|
|
|
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
|
|
}
|