Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
933bc8fb23 |
@@ -18,14 +18,14 @@ https://git.eeqj.de/sneak/secret/milestone/12
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
- 2026-10-14: A wrong passphrase, from `SB_UNLOCK_PASSPHRASE` or typed at the
|
- 2026-10-14: `secret vault import` is gone
|
||||||
prompt, gives the error `wrong passphrase for unlocker <id>`
|
(https://git.eeqj.de/sneak/secret/issues/139). It took only a vault without a
|
||||||
(https://git.eeqj.de/sneak/secret/issues/157). It used to be age's "no
|
long-term key, and `secret init` and `secret vault create` give every vault
|
||||||
identity matched any of the recipients", followed by advice to give the vault
|
one, so it always failed. A mnemonic enters through those two commands, which
|
||||||
a new unlocker from its mnemonic, which would have replaced an unlocker that
|
the help and the note of `secret generate mnemonic` now name; they named
|
||||||
works. The error now wraps `secret.ErrWrongPassphrase` and carries no advice
|
`secret import`, which imports a file as a secret. `secret vault import` now
|
||||||
to use the mnemonic. A `priv.age` that is not an age file still gets that
|
prints the help of `secret vault`, which cobra prints for any subcommand of
|
||||||
advice.
|
`secret vault` that does not exist.
|
||||||
- 2026-10-14: The passphrase and mnemonic prompts ask on the controlling
|
- 2026-10-14: The passphrase and mnemonic prompts ask on the controlling
|
||||||
terminal (`/dev/tty`) instead of stdin, so `echo v | secret add s1 --force`,
|
terminal (`/dev/tty`) instead of stdin, so `echo v | secret add s1 --force`,
|
||||||
`cat file | secret encrypt key` and `cat file.age | secret decrypt key` ask
|
`cat file | secret encrypt key` and `cat file.age | secret decrypt key` ask
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
// Unlock Failure Tests
|
// Unlock Failure Tests
|
||||||
//
|
//
|
||||||
// When a vault cannot be opened through its current unlocker, because a
|
// When a vault cannot be opened through its current unlocker, because a
|
||||||
// file the unlocker needs is missing, the error keeps its cause and ends by
|
// file the unlocker needs is missing or the passphrase is wrong, the error
|
||||||
// saying that the mnemonic still opens that vault, but only for a vault that
|
// keeps its cause and ends by saying that the mnemonic still opens that
|
||||||
// the mnemonic does open, and not when the passphrase could not be read at
|
// vault, but only for a vault that the mnemonic does open, and not when the
|
||||||
// all. A wrong passphrase gives an error that says so, without that advice.
|
// passphrase could not be read at all. When the current unlocker does not
|
||||||
// When the current unlocker does not exist and the vault has another that
|
// exist and the vault has another that `secret unlocker select` accepts, the
|
||||||
// `secret unlocker select` accepts, the error says to select one instead.
|
// error says to select one instead.
|
||||||
// When a secret's current file is missing, the error says how to make a
|
// When a secret's current file is missing, the error says how to make a
|
||||||
// version current again. Each test that pins such advice also follows it.
|
// version current again. Each test that pins such advice also follows it.
|
||||||
|
|
||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"filippo.io/age"
|
||||||
"github.com/awnumar/memguard"
|
"github.com/awnumar/memguard"
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -164,48 +165,26 @@ func TestUnlockFailureNamesMnemonic(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestWrongPassphraseNamesPassphrase checks the error of `secret get` given
|
// TestWrongPassphraseNamesMnemonic checks the error of `secret get` given a
|
||||||
// a passphrase that does not decrypt the passphrase unlocker: it says that
|
// passphrase that does not decrypt the passphrase unlocker: it keeps age's
|
||||||
// the passphrase is wrong, without the advice to use the mnemonic, since the
|
// error and ends with the advice that the mnemonic still opens the vault.
|
||||||
// unlocker still opens the vault with the right passphrase. The test then
|
func TestWrongPassphraseNamesMnemonic(t *testing.T) {
|
||||||
// gives the right passphrase.
|
|
||||||
func TestWrongPassphraseNamesPassphrase(t *testing.T) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
unlockerDir, c := newUnlockTestCLI(t, newTwoVaultFs(t))
|
_, c := newUnlockTestCLI(t, newTwoVaultFs(t))
|
||||||
c.UnlockPassphrase = memguard.NewBufferFromBytes([]byte("wrong passphrase"))
|
c.UnlockPassphrase = memguard.NewBufferFromBytes([]byte("wrong passphrase"))
|
||||||
t.Cleanup(c.UnlockPassphrase.Destroy)
|
t.Cleanup(c.UnlockPassphrase.Destroy)
|
||||||
|
|
||||||
err := c.GetSecret(discardCmd(), "x")
|
err := c.GetSecret(discardCmd(), "x")
|
||||||
|
|
||||||
require.ErrorIs(t, err, secret.ErrWrongPassphrase)
|
var noMatch *age.NoIdentityMatchError
|
||||||
assert.NotContains(t, err.Error(), mnemonicAdvice)
|
|
||||||
|
require.ErrorAs(t, err, &noMatch)
|
||||||
|
|
||||||
require.EqualError(t, err, "failed to unlock vault: "+
|
require.EqualError(t, err, "failed to unlock vault: "+
|
||||||
"failed to get long-term key: failed to get unlocker identity: "+
|
"failed to get long-term key: failed to get unlocker identity: "+
|
||||||
"wrong passphrase for unlocker "+filepath.Base(unlockerDir))
|
"failed to decrypt unlocker private key: failed to create decryptor: "+
|
||||||
|
noMatch.Error()+mnemonicAdvice)
|
||||||
c.UnlockPassphrase = memguard.NewBufferFromBytes([]byte(testPassphrase))
|
|
||||||
t.Cleanup(c.UnlockPassphrase.Destroy)
|
|
||||||
assert.Equal(t, "value", getSecret(t, c, "x"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestDamagedUnlockerNamesMnemonic checks the error of `secret get` when the
|
|
||||||
// passphrase unlocker's priv.age is not an age file: the passphrase is not
|
|
||||||
// blamed, and the error ends with the advice that the mnemonic still opens
|
|
||||||
// the vault.
|
|
||||||
func TestDamagedUnlockerNamesMnemonic(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
fs := newTwoVaultFs(t)
|
|
||||||
unlockerDir, c := newUnlockTestCLI(t, fs)
|
|
||||||
|
|
||||||
require.NoError(t, afero.WriteFile(fs, filepath.Join(unlockerDir, "priv.age"),
|
|
||||||
[]byte("damaged"), secret.FilePerms))
|
|
||||||
|
|
||||||
err := c.GetSecret(discardCmd(), "x")
|
|
||||||
|
|
||||||
require.NotErrorIs(t, err, secret.ErrWrongPassphrase)
|
|
||||||
require.ErrorContains(t, err, mnemonicAdvice)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestMoveUnlockFailureNamesVault checks the error of `secret move` into
|
// TestMoveUnlockFailureNamesVault checks the error of `secret move` into
|
||||||
|
|||||||
@@ -16,10 +16,6 @@ import (
|
|||||||
// empty. A passphrase unlocker that fails with it was not tried.
|
// empty. A passphrase unlocker that fails with it was not tried.
|
||||||
var ErrPassphraseNotRead = errors.New("failed to read passphrase")
|
var ErrPassphraseNotRead = errors.New("failed to read passphrase")
|
||||||
|
|
||||||
// ErrWrongPassphrase is wrapped in the error of GetIdentity when the
|
|
||||||
// passphrase does not decrypt the unlocker's private key.
|
|
||||||
var ErrWrongPassphrase = errors.New("wrong passphrase")
|
|
||||||
|
|
||||||
// PassphraseUnlocker represents a passphrase-protected unlocker
|
// PassphraseUnlocker represents a passphrase-protected unlocker
|
||||||
type PassphraseUnlocker struct {
|
type PassphraseUnlocker struct {
|
||||||
Directory string
|
Directory string
|
||||||
@@ -77,12 +73,6 @@ func (p *PassphraseUnlocker) GetIdentity() (*age.X25519Identity, error) {
|
|||||||
Debug("Failed to decrypt unlocker private key",
|
Debug("Failed to decrypt unlocker private key",
|
||||||
"error", err, "unlocker_id", p.GetID())
|
"error", err, "unlocker_id", p.GetID())
|
||||||
|
|
||||||
// age says that no identity matched when the passphrase is wrong
|
|
||||||
var noMatch *age.NoIdentityMatchError
|
|
||||||
if errors.As(err, &noMatch) {
|
|
||||||
return nil, fmt.Errorf("%w for unlocker %s", ErrWrongPassphrase, p.GetID())
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("failed to decrypt unlocker private key: %w", err)
|
return nil, fmt.Errorf("failed to decrypt unlocker private key: %w", err)
|
||||||
}
|
}
|
||||||
defer privKeyBuffer.Destroy()
|
defer privKeyBuffer.Destroy()
|
||||||
|
|||||||
@@ -291,13 +291,10 @@ func (v *Vault) unlockLongTermKey(
|
|||||||
// without a mnemonic records none, and without its metadata the key cannot
|
// without a mnemonic records none, and without its metadata the key cannot
|
||||||
// be derived. It is not added when the passphrase could not be read: the
|
// be derived. It is not added when the passphrase could not be read: the
|
||||||
// unlocker was not tried, and adding one would need a passphrase read the
|
// unlocker was not tried, and adding one would need a passphrase read the
|
||||||
// same way. Nor is it added when the passphrase is wrong: the unlocker still
|
// same way. Nor is it added when the current unlocker does not exist and the
|
||||||
// opens the vault with the right one. Nor is it added when the current
|
// vault has others: the error already says to select one of them.
|
||||||
// unlocker does not exist and the vault has others: the error already says
|
|
||||||
// to select one of them.
|
|
||||||
func (v *Vault) withMnemonicAdvice(err error) error {
|
func (v *Vault) withMnemonicAdvice(err error) error {
|
||||||
if errors.Is(err, secret.ErrPassphraseNotRead) ||
|
if errors.Is(err, secret.ErrPassphraseNotRead) ||
|
||||||
errors.Is(err, secret.ErrWrongPassphrase) ||
|
|
||||||
errors.Is(err, ErrUnlockerNotFound) {
|
errors.Is(err, ErrUnlockerNotFound) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"filippo.io/age"
|
||||||
"github.com/awnumar/memguard"
|
"github.com/awnumar/memguard"
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -139,7 +140,9 @@ func TestAddSecretForceWithWrongPassphraseChangesNothing(t *testing.T) {
|
|||||||
|
|
||||||
err = vlt.AddSecret(testSecretName, second, true)
|
err = vlt.AddSecret(testSecretName, second, true)
|
||||||
|
|
||||||
require.ErrorIs(t, err, secret.ErrWrongPassphrase)
|
var noMatch *age.NoIdentityMatchError
|
||||||
|
|
||||||
|
require.ErrorAs(t, err, &noMatch)
|
||||||
assert.Equal(t, before, dirContents(t, fs, secretDir))
|
assert.Equal(t, before, dirContents(t, fs, secretDir))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user