Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51c030757d | ||
|
|
23dcea83f9 |
@@ -29,6 +29,19 @@ https://git.eeqj.de/sneak/secret/milestone/12
|
|||||||
the Secure Enclave only decrypts; per-version keys give no forward
|
the Secure Enclave only decrypts; per-version keys give no forward
|
||||||
secrecy; `pub.age` is not age-encrypted; vault metadata holds no vault
|
secrecy; `pub.age` is not age-encrypted; vault metadata holds no vault
|
||||||
name. Testing lists only `make test`.
|
name. Testing lists only `make test`.
|
||||||
|
- 2026-10-04: `secret init` and `secret vault create` create a vault whole or
|
||||||
|
not at all (https://git.eeqj.de/sneak/secret/issues/105).
|
||||||
|
`vault.CreateVault` now takes the unlocker passphrase too, writes the vault
|
||||||
|
directory with its metadata, long-term public key and passphrase unlocker,
|
||||||
|
`longterm.age` included, into a temporary directory, renames that into
|
||||||
|
`vaults.d` once it is complete, and only then makes the vault current.
|
||||||
|
Before, either command killed after the passphrase prompt but before the
|
||||||
|
unlocker was written left a vault with no unlocker, which `vault create` had
|
||||||
|
already made current and which neither command would create again. Killed
|
||||||
|
part-way now, it leaves no vault, and the next command that takes the lock
|
||||||
|
deletes the temporary directory; or, killed between the rename and making
|
||||||
|
the vault current, a complete vault that is not current, which
|
||||||
|
`secret vault select` makes current.
|
||||||
- 2026-10-04: A failed `secret unlocker add keychain` or
|
- 2026-10-04: A failed `secret unlocker add keychain` or
|
||||||
`secret unlocker add secure-enclave` no longer leaves its keychain item or
|
`secret unlocker add secure-enclave` no longer leaves its keychain item or
|
||||||
Secure Enclave key behind (https://git.eeqj.de/sneak/secret/issues/89).
|
Secure Enclave key behind (https://git.eeqj.de/sneak/secret/issues/89).
|
||||||
@@ -269,11 +282,7 @@ https://git.eeqj.de/sneak/secret/milestone/12
|
|||||||
`current-unlocker` never go missing. New versions, new secrets and
|
`current-unlocker` never go missing. New versions, new secrets and
|
||||||
cross-vault copies are built in a temporary directory and renamed
|
cross-vault copies are built in a temporary directory and renamed
|
||||||
into place, and removals rename out of the way first, so a version
|
into place, and removals rename out of the way first, so a version
|
||||||
or secret is never half-added and never half-removed. An
|
or secret is never half-added and never half-removed.
|
||||||
interrupted command can still leave, from `init` or `vault create`
|
|
||||||
killed after the passphrase prompt but before the unlocker is
|
|
||||||
written, a vault with no unlocker, which `vault create` has already
|
|
||||||
made the current vault.
|
|
||||||
- 2026-10-03: The checks run before changing a vault now stop with an
|
- 2026-10-03: The checks run before changing a vault now stop with an
|
||||||
error naming the path and cause when they cannot read what they
|
error naming the path and cause when they cannot read what they
|
||||||
inspect, instead of reading the failure as "nothing there": the
|
inspect, instead of reading the failure as "nothing there": the
|
||||||
|
|||||||
@@ -63,10 +63,10 @@ func newConfirmTestVaults(
|
|||||||
fs := &afero.MemMapFs{}
|
fs := &afero.MemMapFs{}
|
||||||
mnemonic := testMnemonicBuffer(t)
|
mnemonic := testMnemonicBuffer(t)
|
||||||
|
|
||||||
_, err := vault.CreateVault(fs, testStateDir, "other", mnemonic)
|
_, err := vault.CreateVault(fs, testStateDir, "other", mnemonic, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
vlt, err := vault.CreateVault(fs, testStateDir, "work", mnemonic)
|
vlt, err := vault.CreateVault(fs, testStateDir, "work", mnemonic, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
addTestSecret(t, vlt, []byte("older"), false)
|
addTestSecret(t, vlt, []byte("older"), false)
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ package cli_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"maps"
|
||||||
"os"
|
"os"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.eeqj.de/sneak/secret/internal/cli"
|
"git.eeqj.de/sneak/secret/internal/cli"
|
||||||
@@ -155,7 +159,7 @@ func TestStopAtPassphrasePromptLeavesNothing(t *testing.T) {
|
|||||||
require.NoError(t, empty.MkdirAll(testStateDir, secret.DirPerms))
|
require.NoError(t, empty.MkdirAll(testStateDir, secret.DirPerms))
|
||||||
|
|
||||||
withDefault := afero.NewMemMapFs()
|
withDefault := afero.NewMemMapFs()
|
||||||
_, err := vault.CreateVault(withDefault, testStateDir, "default", mnemonic)
|
_, err := vault.CreateVault(withDefault, testStateDir, "default", mnemonic, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
cmd := &cobra.Command{}
|
cmd := &cobra.Command{}
|
||||||
@@ -193,3 +197,192 @@ func TestStopAtPassphrasePromptLeavesNothing(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestStopDuringCreateLeavesWholeVaultOrNone is a regression test for
|
||||||
|
// https://git.eeqj.de/sneak/secret/issues/105: `secret init` or `secret vault
|
||||||
|
// create` killed after the passphrase prompt but before the unlocker was
|
||||||
|
// written left a vault with no unlocker, which neither command would then
|
||||||
|
// create again. After the prompt, each command changes the state directory
|
||||||
|
// only through vault.CreateVault. The test makes that call as the command
|
||||||
|
// does and records the state directory before each change it makes, and once
|
||||||
|
// after it returns: what a stop at that point leaves. Each must hold either
|
||||||
|
// no vault, and not name it current, or exactly the finished vault, which
|
||||||
|
// opens with the passphrase through its current unlocker. The command run
|
||||||
|
// again after a stop first takes the lock, which must delete what the stop
|
||||||
|
// left under a temporary name. Running the command is slow, so it runs once
|
||||||
|
// on each different state the lock leaves, and must create the vault there,
|
||||||
|
// or refuse the one there.
|
||||||
|
//
|
||||||
|
//nolint:paralleltest // commands on the in-memory filesystem share one lock
|
||||||
|
func TestStopDuringCreateLeavesWholeVaultOrNone(t *testing.T) {
|
||||||
|
mnemonic := testMnemonicBuffer(t)
|
||||||
|
passphrase := memguard.NewBufferFromBytes([]byte(testPassphrase))
|
||||||
|
t.Cleanup(passphrase.Destroy)
|
||||||
|
|
||||||
|
cmd := &cobra.Command{}
|
||||||
|
cmd.SetOut(io.Discard)
|
||||||
|
|
||||||
|
t.Run("init", func(t *testing.T) {
|
||||||
|
// From an empty state directory
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
require.NoError(t, fs.MkdirAll(testStateDir, secret.DirPerms))
|
||||||
|
|
||||||
|
requireStopsLeaveWholeVaultOrNone(t, fs, "default",
|
||||||
|
"failed to create default vault: vault default already exists",
|
||||||
|
mnemonic, passphrase,
|
||||||
|
func(c *cli.Instance) error { return c.Init(cmd) })
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("vault create work", func(t *testing.T) {
|
||||||
|
// From a state directory holding the vault "default"
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
_, err := vault.CreateVault(fs, testStateDir, "default", mnemonic, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
requireStopsLeaveWholeVaultOrNone(t, fs, "work", "vault work already exists",
|
||||||
|
mnemonic, passphrase,
|
||||||
|
func(c *cli.Instance) error { return c.CreateVault(cmd, "work") })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// requireStopsLeaveWholeVaultOrNone checks, as
|
||||||
|
// TestStopDuringCreateLeavesWholeVaultOrNone describes, the stops of the
|
||||||
|
// command run, creating the vault name on fs with mnemonic and passphrase.
|
||||||
|
// Run again where the vault is there, the command must fail with exists.
|
||||||
|
func requireStopsLeaveWholeVaultOrNone(
|
||||||
|
t *testing.T, fs afero.Fs, name, exists string,
|
||||||
|
mnemonic, passphrase *memguard.LockedBuffer,
|
||||||
|
run func(c *cli.Instance) error,
|
||||||
|
) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
var stops []map[string]string
|
||||||
|
|
||||||
|
record := func() { stops = append(stops, snapshotStateDir(t, fs)) }
|
||||||
|
|
||||||
|
_, err := vault.CreateVault(hookFs{Fs: fs, before: record},
|
||||||
|
testStateDir, name, mnemonic, passphrase)
|
||||||
|
require.NoError(t, err)
|
||||||
|
record()
|
||||||
|
|
||||||
|
vaultDir := testStateDir + "/vaults.d/" + name
|
||||||
|
require.NotContains(t, stops[0], vaultDir+"/", "no stop before the vault")
|
||||||
|
|
||||||
|
finished := entriesUnder(stops[len(stops)-1], vaultDir)
|
||||||
|
|
||||||
|
opener := vault.NewVault(fs, testStateDir, name)
|
||||||
|
opener.UnlockPassphrase = passphrase
|
||||||
|
|
||||||
|
key, err := opener.UnlockVault()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, finished[vaultDir+"/pub.age"], key.Recipient().String())
|
||||||
|
|
||||||
|
// Each different state the command run again finds once it holds the lock
|
||||||
|
var locked []map[string]string
|
||||||
|
|
||||||
|
for i, stop := range stops {
|
||||||
|
if _, there := stop[vaultDir+"/"]; there {
|
||||||
|
require.Equal(t, finished, entriesUnder(stop, vaultDir),
|
||||||
|
"stop %d left a partial vault", i)
|
||||||
|
} else {
|
||||||
|
require.NotEqual(t, name, stop[testStateDir+"/currentvault"],
|
||||||
|
"stop %d made a missing vault current", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
stopped := newFsFromSnapshot(t, stop)
|
||||||
|
release, err := vault.LockStateDir(stopped, testStateDir)
|
||||||
|
require.NoError(t, err)
|
||||||
|
release()
|
||||||
|
|
||||||
|
state := snapshotStateDir(t, stopped)
|
||||||
|
for path := range state {
|
||||||
|
require.NotContains(t, path, ".tmp-", "stop %d", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !slices.ContainsFunc(locked, func(s map[string]string) bool {
|
||||||
|
return maps.Equal(s, state)
|
||||||
|
}) {
|
||||||
|
locked = append(locked, state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, state := range locked {
|
||||||
|
c := cli.NewCLIInstanceWithStateDir(newFsFromSnapshot(t, state), testStateDir)
|
||||||
|
c.Mnemonic = mnemonic
|
||||||
|
c.UnlockPassphrase = passphrase
|
||||||
|
|
||||||
|
if _, there := state[vaultDir+"/"]; there {
|
||||||
|
require.EqualError(t, run(c), exists)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, run(c))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// entriesUnder returns the entries of a tree recorded by snapshotStateDir
|
||||||
|
// that are under dir.
|
||||||
|
func entriesUnder(tree map[string]string, dir string) map[string]string {
|
||||||
|
entries := map[string]string{}
|
||||||
|
|
||||||
|
for path, content := range tree {
|
||||||
|
if strings.HasPrefix(path, dir+"/") {
|
||||||
|
entries[path] = content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
// hookFs passes every call through to Fs, but first calls before for each
|
||||||
|
// call that can change the filesystem.
|
||||||
|
type hookFs struct {
|
||||||
|
afero.Fs
|
||||||
|
|
||||||
|
before func()
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:ireturn // implements afero.Fs
|
||||||
|
func (h hookFs) Create(name string) (afero.File, error) {
|
||||||
|
h.before()
|
||||||
|
|
||||||
|
return h.Fs.Create(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:ireturn // implements afero.Fs
|
||||||
|
func (h hookFs) OpenFile(
|
||||||
|
name string, flag int, perm os.FileMode,
|
||||||
|
) (afero.File, error) {
|
||||||
|
h.before()
|
||||||
|
|
||||||
|
return h.Fs.OpenFile(name, flag, perm)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h hookFs) Mkdir(name string, perm os.FileMode) error {
|
||||||
|
h.before()
|
||||||
|
|
||||||
|
return h.Fs.Mkdir(name, perm)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h hookFs) MkdirAll(path string, perm os.FileMode) error {
|
||||||
|
h.before()
|
||||||
|
|
||||||
|
return h.Fs.MkdirAll(path, perm)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h hookFs) Remove(name string) error {
|
||||||
|
h.before()
|
||||||
|
|
||||||
|
return h.Fs.Remove(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h hookFs) RemoveAll(path string) error {
|
||||||
|
h.before()
|
||||||
|
|
||||||
|
return h.Fs.RemoveAll(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h hookFs) Rename(oldname, newname string) error {
|
||||||
|
h.before()
|
||||||
|
|
||||||
|
return h.Fs.Rename(oldname, newname)
|
||||||
|
}
|
||||||
|
|||||||
+19
-62
@@ -6,13 +6,10 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"filippo.io/age"
|
|
||||||
"git.eeqj.de/sneak/secret/internal/secret"
|
"git.eeqj.de/sneak/secret/internal/secret"
|
||||||
"git.eeqj.de/sneak/secret/internal/vault"
|
"git.eeqj.de/sneak/secret/internal/vault"
|
||||||
"git.eeqj.de/sneak/secret/pkg/agehd"
|
|
||||||
"github.com/awnumar/memguard"
|
"github.com/awnumar/memguard"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/tyler-smith/go-bip39"
|
"github.com/tyler-smith/go-bip39"
|
||||||
@@ -70,43 +67,6 @@ func (cli *Instance) promptMnemonic() (*memguard.LockedBuffer, func(), error) {
|
|||||||
return mnemonicBuffer, mnemonicBuffer.Destroy, nil
|
return mnemonicBuffer, mnemonicBuffer.Destroy, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// setupDefaultVault creates the default vault and derives its long-term
|
|
||||||
// identity from the mnemonic
|
|
||||||
func (cli *Instance) setupDefaultVault(
|
|
||||||
stateDir string, mnemonic *memguard.LockedBuffer,
|
|
||||||
) (*vault.Vault, *age.X25519Identity, error) {
|
|
||||||
// Create the default vault - it will handle key derivation internally
|
|
||||||
secret.Debug("Creating default vault")
|
|
||||||
|
|
||||||
vlt, err := vault.CreateVault(cli.fs, cli.stateDir, "default", mnemonic)
|
|
||||||
if err != nil {
|
|
||||||
secret.Debug("Failed to create default vault", "error", err)
|
|
||||||
|
|
||||||
return nil, nil, fmt.Errorf("failed to create default vault: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the vault metadata to retrieve the derivation index
|
|
||||||
vaultDir := filepath.Join(stateDir, "vaults.d", "default")
|
|
||||||
|
|
||||||
metadata, err := vault.LoadVaultMetadata(cli.fs, vaultDir)
|
|
||||||
if err != nil {
|
|
||||||
secret.Debug("Failed to load vault metadata", "error", err)
|
|
||||||
|
|
||||||
return nil, nil, fmt.Errorf("failed to load vault metadata: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Derive the long-term key using the same index that CreateVault used
|
|
||||||
ltIdentity, err := agehd.DeriveIdentity(mnemonic.String(), metadata.DerivationIndex)
|
|
||||||
if err != nil {
|
|
||||||
secret.Debug("Failed to derive long-term key", "error", err)
|
|
||||||
|
|
||||||
return nil, nil, fmt.Errorf(
|
|
||||||
"failed to derive long-term key from mnemonic: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return vlt, ltIdentity, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init initializes the secret manager, holding the state directory lock
|
// Init initializes the secret manager, holding the state directory lock
|
||||||
// while initialize runs
|
// while initialize runs
|
||||||
func (cli *Instance) Init(cmd *cobra.Command) error {
|
func (cli *Instance) Init(cmd *cobra.Command) error {
|
||||||
@@ -173,34 +133,31 @@ func (cli *Instance) initialize(cmd *cobra.Command) error {
|
|||||||
}
|
}
|
||||||
defer cleanupPassphrase()
|
defer cleanupPassphrase()
|
||||||
|
|
||||||
// Create the default vault and derive its long-term key
|
// Create the default vault with its passphrase unlocker
|
||||||
vlt, ltIdentity, err := cli.setupDefaultVault(stateDir, mnemonic)
|
secret.Debug("Creating default vault")
|
||||||
|
|
||||||
|
vlt, err := vault.CreateVault(cli.fs, cli.stateDir, "default",
|
||||||
|
mnemonic, passphraseBuffer)
|
||||||
|
if err != nil {
|
||||||
|
secret.Debug("Failed to create default vault", "error", err)
|
||||||
|
|
||||||
|
return fmt.Errorf("failed to create default vault: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ltIdentity, err := vlt.GetOrDeriveLongTermKey()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get long-term key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
unlocker, err := vlt.GetCurrentUnlocker()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
ltPubKey := ltIdentity.Recipient().String()
|
|
||||||
|
|
||||||
// Unlock the vault with the derived long-term key
|
|
||||||
vlt.Unlock(ltIdentity)
|
|
||||||
|
|
||||||
// Create passphrase-protected unlocker
|
|
||||||
secret.Debug("Creating passphrase-protected unlocker")
|
|
||||||
|
|
||||||
passphraseUnlocker, err := vlt.CreatePassphraseUnlocker(passphraseBuffer)
|
|
||||||
if err != nil {
|
|
||||||
secret.Debug("Failed to create unlocker", "error", err)
|
|
||||||
|
|
||||||
return fmt.Errorf("failed to create unlocker: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: CreatePassphraseUnlocker already encrypts and writes the long-term
|
|
||||||
// private key to longterm.age, so no need to do it again here.
|
|
||||||
|
|
||||||
if cmd != nil {
|
if cmd != nil {
|
||||||
cmd.Printf("\nDefault vault created and configured\n")
|
cmd.Printf("\nDefault vault created and configured\n")
|
||||||
cmd.Printf("Long-term public key: %s\n", ltPubKey)
|
cmd.Printf("Long-term public key: %s\n", ltIdentity.Recipient().String())
|
||||||
cmd.Printf("Unlocker ID: %s\n", passphraseUnlocker.GetID())
|
cmd.Printf("Unlocker ID: %s\n", unlocker.GetID())
|
||||||
cmd.Println("\nYour secret manager is ready to use!")
|
cmd.Println("\nYour secret manager is ready to use!")
|
||||||
cmd.Println("Note: When using SB_SECRET_MNEMONIC environment variable,")
|
cmd.Println("Note: When using SB_SECRET_MNEMONIC environment variable,")
|
||||||
cmd.Println("unlockers are not required for secret operations.")
|
cmd.Println("unlockers are not required for secret operations.")
|
||||||
|
|||||||
@@ -2563,7 +2563,7 @@ func secretRmCommand(ctx context.Context, t *testing.T) (*exec.Cmd, string) {
|
|||||||
mnemonic := memguard.NewBufferFromBytes([]byte(testMnemonic))
|
mnemonic := memguard.NewBufferFromBytes([]byte(testMnemonic))
|
||||||
defer mnemonic.Destroy()
|
defer mnemonic.Destroy()
|
||||||
|
|
||||||
vlt, err := vault.CreateVault(afero.NewOsFs(), stateDir, "default", mnemonic)
|
vlt, err := vault.CreateVault(afero.NewOsFs(), stateDir, "default", mnemonic, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
value := memguard.NewBufferFromBytes([]byte("value"))
|
value := memguard.NewBufferFromBytes([]byte("value"))
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ func TestLeftoversRemovedByNextChangingCommand(t *testing.T) {
|
|||||||
|
|
||||||
fs := newTwoVaultFs(t)
|
fs := newTwoVaultFs(t)
|
||||||
|
|
||||||
_, err := vault.CreateVault(fs, testStateDir, ".tmp-0", nil)
|
_, err := vault.CreateVault(fs, testStateDir, ".tmp-0", nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, vault.SelectVault(fs, testStateDir, "default"))
|
require.NoError(t, vault.SelectVault(fs, testStateDir, "default"))
|
||||||
|
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ func TestConcurrentAddsKeepEveryVersion(t *testing.T) {
|
|||||||
{"real", afero.NewOsFs(), t.TempDir()},
|
{"real", afero.NewOsFs(), t.TempDir()},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
_, err := vault.CreateVault(tc.fs, tc.stateDir, "default", mnemonic)
|
_, err := vault.CreateVault(tc.fs, tc.stateDir, "default", mnemonic, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// One add creates the secret; the others find that it exists
|
// One add creates the secret; the others find that it exists
|
||||||
@@ -185,7 +185,7 @@ func (r *readNotifier) Read(p []byte) (int, error) {
|
|||||||
//nolint:paralleltest // times commands against the in-memory lock all tests share
|
//nolint:paralleltest // times commands against the in-memory lock all tests share
|
||||||
func TestEncryptPipedIntoAdd(t *testing.T) {
|
func TestEncryptPipedIntoAdd(t *testing.T) {
|
||||||
fs := afero.NewMemMapFs()
|
fs := afero.NewMemMapFs()
|
||||||
_, err := vault.CreateVault(fs, testStateDir, "default", testMnemonicBuffer(t))
|
_, err := vault.CreateVault(fs, testStateDir, "default", testMnemonicBuffer(t), nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, afero.WriteFile(fs, testInput, []byte("piped"), 0o600))
|
require.NoError(t, afero.WriteFile(fs, testInput, []byte("piped"), 0o600))
|
||||||
|
|
||||||
@@ -292,14 +292,14 @@ func setupEveryCommand(
|
|||||||
|
|
||||||
mnemonic := testMnemonicBuffer(t)
|
mnemonic := testMnemonicBuffer(t)
|
||||||
|
|
||||||
other, err := vault.CreateVault(fs, testStateDir, "other", mnemonic)
|
other, err := vault.CreateVault(fs, testStateDir, "other", mnemonic, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
otherDir, err := other.GetDirectory()
|
otherDir, err := other.GetDirectory()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, fs.Remove(filepath.Join(otherDir, "pub.age")))
|
require.NoError(t, fs.Remove(filepath.Join(otherDir, "pub.age")))
|
||||||
|
|
||||||
vlt, err := vault.CreateVault(fs, testStateDir, "work", mnemonic)
|
vlt, err := vault.CreateVault(fs, testStateDir, "work", mnemonic, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
addTestSecret(t, vlt, []byte("older"), false)
|
addTestSecret(t, vlt, []byte("older"), false)
|
||||||
@@ -487,7 +487,7 @@ func TestEncryptWithExistingKeyTakesNoLock(t *testing.T) {
|
|||||||
mnemonic := testMnemonicBuffer(t)
|
mnemonic := testMnemonicBuffer(t)
|
||||||
|
|
||||||
fs := afero.NewMemMapFs()
|
fs := afero.NewMemMapFs()
|
||||||
_, err := vault.CreateVault(fs, testStateDir, "default", mnemonic)
|
_, err := vault.CreateVault(fs, testStateDir, "default", mnemonic, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, afero.WriteFile(fs, testInput, []byte("input"), 0o600))
|
require.NoError(t, afero.WriteFile(fs, testInput, []byte("input"), 0o600))
|
||||||
|
|
||||||
@@ -525,7 +525,7 @@ func TestEncryptWithExistingKeyTakesNoLock(t *testing.T) {
|
|||||||
//nolint:paralleltest // times commands against the in-memory lock all tests share
|
//nolint:paralleltest // times commands against the in-memory lock all tests share
|
||||||
func TestEncryptStreamsUnlocked(t *testing.T) {
|
func TestEncryptStreamsUnlocked(t *testing.T) {
|
||||||
fs := afero.NewMemMapFs()
|
fs := afero.NewMemMapFs()
|
||||||
_, err := vault.CreateVault(fs, testStateDir, "default", testMnemonicBuffer(t))
|
_, err := vault.CreateVault(fs, testStateDir, "default", testMnemonicBuffer(t), nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, afero.WriteFile(fs, testInput, []byte("streamed"), 0o600))
|
require.NoError(t, afero.WriteFile(fs, testInput, []byte("streamed"), 0o600))
|
||||||
|
|
||||||
|
|||||||
@@ -156,10 +156,10 @@ func TestMoveOntoSameSecretUnderAnotherNameIsRejected(t *testing.T) {
|
|||||||
vaultsDir := filepath.Join(stateDir, "vaults.d")
|
vaultsDir := filepath.Join(stateDir, "vaults.d")
|
||||||
|
|
||||||
// "default" is created last, so it is the current vault.
|
// "default" is created last, so it is the current vault.
|
||||||
_, err := vault.CreateVault(fs, stateDir, "other", testMnemonicBuffer(t))
|
_, err := vault.CreateVault(fs, stateDir, "other", testMnemonicBuffer(t), nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
vlt, err := vault.CreateVault(fs, stateDir, "default", testMnemonicBuffer(t))
|
vlt, err := vault.CreateVault(fs, stateDir, "default", testMnemonicBuffer(t), nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = vlt.AddSecret("x", memguard.NewBufferFromBytes([]byte("value")), false)
|
err = vlt.AddSecret("x", memguard.NewBufferFromBytes([]byte("value")), false)
|
||||||
@@ -205,7 +205,7 @@ func TestForcedCaseOnlyMoveOnCaseSensitiveFilesystem(t *testing.T) {
|
|||||||
fs := afero.NewOsFs()
|
fs := afero.NewOsFs()
|
||||||
stateDir := t.TempDir()
|
stateDir := t.TempDir()
|
||||||
|
|
||||||
vlt, err := vault.CreateVault(fs, stateDir, "default", testMnemonicBuffer(t))
|
vlt, err := vault.CreateVault(fs, stateDir, "default", testMnemonicBuffer(t), nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = vlt.AddSecret("Foo", memguard.NewBufferFromBytes([]byte("upper")), false)
|
err = vlt.AddSecret("Foo", memguard.NewBufferFromBytes([]byte("upper")), false)
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ func newTwoVaultFs(t *testing.T) afero.Fs {
|
|||||||
mnemonic := testMnemonicBuffer(t)
|
mnemonic := testMnemonicBuffer(t)
|
||||||
|
|
||||||
for _, name := range []string{"work", "default"} {
|
for _, name := range []string{"work", "default"} {
|
||||||
vlt, err := vault.CreateVault(fs, testStateDir, name, mnemonic)
|
vlt, err := vault.CreateVault(fs, testStateDir, name, mnemonic, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = vlt.AddSecret("x", memguard.NewBufferFromBytes([]byte("value")), false)
|
err = vlt.AddSecret("x", memguard.NewBufferFromBytes([]byte("value")), false)
|
||||||
|
|||||||
@@ -71,7 +71,8 @@ func newSizeTestVault(t *testing.T) (afero.Fs, *vault.Vault) {
|
|||||||
fs := afero.NewMemMapFs()
|
fs := afero.NewMemMapFs()
|
||||||
|
|
||||||
// Create vault
|
// Create vault
|
||||||
_, err := vault.CreateVault(fs, testStateDir, testVaultName, testMnemonicBuffer(t))
|
_, err := vault.CreateVault(fs, testStateDir, testVaultName,
|
||||||
|
testMnemonicBuffer(t), nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Set current vault
|
// Set current vault
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ func TestAddPGPUnlocker(t *testing.T) {
|
|||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
fs := afero.NewMemMapFs()
|
fs := afero.NewMemMapFs()
|
||||||
vlt, err := vault.CreateVault(fs, listTestStateDir, listTestVaultName,
|
vlt, err := vault.CreateVault(fs, listTestStateDir, listTestVaultName,
|
||||||
testMnemonicBuffer(t))
|
testMnemonicBuffer(t), nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = vlt.AddSecret(addTestSecretName,
|
err = vlt.AddSecret(addTestSecretName,
|
||||||
|
|||||||
+8
-22
@@ -293,40 +293,26 @@ func (cli *Instance) CreateVault(cmd *cobra.Command, name string) error {
|
|||||||
}
|
}
|
||||||
defer cleanupPassphrase()
|
defer cleanupPassphrase()
|
||||||
|
|
||||||
// Create the vault - it will handle key derivation internally
|
// Create the vault with its passphrase unlocker
|
||||||
vlt, err := vault.CreateVault(cli.fs, cli.stateDir, name, mnemonic)
|
vlt, err := vault.CreateVault(cli.fs, cli.stateDir, name,
|
||||||
|
mnemonic, passphraseBuffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the vault metadata to retrieve the derivation index
|
ltIdentity, err := vlt.GetOrDeriveLongTermKey()
|
||||||
vaultDir := filepath.Join(cli.stateDir, "vaults.d", name)
|
|
||||||
|
|
||||||
metadata, err := vault.LoadVaultMetadata(cli.fs, vaultDir)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to load vault metadata: %w", err)
|
return fmt.Errorf("failed to get long-term key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Derive the long-term key using the same index that CreateVault used
|
unlocker, err := vlt.GetCurrentUnlocker()
|
||||||
ltIdentity, err := agehd.DeriveIdentity(mnemonicStr, metadata.DerivationIndex)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to derive long-term key from mnemonic: %w", err)
|
return err
|
||||||
}
|
|
||||||
|
|
||||||
// Unlock the vault with the derived long-term key
|
|
||||||
vlt.Unlock(ltIdentity)
|
|
||||||
|
|
||||||
// Create passphrase-protected unlocker
|
|
||||||
secret.Debug("Creating passphrase-protected unlocker")
|
|
||||||
|
|
||||||
passphraseUnlocker, err := vlt.CreatePassphraseUnlocker(passphraseBuffer)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create unlocker: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Printf("Created vault '%s'\n", vlt.GetName())
|
cmd.Printf("Created vault '%s'\n", vlt.GetName())
|
||||||
cmd.Printf("Long-term public key: %s\n", ltIdentity.Recipient().String())
|
cmd.Printf("Long-term public key: %s\n", ltIdentity.Recipient().String())
|
||||||
cmd.Printf("Unlocker ID: %s\n", passphraseUnlocker.GetID())
|
cmd.Printf("Unlocker ID: %s\n", unlocker.GetID())
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,8 @@ func setupTestVault(t *testing.T, fs afero.Fs) {
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
// Create vault
|
// Create vault
|
||||||
vlt, err := vault.CreateVault(fs, testStateDir, "default", testMnemonicBuffer(t))
|
vlt, err := vault.CreateVault(fs, testStateDir, "default",
|
||||||
|
testMnemonicBuffer(t), nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Derive and store long-term key from mnemonic
|
// Derive and store long-term key from mnemonic
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ func newVaultWithSecret(
|
|||||||
) *vault.Vault {
|
) *vault.Vault {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
vlt, err := vault.CreateVault(fs, stateDir, name, testMnemonicBuffer(t))
|
vlt, err := vault.CreateVault(fs, stateDir, name, testMnemonicBuffer(t), nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
buffer := memguard.NewBufferFromBytes([]byte(value))
|
buffer := memguard.NewBufferFromBytes([]byte(value))
|
||||||
@@ -353,7 +353,7 @@ func TestLongestNames(t *testing.T) {
|
|||||||
fs := afero.NewOsFs()
|
fs := afero.NewOsFs()
|
||||||
name := strings.Repeat("a", longestName)
|
name := strings.Repeat("a", longestName)
|
||||||
|
|
||||||
vlt, err := vault.CreateVault(fs, t.TempDir(), name, testMnemonicBuffer(t))
|
vlt, err := vault.CreateVault(fs, t.TempDir(), name, testMnemonicBuffer(t), nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
value := memguard.NewBufferFromBytes([]byte("long"))
|
value := memguard.NewBufferFromBytes([]byte("long"))
|
||||||
@@ -647,7 +647,7 @@ func TestPassphraseUnlockerGetsKeyFirst(t *testing.T) {
|
|||||||
|
|
||||||
// No mnemonic, and no current unlocker to get the key from
|
// No mnemonic, and no current unlocker to get the key from
|
||||||
base := afero.NewMemMapFs()
|
base := afero.NewMemMapFs()
|
||||||
_, err := vault.CreateVault(base, testVaultStateDir, testVaultName, nil)
|
_, err := vault.CreateVault(base, testVaultStateDir, testVaultName, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
fs := hookFs{Fs: base, before: func(_, path string) error {
|
fs := hookFs{Fs: base, before: func(_, path string) error {
|
||||||
@@ -679,7 +679,7 @@ func TestPassphraseUnlockerIsWholeOrAbsent(t *testing.T) {
|
|||||||
|
|
||||||
base, stateDir := tfs.open(t)
|
base, stateDir := tfs.open(t)
|
||||||
vlt, err := vault.CreateVault(base, stateDir, testVaultName,
|
vlt, err := vault.CreateVault(base, stateDir, testVaultName,
|
||||||
testMnemonicBuffer(t))
|
testMnemonicBuffer(t), nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
vaultDir, err := vlt.GetDirectory()
|
vaultDir, err := vlt.GetDirectory()
|
||||||
@@ -728,7 +728,7 @@ func TestPassphraseUnlockerReplacementKeepsVaultOpen(t *testing.T) {
|
|||||||
|
|
||||||
base, stateDir := tfs.open(t)
|
base, stateDir := tfs.open(t)
|
||||||
vlt, err := vault.CreateVault(base, stateDir, testVaultName,
|
vlt, err := vault.CreateVault(base, stateDir, testVaultName,
|
||||||
testMnemonicBuffer(t))
|
testMnemonicBuffer(t), nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ltIdentity, err := vlt.GetOrDeriveLongTermKey()
|
ltIdentity, err := vlt.GetOrDeriveLongTermKey()
|
||||||
@@ -911,7 +911,7 @@ func TestSecureEnclaveUnlockerFailureDeletesKey(t *testing.T) {
|
|||||||
|
|
||||||
mnemonic := testMnemonicBuffer(t)
|
mnemonic := testMnemonicBuffer(t)
|
||||||
base := afero.NewMemMapFs()
|
base := afero.NewMemMapFs()
|
||||||
_, err := vault.CreateVault(base, testVaultStateDir, testVaultName, mnemonic)
|
_, err := vault.CreateVault(base, testVaultStateDir, testVaultName, mnemonic, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// The unlocker's directory is named se-<label of its Secure Enclave key>
|
// The unlocker's directory is named se-<label of its Secure Enclave key>
|
||||||
|
|||||||
@@ -330,7 +330,7 @@ func testCreatePGPUnlocker(
|
|||||||
mnemonic := testMnemonicBuffer(t)
|
mnemonic := testMnemonicBuffer(t)
|
||||||
|
|
||||||
// Create a test vault directory structure
|
// Create a test vault directory structure
|
||||||
vlt, err := vault.CreateVault(fs, stateDir, vaultName, mnemonic)
|
vlt, err := vault.CreateVault(fs, stateDir, vaultName, mnemonic, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create vault: %v", err)
|
t.Fatalf("Failed to create vault: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ func TestCreatePGPUnlockerFailureWritesNothing(t *testing.T) {
|
|||||||
installFakeGPG(t)
|
installFakeGPG(t)
|
||||||
|
|
||||||
base := afero.NewMemMapFs()
|
base := afero.NewMemMapFs()
|
||||||
vlt, err := vault.CreateVault(base, testVaultStateDir, testVaultName, nil)
|
vlt, err := vault.CreateVault(base, testVaultStateDir, testVaultName, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
fs := hookFs{Fs: base, before: func(_, path string) error {
|
fs := hookFs{Fs: base, before: func(_, path string) error {
|
||||||
@@ -87,7 +87,7 @@ func TestPGPUnlockerAddedTwiceKeepsFirst(t *testing.T) {
|
|||||||
|
|
||||||
fs := afero.NewMemMapFs()
|
fs := afero.NewMemMapFs()
|
||||||
mnemonic := testMnemonicBuffer(t)
|
mnemonic := testMnemonicBuffer(t)
|
||||||
_, err := vault.CreateVault(fs, testVaultStateDir, testVaultName, mnemonic)
|
_, err := vault.CreateVault(fs, testVaultStateDir, testVaultName, mnemonic, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
first, err := secret.CreatePGPUnlocker(
|
first, err := secret.CreatePGPUnlocker(
|
||||||
|
|||||||
@@ -31,6 +31,11 @@ var (
|
|||||||
// Composed as "vault <name> already exists".
|
// Composed as "vault <name> already exists".
|
||||||
ErrVaultExists = errors.New("already exists")
|
ErrVaultExists = errors.New("already exists")
|
||||||
|
|
||||||
|
// ErrUnlockerWithoutMnemonic indicates that CreateVault was given a
|
||||||
|
// passphrase for an unlocker but no mnemonic to derive the long-term key
|
||||||
|
// it unlocks. Composed as "vault <name> needs a mnemonic for an unlocker".
|
||||||
|
ErrUnlockerWithoutMnemonic = errors.New("needs a mnemonic for an unlocker")
|
||||||
|
|
||||||
// ErrNilValueBuffer indicates a nil value buffer was supplied.
|
// ErrNilValueBuffer indicates a nil value buffer was supplied.
|
||||||
ErrNilValueBuffer = errors.New("value buffer is nil")
|
ErrNilValueBuffer = errors.New("value buffer is nil")
|
||||||
|
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ func testCurrentVaultFileHandling(t *testing.T, fs afero.Fs, tempDir string) {
|
|||||||
|
|
||||||
// Create a test vault
|
// Create a test vault
|
||||||
vlt, err := vault.CreateVault(fs, stateDir, testVaultName,
|
vlt, err := vault.CreateVault(fs, stateDir, testVaultName,
|
||||||
testMnemonicBuffer(t))
|
testMnemonicBuffer(t), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create vault: %v", err)
|
t.Fatalf("Failed to create vault: %v", err)
|
||||||
}
|
}
|
||||||
@@ -147,7 +147,7 @@ func testDeepPathSecrets(t *testing.T, fs afero.Fs, tempDir string) {
|
|||||||
// Create a test vault - CreateVault writes the public key derived from
|
// Create a test vault - CreateVault writes the public key derived from
|
||||||
// the mnemonic
|
// the mnemonic
|
||||||
vlt, err := vault.CreateVault(fs, stateDir, testVaultName,
|
vlt, err := vault.CreateVault(fs, stateDir, testVaultName,
|
||||||
testMnemonicBuffer(t))
|
testMnemonicBuffer(t), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create vault: %v", err)
|
t.Fatalf("Failed to create vault: %v", err)
|
||||||
}
|
}
|
||||||
@@ -223,7 +223,7 @@ func testKeyCaching(t *testing.T, fs afero.Fs, tempDir string) {
|
|||||||
// Create a test vault - CreateVault writes the public key derived from
|
// Create a test vault - CreateVault writes the public key derived from
|
||||||
// the mnemonic
|
// the mnemonic
|
||||||
vlt, err := vault.CreateVault(fs, stateDir, testVaultName,
|
vlt, err := vault.CreateVault(fs, stateDir, testVaultName,
|
||||||
testMnemonicBuffer(t))
|
testMnemonicBuffer(t), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create vault: %v", err)
|
t.Fatalf("Failed to create vault: %v", err)
|
||||||
}
|
}
|
||||||
@@ -324,7 +324,7 @@ func testVaultNameValidation(t *testing.T, fs afero.Fs, tempDir string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, name := range validNames {
|
for _, name := range validNames {
|
||||||
_, err := vault.CreateVault(fs, stateDir, name, testMnemonicBuffer(t))
|
_, err := vault.CreateVault(fs, stateDir, name, testMnemonicBuffer(t), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Failed to create vault with valid name %q: %v", name, err)
|
t.Errorf("Failed to create vault with valid name %q: %v", name, err)
|
||||||
}
|
}
|
||||||
@@ -340,7 +340,7 @@ func testVaultNameValidation(t *testing.T, fs afero.Fs, tempDir string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, name := range invalidNames {
|
for _, name := range invalidNames {
|
||||||
_, err := vault.CreateVault(fs, stateDir, name, testMnemonicBuffer(t))
|
_, err := vault.CreateVault(fs, stateDir, name, testMnemonicBuffer(t), nil)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("Expected error creating vault with invalid name %q, "+
|
t.Errorf("Expected error creating vault with invalid name %q, "+
|
||||||
"but got none", name)
|
"but got none", name)
|
||||||
@@ -361,7 +361,7 @@ func testMultipleVaults(t *testing.T, fs afero.Fs, tempDir string) {
|
|||||||
// Create three vaults
|
// Create three vaults
|
||||||
vaultNames := []string{"vault1", "vault2", "vault3"}
|
vaultNames := []string{"vault1", "vault2", "vault3"}
|
||||||
for _, name := range vaultNames {
|
for _, name := range vaultNames {
|
||||||
_, err := vault.CreateVault(fs, stateDir, name, testMnemonicBuffer(t))
|
_, err := vault.CreateVault(fs, stateDir, name, testMnemonicBuffer(t), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create vault %s: %v", name, err)
|
t.Fatalf("Failed to create vault %s: %v", name, err)
|
||||||
}
|
}
|
||||||
@@ -411,12 +411,12 @@ func testVaultIsolation(t *testing.T, fs afero.Fs, tempDir string) {
|
|||||||
|
|
||||||
// Create two vaults - CreateVault writes the public key derived from
|
// Create two vaults - CreateVault writes the public key derived from
|
||||||
// the mnemonic
|
// the mnemonic
|
||||||
vault1, err := vault.CreateVault(fs, stateDir, "vault1", testMnemonicBuffer(t))
|
vault1, err := vault.CreateVault(fs, stateDir, "vault1", testMnemonicBuffer(t), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create vault1: %v", err)
|
t.Fatalf("Failed to create vault1: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
vault2, err := vault.CreateVault(fs, stateDir, "vault2", testMnemonicBuffer(t))
|
vault2, err := vault.CreateVault(fs, stateDir, "vault2", testMnemonicBuffer(t), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create vault2: %v", err)
|
t.Fatalf("Failed to create vault2: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ func TestVersionIntegrationWorkflow(t *testing.T) {
|
|||||||
fs := afero.NewMemMapFs()
|
fs := afero.NewMemMapFs()
|
||||||
|
|
||||||
// Create vault without a long-term key, which is set up below
|
// Create vault without a long-term key, which is set up below
|
||||||
vault, err := CreateVault(fs, testStateDir, "test", nil)
|
vault, err := CreateVault(fs, testStateDir, "test", nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Derive and store long-term key from mnemonic
|
// Derive and store long-term key from mnemonic
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"filippo.io/age"
|
||||||
"git.eeqj.de/sneak/secret/internal/secret"
|
"git.eeqj.de/sneak/secret/internal/secret"
|
||||||
"git.eeqj.de/sneak/secret/pkg/agehd"
|
"git.eeqj.de/sneak/secret/pkg/agehd"
|
||||||
"github.com/awnumar/memguard"
|
"github.com/awnumar/memguard"
|
||||||
@@ -152,16 +153,17 @@ func ListVaults(fs afero.Fs, stateDir string) ([]string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// processMnemonicForVault handles mnemonic processing for vault creation.
|
// processMnemonicForVault handles mnemonic processing for vault creation.
|
||||||
// It returns the derivation index, public key hash, and family hash.
|
// It returns the long-term key, nil when there is no mnemonic, and the
|
||||||
|
// derivation index, public key hash, and family hash.
|
||||||
func processMnemonicForVault(
|
func processMnemonicForVault(
|
||||||
fs afero.Fs, stateDir, vaultDir, vaultName string,
|
fs afero.Fs, stateDir, vaultDir, vaultName string,
|
||||||
mnemonicBuffer *memguard.LockedBuffer,
|
mnemonicBuffer *memguard.LockedBuffer,
|
||||||
) (uint32, string, string, error) {
|
) (*age.X25519Identity, uint32, string, string, error) {
|
||||||
if mnemonicBuffer == nil {
|
if mnemonicBuffer == nil {
|
||||||
secret.Debug("No mnemonic given, vault created without long-term key",
|
secret.Debug("No mnemonic given, vault created without long-term key",
|
||||||
"vault", vaultName)
|
"vault", vaultName)
|
||||||
// Use 0 for derivation index when no mnemonic is provided
|
// Use 0 for derivation index when no mnemonic is provided
|
||||||
return 0, "", "", nil
|
return nil, 0, "", "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
mnemonic := mnemonicBuffer.String()
|
mnemonic := mnemonicBuffer.String()
|
||||||
@@ -171,13 +173,14 @@ func processMnemonicForVault(
|
|||||||
// Get the next available derivation index for this mnemonic
|
// Get the next available derivation index for this mnemonic
|
||||||
derivationIndex, err := GetNextDerivationIndex(fs, stateDir, mnemonic)
|
derivationIndex, err := GetNextDerivationIndex(fs, stateDir, mnemonic)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, "", "", fmt.Errorf("failed to get next derivation index: %w", err)
|
return nil, 0, "", "",
|
||||||
|
fmt.Errorf("failed to get next derivation index: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Derive the long-term key using the actual derivation index
|
// Derive the long-term key using the actual derivation index
|
||||||
ltIdentity, err := agehd.DeriveIdentity(mnemonic, derivationIndex)
|
ltIdentity, err := agehd.DeriveIdentity(mnemonic, derivationIndex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, "", "", fmt.Errorf("failed to derive long-term key: %w", err)
|
return nil, 0, "", "", fmt.Errorf("failed to derive long-term key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the public key
|
// Write the public key
|
||||||
@@ -187,7 +190,8 @@ func processMnemonicForVault(
|
|||||||
|
|
||||||
err = secret.WriteFileAtomic(fs, ltPubKeyPath, []byte(ltPubKey))
|
err = secret.WriteFileAtomic(fs, ltPubKeyPath, []byte(ltPubKey))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, "", "", fmt.Errorf("failed to write long-term public key: %w", err)
|
return nil, 0, "", "",
|
||||||
|
fmt.Errorf("failed to write long-term public key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
secret.Debug("Wrote long-term public key", "path", ltPubKeyPath)
|
secret.Debug("Wrote long-term public key", "path", ltPubKeyPath)
|
||||||
@@ -199,24 +203,33 @@ func processMnemonicForVault(
|
|||||||
// This is used to identify which vaults belong to the same mnemonic family
|
// This is used to identify which vaults belong to the same mnemonic family
|
||||||
identity0, err := agehd.DeriveIdentity(mnemonic, 0)
|
identity0, err := agehd.DeriveIdentity(mnemonic, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, "", "", fmt.Errorf("failed to derive identity for index 0: %w", err)
|
return nil, 0, "", "",
|
||||||
|
fmt.Errorf("failed to derive identity for index 0: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
familyHash := ComputeDoubleSHA256([]byte(identity0.Recipient().String()))
|
familyHash := ComputeDoubleSHA256([]byte(identity0.Recipient().String()))
|
||||||
|
|
||||||
return derivationIndex, publicKeyHash, familyHash, nil
|
return ltIdentity, derivationIndex, publicKeyHash, familyHash, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateVault creates a new vault and selects it as the current vault. When
|
// CreateVault creates a new vault and selects it as the current vault. When
|
||||||
// mnemonic is not nil, the vault's long-term key is derived from it, and the
|
// mnemonic is not nil, the vault's long-term key is derived from it, and the
|
||||||
// returned vault has it as its Mnemonic; when it is nil, the vault has no
|
// returned vault has it as its Mnemonic; when it is nil, the vault has no
|
||||||
// long-term key until one is imported. It refuses a vault that already
|
// long-term key until one is imported. When passphrase is not nil, the vault
|
||||||
// exists before writing anything: creating it again would replace its keys,
|
// gets a passphrase unlocker protected by it, as its current unlocker; that
|
||||||
// and its secrets could no longer be decrypted. The commands that call it
|
// needs a mnemonic. It refuses a vault that already exists before writing
|
||||||
// hold the state directory lock, so no other command can create the vault
|
// anything: creating it again would replace its keys, and its secrets could
|
||||||
// between the check and the writes.
|
// no longer be decrypted. The commands that call it hold the state directory
|
||||||
|
// lock, so no other command can create the vault between the check and the
|
||||||
|
// writes.
|
||||||
|
//
|
||||||
|
// The vault is written whole into a temporary directory, which is renamed
|
||||||
|
// into vaults.d only once complete, and only then selected: a crash at any
|
||||||
|
// point leaves either no vault or a complete one. The next command that
|
||||||
|
// takes the lock deletes what the crash left under a temporary name.
|
||||||
func CreateVault(
|
func CreateVault(
|
||||||
fs afero.Fs, stateDir string, name string, mnemonic *memguard.LockedBuffer,
|
fs afero.Fs, stateDir string, name string,
|
||||||
|
mnemonic, passphrase *memguard.LockedBuffer,
|
||||||
) (*Vault, error) {
|
) (*Vault, error) {
|
||||||
secret.Debug("Creating new vault", "name", name, "state_dir", stateDir)
|
secret.Debug("Creating new vault", "name", name, "state_dir", stateDir)
|
||||||
|
|
||||||
@@ -240,51 +253,19 @@ func CreateVault(
|
|||||||
return nil, fmt.Errorf("vault %s %w", name, ErrVaultExists)
|
return nil, fmt.Errorf("vault %s %w", name, ErrVaultExists)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create vault directory structure
|
if passphrase != nil && mnemonic == nil {
|
||||||
|
return nil, fmt.Errorf("vault %s %w", name, ErrUnlockerWithoutMnemonic)
|
||||||
|
}
|
||||||
|
|
||||||
secret.Debug("Creating vault directory structure", "vault_dir", vaultDir)
|
secret.Debug("Creating vault directory structure", "vault_dir", vaultDir)
|
||||||
|
|
||||||
// Create main vault directory
|
err = secret.WriteDir(fs, vaultDir, func(dir string) error {
|
||||||
err = fs.MkdirAll(vaultDir, secret.DirPerms)
|
return writeVaultFiles(fs, stateDir, dir, name, mnemonic, passphrase)
|
||||||
if err != nil {
|
})
|
||||||
return nil, fmt.Errorf("failed to create vault directory: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create secrets directory
|
|
||||||
secretsDir := filepath.Join(vaultDir, "secrets.d")
|
|
||||||
|
|
||||||
err = fs.MkdirAll(secretsDir, secret.DirPerms)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create secrets directory: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create unlockers directory
|
|
||||||
unlockersDir := filepath.Join(vaultDir, "unlockers.d")
|
|
||||||
|
|
||||||
err = fs.MkdirAll(unlockersDir, secret.DirPerms)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create unlockers directory: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process mnemonic if available
|
|
||||||
derivationIndex, publicKeyHash, familyHash, err := processMnemonicForVault(
|
|
||||||
fs, stateDir, vaultDir, name, mnemonic)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save vault metadata
|
|
||||||
metadata := &Metadata{
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
DerivationIndex: derivationIndex,
|
|
||||||
PublicKeyHash: publicKeyHash,
|
|
||||||
MnemonicFamilyHash: familyHash,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = SaveVaultMetadata(fs, vaultDir, metadata)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to save vault metadata: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Select the newly created vault as current
|
// Select the newly created vault as current
|
||||||
secret.Debug("Selecting newly created vault as current", "name", name)
|
secret.Debug("Selecting newly created vault as current", "name", name)
|
||||||
|
|
||||||
@@ -302,6 +283,47 @@ func CreateVault(
|
|||||||
return vlt, nil
|
return vlt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// writeVaultFiles writes the files of the new vault name into vaultDir: its
|
||||||
|
// secrets and unlockers directories, its long-term public key and metadata,
|
||||||
|
// and, when passphrase is not nil, a passphrase unlocker as its current one.
|
||||||
|
func writeVaultFiles(
|
||||||
|
fs afero.Fs, stateDir, vaultDir, name string,
|
||||||
|
mnemonic, passphrase *memguard.LockedBuffer,
|
||||||
|
) error {
|
||||||
|
for _, subdir := range []string{"secrets.d", "unlockers.d"} {
|
||||||
|
err := fs.MkdirAll(filepath.Join(vaultDir, subdir), secret.DirPerms)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create %s directory: %w", subdir, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ltIdentity, derivationIndex, publicKeyHash, familyHash, err :=
|
||||||
|
processMnemonicForVault(fs, stateDir, vaultDir, name, mnemonic)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata := &Metadata{
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
DerivationIndex: derivationIndex,
|
||||||
|
PublicKeyHash: publicKeyHash,
|
||||||
|
MnemonicFamilyHash: familyHash,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = SaveVaultMetadata(fs, vaultDir, metadata)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to save vault metadata: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if passphrase == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = writePassphraseUnlocker(fs, vaultDir, ltIdentity, passphrase)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// SelectVault selects the given vault as the current vault
|
// SelectVault selects the given vault as the current vault
|
||||||
func SelectVault(fs afero.Fs, stateDir string, name string) error {
|
func SelectVault(fs afero.Fs, stateDir string, name string) error {
|
||||||
secret.Debug("Selecting vault", "vault_name", name, "state_dir", stateDir)
|
secret.Debug("Selecting vault", "vault_name", name, "state_dir", stateDir)
|
||||||
|
|||||||
@@ -304,7 +304,7 @@ func TestWorkflowMismatch(t *testing.T) {
|
|||||||
fs := afero.NewOsFs()
|
fs := afero.NewOsFs()
|
||||||
|
|
||||||
// Test Case 1: Create vault WITH mnemonic (like init command)
|
// Test Case 1: Create vault WITH mnemonic (like init command)
|
||||||
_, err := vault.CreateVault(fs, tempDir, "default", testMnemonicBuffer(t))
|
_, err := vault.CreateVault(fs, tempDir, "default", testMnemonicBuffer(t), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create vault with mnemonic: %v", err)
|
t.Fatalf("Failed to create vault with mnemonic: %v", err)
|
||||||
}
|
}
|
||||||
@@ -321,7 +321,7 @@ func TestWorkflowMismatch(t *testing.T) {
|
|||||||
metadata1.DerivationIndex, metadata1.PublicKeyHash)
|
metadata1.DerivationIndex, metadata1.PublicKeyHash)
|
||||||
|
|
||||||
// Test Case 2: Create vault WITHOUT mnemonic, then import (work vault)
|
// Test Case 2: Create vault WITHOUT mnemonic, then import (work vault)
|
||||||
_, err = vault.CreateVault(fs, tempDir, "work", nil)
|
_, err = vault.CreateVault(fs, tempDir, "work", nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create vault without mnemonic: %v", err)
|
t.Fatalf("Failed to create vault without mnemonic: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ func TestGetSecretVersionRejectsPathTraversal(t *testing.T) {
|
|||||||
fs := afero.NewMemMapFs()
|
fs := afero.NewMemMapFs()
|
||||||
|
|
||||||
vlt, err := vault.CreateVault(fs, testStateDir, testVaultName,
|
vlt, err := vault.CreateVault(fs, testStateDir, testVaultName,
|
||||||
testMnemonicBuffer(t))
|
testMnemonicBuffer(t), nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Add a legitimate secret so the vault is set up
|
// Add a legitimate secret so the vault is set up
|
||||||
@@ -57,7 +57,7 @@ func TestGetSecretRejectsPathTraversal(t *testing.T) {
|
|||||||
fs := afero.NewMemMapFs()
|
fs := afero.NewMemMapFs()
|
||||||
|
|
||||||
vlt, err := vault.CreateVault(fs, testStateDir, testVaultName,
|
vlt, err := vault.CreateVault(fs, testStateDir, testVaultName,
|
||||||
testMnemonicBuffer(t))
|
testMnemonicBuffer(t), nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
_, err = vlt.GetSecret("../../../etc/passwd")
|
_, err = vlt.GetSecret("../../../etc/passwd")
|
||||||
@@ -73,7 +73,7 @@ func TestGetSecretObjectRejectsPathTraversal(t *testing.T) {
|
|||||||
fs := afero.NewMemMapFs()
|
fs := afero.NewMemMapFs()
|
||||||
|
|
||||||
vlt, err := vault.CreateVault(fs, testStateDir, testVaultName,
|
vlt, err := vault.CreateVault(fs, testStateDir, testVaultName,
|
||||||
testMnemonicBuffer(t))
|
testMnemonicBuffer(t), nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
maliciousNames := []string{
|
maliciousNames := []string{
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ func createTestVaultWithKey(t *testing.T, fs afero.Fs) *Vault {
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
// Create vault without a long-term key, which is set up below
|
// Create vault without a long-term key, which is set up below
|
||||||
vault, err := CreateVault(fs, testStateDir, "test", nil)
|
vault, err := CreateVault(fs, testStateDir, "test", nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Derive and store long-term key from mnemonic
|
// Derive and store long-term key from mnemonic
|
||||||
|
|||||||
+34
-18
@@ -390,8 +390,31 @@ func (v *Vault) CreatePassphraseUnlocker(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unlocker, err := writePassphraseUnlocker(v.fs, vaultDir, ltIdentity, passphrase)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, oldDir := range oldDirs {
|
||||||
|
err = secret.RemoveDirAtomic(v.fs, oldDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"created and selected the new passphrase unlocker: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return unlocker, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// writePassphraseUnlocker writes a new passphrase unlocker of the long-term
|
||||||
|
// key ltIdentity into the vault directory vaultDir, in a directory of its own,
|
||||||
|
// and makes it the vault's current unlocker.
|
||||||
|
func writePassphraseUnlocker(
|
||||||
|
fs afero.Fs, vaultDir string, ltIdentity *age.X25519Identity,
|
||||||
|
passphrase *memguard.LockedBuffer,
|
||||||
|
) (*secret.PassphraseUnlocker, error) {
|
||||||
createdAt := time.Now()
|
createdAt := time.Now()
|
||||||
unlockerDir := filepath.Join(unlockersDir, unlockerTypePassphrase+"-"+
|
unlockerDir := filepath.Join(vaultDir, "unlockers.d", unlockerTypePassphrase+"-"+
|
||||||
createdAt.UTC().Format(secret.UnlockerTimeFormat))
|
createdAt.UTC().Format(secret.UnlockerTimeFormat))
|
||||||
|
|
||||||
// Generate new age keypair for unlocker
|
// Generate new age keypair for unlocker
|
||||||
@@ -422,8 +445,8 @@ func (v *Vault) CreatePassphraseUnlocker(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Write the unlocker's files, the metadata last
|
// Write the unlocker's files, the metadata last
|
||||||
err = secret.WriteDir(v.fs, unlockerDir, func(dir string) error {
|
err = secret.WriteDir(fs, unlockerDir, func(dir string) error {
|
||||||
return v.writeUnlockerFiles(dir, unlockerIdentity, passphrase,
|
return writeUnlockerFiles(fs, dir, unlockerIdentity, passphrase,
|
||||||
encryptedLtPrivKey, metadataBytes)
|
encryptedLtPrivKey, metadataBytes)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -434,21 +457,13 @@ func (v *Vault) CreatePassphraseUnlocker(
|
|||||||
// passphrase unlocker created in the same minute has the same ID.
|
// passphrase unlocker created in the same minute has the same ID.
|
||||||
currentUnlockerPath := filepath.Join(vaultDir, "current-unlocker")
|
currentUnlockerPath := filepath.Join(vaultDir, "current-unlocker")
|
||||||
|
|
||||||
err = secret.WriteFileAtomic(v.fs, currentUnlockerPath,
|
err = secret.WriteFileAtomic(fs, currentUnlockerPath,
|
||||||
[]byte(filepath.Base(unlockerDir)))
|
[]byte(filepath.Base(unlockerDir)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to select new unlocker: %w", err)
|
return nil, fmt.Errorf("failed to select new unlocker: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, oldDir := range oldDirs {
|
return secret.NewPassphraseUnlocker(fs, unlockerDir, metadata), nil
|
||||||
err = secret.RemoveDirAtomic(v.fs, oldDir)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
"created and selected the new passphrase unlocker: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return secret.NewPassphraseUnlocker(v.fs, unlockerDir, metadata), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// passphraseUnlockerDirs returns the directories in unlockersDir that hold
|
// passphraseUnlockerDirs returns the directories in unlockersDir that hold
|
||||||
@@ -514,7 +529,8 @@ func (v *Vault) readUnlockerMetadata(unlockerDir string) (UnlockerMetadata, erro
|
|||||||
// writeUnlockerFiles writes the files of a passphrase unlocker into
|
// writeUnlockerFiles writes the files of a passphrase unlocker into
|
||||||
// unlockerDir: its public key, its passphrase-encrypted private key, the
|
// unlockerDir: its public key, its passphrase-encrypted private key, the
|
||||||
// long-term private key encrypted to it, and its metadata, last.
|
// long-term private key encrypted to it, and its metadata, last.
|
||||||
func (v *Vault) writeUnlockerFiles(
|
func writeUnlockerFiles(
|
||||||
|
fs afero.Fs,
|
||||||
unlockerDir string,
|
unlockerDir string,
|
||||||
unlockerIdentity *age.X25519Identity,
|
unlockerIdentity *age.X25519Identity,
|
||||||
passphrase *memguard.LockedBuffer,
|
passphrase *memguard.LockedBuffer,
|
||||||
@@ -523,7 +539,7 @@ func (v *Vault) writeUnlockerFiles(
|
|||||||
// Write public key
|
// Write public key
|
||||||
pubKeyPath := filepath.Join(unlockerDir, "pub.age")
|
pubKeyPath := filepath.Join(unlockerDir, "pub.age")
|
||||||
|
|
||||||
err := secret.WriteFileAtomic(v.fs, pubKeyPath,
|
err := secret.WriteFileAtomic(fs, pubKeyPath,
|
||||||
[]byte(unlockerIdentity.Recipient().String()))
|
[]byte(unlockerIdentity.Recipient().String()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to write unlocker public key: %w", err)
|
return fmt.Errorf("failed to write unlocker public key: %w", err)
|
||||||
@@ -541,18 +557,18 @@ func (v *Vault) writeUnlockerFiles(
|
|||||||
// Write encrypted private key
|
// Write encrypted private key
|
||||||
privKeyPath := filepath.Join(unlockerDir, "priv.age")
|
privKeyPath := filepath.Join(unlockerDir, "priv.age")
|
||||||
|
|
||||||
err = secret.WriteFileAtomic(v.fs, privKeyPath, encryptedPrivKey)
|
err = secret.WriteFileAtomic(fs, privKeyPath, encryptedPrivKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to write encrypted unlocker private key: %w", err)
|
return fmt.Errorf("failed to write encrypted unlocker private key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = secret.WriteFileAtomic(v.fs,
|
err = secret.WriteFileAtomic(fs,
|
||||||
filepath.Join(unlockerDir, "longterm.age"), encryptedLtPrivKey)
|
filepath.Join(unlockerDir, "longterm.age"), encryptedLtPrivKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to write encrypted long-term private key: %w", err)
|
return fmt.Errorf("failed to write encrypted long-term private key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = secret.WriteFileAtomic(v.fs,
|
err = secret.WriteFileAtomic(fs,
|
||||||
filepath.Join(unlockerDir, "unlocker-metadata.json"), metadataBytes)
|
filepath.Join(unlockerDir, "unlocker-metadata.json"), metadataBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to write unlocker metadata: %w", err)
|
return fmt.Errorf("failed to write unlocker metadata: %w", err)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package vault_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"errors"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -72,7 +73,7 @@ func testCreateVault(t *testing.T, fs afero.Fs) {
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
vlt, err := vault.CreateVault(fs, testStateDir, testVaultName,
|
vlt, err := vault.CreateVault(fs, testStateDir, testVaultName,
|
||||||
testMnemonicBuffer(t))
|
testMnemonicBuffer(t), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create vault: %v", err)
|
t.Fatalf("Failed to create vault: %v", err)
|
||||||
}
|
}
|
||||||
@@ -298,7 +299,7 @@ func TestListUnlockers_SkipsMissingMetadata(t *testing.T) {
|
|||||||
|
|
||||||
// Create vault
|
// Create vault
|
||||||
vlt, err := vault.CreateVault(fs, testStateDir, testVaultName,
|
vlt, err := vault.CreateVault(fs, testStateDir, testVaultName,
|
||||||
testMnemonicBuffer(t))
|
testMnemonicBuffer(t), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create vault: %v", err)
|
t.Fatalf("Failed to create vault: %v", err)
|
||||||
}
|
}
|
||||||
@@ -344,3 +345,29 @@ func TestListUnlockers_SkipsMissingMetadata(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestCreateVaultUnlockerNeedsMnemonic checks that CreateVault, given a
|
||||||
|
// passphrase for an unlocker but no mnemonic to derive the long-term key from,
|
||||||
|
// fails without writing anything.
|
||||||
|
func TestCreateVaultUnlockerNeedsMnemonic(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
|
||||||
|
passphrase := memguard.NewBufferFromBytes([]byte(testPassphrase))
|
||||||
|
defer passphrase.Destroy()
|
||||||
|
|
||||||
|
_, err := vault.CreateVault(fs, testStateDir, testVaultName, nil, passphrase)
|
||||||
|
if !errors.Is(err, vault.ErrUnlockerWithoutMnemonic) {
|
||||||
|
t.Fatalf("Expected ErrUnlockerWithoutMnemonic, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
exists, err := afero.Exists(fs, testStateDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to check for the state directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
t.Errorf("CreateVault wrote the state directory")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user