Create a vault whole in a temporary directory, then select it (closes #105)
check / check (push) Failing after 2s
check / check (push) Failing after 2s
vault.CreateVault takes the unlocker passphrase and writes the vault directory, its metadata, long-term public key and passphrase unlocker into a temporary directory, renames that into vaults.d once complete, and only then makes the vault current. secret init and secret vault create call it once instead of adding the unlocker afterwards, so a kill part-way leaves either no vault, whose temporary directory the next command that takes the lock deletes, or a complete one. A test records the state directory before every change the call makes and checks each state, and the command run again from it. Model: opus-5-5
This commit is contained in:
@@ -63,10 +63,10 @@ func newConfirmTestVaults(
|
||||
fs := &afero.MemMapFs{}
|
||||
mnemonic := testMnemonicBuffer(t)
|
||||
|
||||
_, err := vault.CreateVault(fs, testStateDir, "other", mnemonic)
|
||||
_, err := vault.CreateVault(fs, testStateDir, "other", mnemonic, nil)
|
||||
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)
|
||||
|
||||
addTestSecret(t, vlt, []byte("older"), false)
|
||||
|
||||
@@ -2,7 +2,11 @@ package cli_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"maps"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.eeqj.de/sneak/secret/internal/cli"
|
||||
@@ -155,7 +159,7 @@ func TestStopAtPassphrasePromptLeavesNothing(t *testing.T) {
|
||||
require.NoError(t, empty.MkdirAll(testStateDir, secret.DirPerms))
|
||||
|
||||
withDefault := afero.NewMemMapFs()
|
||||
_, err := vault.CreateVault(withDefault, testStateDir, "default", mnemonic)
|
||||
_, err := vault.CreateVault(withDefault, testStateDir, "default", mnemonic, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
@@ -193,3 +197,190 @@ 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
|
||||
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/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"filippo.io/age"
|
||||
"git.eeqj.de/sneak/secret/internal/secret"
|
||||
"git.eeqj.de/sneak/secret/internal/vault"
|
||||
"git.eeqj.de/sneak/secret/pkg/agehd"
|
||||
"github.com/awnumar/memguard"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/tyler-smith/go-bip39"
|
||||
@@ -70,43 +67,6 @@ func (cli *Instance) promptMnemonic() (*memguard.LockedBuffer, func(), error) {
|
||||
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
|
||||
// while initialize runs
|
||||
func (cli *Instance) Init(cmd *cobra.Command) error {
|
||||
@@ -173,34 +133,31 @@ func (cli *Instance) initialize(cmd *cobra.Command) error {
|
||||
}
|
||||
defer cleanupPassphrase()
|
||||
|
||||
// Create the default vault and derive its long-term key
|
||||
vlt, ltIdentity, err := cli.setupDefaultVault(stateDir, mnemonic)
|
||||
// Create the default vault with its passphrase unlocker
|
||||
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 {
|
||||
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 {
|
||||
cmd.Printf("\nDefault vault created and configured\n")
|
||||
cmd.Printf("Long-term public key: %s\n", ltPubKey)
|
||||
cmd.Printf("Unlocker ID: %s\n", passphraseUnlocker.GetID())
|
||||
cmd.Printf("Long-term public key: %s\n", ltIdentity.Recipient().String())
|
||||
cmd.Printf("Unlocker ID: %s\n", unlocker.GetID())
|
||||
cmd.Println("\nYour secret manager is ready to use!")
|
||||
cmd.Println("Note: When using SB_SECRET_MNEMONIC environment variable,")
|
||||
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))
|
||||
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)
|
||||
|
||||
value := memguard.NewBufferFromBytes([]byte("value"))
|
||||
|
||||
@@ -28,7 +28,7 @@ func TestLeftoversRemovedByNextChangingCommand(t *testing.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, vault.SelectVault(fs, testStateDir, "default"))
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ func TestConcurrentAddsKeepEveryVersion(t *testing.T) {
|
||||
{"real", afero.NewOsFs(), t.TempDir()},
|
||||
} {
|
||||
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)
|
||||
|
||||
// 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
|
||||
func TestEncryptPipedIntoAdd(t *testing.T) {
|
||||
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, afero.WriteFile(fs, testInput, []byte("piped"), 0o600))
|
||||
|
||||
@@ -292,14 +292,14 @@ func setupEveryCommand(
|
||||
|
||||
mnemonic := testMnemonicBuffer(t)
|
||||
|
||||
other, err := vault.CreateVault(fs, testStateDir, "other", mnemonic)
|
||||
other, err := vault.CreateVault(fs, testStateDir, "other", mnemonic, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
otherDir, err := other.GetDirectory()
|
||||
require.NoError(t, err)
|
||||
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)
|
||||
|
||||
addTestSecret(t, vlt, []byte("older"), false)
|
||||
@@ -487,7 +487,7 @@ func TestEncryptWithExistingKeyTakesNoLock(t *testing.T) {
|
||||
mnemonic := testMnemonicBuffer(t)
|
||||
|
||||
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, 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
|
||||
func TestEncryptStreamsUnlocked(t *testing.T) {
|
||||
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, afero.WriteFile(fs, testInput, []byte("streamed"), 0o600))
|
||||
|
||||
|
||||
@@ -156,10 +156,10 @@ func TestMoveOntoSameSecretUnderAnotherNameIsRejected(t *testing.T) {
|
||||
vaultsDir := filepath.Join(stateDir, "vaults.d")
|
||||
|
||||
// "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)
|
||||
|
||||
vlt, err := vault.CreateVault(fs, stateDir, "default", testMnemonicBuffer(t))
|
||||
vlt, err := vault.CreateVault(fs, stateDir, "default", testMnemonicBuffer(t), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = vlt.AddSecret("x", memguard.NewBufferFromBytes([]byte("value")), false)
|
||||
@@ -205,7 +205,7 @@ func TestForcedCaseOnlyMoveOnCaseSensitiveFilesystem(t *testing.T) {
|
||||
fs := afero.NewOsFs()
|
||||
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)
|
||||
|
||||
err = vlt.AddSecret("Foo", memguard.NewBufferFromBytes([]byte("upper")), false)
|
||||
|
||||
@@ -68,7 +68,7 @@ func newTwoVaultFs(t *testing.T) afero.Fs {
|
||||
mnemonic := testMnemonicBuffer(t)
|
||||
|
||||
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)
|
||||
|
||||
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()
|
||||
|
||||
// Create vault
|
||||
_, err := vault.CreateVault(fs, testStateDir, testVaultName, testMnemonicBuffer(t))
|
||||
_, err := vault.CreateVault(fs, testStateDir, testVaultName,
|
||||
testMnemonicBuffer(t), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Set current vault
|
||||
|
||||
@@ -47,7 +47,7 @@ func TestAddPGPUnlocker(t *testing.T) {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
fs := afero.NewMemMapFs()
|
||||
vlt, err := vault.CreateVault(fs, listTestStateDir, listTestVaultName,
|
||||
testMnemonicBuffer(t))
|
||||
testMnemonicBuffer(t), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = vlt.AddSecret(addTestSecretName,
|
||||
|
||||
+8
-22
@@ -293,40 +293,26 @@ func (cli *Instance) CreateVault(cmd *cobra.Command, name string) error {
|
||||
}
|
||||
defer cleanupPassphrase()
|
||||
|
||||
// Create the vault - it will handle key derivation internally
|
||||
vlt, err := vault.CreateVault(cli.fs, cli.stateDir, name, mnemonic)
|
||||
// Create the vault with its passphrase unlocker
|
||||
vlt, err := vault.CreateVault(cli.fs, cli.stateDir, name,
|
||||
mnemonic, passphraseBuffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the vault metadata to retrieve the derivation index
|
||||
vaultDir := filepath.Join(cli.stateDir, "vaults.d", name)
|
||||
|
||||
metadata, err := vault.LoadVaultMetadata(cli.fs, vaultDir)
|
||||
ltIdentity, err := vlt.GetOrDeriveLongTermKey()
|
||||
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
|
||||
ltIdentity, err := agehd.DeriveIdentity(mnemonicStr, metadata.DerivationIndex)
|
||||
unlocker, err := vlt.GetCurrentUnlocker()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to derive long-term key from mnemonic: %w", 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)
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.Printf("Created vault '%s'\n", vlt.GetName())
|
||||
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
|
||||
}
|
||||
|
||||
@@ -73,7 +73,8 @@ func setupTestVault(t *testing.T, fs afero.Fs) {
|
||||
t.Helper()
|
||||
|
||||
// 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)
|
||||
|
||||
// Derive and store long-term key from mnemonic
|
||||
|
||||
Reference in New Issue
Block a user