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 was merged in pull request #108.
This commit is contained in:
@@ -31,6 +31,11 @@ var (
|
||||
// Composed as "vault <name> 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 = errors.New("value buffer is nil")
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ func testCurrentVaultFileHandling(t *testing.T, fs afero.Fs, tempDir string) {
|
||||
|
||||
// Create a test vault
|
||||
vlt, err := vault.CreateVault(fs, stateDir, testVaultName,
|
||||
testMnemonicBuffer(t))
|
||||
testMnemonicBuffer(t), nil)
|
||||
if err != nil {
|
||||
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
|
||||
// the mnemonic
|
||||
vlt, err := vault.CreateVault(fs, stateDir, testVaultName,
|
||||
testMnemonicBuffer(t))
|
||||
testMnemonicBuffer(t), nil)
|
||||
if err != nil {
|
||||
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
|
||||
// the mnemonic
|
||||
vlt, err := vault.CreateVault(fs, stateDir, testVaultName,
|
||||
testMnemonicBuffer(t))
|
||||
testMnemonicBuffer(t), nil)
|
||||
if err != nil {
|
||||
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 {
|
||||
_, err := vault.CreateVault(fs, stateDir, name, testMnemonicBuffer(t))
|
||||
_, err := vault.CreateVault(fs, stateDir, name, testMnemonicBuffer(t), nil)
|
||||
if err != nil {
|
||||
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 {
|
||||
_, err := vault.CreateVault(fs, stateDir, name, testMnemonicBuffer(t))
|
||||
_, err := vault.CreateVault(fs, stateDir, name, testMnemonicBuffer(t), nil)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error creating vault with invalid name %q, "+
|
||||
"but got none", name)
|
||||
@@ -361,7 +361,7 @@ func testMultipleVaults(t *testing.T, fs afero.Fs, tempDir string) {
|
||||
// Create three vaults
|
||||
vaultNames := []string{"vault1", "vault2", "vault3"}
|
||||
for _, name := range vaultNames {
|
||||
_, err := vault.CreateVault(fs, stateDir, name, testMnemonicBuffer(t))
|
||||
_, err := vault.CreateVault(fs, stateDir, name, testMnemonicBuffer(t), nil)
|
||||
if err != nil {
|
||||
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
|
||||
// the mnemonic
|
||||
vault1, err := vault.CreateVault(fs, stateDir, "vault1", testMnemonicBuffer(t))
|
||||
vault1, err := vault.CreateVault(fs, stateDir, "vault1", testMnemonicBuffer(t), nil)
|
||||
if err != nil {
|
||||
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 {
|
||||
t.Fatalf("Failed to create vault2: %v", err)
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ func TestVersionIntegrationWorkflow(t *testing.T) {
|
||||
fs := afero.NewMemMapFs()
|
||||
|
||||
// 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)
|
||||
|
||||
// Derive and store long-term key from mnemonic
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"filippo.io/age"
|
||||
"git.eeqj.de/sneak/secret/internal/secret"
|
||||
"git.eeqj.de/sneak/secret/pkg/agehd"
|
||||
"github.com/awnumar/memguard"
|
||||
@@ -152,16 +153,17 @@ func ListVaults(fs afero.Fs, stateDir string) ([]string, error) {
|
||||
}
|
||||
|
||||
// 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(
|
||||
fs afero.Fs, stateDir, vaultDir, vaultName string,
|
||||
mnemonicBuffer *memguard.LockedBuffer,
|
||||
) (uint32, string, string, error) {
|
||||
) (*age.X25519Identity, uint32, string, string, error) {
|
||||
if mnemonicBuffer == nil {
|
||||
secret.Debug("No mnemonic given, vault created without long-term key",
|
||||
"vault", vaultName)
|
||||
// Use 0 for derivation index when no mnemonic is provided
|
||||
return 0, "", "", nil
|
||||
return nil, 0, "", "", nil
|
||||
}
|
||||
|
||||
mnemonic := mnemonicBuffer.String()
|
||||
@@ -171,13 +173,14 @@ func processMnemonicForVault(
|
||||
// Get the next available derivation index for this mnemonic
|
||||
derivationIndex, err := GetNextDerivationIndex(fs, stateDir, mnemonic)
|
||||
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
|
||||
ltIdentity, err := agehd.DeriveIdentity(mnemonic, derivationIndex)
|
||||
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
|
||||
@@ -187,7 +190,8 @@ func processMnemonicForVault(
|
||||
|
||||
err = secret.WriteFileAtomic(fs, ltPubKeyPath, []byte(ltPubKey))
|
||||
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)
|
||||
@@ -199,24 +203,33 @@ func processMnemonicForVault(
|
||||
// This is used to identify which vaults belong to the same mnemonic family
|
||||
identity0, err := agehd.DeriveIdentity(mnemonic, 0)
|
||||
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()))
|
||||
|
||||
return derivationIndex, publicKeyHash, familyHash, nil
|
||||
return ltIdentity, derivationIndex, publicKeyHash, familyHash, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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
|
||||
// exists before writing anything: creating it again would replace its keys,
|
||||
// and its secrets could 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.
|
||||
// long-term key until one is imported. When passphrase is not nil, the vault
|
||||
// gets a passphrase unlocker protected by it, as its current unlocker; that
|
||||
// needs a mnemonic. It refuses a vault that already exists before writing
|
||||
// anything: creating it again would replace its keys, and its secrets could
|
||||
// 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(
|
||||
fs afero.Fs, stateDir string, name string, mnemonic *memguard.LockedBuffer,
|
||||
fs afero.Fs, stateDir string, name string,
|
||||
mnemonic, passphrase *memguard.LockedBuffer,
|
||||
) (*Vault, error) {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
// Create main vault directory
|
||||
err = fs.MkdirAll(vaultDir, secret.DirPerms)
|
||||
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)
|
||||
err = secret.WriteDir(fs, vaultDir, func(dir string) error {
|
||||
return writeVaultFiles(fs, stateDir, dir, name, mnemonic, passphrase)
|
||||
})
|
||||
if err != nil {
|
||||
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
|
||||
secret.Debug("Selecting newly created vault as current", "name", name)
|
||||
|
||||
@@ -302,6 +283,47 @@ func CreateVault(
|
||||
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
|
||||
func SelectVault(fs afero.Fs, stateDir string, name string) error {
|
||||
secret.Debug("Selecting vault", "vault_name", name, "state_dir", stateDir)
|
||||
|
||||
@@ -304,7 +304,7 @@ func TestWorkflowMismatch(t *testing.T) {
|
||||
fs := afero.NewOsFs()
|
||||
|
||||
// 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 {
|
||||
t.Fatalf("Failed to create vault with mnemonic: %v", err)
|
||||
}
|
||||
@@ -321,7 +321,7 @@ func TestWorkflowMismatch(t *testing.T) {
|
||||
metadata1.DerivationIndex, metadata1.PublicKeyHash)
|
||||
|
||||
// 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 {
|
||||
t.Fatalf("Failed to create vault without mnemonic: %v", err)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ func TestGetSecretVersionRejectsPathTraversal(t *testing.T) {
|
||||
fs := afero.NewMemMapFs()
|
||||
|
||||
vlt, err := vault.CreateVault(fs, testStateDir, testVaultName,
|
||||
testMnemonicBuffer(t))
|
||||
testMnemonicBuffer(t), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Add a legitimate secret so the vault is set up
|
||||
@@ -57,7 +57,7 @@ func TestGetSecretRejectsPathTraversal(t *testing.T) {
|
||||
fs := afero.NewMemMapFs()
|
||||
|
||||
vlt, err := vault.CreateVault(fs, testStateDir, testVaultName,
|
||||
testMnemonicBuffer(t))
|
||||
testMnemonicBuffer(t), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = vlt.GetSecret("../../../etc/passwd")
|
||||
@@ -73,7 +73,7 @@ func TestGetSecretObjectRejectsPathTraversal(t *testing.T) {
|
||||
fs := afero.NewMemMapFs()
|
||||
|
||||
vlt, err := vault.CreateVault(fs, testStateDir, testVaultName,
|
||||
testMnemonicBuffer(t))
|
||||
testMnemonicBuffer(t), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
maliciousNames := []string{
|
||||
|
||||
@@ -66,7 +66,7 @@ func createTestVaultWithKey(t *testing.T, fs afero.Fs) *Vault {
|
||||
t.Helper()
|
||||
|
||||
// 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)
|
||||
|
||||
// Derive and store long-term key from mnemonic
|
||||
|
||||
+34
-18
@@ -390,8 +390,31 @@ func (v *Vault) CreatePassphraseUnlocker(
|
||||
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()
|
||||
unlockerDir := filepath.Join(unlockersDir, unlockerTypePassphrase+"-"+
|
||||
unlockerDir := filepath.Join(vaultDir, "unlockers.d", unlockerTypePassphrase+"-"+
|
||||
createdAt.UTC().Format(secret.UnlockerTimeFormat))
|
||||
|
||||
// Generate new age keypair for unlocker
|
||||
@@ -422,8 +445,8 @@ func (v *Vault) CreatePassphraseUnlocker(
|
||||
}
|
||||
|
||||
// Write the unlocker's files, the metadata last
|
||||
err = secret.WriteDir(v.fs, unlockerDir, func(dir string) error {
|
||||
return v.writeUnlockerFiles(dir, unlockerIdentity, passphrase,
|
||||
err = secret.WriteDir(fs, unlockerDir, func(dir string) error {
|
||||
return writeUnlockerFiles(fs, dir, unlockerIdentity, passphrase,
|
||||
encryptedLtPrivKey, metadataBytes)
|
||||
})
|
||||
if err != nil {
|
||||
@@ -434,21 +457,13 @@ func (v *Vault) CreatePassphraseUnlocker(
|
||||
// passphrase unlocker created in the same minute has the same ID.
|
||||
currentUnlockerPath := filepath.Join(vaultDir, "current-unlocker")
|
||||
|
||||
err = secret.WriteFileAtomic(v.fs, currentUnlockerPath,
|
||||
err = secret.WriteFileAtomic(fs, currentUnlockerPath,
|
||||
[]byte(filepath.Base(unlockerDir)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to select new unlocker: %w", 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 secret.NewPassphraseUnlocker(v.fs, unlockerDir, metadata), nil
|
||||
return secret.NewPassphraseUnlocker(fs, unlockerDir, metadata), nil
|
||||
}
|
||||
|
||||
// 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
|
||||
// unlockerDir: its public key, its passphrase-encrypted private key, the
|
||||
// long-term private key encrypted to it, and its metadata, last.
|
||||
func (v *Vault) writeUnlockerFiles(
|
||||
func writeUnlockerFiles(
|
||||
fs afero.Fs,
|
||||
unlockerDir string,
|
||||
unlockerIdentity *age.X25519Identity,
|
||||
passphrase *memguard.LockedBuffer,
|
||||
@@ -523,7 +539,7 @@ func (v *Vault) writeUnlockerFiles(
|
||||
// Write public key
|
||||
pubKeyPath := filepath.Join(unlockerDir, "pub.age")
|
||||
|
||||
err := secret.WriteFileAtomic(v.fs, pubKeyPath,
|
||||
err := secret.WriteFileAtomic(fs, pubKeyPath,
|
||||
[]byte(unlockerIdentity.Recipient().String()))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write unlocker public key: %w", err)
|
||||
@@ -541,18 +557,18 @@ func (v *Vault) writeUnlockerFiles(
|
||||
// Write encrypted private key
|
||||
privKeyPath := filepath.Join(unlockerDir, "priv.age")
|
||||
|
||||
err = secret.WriteFileAtomic(v.fs, privKeyPath, encryptedPrivKey)
|
||||
err = secret.WriteFileAtomic(fs, privKeyPath, encryptedPrivKey)
|
||||
if err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write unlocker metadata: %w", err)
|
||||
|
||||
@@ -2,6 +2,7 @@ package vault_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"testing"
|
||||
@@ -72,7 +73,7 @@ func testCreateVault(t *testing.T, fs afero.Fs) {
|
||||
t.Helper()
|
||||
|
||||
vlt, err := vault.CreateVault(fs, testStateDir, testVaultName,
|
||||
testMnemonicBuffer(t))
|
||||
testMnemonicBuffer(t), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create vault: %v", err)
|
||||
}
|
||||
@@ -298,7 +299,7 @@ func TestListUnlockers_SkipsMissingMetadata(t *testing.T) {
|
||||
|
||||
// Create vault
|
||||
vlt, err := vault.CreateVault(fs, testStateDir, testVaultName,
|
||||
testMnemonicBuffer(t))
|
||||
testMnemonicBuffer(t), nil)
|
||||
if err != nil {
|
||||
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