Compare commits
1
Commits
next
...
eadaad2040
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eadaad2040 |
@@ -438,6 +438,15 @@ asks when stdin is a pipe, as in `echo value | secret add name --force` or
|
|||||||
`cat file | secret encrypt key`. With no controlling terminal, as under cron or
|
`cat file | secret encrypt key`. With no controlling terminal, as under cron or
|
||||||
in a CI job, the command fails and names the variable to set.
|
in a CI job, the command fails and names the variable to set.
|
||||||
|
|
||||||
|
A command that changes anything locks the state directory, through its file
|
||||||
|
`lock`, and another such command waits until the lock is released.
|
||||||
|
`secret init`, `secret vault create`, `secret unlocker add`, and `secret add`,
|
||||||
|
`secret import` and `secret generate secret` with `--force`, ask for the
|
||||||
|
mnemonic or passphrase before they take the lock, so a prompt left open in one
|
||||||
|
terminal holds up no command in another. A forced add of a secret that does not
|
||||||
|
exist yet fails, and changes nothing, if another command creates the secret
|
||||||
|
while it waits for the lock; run it again to replace the secret.
|
||||||
|
|
||||||
## Security Features
|
## Security Features
|
||||||
|
|
||||||
### Encryption
|
### Encryption
|
||||||
|
|||||||
@@ -175,12 +175,6 @@ func (cli *Instance) GenerateSecret(
|
|||||||
return fmt.Errorf("failed to generate random secret: %w", err)
|
return fmt.Errorf("failed to generate random secret: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
release, err := vault.LockStateDir(cli.fs, cli.stateDir)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer release()
|
|
||||||
|
|
||||||
// Store the secret in the vault
|
// Store the secret in the vault
|
||||||
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -193,7 +187,7 @@ func (cli *Instance) GenerateSecret(
|
|||||||
secretBuffer := memguard.NewBufferFromBytes([]byte(secretValue))
|
secretBuffer := memguard.NewBufferFromBytes([]byte(secretValue))
|
||||||
defer secretBuffer.Destroy()
|
defer secretBuffer.Destroy()
|
||||||
|
|
||||||
err = vlt.AddSecret(secretName, secretBuffer, force)
|
err = cli.storeSecret(vlt, secretName, secretBuffer, force)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-27
@@ -67,36 +67,15 @@ func (cli *Instance) promptMnemonic() (*memguard.LockedBuffer, func(), error) {
|
|||||||
return mnemonicBuffer, mnemonicBuffer.Destroy, nil
|
return mnemonicBuffer, mnemonicBuffer.Destroy, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init initializes the secret manager, holding the state directory lock
|
// Init initializes the secret manager. It asks for the mnemonic and the
|
||||||
// while initialize runs
|
// passphrase before it takes the state directory lock, so that no other
|
||||||
|
// command waits while the user answers, and holds the lock while
|
||||||
|
// initialize runs.
|
||||||
func (cli *Instance) Init(cmd *cobra.Command) error {
|
func (cli *Instance) Init(cmd *cobra.Command) error {
|
||||||
release, err := vault.LockStateDir(cli.fs, cli.stateDir)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer release()
|
|
||||||
|
|
||||||
return cli.initialize(cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
// initialize creates the state directory, the default vault and its first
|
|
||||||
// unlocker
|
|
||||||
func (cli *Instance) initialize(cmd *cobra.Command) error {
|
|
||||||
secret.Debug("Starting secret manager initialization")
|
secret.Debug("Starting secret manager initialization")
|
||||||
|
|
||||||
// Create state directory
|
|
||||||
stateDir := cli.GetStateDir()
|
|
||||||
secret.DebugWith("Creating state directory", slog.String("path", stateDir))
|
|
||||||
|
|
||||||
err := cli.fs.MkdirAll(stateDir, secret.DirPerms)
|
|
||||||
if err != nil {
|
|
||||||
secret.Debug("Failed to create state directory", "error", err)
|
|
||||||
|
|
||||||
return fmt.Errorf("failed to create state directory: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refuse an existing default vault before asking for anything
|
// Refuse an existing default vault before asking for anything
|
||||||
err = vault.CheckNewVaultName(cli.fs, cli.stateDir, defaultVaultName)
|
err := vault.CheckNewVaultName(cli.fs, cli.stateDir, defaultVaultName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create default vault: %w", err)
|
return fmt.Errorf("failed to create default vault: %w", err)
|
||||||
}
|
}
|
||||||
@@ -135,11 +114,38 @@ func (cli *Instance) initialize(cmd *cobra.Command) error {
|
|||||||
}
|
}
|
||||||
defer cleanupPassphrase()
|
defer cleanupPassphrase()
|
||||||
|
|
||||||
|
release, err := vault.LockStateDir(cli.fs, cli.stateDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer release()
|
||||||
|
|
||||||
|
return cli.initialize(cmd, mnemonic, passphraseBuffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize creates the state directory, and the default vault from
|
||||||
|
// mnemonic with a passphrase unlocker protected by passphrase. Another
|
||||||
|
// command may have created the vault since Init checked: vault.CreateVault
|
||||||
|
// checks again.
|
||||||
|
func (cli *Instance) initialize(
|
||||||
|
cmd *cobra.Command, mnemonic, passphrase *memguard.LockedBuffer,
|
||||||
|
) error {
|
||||||
|
// Create state directory
|
||||||
|
stateDir := cli.GetStateDir()
|
||||||
|
secret.DebugWith("Creating state directory", slog.String("path", stateDir))
|
||||||
|
|
||||||
|
err := cli.fs.MkdirAll(stateDir, secret.DirPerms)
|
||||||
|
if err != nil {
|
||||||
|
secret.Debug("Failed to create state directory", "error", err)
|
||||||
|
|
||||||
|
return fmt.Errorf("failed to create state directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Create the default vault with its passphrase unlocker
|
// Create the default vault with its passphrase unlocker
|
||||||
secret.Debug("Creating default vault")
|
secret.Debug("Creating default vault")
|
||||||
|
|
||||||
vlt, err := vault.CreateVault(cli.fs, cli.stateDir, defaultVaultName,
|
vlt, err := vault.CreateVault(cli.fs, cli.stateDir, defaultVaultName,
|
||||||
mnemonic, passphraseBuffer)
|
mnemonic, passphrase)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
secret.Debug("Failed to create default vault", "error", err)
|
secret.Debug("Failed to create default vault", "error", err)
|
||||||
|
|
||||||
|
|||||||
+277
-14
@@ -280,9 +280,9 @@ func stateDirModTimes(t *testing.T, fs afero.Fs) map[string]int64 {
|
|||||||
return modTimes
|
return modTimes
|
||||||
}
|
}
|
||||||
|
|
||||||
// setupEveryCommand makes what each command in
|
// setupEveryCommand makes what the commands that the lock tests below run
|
||||||
// TestChangingCommandsWaitForLock needs: the current vault "work" with two
|
// need: the current vault "work" with two versions of "test/secret", the
|
||||||
// versions of "test/secret", the vault "other", and the file testInput.
|
// vault "other", and the file testInput.
|
||||||
// There is no vault "default", which init creates. If withUnlocker is set,
|
// There is no vault "default", which init creates. If withUnlocker is set,
|
||||||
// it also gives "work" a passphrase unlocker, which is slow. It returns the
|
// it also gives "work" a passphrase unlocker, which is slow. It returns the
|
||||||
// older version and the unlocker's ID.
|
// older version and the unlocker's ID.
|
||||||
@@ -346,6 +346,25 @@ func waitingForLock() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// requireWaitingForLock returns once a command waits for the state
|
||||||
|
// directory lock. It fails the test if the command finishes first, sending
|
||||||
|
// its error on done, or never waits.
|
||||||
|
func requireWaitingForLock(t *testing.T, done <-chan error) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
timeout := time.After(lockWait)
|
||||||
|
|
||||||
|
for !waitingForLock() {
|
||||||
|
select {
|
||||||
|
case err := <-done:
|
||||||
|
t.Fatalf("finished while the lock was held, with error %v", err)
|
||||||
|
case <-timeout:
|
||||||
|
t.Fatal("never waited for the lock")
|
||||||
|
case <-time.After(time.Millisecond):
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// requireWaitsForLock runs a command, given what setupEveryCommand made,
|
// requireWaitsForLock runs a command, given what setupEveryCommand made,
|
||||||
// while holding the state directory lock. The command must neither finish
|
// while holding the state directory lock. The command must neither finish
|
||||||
// nor change anything before it waits for the lock, and must succeed once
|
// nor change anything before it waits for the lock, and must succeed once
|
||||||
@@ -385,17 +404,7 @@ func requireWaitsForLock(
|
|||||||
|
|
||||||
go func() { done <- run(cli, olderVersion, unlockerID) }()
|
go func() { done <- run(cli, olderVersion, unlockerID) }()
|
||||||
|
|
||||||
timeout := time.After(lockWait)
|
requireWaitingForLock(t, done)
|
||||||
|
|
||||||
for !waitingForLock() {
|
|
||||||
select {
|
|
||||||
case err := <-done:
|
|
||||||
t.Fatalf("finished while the lock was held, with error %v", err)
|
|
||||||
case <-timeout:
|
|
||||||
t.Fatal("never waited for the lock")
|
|
||||||
case <-time.After(time.Millisecond):
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, before, stateDirModTimes(t, fs),
|
assert.Equal(t, before, stateDirModTimes(t, fs),
|
||||||
"changed the state directory before waiting for the lock")
|
"changed the state directory before waiting for the lock")
|
||||||
@@ -472,6 +481,260 @@ func TestChangingCommandsWaitForLock(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestCommandsAskBeforeLocking is a regression test for
|
||||||
|
// https://git.eeqj.de/sneak/secret/issues/152: these commands took the
|
||||||
|
// state directory lock before asking for the mnemonic or a passphrase, so
|
||||||
|
// while one waited at its prompt, every other command that changes anything
|
||||||
|
// waited for it. Each runs here while the test holds the lock, given what
|
||||||
|
// takes it to the last step at which it can ask: the passphrase prompt of
|
||||||
|
// init and vault create, and the unlock of the vault, which asks for the
|
||||||
|
// passphrase of its passphrase unlocker, for the others. The tests have no
|
||||||
|
// terminal, so a prompt fails at once. Each command must get there, and
|
||||||
|
// fail, without waiting for the lock.
|
||||||
|
//
|
||||||
|
//nolint:paralleltest // waitingForLock sees any test's command waiting for the lock
|
||||||
|
func TestCommandsAskBeforeLocking(t *testing.T) {
|
||||||
|
newTestGPGKey(t)
|
||||||
|
|
||||||
|
mnemonic := testMnemonicBuffer(t)
|
||||||
|
|
||||||
|
wrongPassphrase := memguard.NewBufferFromBytes([]byte("wrong-passphrase"))
|
||||||
|
t.Cleanup(wrongPassphrase.Destroy)
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
run func(cli *Instance) error
|
||||||
|
// want is wrapped in the error the command fails with
|
||||||
|
want error
|
||||||
|
}{
|
||||||
|
{"init", func(cli *Instance) error {
|
||||||
|
cli.Mnemonic = mnemonic
|
||||||
|
|
||||||
|
return cli.Init(cli.cmd)
|
||||||
|
}, secret.ErrNoTerminal},
|
||||||
|
{"vault create", func(cli *Instance) error {
|
||||||
|
cli.Mnemonic = mnemonic
|
||||||
|
|
||||||
|
return cli.CreateVault(cli.cmd, "created")
|
||||||
|
}, secret.ErrNoTerminal},
|
||||||
|
// With no terminal to ask for the new passphrase on, the new
|
||||||
|
// unlocker takes the one in SB_UNLOCK_PASSPHRASE, which then fails
|
||||||
|
// to unlock the vault
|
||||||
|
{"unlocker add passphrase", func(cli *Instance) error {
|
||||||
|
cli.UnlockPassphrase = wrongPassphrase
|
||||||
|
|
||||||
|
return cli.UnlockersAdd("passphrase", cli.cmd)
|
||||||
|
}, secret.ErrWrongPassphrase},
|
||||||
|
{"unlocker add pgp", func(cli *Instance) error {
|
||||||
|
cli.cmd.Flags().String("keyid", unreadableTestGPGUserID, "")
|
||||||
|
|
||||||
|
return cli.UnlockersAdd("pgp", cli.cmd)
|
||||||
|
}, secret.ErrNoTerminal},
|
||||||
|
{"add --force", func(cli *Instance) error {
|
||||||
|
return cli.AddSecret("test/secret", true)
|
||||||
|
}, secret.ErrNoTerminal},
|
||||||
|
{"import --force", func(cli *Instance) error {
|
||||||
|
return cli.ImportSecret(cli.cmd, "test/secret", testInput, true)
|
||||||
|
}, secret.ErrNoTerminal},
|
||||||
|
{"generate secret --force", func(cli *Instance) error {
|
||||||
|
return cli.GenerateSecret(cli.cmd, "test/secret", 16, "base58", true)
|
||||||
|
}, secret.ErrNoTerminal},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
requireAsksBeforeLocking(t, tc.run, tc.want)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// startWhileLocked makes what setupEveryCommand makes, with the passphrase
|
||||||
|
// unlocker, on fs, an empty in-memory filesystem, takes the state directory
|
||||||
|
// lock there, and starts run, given a CLI instance on fs with "value" on
|
||||||
|
// stdin. It returns the function that releases the lock, which does so only
|
||||||
|
// the first time it is called, and the channel that run's error comes on.
|
||||||
|
// The lock is also released when the test ends, so that later tests can
|
||||||
|
// take it.
|
||||||
|
func startWhileLocked(
|
||||||
|
t *testing.T, fs afero.Fs, run func(cli *Instance) error,
|
||||||
|
) (func(), <-chan error) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
setupEveryCommand(t, fs, true)
|
||||||
|
|
||||||
|
release, err := vault.LockStateDir(fs, testStateDir)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
release = sync.OnceFunc(release)
|
||||||
|
t.Cleanup(release)
|
||||||
|
|
||||||
|
cli := NewCLIInstanceWithStateDir(fs, testStateDir)
|
||||||
|
cli.cmd = &cobra.Command{}
|
||||||
|
cli.cmd.SetIn(strings.NewReader("value"))
|
||||||
|
cli.cmd.SetOut(io.Discard)
|
||||||
|
|
||||||
|
done := make(chan error, 1)
|
||||||
|
|
||||||
|
go func() { done <- run(cli) }()
|
||||||
|
|
||||||
|
return release, done
|
||||||
|
}
|
||||||
|
|
||||||
|
// requireAsksBeforeLocking starts run while holding the state directory
|
||||||
|
// lock, as startWhileLocked does. run must fail, with an error wrapping
|
||||||
|
// want, without waiting for the lock.
|
||||||
|
func requireAsksBeforeLocking(
|
||||||
|
t *testing.T, run func(cli *Instance) error, want error,
|
||||||
|
) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
release, done := startWhileLocked(t, afero.NewMemMapFs(), run)
|
||||||
|
|
||||||
|
timeout := time.After(lockWait)
|
||||||
|
|
||||||
|
for !waitingForLock() {
|
||||||
|
select {
|
||||||
|
case err := <-done:
|
||||||
|
require.ErrorIs(t, err, want)
|
||||||
|
|
||||||
|
return
|
||||||
|
case <-timeout:
|
||||||
|
t.Fatal("neither failed nor waited for the lock")
|
||||||
|
case <-time.After(time.Millisecond):
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let the command go on, so that it releases the lock
|
||||||
|
release()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(lockWait):
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Fatal("waited for the lock before asking")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCommandsCheckAgainOnceLocked checks that commands which check what
|
||||||
|
// they change before they ask for anything, and so before they take the
|
||||||
|
// state directory lock, check again once they hold it: another command may
|
||||||
|
// have changed it in the meantime. Each runs while the test holds the lock,
|
||||||
|
// given what it needs to ask for nothing. Once it waits for the lock, the
|
||||||
|
// test makes the change another command could have made, then releases the
|
||||||
|
// lock. The command must fail, and change nothing.
|
||||||
|
//
|
||||||
|
//nolint:paralleltest // waitingForLock sees any test's command waiting for the lock
|
||||||
|
func TestCommandsCheckAgainOnceLocked(t *testing.T) {
|
||||||
|
fingerprint := newTestGPGKey(t)
|
||||||
|
|
||||||
|
mnemonic := testMnemonicBuffer(t)
|
||||||
|
|
||||||
|
passphrase := memguard.NewBufferFromBytes([]byte(testPassphrase))
|
||||||
|
t.Cleanup(passphrase.Destroy)
|
||||||
|
|
||||||
|
// work returns the vault "work" on fs, opened with the mnemonic
|
||||||
|
work := func(fs afero.Fs) *vault.Vault {
|
||||||
|
vlt := vault.NewVault(fs, testStateDir, "work")
|
||||||
|
vlt.Mnemonic = mnemonic
|
||||||
|
|
||||||
|
return vlt
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
run func(cli *Instance) error
|
||||||
|
// change is what another command changes while run waits
|
||||||
|
change func(fs afero.Fs) error
|
||||||
|
// want is wrapped in the error the command fails with
|
||||||
|
want error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"unlocker add, vault removed",
|
||||||
|
func(cli *Instance) error {
|
||||||
|
cli.Mnemonic, cli.UnlockPassphrase = mnemonic, passphrase
|
||||||
|
|
||||||
|
return cli.UnlockersAdd("passphrase", cli.cmd)
|
||||||
|
},
|
||||||
|
func(fs afero.Fs) error {
|
||||||
|
return fs.RemoveAll(testStateDir + "/vaults.d/work")
|
||||||
|
},
|
||||||
|
vault.ErrVaultNotFound,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unlocker add pgp, key added",
|
||||||
|
func(cli *Instance) error {
|
||||||
|
cli.Mnemonic = mnemonic
|
||||||
|
cli.cmd.Flags().String("keyid", unreadableTestGPGUserID, "")
|
||||||
|
|
||||||
|
return cli.UnlockersAdd("pgp", cli.cmd)
|
||||||
|
},
|
||||||
|
func(fs afero.Fs) error {
|
||||||
|
_, err := secret.CreatePGPUnlocker(work(fs),
|
||||||
|
unreadableTestGPGUserID, fingerprint)
|
||||||
|
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
errGPGKeyAlreadyUnlocker,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Without the mnemonic or a passphrase, replacing the secret
|
||||||
|
// would ask for the passphrase; it does not exist yet, so the
|
||||||
|
// add asks for nothing
|
||||||
|
"add --force, secret added",
|
||||||
|
func(cli *Instance) error {
|
||||||
|
return cli.AddSecret("added", true)
|
||||||
|
},
|
||||||
|
func(fs afero.Fs) error {
|
||||||
|
value := memguard.NewBufferFromBytes([]byte("other"))
|
||||||
|
defer value.Destroy()
|
||||||
|
|
||||||
|
return work(fs).AddSecret("added", value, false)
|
||||||
|
},
|
||||||
|
errAddedMeanwhile,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
requireChecksAgainOnceLocked(t, tc.run, tc.change, tc.want)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// requireChecksAgainOnceLocked starts run while holding the state directory
|
||||||
|
// lock, as startWhileLocked does. Once run waits for the lock, change makes
|
||||||
|
// the change that another command could have made in the meantime, and the
|
||||||
|
// lock is released. run must then fail, with an error wrapping want, and
|
||||||
|
// change nothing.
|
||||||
|
func requireChecksAgainOnceLocked(
|
||||||
|
t *testing.T, run func(cli *Instance) error,
|
||||||
|
change func(fs afero.Fs) error, want error,
|
||||||
|
) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
release, done := startWhileLocked(t, fs, run)
|
||||||
|
|
||||||
|
requireWaitingForLock(t, done)
|
||||||
|
require.NoError(t, change(fs))
|
||||||
|
|
||||||
|
// Left out: taking and releasing the lock writes the lock file
|
||||||
|
lockFile := testStateDir + "/lock"
|
||||||
|
|
||||||
|
before := stateDirModTimes(t, fs)
|
||||||
|
delete(before, lockFile)
|
||||||
|
|
||||||
|
release()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-done:
|
||||||
|
require.ErrorIs(t, err, want)
|
||||||
|
case <-time.After(lockWait):
|
||||||
|
t.Fatal("did not finish once the lock was released")
|
||||||
|
}
|
||||||
|
|
||||||
|
after := stateDirModTimes(t, fs)
|
||||||
|
delete(after, lockFile)
|
||||||
|
|
||||||
|
assert.Equal(t, before, after, "changed the state directory")
|
||||||
|
}
|
||||||
|
|
||||||
// TestEncryptWithExistingKeyTakesNoLock checks that secret encrypt with a
|
// TestEncryptWithExistingKeyTakesNoLock checks that secret encrypt with a
|
||||||
// key that already exists, which only reads the state directory, finishes
|
// key that already exists, which only reads the state directory, finishes
|
||||||
// while another command holds the state directory lock.
|
// while another command holds the state directory lock.
|
||||||
|
|||||||
+58
-17
@@ -47,6 +47,10 @@ var (
|
|||||||
errCrossVaultSourceUnqualified = errors.New(
|
errCrossVaultSourceUnqualified = errors.New(
|
||||||
"source must specify vault (e.g., vault:secret) for cross-vault move")
|
"source must specify vault (e.g., vault:secret) for cross-vault move")
|
||||||
errMoveOntoItself = errors.New("cannot be moved onto itself")
|
errMoveOntoItself = errors.New("cannot be moved onto itself")
|
||||||
|
// errAddedMeanwhile carries only the message tail; storeSecret composes
|
||||||
|
// "secret <name> was added by another command ...".
|
||||||
|
errAddedMeanwhile = errors.New("was added by another command in the " +
|
||||||
|
"meantime, so nothing was changed; run the command again to replace it")
|
||||||
)
|
)
|
||||||
|
|
||||||
// ParseVaultSecretRef parses a "vault:secret" or just "secret" reference
|
// ParseVaultSecretRef parses a "vault:secret" or just "secret" reference
|
||||||
@@ -422,26 +426,70 @@ func (cli *Instance) AddSecret(secretName string, force bool) error {
|
|||||||
// Locked only now that stdin has been read: in `secret encrypt key |
|
// Locked only now that stdin has been read: in `secret encrypt key |
|
||||||
// secret add name`, holding the lock while reading would leave each
|
// secret add name`, holding the lock while reading would leave each
|
||||||
// command waiting for the other.
|
// command waiting for the other.
|
||||||
|
return cli.storeSecret(vlt, secretName, valueBuffer, force)
|
||||||
|
}
|
||||||
|
|
||||||
|
// storeSecret adds the secret name with value to vlt, as vault.AddSecret
|
||||||
|
// does, holding the state directory lock while it does. With force, adding
|
||||||
|
// an existing secret replaces its current version, which needs the vault's
|
||||||
|
// long-term key. Getting the key can ask for the passphrase, so storeSecret
|
||||||
|
// gets it before it takes the lock, and no other command waits while the
|
||||||
|
// user answers. If, once the lock is held, the add replaces a version after
|
||||||
|
// all, because another command added the secret in the meantime, it fails
|
||||||
|
// rather than ask with the lock held.
|
||||||
|
func (cli *Instance) storeSecret(
|
||||||
|
vlt *vault.Vault, name string, value *memguard.LockedBuffer, force bool,
|
||||||
|
) error {
|
||||||
|
err := vault.ValidateSecretName(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
replacing, err := replacesVersion(vlt, name, force)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if replacing {
|
||||||
|
_, err = vlt.GetOrDeriveLongTermKey()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get long-term key: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
release, err := vault.LockStateDir(cli.fs, cli.stateDir)
|
release, err := vault.LockStateDir(cli.fs, cli.stateDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer release()
|
defer release()
|
||||||
|
|
||||||
// Add the secret to the vault
|
if !replacing {
|
||||||
secret.Debug("Calling vault.AddSecret", "secret_name", secretName,
|
replacing, err = replacesVersion(vlt, name, force)
|
||||||
"value_length", valueBuffer.Size(), "force", force)
|
|
||||||
|
|
||||||
err = vlt.AddSecret(secretName, valueBuffer, force)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
secret.Debug("vault.AddSecret failed", "error", err)
|
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
secret.Debug("vault.AddSecret completed successfully")
|
if replacing {
|
||||||
|
return fmt.Errorf("secret %s %w", name, errAddedMeanwhile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return vlt.AddSecret(name, value, force)
|
||||||
|
}
|
||||||
|
|
||||||
|
// replacesVersion reports whether adding the secret name to vlt replaces
|
||||||
|
// the secret's current version: whether force is set and the secret has one.
|
||||||
|
func replacesVersion(vlt *vault.Vault, name string, force bool) (bool, error) {
|
||||||
|
if !force {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
exists, err := secret.NewSecret(vlt, name).Exists()
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to check if secret exists: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return exists, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSecret retrieves and prints the current version of a secret
|
// GetSecret retrieves and prints the current version of a secret
|
||||||
@@ -707,14 +755,7 @@ func (cli *Instance) ImportSecret(
|
|||||||
|
|
||||||
// Locked only now that the file has been read, as in AddSecret: the
|
// Locked only now that the file has been read, as in AddSecret: the
|
||||||
// file may be a pipe written by another secret command.
|
// file may be a pipe written by another secret command.
|
||||||
release, err := vault.LockStateDir(cli.fs, cli.stateDir)
|
err = cli.storeSecret(vlt, secretName, valueBuffer, force)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer release()
|
|
||||||
|
|
||||||
// Store the secret in the vault
|
|
||||||
err = vlt.AddSecret(secretName, valueBuffer, force)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
+72
-20
@@ -404,12 +404,6 @@ func (cli *Instance) printUnlockersTable(unlockers []UnlockerInfo) error {
|
|||||||
|
|
||||||
// UnlockersAdd adds a new unlocker
|
// UnlockersAdd adds a new unlocker
|
||||||
func (cli *Instance) UnlockersAdd(unlockerType string, cmd *cobra.Command) error {
|
func (cli *Instance) UnlockersAdd(unlockerType string, cmd *cobra.Command) error {
|
||||||
release, err := vault.LockStateDir(cli.fs, cli.stateDir)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer release()
|
|
||||||
|
|
||||||
switch unlockerType {
|
switch unlockerType {
|
||||||
case unlockerTypePassphrase:
|
case unlockerTypePassphrase:
|
||||||
return cli.addPassphraseUnlocker(cmd)
|
return cli.addPassphraseUnlocker(cmd)
|
||||||
@@ -423,6 +417,34 @@ func (cli *Instance) UnlockersAdd(unlockerType string, cmd *cobra.Command) error
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// unlockThenLock gets the long-term key of vlt, the vault an unlocker is
|
||||||
|
// added to, then takes the state directory lock and returns the function
|
||||||
|
// that releases it. Getting the key can ask for a passphrase, so it comes
|
||||||
|
// before the lock, as the question of askThenLock does, and no other
|
||||||
|
// command waits while the user answers. Once the lock is held, it fails if
|
||||||
|
// another command has removed the vault in the meantime, since writing the
|
||||||
|
// unlocker would create the vault's directory again, holding nothing else.
|
||||||
|
func (cli *Instance) unlockThenLock(vlt *vault.Vault) (func(), error) {
|
||||||
|
_, err := vlt.GetOrDeriveLongTermKey()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get long-term key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
release, err := vault.LockStateDir(cli.fs, cli.stateDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = cli.existingVault(vlt.GetName())
|
||||||
|
if err != nil {
|
||||||
|
release()
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return release, nil
|
||||||
|
}
|
||||||
|
|
||||||
// autoSelectUnlocker selects the newly created unlocker as current,
|
// autoSelectUnlocker selects the newly created unlocker as current,
|
||||||
// printing a warning if selection fails
|
// printing a warning if selection fails
|
||||||
func autoSelectUnlocker(cmd *cobra.Command, vlt *vault.Vault, unlockerID string) {
|
func autoSelectUnlocker(cmd *cobra.Command, vlt *vault.Vault, unlockerID string) {
|
||||||
@@ -442,9 +464,6 @@ func (cli *Instance) addPassphraseUnlocker(cmd *cobra.Command) error {
|
|||||||
return fmt.Errorf("failed to get current vault: %w", err)
|
return fmt.Errorf("failed to get current vault: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// For passphrase unlockers, we don't need the vault to be unlocked
|
|
||||||
// The CreatePassphraseUnlocker method will handle getting the
|
|
||||||
// long-term key
|
|
||||||
vlt.Mnemonic, vlt.UnlockPassphrase = cli.Mnemonic, cli.UnlockPassphrase
|
vlt.Mnemonic, vlt.UnlockPassphrase = cli.Mnemonic, cli.UnlockPassphrase
|
||||||
|
|
||||||
// The new passphrase is asked for even when SB_UNLOCK_PASSPHRASE is
|
// The new passphrase is asked for even when SB_UNLOCK_PASSPHRASE is
|
||||||
@@ -464,6 +483,12 @@ func (cli *Instance) addPassphraseUnlocker(cmd *cobra.Command) error {
|
|||||||
defer passphraseBuffer.Destroy()
|
defer passphraseBuffer.Destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
release, err := cli.unlockThenLock(vlt)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer release()
|
||||||
|
|
||||||
passphraseUnlocker, err := vlt.CreatePassphraseUnlocker(passphraseBuffer)
|
passphraseUnlocker, err := vlt.CreatePassphraseUnlocker(passphraseBuffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -502,12 +527,13 @@ func (cli *Instance) addPGPUnlocker(cmd *cobra.Command) error {
|
|||||||
cmd.Printf("Using default GPG key: %s\n", gpgKeyID)
|
cmd.Printf("Using default GPG key: %s\n", gpgKeyID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this key is already added as an unlocker
|
|
||||||
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get current vault: %w", err)
|
return fmt.Errorf("failed to get current vault: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
vlt.Mnemonic, vlt.UnlockPassphrase = cli.Mnemonic, cli.UnlockPassphrase
|
||||||
|
|
||||||
// Resolve the GPG key ID to its fingerprint, once: the duplicate check
|
// Resolve the GPG key ID to its fingerprint, once: the duplicate check
|
||||||
// and the new unlocker's metadata both use this result
|
// and the new unlocker's metadata both use this result
|
||||||
fingerprint, err := secret.ResolveGPGKeyFingerprint(gpgKeyID)
|
fingerprint, err := secret.ResolveGPGKeyFingerprint(gpgKeyID)
|
||||||
@@ -515,21 +541,26 @@ func (cli *Instance) addPGPUnlocker(cmd *cobra.Command) error {
|
|||||||
return fmt.Errorf("failed to resolve GPG key fingerprint: %w", err)
|
return fmt.Errorf("failed to resolve GPG key fingerprint: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this GPG key is already added
|
// Refuse a key that is already an unlocker before asking for anything,
|
||||||
exists, err := cli.pgpUnlockerExists(vlt, fingerprint)
|
// and again once the lock is held, since another command may have added
|
||||||
|
// it in the meantime
|
||||||
|
err = cli.checkPGPKeyNotAdded(vlt, gpgKeyID, fingerprint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf(
|
return err
|
||||||
"could not check whether GPG key %s is already an unlocker: %w",
|
|
||||||
gpgKeyID, err,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if exists {
|
release, err := cli.unlockThenLock(vlt)
|
||||||
return fmt.Errorf("GPG key %s %w", gpgKeyID, errGPGKeyAlreadyUnlocker)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer release()
|
||||||
|
|
||||||
|
err = cli.checkPGPKeyNotAdded(vlt, gpgKeyID, fingerprint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
pgpUnlocker, err := secret.CreatePGPUnlocker(cli.fs, cli.stateDir,
|
pgpUnlocker, err := secret.CreatePGPUnlocker(vlt, gpgKeyID, fingerprint)
|
||||||
gpgKeyID, fingerprint, cli.Mnemonic, cli.UnlockPassphrase)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -704,6 +735,27 @@ func (cli *Instance) UnlockerSelect(unlockerID string) error {
|
|||||||
return vlt.SelectUnlocker(unlockerID)
|
return vlt.SelectUnlocker(unlockerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkPGPKeyNotAdded fails when vlt already has a PGP unlocker for the GPG
|
||||||
|
// key gpgKeyID, whose fingerprint is fingerprint, or when that cannot be
|
||||||
|
// checked.
|
||||||
|
func (cli *Instance) checkPGPKeyNotAdded(
|
||||||
|
vlt *vault.Vault, gpgKeyID, fingerprint string,
|
||||||
|
) error {
|
||||||
|
exists, err := cli.pgpUnlockerExists(vlt, fingerprint)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"could not check whether GPG key %s is already an unlocker: %w",
|
||||||
|
gpgKeyID, err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
return fmt.Errorf("GPG key %s %w", gpgKeyID, errGPGKeyAlreadyUnlocker)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// pgpUnlockerExists reports whether the vault already has a PGP unlocker
|
// pgpUnlockerExists reports whether the vault already has a PGP unlocker
|
||||||
// for the GPG key with the given fingerprint. It returns an error, and no
|
// for the GPG key with the given fingerprint. It returns an error, and no
|
||||||
// answer, when unlockers.d or an unlocker's metadata file cannot be read;
|
// answer, when unlockers.d or an unlocker's metadata file cannot be read;
|
||||||
|
|||||||
@@ -9,11 +9,12 @@
|
|||||||
// "nothing there" and go ahead.
|
// "nothing there" and go ahead.
|
||||||
//
|
//
|
||||||
// The tests make the look fail with a wrapper around the in-memory
|
// The tests make the look fail with a wrapper around the in-memory
|
||||||
// filesystem, which the state directory lock refuses. So they call the
|
// filesystem, which the state directory lock refuses. So they call a
|
||||||
// function each command runs once it holds the lock, such as addPGPUnlocker
|
// function that makes the look before any lock is taken: addPGPUnlocker for
|
||||||
// for UnlockersAdd, or, for a removal, the function that makes its checks,
|
// UnlockersAdd, which looks again once it holds the lock, or, for a removal,
|
||||||
// such as findVaultToRemove for RemoveVault, which runs again under the
|
// the function that makes its checks, such as findVaultToRemove for
|
||||||
// lock before anything is removed, with --force or without.
|
// RemoveVault, which runs again under the lock before anything is removed,
|
||||||
|
// with --force or without.
|
||||||
|
|
||||||
//nolint:testpackage // white-box test of unexported internals
|
//nolint:testpackage // white-box test of unexported internals
|
||||||
package cli
|
package cli
|
||||||
|
|||||||
+12
-9
@@ -222,18 +222,14 @@ func (cli *Instance) resolvePassphrase() (*memguard.LockedBuffer, func(), error)
|
|||||||
return passphraseBuffer, passphraseBuffer.Destroy, nil
|
return passphraseBuffer, passphraseBuffer.Destroy, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateVault creates a new vault
|
// CreateVault creates a new vault. It asks for the mnemonic and the
|
||||||
|
// passphrase before it takes the state directory lock, so that no other
|
||||||
|
// command waits while the user answers.
|
||||||
func (cli *Instance) CreateVault(cmd *cobra.Command, name string) error {
|
func (cli *Instance) CreateVault(cmd *cobra.Command, name string) error {
|
||||||
secret.Debug("Creating new vault", "name", name, "state_dir", cli.stateDir)
|
secret.Debug("Creating new vault", "name", name, "state_dir", cli.stateDir)
|
||||||
|
|
||||||
release, err := vault.LockStateDir(cli.fs, cli.stateDir)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer release()
|
|
||||||
|
|
||||||
// Refuse an invalid name or an existing vault before asking for anything
|
// Refuse an invalid name or an existing vault before asking for anything
|
||||||
err = vault.CheckNewVaultName(cli.fs, cli.stateDir, name)
|
err := vault.CheckNewVaultName(cli.fs, cli.stateDir, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -265,7 +261,14 @@ func (cli *Instance) CreateVault(cmd *cobra.Command, name string) error {
|
|||||||
}
|
}
|
||||||
defer cleanupPassphrase()
|
defer cleanupPassphrase()
|
||||||
|
|
||||||
// Create the vault with its passphrase unlocker
|
release, err := vault.LockStateDir(cli.fs, cli.stateDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer release()
|
||||||
|
|
||||||
|
// Create the vault with its passphrase unlocker. Another command may
|
||||||
|
// have created it since the check above: vault.CreateVault checks again.
|
||||||
vlt, err := vault.CreateVault(cli.fs, cli.stateDir, name,
|
vlt, err := vault.CreateVault(cli.fs, cli.stateDir, name,
|
||||||
mnemonic, passphraseBuffer)
|
mnemonic, passphraseBuffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -375,8 +375,7 @@ func testCreatePGPUnlocker(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Now create a PGP unlock key (this will use our custom GPGEncryptFunc)
|
// Now create a PGP unlock key (this will use our custom GPGEncryptFunc)
|
||||||
pgpUnlocker, err := secret.CreatePGPUnlocker(
|
pgpUnlocker, err := secret.CreatePGPUnlocker(vlt, keyID, fingerprint)
|
||||||
fs, stateDir, keyID, fingerprint, mnemonic, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create PGP unlock key: %v", err)
|
t.Fatalf("Failed to create PGP unlock key: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -192,58 +192,45 @@ func generatePGPUnlockerName() (string, error) {
|
|||||||
return fmt.Sprintf("%s-pgp-%s", hostname, enrollmentTime), nil
|
return fmt.Sprintf("%s-pgp-%s", hostname, enrollmentTime), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// pgpUnlockerDir returns the current vault and the directory in it for a
|
// pgpUnlockerDir returns the directory in vault for a new PGP unlocker,
|
||||||
// new PGP unlocker, named after the host and the time.
|
// named after the host and the time.
|
||||||
//
|
func pgpUnlockerDir(vault VaultInterface) (string, error) {
|
||||||
//nolint:ireturn // the vault is only available behind VaultInterface
|
|
||||||
func pgpUnlockerDir(
|
|
||||||
fs afero.Fs, stateDir string,
|
|
||||||
) (VaultInterface, string, error) {
|
|
||||||
// Get current vault
|
|
||||||
vault, err := GetCurrentVault(fs, stateDir)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", fmt.Errorf("failed to get current vault: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate the unlocker name based on hostname and time
|
// Generate the unlocker name based on hostname and time
|
||||||
unlockerName, err := generatePGPUnlockerName()
|
unlockerName, err := generatePGPUnlockerName()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", fmt.Errorf("failed to generate unlocker name: %w", err)
|
return "", fmt.Errorf("failed to generate unlocker name: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
vaultDir, err := vault.GetDirectory()
|
vaultDir, err := vault.GetDirectory()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", fmt.Errorf("failed to get vault directory: %w", err)
|
return "", fmt.Errorf("failed to get vault directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return vault, filepath.Join(vaultDir, "unlockers.d", unlockerName), nil
|
return filepath.Join(vaultDir, "unlockers.d", unlockerName), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreatePGPUnlocker creates a new PGP unlocker and stores it in the vault.
|
// CreatePGPUnlocker creates a new PGP unlocker and stores it in vault.
|
||||||
// It encrypts to the GPG key gpgKeyID and records fingerprint, that key's
|
// It encrypts to the GPG key gpgKeyID and records fingerprint, that key's
|
||||||
// fingerprint as ResolveGPGKeyFingerprint returns it, in the metadata.
|
// fingerprint as ResolveGPGKeyFingerprint returns it, in the metadata.
|
||||||
// Everything that can fail short of writing a file is done before anything
|
// Everything that can fail short of writing a file is done before anything
|
||||||
// is written, and the files are written through WriteDir, so a failure
|
// is written, and the files are written through WriteDir, so a failure
|
||||||
// leaves no partial unlocker. The long-term key comes from mnemonic when it
|
// leaves no partial unlocker. The long-term key comes from vault's
|
||||||
// is not nil, else from the current unlocker, which is given passphrase when
|
// GetOrDeriveLongTermKey: the key it holds when it is unlocked, else the
|
||||||
// it is a passphrase unlocker.
|
// key from its mnemonic or its current unlocker.
|
||||||
func CreatePGPUnlocker(
|
func CreatePGPUnlocker(
|
||||||
fs afero.Fs, stateDir, gpgKeyID, fingerprint string,
|
vault VaultInterface, gpgKeyID, fingerprint string,
|
||||||
mnemonic, passphrase *memguard.LockedBuffer,
|
|
||||||
) (*PGPUnlocker, error) {
|
) (*PGPUnlocker, error) {
|
||||||
err := checkGPGAvailable()
|
err := checkGPGAvailable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
vault, unlockerDir, err := pgpUnlockerDir(fs, stateDir)
|
unlockerDir, err := pgpUnlockerDir(vault)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// The vault's GetOrDeriveLongTermKey, in step 2, uses both
|
fs := vault.GetFilesystem()
|
||||||
vault.SetMnemonic(mnemonic)
|
|
||||||
vault.SetUnlockPassphrase(passphrase)
|
|
||||||
|
|
||||||
// Step 1: Generate a new age keypair for the PGP unlocker
|
// Step 1: Generate a new age keypair for the PGP unlocker
|
||||||
ageIdentity, err := age.GenerateX25519Identity()
|
ageIdentity, err := age.GenerateX25519Identity()
|
||||||
@@ -295,7 +282,7 @@ func CreatePGPUnlocker(
|
|||||||
func encryptPGPUnlockerKeys(
|
func encryptPGPUnlockerKeys(
|
||||||
vault VaultInterface, ageIdentity *age.X25519Identity, gpgKeyID string,
|
vault VaultInterface, ageIdentity *age.X25519Identity, gpgKeyID string,
|
||||||
) ([]byte, []byte, error) {
|
) ([]byte, []byte, error) {
|
||||||
// From the mnemonic or the current unlocker, as for a passphrase unlocker
|
// The key vault holds, or else from its mnemonic or current unlocker
|
||||||
ltIdentity, err := vault.GetOrDeriveLongTermKey()
|
ltIdentity, err := vault.GetOrDeriveLongTermKey()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("failed to get long-term key: %w", err)
|
return nil, nil, fmt.Errorf("failed to get long-term key: %w", err)
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ func TestCreatePGPUnlockerFailureWritesNothing(t *testing.T) {
|
|||||||
}}
|
}}
|
||||||
|
|
||||||
_, err = secret.CreatePGPUnlocker(
|
_, err = secret.CreatePGPUnlocker(
|
||||||
fs, testVaultStateDir, testGPGKeyID, testGPGFingerprint, nil, nil)
|
vault.NewVault(fs, testVaultStateDir, testVaultName),
|
||||||
|
testGPGKeyID, testGPGFingerprint)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
vaultDir, err := vlt.GetDirectory()
|
vaultDir, err := vlt.GetDirectory()
|
||||||
@@ -87,17 +88,15 @@ func TestPGPUnlockerAddedTwiceKeepsFirst(t *testing.T) {
|
|||||||
|
|
||||||
fs := afero.NewMemMapFs()
|
fs := afero.NewMemMapFs()
|
||||||
mnemonic := testMnemonicBuffer(t)
|
mnemonic := testMnemonicBuffer(t)
|
||||||
_, err := vault.CreateVault(fs, testVaultStateDir, testVaultName, mnemonic, nil)
|
vlt, err := vault.CreateVault(fs, testVaultStateDir, testVaultName, mnemonic, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
first, err := secret.CreatePGPUnlocker(
|
first, err := secret.CreatePGPUnlocker(vlt, testGPGKeyID, testGPGFingerprint)
|
||||||
fs, testVaultStateDir, testGPGKeyID, testGPGFingerprint, mnemonic, nil)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
firstFiles := dirFiles(t, fs, first.GetDirectory())
|
firstFiles := dirFiles(t, fs, first.GetDirectory())
|
||||||
|
|
||||||
second, err := secret.CreatePGPUnlocker(
|
second, err := secret.CreatePGPUnlocker(vlt, testGPGKeyID, testGPGFingerprint)
|
||||||
fs, testVaultStateDir, testGPGKeyID, testGPGFingerprint, mnemonic, nil)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotEqual(t, first.GetDirectory(), second.GetDirectory())
|
assert.NotEqual(t, first.GetDirectory(), second.GetDirectory())
|
||||||
assert.Equal(t, firstFiles, dirFiles(t, fs, first.GetDirectory()))
|
assert.Equal(t, firstFiles, dirFiles(t, fs, first.GetDirectory()))
|
||||||
|
|||||||
@@ -235,9 +235,9 @@ func processMnemonicForVault(
|
|||||||
// vault: one wrapping ErrInvalidVaultName when it is not a valid vault name,
|
// vault: one wrapping ErrInvalidVaultName when it is not a valid vault name,
|
||||||
// and one wrapping ErrVaultExists when that vault already exists. Creating an
|
// and one wrapping ErrVaultExists when that vault already exists. Creating an
|
||||||
// existing vault again would replace its keys, and its secrets could no
|
// existing vault again would replace its keys, and its secrets could no
|
||||||
// longer be decrypted. CreateVault checks this before writing anything; the
|
// longer be decrypted. The commands that create a vault check it before they
|
||||||
// commands that create a vault also check it before they ask for the
|
// ask for the mnemonic or passphrase, and CreateVault checks it again, under
|
||||||
// mnemonic or passphrase, holding the state directory lock.
|
// the state directory lock, before writing anything.
|
||||||
func CheckNewVaultName(fs afero.Fs, stateDir string, name string) error {
|
func CheckNewVaultName(fs afero.Fs, stateDir string, name string) error {
|
||||||
err := ValidateVaultName(name)
|
err := ValidateVaultName(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user