Create a vault whole in a temporary directory, then select it (closes #105)
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:
2026-10-04 20:42:03 +02:00
parent f2f89c8a06
commit 23dcea83f9
25 changed files with 421 additions and 204 deletions
+34 -18
View File
@@ -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)