Lock the state directory and write vault files atomically (closes #34)
check / check (push) Successful in 1m31s
check / check (push) Successful in 1m31s
Each command that changes the state directory holds one lock: flock(2) on `lock` in the state directory, dropped by the kernel if the process dies, or a process-wide mutex on the in-memory test filesystem. It covers the state directory, not each vault, because `currentvault`, `vault create` and cross-vault moves span vaults, and a lock file in a vault would be deleted by `vault remove` under a waiting command. Files go through `secret.WriteFileAtomic`; versions, new secrets and cross-vault copies are built in a temporary directory and renamed into place; removals rename out of the way first. Left for later: replacing an unlocker (#71) and deleting what an interrupted command leaves under a `.tmp-` name (#75). Model: opus-5-5 Co-authored-by: clawbot <sneak+clawbot@sneak.cloud>
This commit was merged in pull request #69.
This commit is contained in:
+40
-17
@@ -70,9 +70,44 @@ func newDecryptCmd() *cobra.Command {
|
||||
)
|
||||
}
|
||||
|
||||
// storeNewEncryptionKey generates an age secret key and stores it as the
|
||||
// named secret, holding the state directory lock while it does. It fails
|
||||
// with vault.ErrSecretExists if another command stored the secret first.
|
||||
// The caller must destroy the returned buffer.
|
||||
func (cli *Instance) storeNewEncryptionKey(
|
||||
vlt *vault.Vault, secretName string,
|
||||
) (*memguard.LockedBuffer, error) {
|
||||
release, err := vault.LockStateDir(cli.fs, cli.stateDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer release()
|
||||
|
||||
identity, err := age.GenerateX25519Identity()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate age key: %w", err)
|
||||
}
|
||||
|
||||
// Store the generated key directly in a secure buffer
|
||||
secureBuffer := memguard.NewBufferFromBytes([]byte(identity.String()))
|
||||
|
||||
err = vlt.AddSecret(secretName, secureBuffer, false)
|
||||
if err != nil {
|
||||
secureBuffer.Destroy()
|
||||
|
||||
return nil, fmt.Errorf("failed to store age key: %w", err)
|
||||
}
|
||||
|
||||
return secureBuffer, nil
|
||||
}
|
||||
|
||||
// resolveEncryptionKey returns a secure buffer holding the age secret key
|
||||
// for the named secret, generating and storing a new key if the secret
|
||||
// does not exist. The caller must destroy the returned buffer.
|
||||
// does not exist. The caller must destroy the returned buffer. Only storing
|
||||
// a new key takes the state directory lock, so that reading an existing key
|
||||
// works on a read-only state directory and keeps no other command waiting
|
||||
// at the passphrase prompt, and Encrypt streams its input and output
|
||||
// unlocked.
|
||||
func (cli *Instance) resolveEncryptionKey(
|
||||
vlt *vault.Vault, secretName string,
|
||||
) (*memguard.LockedBuffer, error) {
|
||||
@@ -85,23 +120,11 @@ func (cli *Instance) resolveEncryptionKey(
|
||||
}
|
||||
|
||||
if !exists {
|
||||
// Secret doesn't exist, generate new age key and store it
|
||||
identity, err := age.GenerateX25519Identity()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate age key: %w", err)
|
||||
key, err := cli.storeNewEncryptionKey(vlt, secretName)
|
||||
if !errors.Is(err, vault.ErrSecretExists) {
|
||||
return key, err
|
||||
}
|
||||
|
||||
// Store the generated key directly in a secure buffer
|
||||
secureBuffer := memguard.NewBufferFromBytes([]byte(identity.String()))
|
||||
|
||||
err = vlt.AddSecret(secretName, secureBuffer, false)
|
||||
if err != nil {
|
||||
secureBuffer.Destroy()
|
||||
|
||||
return nil, fmt.Errorf("failed to store age key: %w", err)
|
||||
}
|
||||
|
||||
return secureBuffer, nil
|
||||
// Another command stored the key since the check above: read it
|
||||
}
|
||||
|
||||
// Secret exists, get the age secret key from it
|
||||
|
||||
Reference in New Issue
Block a user