Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a6d937b8e | ||
|
|
5371382cb4 |
@@ -197,7 +197,8 @@ for confirmation, naming the version, the secret and its vault (see
|
|||||||
|
|
||||||
#### `secret generate mnemonic`
|
#### `secret generate mnemonic`
|
||||||
|
|
||||||
Generates a cryptographically secure BIP39 mnemonic phrase.
|
Generates a cryptographically secure BIP39 mnemonic phrase and prints it to
|
||||||
|
stdout. The warning to keep it safe goes to stderr.
|
||||||
|
|
||||||
#### `secret generate secret <name> [--length=16] [--type=base58] [--force]`
|
#### `secret generate secret <name> [--length=16] [--type=base58] [--force]`
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,22 @@ https://git.eeqj.de/sneak/secret/milestone/12
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-10-14: `secret generate mnemonic`, `secret vault list` and
|
||||||
|
`secret unlocker list` print their output to stdout, as `secret list` does, so
|
||||||
|
it can be piped or captured (https://git.eeqj.de/sneak/secret/issues/138).
|
||||||
|
They printed with cobra's `cmd.Println` and `cmd.Printf`, which write to
|
||||||
|
stderr when no output writer is set, so `secret vault list --json | jq`
|
||||||
|
received nothing. "No versions found" from `secret version list` moved to
|
||||||
|
stdout too. Messages such as "Created vault", and the warning after the
|
||||||
|
mnemonic, stay on stderr.
|
||||||
|
- 2026-10-14: A `secret add --force` that cannot unlock the vault, given a wrong
|
||||||
|
passphrase or mnemonic, leaves the secret as it was
|
||||||
|
(https://git.eeqj.de/sneak/secret/issues/136). The new version used to be
|
||||||
|
written before the vault was unlocked to set `notAfter` on the version it
|
||||||
|
replaces, so each failed add left one more version that `secret version list`
|
||||||
|
showed. The vault is now unlocked, and that version's metadata read, before
|
||||||
|
anything is written. `generate secret --force` and `import --force` take the
|
||||||
|
same path.
|
||||||
- 2026-10-12: A mnemonic typed with extra spaces, tabs or line breaks gives the
|
- 2026-10-12: A mnemonic typed with extra spaces, tabs or line breaks gives the
|
||||||
same long-term key as its words separated by single spaces
|
same long-term key as its words separated by single spaces
|
||||||
(https://git.eeqj.de/sneak/secret/issues/135). The key was derived from the
|
(https://git.eeqj.de/sneak/secret/issues/135). The key was derived from the
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ func (cli *Instance) GenerateMnemonic(cmd *cobra.Command) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Output mnemonic to stdout
|
// Output mnemonic to stdout
|
||||||
cmd.Println(mnemonic)
|
_, _ = fmt.Fprintln(cmd.OutOrStdout(), mnemonic)
|
||||||
|
|
||||||
// Output helpful information to stderr
|
// Output helpful information to stderr
|
||||||
fmt.Fprintln(os.Stderr, "")
|
fmt.Fprintln(os.Stderr, "")
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package cli_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -9,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"sneak.berlin/go/secret/internal/bip39"
|
||||||
"sneak.berlin/go/secret/internal/secret"
|
"sneak.berlin/go/secret/internal/secret"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -90,3 +92,66 @@ func TestGetCommandOutputsToStdout(t *testing.T) {
|
|||||||
// Nothing should be in stderr
|
// Nothing should be in stderr
|
||||||
assert.Empty(t, stderr.String(), "stderr should be empty")
|
assert.Empty(t, stderr.String(), "stderr should be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestMnemonicAndListingsOutputToStdout tests that 'secret generate
|
||||||
|
// mnemonic', 'secret vault list' and 'secret unlocker list' print their
|
||||||
|
// output to stdout, so that it can be piped
|
||||||
|
func TestMnemonicAndListingsOutputToStdout(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
secretPath := secretBinaryPath(t)
|
||||||
|
|
||||||
|
// run runs the secret binary and returns what it wrote to stdout and to
|
||||||
|
// stderr
|
||||||
|
run := func(args ...string) (string, string) {
|
||||||
|
//nolint:gosec // G204: test executes the freshly built secret binary
|
||||||
|
cmd := exec.CommandContext(t.Context(), secretPath, args...)
|
||||||
|
cmd.Env = []string{
|
||||||
|
secret.EnvStateDir + "=" + tempDir,
|
||||||
|
secret.EnvMnemonic + "=" + testMnemonic,
|
||||||
|
secret.EnvUnlockPassphrase + "=test-passphrase",
|
||||||
|
"PATH=/usr/bin:/bin",
|
||||||
|
}
|
||||||
|
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
|
err := cmd.Run()
|
||||||
|
require.NoError(t, err, "%v should succeed: %s", args, stderr.String())
|
||||||
|
|
||||||
|
return stdout.String(), stderr.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout, stderr := run("generate", "mnemonic")
|
||||||
|
assert.True(t, bip39.IsMnemonicValid(strings.TrimSpace(stdout)),
|
||||||
|
"stdout should hold the mnemonic, not %q", stdout)
|
||||||
|
assert.Contains(t, stderr, "Save this mnemonic phrase securely",
|
||||||
|
"the warning should be on stderr")
|
||||||
|
|
||||||
|
run("init")
|
||||||
|
|
||||||
|
stdout, _ = run("vault", "list", "--json")
|
||||||
|
|
||||||
|
var vaults map[string]any
|
||||||
|
|
||||||
|
require.NoError(t, json.Unmarshal([]byte(stdout), &vaults),
|
||||||
|
"stdout should hold the vaults as JSON, not %q", stdout)
|
||||||
|
assert.Equal(t, "default", vaults["currentVault"])
|
||||||
|
|
||||||
|
stdout, _ = run("unlocker", "list", "--json")
|
||||||
|
|
||||||
|
var unlockers map[string]any
|
||||||
|
|
||||||
|
require.NoError(t, json.Unmarshal([]byte(stdout), &unlockers),
|
||||||
|
"stdout should hold the unlockers as JSON, not %q", stdout)
|
||||||
|
assert.Len(t, unlockers["unlockers"], 1)
|
||||||
|
|
||||||
|
stdout, _ = run("vault", "list")
|
||||||
|
assert.Contains(t, stdout, "default (current)")
|
||||||
|
|
||||||
|
stdout, _ = run("unlocker", "list")
|
||||||
|
assert.Contains(t, stdout, "Total: 1 unlocker(s)")
|
||||||
|
}
|
||||||
|
|||||||
@@ -323,22 +323,25 @@ func (cli *Instance) printUnlockersJSON(
|
|||||||
return fmt.Errorf("failed to marshal JSON: %w", err)
|
return fmt.Errorf("failed to marshal JSON: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cli.cmd.Println(string(jsonBytes))
|
_, _ = fmt.Fprintln(cli.cmd.OutOrStdout(), string(jsonBytes))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// printUnlockersTable prints unlockers in a formatted table
|
// printUnlockersTable prints unlockers in a formatted table
|
||||||
func (cli *Instance) printUnlockersTable(unlockers []UnlockerInfo) error {
|
func (cli *Instance) printUnlockersTable(unlockers []UnlockerInfo) error {
|
||||||
|
out := cli.cmd.OutOrStdout()
|
||||||
|
|
||||||
if len(unlockers) == 0 {
|
if len(unlockers) == 0 {
|
||||||
cli.cmd.Println("No unlockers found in current vault.")
|
_, _ = fmt.Fprintln(out, "No unlockers found in current vault.")
|
||||||
cli.cmd.Println("Run 'secret unlocker add passphrase' to create one.")
|
_, _ = fmt.Fprintln(out, "Run 'secret unlocker add passphrase' to create one.")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cli.cmd.Printf(" %-40s %-12s %-20s %s\n", "UNLOCKER ID", "TYPE", "CREATED", "FLAGS")
|
_, _ = fmt.Fprintf(out, " %-40s %-12s %-20s %s\n",
|
||||||
cli.cmd.Printf(" %-40s %-12s %-20s %s\n",
|
"UNLOCKER ID", "TYPE", "CREATED", "FLAGS")
|
||||||
|
_, _ = fmt.Fprintf(out, " %-40s %-12s %-20s %s\n",
|
||||||
strings.Repeat("-", unlockerIDWidth), strings.Repeat("-", unlockerTypeWidth),
|
strings.Repeat("-", unlockerIDWidth), strings.Repeat("-", unlockerTypeWidth),
|
||||||
strings.Repeat("-", unlockerDateWidth), strings.Repeat("-", unlockerFlagsWidth))
|
strings.Repeat("-", unlockerDateWidth), strings.Repeat("-", unlockerFlagsWidth))
|
||||||
|
|
||||||
@@ -353,7 +356,7 @@ func (cli *Instance) printUnlockersTable(unlockers []UnlockerInfo) error {
|
|||||||
prefix = "* "
|
prefix = "* "
|
||||||
}
|
}
|
||||||
|
|
||||||
cli.cmd.Printf("%s%-40s %-12s %-20s %s\n",
|
_, _ = fmt.Fprintf(out, "%s%-40s %-12s %-20s %s\n",
|
||||||
prefix,
|
prefix,
|
||||||
unlocker.ID,
|
unlocker.ID,
|
||||||
unlocker.Type,
|
unlocker.Type,
|
||||||
@@ -361,7 +364,7 @@ func (cli *Instance) printUnlockersTable(unlockers []UnlockerInfo) error {
|
|||||||
flags)
|
flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
cli.cmd.Printf("\nTotal: %d unlocker(s)\n", len(unlockers))
|
_, _ = fmt.Fprintf(out, "\nTotal: %d unlocker(s)\n", len(unlockers))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -186,6 +186,8 @@ func (cli *Instance) ListVaults(cmd *cobra.Command, jsonOutput bool) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
out := cmd.OutOrStdout()
|
||||||
|
|
||||||
if jsonOutput { //nolint:nestif // Separate JSON and text output formatting logic
|
if jsonOutput { //nolint:nestif // Separate JSON and text output formatting logic
|
||||||
// Get current vault name for context
|
// Get current vault name for context
|
||||||
currentVault := ""
|
currentVault := ""
|
||||||
@@ -205,13 +207,13 @@ func (cli *Instance) ListVaults(cmd *cobra.Command, jsonOutput bool) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Println(string(jsonBytes))
|
_, _ = fmt.Fprintln(out, string(jsonBytes))
|
||||||
} else {
|
} else {
|
||||||
// Text output
|
// Text output
|
||||||
cmd.Println("Available vaults:")
|
_, _ = fmt.Fprintln(out, "Available vaults:")
|
||||||
|
|
||||||
if len(vaults) == 0 {
|
if len(vaults) == 0 {
|
||||||
cmd.Println(" (none)")
|
_, _ = fmt.Fprintln(out, " (none)")
|
||||||
} else {
|
} else {
|
||||||
// Try to get current vault for marking
|
// Try to get current vault for marking
|
||||||
currentVault := ""
|
currentVault := ""
|
||||||
@@ -223,9 +225,9 @@ func (cli *Instance) ListVaults(cmd *cobra.Command, jsonOutput bool) error {
|
|||||||
|
|
||||||
for _, vaultName := range vaults {
|
for _, vaultName := range vaults {
|
||||||
if vaultName == currentVault {
|
if vaultName == currentVault {
|
||||||
cmd.Printf(" %s (current)\n", vaultName)
|
_, _ = fmt.Fprintf(out, " %s (current)\n", vaultName)
|
||||||
} else {
|
} else {
|
||||||
cmd.Printf(" %s\n", vaultName)
|
_, _ = fmt.Fprintf(out, " %s\n", vaultName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ func (cli *Instance) ListVersions(cmd *cobra.Command, secretName string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(versions) == 0 {
|
if len(versions) == 0 {
|
||||||
cmd.Println("No versions found")
|
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "No versions found")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+39
-12
@@ -219,6 +219,15 @@ func (v *Vault) addVersion(
|
|||||||
name, secretDir string, value *memguard.LockedBuffer,
|
name, secretDir string, value *memguard.LockedBuffer,
|
||||||
previousVersion *secret.Version,
|
previousVersion *secret.Version,
|
||||||
) error {
|
) error {
|
||||||
|
// Unlock the vault before writing anything, so that a wrong passphrase
|
||||||
|
// or mnemonic leaves the secret as it was
|
||||||
|
err := v.loadPreviousVersionMetadata(previousVersion)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Taken after the unlock, which can wait for the user to type the
|
||||||
|
// passphrase, so that the new version starts when it is created
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
// Create the new version and save the encrypted value
|
// Create the new version and save the encrypted value
|
||||||
@@ -632,8 +641,35 @@ func (v *Vault) checkExistingSecret(
|
|||||||
return true, previousVersion, nil
|
return true, previousVersion, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loadPreviousVersionMetadata unlocks the vault and loads the metadata of
|
||||||
|
// the version being superseded. It is a no-op when previousVersion is nil.
|
||||||
|
func (v *Vault) loadPreviousVersionMetadata(
|
||||||
|
previousVersion *secret.Version,
|
||||||
|
) error {
|
||||||
|
if previousVersion == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ltIdentity, err := v.GetOrDeriveLongTermKey()
|
||||||
|
if err != nil {
|
||||||
|
secret.Debug("Failed to get long-term key for previous version", "error", err)
|
||||||
|
|
||||||
|
return fmt.Errorf("failed to get long-term key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = previousVersion.LoadMetadata(ltIdentity)
|
||||||
|
if err != nil {
|
||||||
|
secret.Debug("Failed to load previous version metadata", "error", err)
|
||||||
|
|
||||||
|
return fmt.Errorf("failed to load previous version metadata: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// updatePreviousVersion sets the notAfter timestamp on the version being
|
// updatePreviousVersion sets the notAfter timestamp on the version being
|
||||||
// superseded. It is a no-op when previousVersion is nil.
|
// superseded, whose metadata loadPreviousVersionMetadata has loaded. It is
|
||||||
|
// a no-op when previousVersion is nil.
|
||||||
func (v *Vault) updatePreviousVersion(
|
func (v *Vault) updatePreviousVersion(
|
||||||
previousVersion *secret.Version, now *time.Time,
|
previousVersion *secret.Version, now *time.Time,
|
||||||
) error {
|
) error {
|
||||||
@@ -641,22 +677,13 @@ func (v *Vault) updatePreviousVersion(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get long-term key to decrypt/encrypt metadata
|
// loadPreviousVersionMetadata unlocked the vault, so this returns the
|
||||||
|
// key it keeps in memory
|
||||||
ltIdentity, err := v.GetOrDeriveLongTermKey()
|
ltIdentity, err := v.GetOrDeriveLongTermKey()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
secret.Debug("Failed to get long-term key for metadata update", "error", err)
|
|
||||||
|
|
||||||
return fmt.Errorf("failed to get long-term key: %w", err)
|
return fmt.Errorf("failed to get long-term key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load previous version metadata
|
|
||||||
err = previousVersion.LoadMetadata(ltIdentity)
|
|
||||||
if err != nil {
|
|
||||||
secret.Debug("Failed to load previous version metadata", "error", err)
|
|
||||||
|
|
||||||
return fmt.Errorf("failed to load previous version metadata: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update notAfter timestamp
|
// Update notAfter timestamp
|
||||||
previousVersion.Metadata.NotAfter = now
|
previousVersion.Metadata.NotAfter = now
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
// version
|
// version
|
||||||
// - TestVaultVersionTimestamps: Tests timestamp logic (notBefore/notAfter)
|
// - TestVaultVersionTimestamps: Tests timestamp logic (notBefore/notAfter)
|
||||||
// across versions
|
// across versions
|
||||||
|
// - TestVaultVersionTimestampsAfterUnlock: Tests that those timestamps are
|
||||||
|
// taken after AddSecret unlocks the vault
|
||||||
// - TestVaultGetNonExistentVersion: Tests error handling for invalid versions
|
// - TestVaultGetNonExistentVersion: Tests error handling for invalid versions
|
||||||
// - TestUpdateVersionMetadata: Tests metadata update functionality
|
// - TestUpdateVersionMetadata: Tests metadata update functionality
|
||||||
//
|
//
|
||||||
@@ -295,6 +297,70 @@ func TestVaultVersionTimestamps(t *testing.T) {
|
|||||||
assert.Nil(t, secondVersion.Metadata.NotAfter) // Current version
|
assert.Nil(t, secondVersion.Metadata.NotAfter) // Current version
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// longTermKeyReadFs records when longterm.age, the last file an unlock
|
||||||
|
// reads, was last opened.
|
||||||
|
type longTermKeyReadFs struct {
|
||||||
|
afero.Fs
|
||||||
|
|
||||||
|
readAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:ireturn // afero.File is the interface required by afero.Fs
|
||||||
|
func (f *longTermKeyReadFs) Open(name string) (afero.File, error) {
|
||||||
|
if filepath.Base(name) == "longterm.age" {
|
||||||
|
f.readAt = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
return f.Fs.Open(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestVaultVersionTimestampsAfterUnlock checks that when AddSecret has to
|
||||||
|
// unlock the vault to replace a version, the new version's notBefore and the
|
||||||
|
// replaced version's notAfter are taken after the unlock, which can wait for
|
||||||
|
// the user to type the passphrase.
|
||||||
|
func TestVaultVersionTimestampsAfterUnlock(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
fs := &longTermKeyReadFs{Fs: afero.NewMemMapFs()}
|
||||||
|
|
||||||
|
mnemonic := memguard.NewBufferFromBytes([]byte(testMnemonic))
|
||||||
|
defer mnemonic.Destroy()
|
||||||
|
|
||||||
|
passphrase := memguard.NewBufferFromBytes([]byte("test-passphrase"))
|
||||||
|
defer passphrase.Destroy()
|
||||||
|
|
||||||
|
created, err := CreateVault(fs, testStateDir, "test", mnemonic, passphrase)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
addTestSecretToVault(t, created, testSecretPath, []byte("version-1"), false)
|
||||||
|
|
||||||
|
// A new instance is locked; AddSecret unlocks it with the passphrase
|
||||||
|
vault := NewVault(fs, testStateDir, "test")
|
||||||
|
vault.SetUnlockPassphrase(passphrase)
|
||||||
|
|
||||||
|
addTestSecretToVault(t, vault, testSecretPath, []byte("version-2"), true)
|
||||||
|
require.False(t, fs.readAt.IsZero(), "AddSecret did not unlock the vault")
|
||||||
|
|
||||||
|
ltIdentity, err := vault.GetOrDeriveLongTermKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
vaultDir, _ := vault.GetDirectory()
|
||||||
|
secretDir := filepath.Join(vaultDir, "secrets.d", "test%secret")
|
||||||
|
|
||||||
|
versions, err := secret.ListVersions(fs, secretDir)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, versions, 2)
|
||||||
|
|
||||||
|
newVersion := secret.NewVersion(vault, testSecretPath, versions[0])
|
||||||
|
require.NoError(t, newVersion.LoadMetadata(ltIdentity))
|
||||||
|
|
||||||
|
replacedVersion := secret.NewVersion(vault, testSecretPath, versions[1])
|
||||||
|
require.NoError(t, replacedVersion.LoadMetadata(ltIdentity))
|
||||||
|
|
||||||
|
assert.False(t, newVersion.Metadata.NotBefore.Before(fs.readAt))
|
||||||
|
assert.False(t, replacedVersion.Metadata.NotAfter.Before(fs.readAt))
|
||||||
|
}
|
||||||
|
|
||||||
func TestVaultGetNonExistentVersion(t *testing.T) {
|
func TestVaultGetNonExistentVersion(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"filippo.io/age"
|
||||||
"github.com/awnumar/memguard"
|
"github.com/awnumar/memguard"
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -103,3 +104,78 @@ func TestAddSecretCleansUpOnFailure(t *testing.T) {
|
|||||||
require.Len(t, entries, 1)
|
require.Len(t, entries, 1)
|
||||||
assert.Equal(t, "pub.age", entries[0].Name())
|
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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user