Compare commits
3
Commits
0982d45868
...
7a80f4b882
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a80f4b882 | ||
|
|
6bc95f7c69 | ||
|
|
fdfe2c92f6 |
@@ -142,7 +142,11 @@ value may be over 100 MiB, whatever the limit.
|
|||||||
|
|
||||||
#### `secret add <secret-name> [--force]`
|
#### `secret add <secret-name> [--force]`
|
||||||
|
|
||||||
Adds a secret to the current vault. Reads the secret value from stdin.
|
Adds a secret to the current vault. Reads the secret value from stdin. When the
|
||||||
|
value ends in a newline, that one newline is dropped, so
|
||||||
|
`echo value | secret add name` stores `value`; from `printf 'a\n\n'` it stores
|
||||||
|
`a` and one newline. To keep a file's final newline, as in a PEM key, use
|
||||||
|
`secret import`, which stores the file's bytes unchanged.
|
||||||
|
|
||||||
- `--force, -f`: Overwrite existing secret
|
- `--force, -f`: Overwrite existing secret
|
||||||
|
|
||||||
@@ -155,13 +159,13 @@ contain `//`, or have `..` as a path segment.
|
|||||||
|
|
||||||
#### `secret get <secret-name> [--version <version>]`
|
#### `secret get <secret-name> [--version <version>]`
|
||||||
|
|
||||||
Retrieves and outputs a secret value to stdout. A failed write of the value,
|
Writes the secret's value to stdout exactly as stored, with no newline added
|
||||||
such as to a file on a full disk, prints the error and exits 1, with two
|
after it. A failed write of the value, such as to a file on a full disk, prints
|
||||||
exceptions. If the program reading from a pipe has already exited, as in
|
the error and exits 1, with two exceptions. If the program reading from a pipe
|
||||||
`secret get s | true`, the broken-pipe signal (SIGPIPE) ends `secret` with no
|
has already exited, as in `secret get s | true`, the broken-pipe signal
|
||||||
message and the shell reports exit status 141. If the shell closed stdout before
|
(SIGPIPE) ends `secret` with no message and the shell reports exit status 141.
|
||||||
`secret` started (`secret get s >&-`), the value is written to `/dev/null` and
|
If the shell closed stdout before `secret` started (`secret get s >&-`), the
|
||||||
`secret get` exits 0.
|
value is written to `/dev/null` and `secret get` exits 0.
|
||||||
|
|
||||||
- `--version, -v`: Get a specific version (default: current)
|
- `--version, -v`: Get a specific version (default: current)
|
||||||
|
|
||||||
@@ -267,6 +271,12 @@ Creates a new unlocker of the specified type:
|
|||||||
A vault has one passphrase unlocker: adding one replaces the one the vault has,
|
A vault has one passphrase unlocker: adding one replaces the one the vault has,
|
||||||
which is removed only once the new one is the current unlocker.
|
which is removed only once the new one is the current unlocker.
|
||||||
|
|
||||||
|
`secret unlocker add passphrase` asks on the terminal for the new passphrase
|
||||||
|
even when `SB_UNLOCK_PASSPHRASE` is set, and uses the variable to unlock the
|
||||||
|
vault. With no controlling terminal, as under cron or in a CI job, the new
|
||||||
|
unlocker takes its passphrase from `SB_UNLOCK_PASSPHRASE`, and the command says
|
||||||
|
so.
|
||||||
|
|
||||||
#### `secret unlocker remove <unlocker-id> [--force]` / `secret unlocker rm` ⚠️ 🛑
|
#### `secret unlocker remove <unlocker-id> [--force]` / `secret unlocker rm` ⚠️ 🛑
|
||||||
|
|
||||||
**DANGER**: Permanently removes an unlocker. It first asks for confirmation,
|
**DANGER**: Permanently removes an unlocker. It first asks for confirmation,
|
||||||
@@ -300,7 +310,7 @@ Selects an unlocker as the current default for operations.
|
|||||||
#### `secret import <secret-name> --source <filename>`
|
#### `secret import <secret-name> --source <filename>`
|
||||||
|
|
||||||
Imports a secret from a file and stores it in the current vault under the given
|
Imports a secret from a file and stores it in the current vault under the given
|
||||||
name.
|
name. The file's bytes are stored unchanged, a final newline included.
|
||||||
|
|
||||||
### Encryption Operations
|
### Encryption Operations
|
||||||
|
|
||||||
|
|||||||
@@ -138,6 +138,148 @@ func TestInitWritesNoBlankLineAfterMnemonic(t *testing.T) {
|
|||||||
"stderr: %q", stderr.String())
|
"stderr: %q", stderr.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestUnlockerAddAsksForNewPassphrase is a regression test for
|
||||||
|
// https://git.eeqj.de/sneak/secret/issues/154: with SB_UNLOCK_PASSPHRASE set,
|
||||||
|
// `secret unlocker add passphrase` gave the new unlocker the passphrase in
|
||||||
|
// the variable without asking. At a terminal it must ask for the new
|
||||||
|
// passphrase there, twice, and unlock the vault with the variable, so that
|
||||||
|
// `secret get` then opens the vault with the passphrase typed. pty.Open is
|
||||||
|
// described above TestRemoveIgnoresTerminalOnStdout.
|
||||||
|
func TestUnlockerAddAsksForNewPassphrase(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const newPassphrase = "new-passphrase"
|
||||||
|
|
||||||
|
stateDir := t.TempDir()
|
||||||
|
|
||||||
|
passphrase := memguard.NewBufferFromBytes([]byte(testPassphrase))
|
||||||
|
defer passphrase.Destroy()
|
||||||
|
|
||||||
|
vlt, err := vault.CreateVault(afero.NewOsFs(), stateDir, "default",
|
||||||
|
testMnemonicBuffer(t), passphrase)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
value := memguard.NewBufferFromBytes([]byte("v1"))
|
||||||
|
defer value.Destroy()
|
||||||
|
|
||||||
|
require.NoError(t, vlt.AddSecret("s1", value, false))
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(t.Context(), commandWait)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
//nolint:gosec // G204: test executes the freshly built secret binary
|
||||||
|
cmd := exec.CommandContext(ctx, secretBinaryPath(t),
|
||||||
|
"unlocker", "add", "passphrase")
|
||||||
|
cmd.Env = []string{
|
||||||
|
secret.EnvStateDir + "=" + stateDir,
|
||||||
|
secret.EnvUnlockPassphrase + "=" + testPassphrase,
|
||||||
|
"PATH=" + os.Getenv("PATH"),
|
||||||
|
"HOME=" + os.Getenv("HOME"),
|
||||||
|
}
|
||||||
|
|
||||||
|
ptmx, tty, err := pty.Open()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
defer func() { _ = ptmx.Close() }()
|
||||||
|
|
||||||
|
deadline, _ := ctx.Deadline()
|
||||||
|
require.NoError(t, ptmx.SetReadDeadline(deadline))
|
||||||
|
|
||||||
|
cmd.Stderr = tty
|
||||||
|
// Ctty is a descriptor in the new process: 2, its stderr.
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true, Setctty: true, Ctty: 2}
|
||||||
|
|
||||||
|
require.NoError(t, cmd.Start())
|
||||||
|
|
||||||
|
_ = tty.Close()
|
||||||
|
|
||||||
|
var (
|
||||||
|
shown []byte
|
||||||
|
char byte
|
||||||
|
)
|
||||||
|
|
||||||
|
terminal := bufio.NewReader(ptmx)
|
||||||
|
|
||||||
|
for _, prompt := range []string{
|
||||||
|
"Enter passphrase for unlocker: ", "Confirm passphrase: ",
|
||||||
|
} {
|
||||||
|
for !bytes.HasSuffix(shown, []byte(prompt)) {
|
||||||
|
char, err = terminal.ReadByte()
|
||||||
|
require.NoError(t, err,
|
||||||
|
"secret unlocker add passphrase did not ask on the terminal: %s",
|
||||||
|
shown)
|
||||||
|
|
||||||
|
shown = append(shown, char)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = ptmx.WriteString(newPassphrase + "\n")
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The read ends once the command has exited and so closed the terminal.
|
||||||
|
rest, _ := io.ReadAll(terminal)
|
||||||
|
|
||||||
|
require.NoError(t, cmd.Wait(),
|
||||||
|
"secret unlocker add passphrase: %s%s", shown, rest)
|
||||||
|
|
||||||
|
// The binary, not this test process, opens the vault: the new unlocker
|
||||||
|
// has age's full scrypt work factor, which is slow under the race
|
||||||
|
// detector.
|
||||||
|
assert.Equal(t, "v1",
|
||||||
|
runWithoutTerminal(t, stateDir, newPassphrase, "get", "s1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestUnlockerAddWithoutTerminalSaysPassphraseIsVariable is a regression
|
||||||
|
// test for https://git.eeqj.de/sneak/secret/issues/154: with no terminal to
|
||||||
|
// ask on, `secret unlocker add passphrase` gives the new unlocker the
|
||||||
|
// passphrase in SB_UNLOCK_PASSPHRASE, and its output must say so.
|
||||||
|
func TestUnlockerAddWithoutTerminalSaysPassphraseIsVariable(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
stateDir := t.TempDir()
|
||||||
|
|
||||||
|
passphrase := memguard.NewBufferFromBytes([]byte(testPassphrase))
|
||||||
|
defer passphrase.Destroy()
|
||||||
|
|
||||||
|
_, err := vault.CreateVault(afero.NewOsFs(), stateDir, "default",
|
||||||
|
testMnemonicBuffer(t), passphrase)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
output := runWithoutTerminal(t, stateDir, testPassphrase,
|
||||||
|
"unlocker", "add", "passphrase")
|
||||||
|
|
||||||
|
assert.Contains(t, output, "The new unlocker takes its passphrase "+
|
||||||
|
"from SB_UNLOCK_PASSPHRASE, since there is no terminal to ask for a "+
|
||||||
|
"new one on\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// runWithoutTerminal runs the secret binary with args and SB_UNLOCK_PASSPHRASE
|
||||||
|
// set to passphrase, in a new session, which has no terminal, and returns
|
||||||
|
// what it printed once it has succeeded.
|
||||||
|
func runWithoutTerminal(
|
||||||
|
t *testing.T, stateDir, passphrase string, args ...string,
|
||||||
|
) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(t.Context(), commandWait)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
//nolint:gosec // G204: test executes the freshly built secret binary
|
||||||
|
cmd := exec.CommandContext(ctx, secretBinaryPath(t), args...)
|
||||||
|
cmd.Env = []string{
|
||||||
|
secret.EnvStateDir + "=" + stateDir,
|
||||||
|
secret.EnvUnlockPassphrase + "=" + passphrase,
|
||||||
|
"PATH=" + os.Getenv("PATH"),
|
||||||
|
"HOME=" + os.Getenv("HOME"),
|
||||||
|
}
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||||
|
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
require.NoError(t, err, "secret %v: %s", args, output)
|
||||||
|
|
||||||
|
return string(output)
|
||||||
|
}
|
||||||
|
|
||||||
// runAtTerminal runs the secret binary with args and input on stdin, in a
|
// runAtTerminal runs the secret binary with args and input on stdin, in a
|
||||||
// new session whose controlling terminal is a new terminal, which is also
|
// new session whose controlling terminal is a new terminal, which is also
|
||||||
// its stderr: a command typed at a terminal with its input piped. pty.Open
|
// its stderr: a command typed at a terminal with its input piped. pty.Open
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
//nolint:testpackage // white-box test of unexported internals
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestAddDropsOneTrailingNewlineImportKeepsIt pins the rule the README
|
||||||
|
// gives: `secret add` drops one trailing newline from stdin, and
|
||||||
|
// `secret import` stores the file's bytes unchanged.
|
||||||
|
func TestAddDropsOneTrailingNewlineImportKeepsIt(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
fs, vlt := newSizeTestVault(t)
|
||||||
|
value := []byte("a\n\n")
|
||||||
|
|
||||||
|
cmd := &cobra.Command{}
|
||||||
|
cmd.SetIn(bytes.NewReader(value))
|
||||||
|
|
||||||
|
cli := NewCLIInstanceWithStateDir(fs, testStateDir)
|
||||||
|
cli.cmd = cmd
|
||||||
|
|
||||||
|
require.NoError(t, cli.AddSecret("added", false))
|
||||||
|
|
||||||
|
sourceFile := "/test/value"
|
||||||
|
require.NoError(t, afero.WriteFile(fs, sourceFile, value, 0o600))
|
||||||
|
require.NoError(t, cli.ImportSecret(cmd, "imported", sourceFile, false))
|
||||||
|
|
||||||
|
added, err := vlt.GetSecret("added")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
defer added.Destroy()
|
||||||
|
|
||||||
|
imported, err := vlt.GetSecret("imported")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
defer imported.Destroy()
|
||||||
|
|
||||||
|
assert.Equal(t, []byte("a\n"), added.Bytes())
|
||||||
|
assert.Equal(t, []byte("a\n\n"), imported.Bytes())
|
||||||
|
}
|
||||||
@@ -441,15 +441,20 @@ func (cli *Instance) addPassphraseUnlocker(cmd *cobra.Command) error {
|
|||||||
// long-term key
|
// long-term key
|
||||||
vlt.Mnemonic, vlt.UnlockPassphrase = cli.Mnemonic, cli.UnlockPassphrase
|
vlt.Mnemonic, vlt.UnlockPassphrase = cli.Mnemonic, cli.UnlockPassphrase
|
||||||
|
|
||||||
// The new unlocker gets the passphrase from the environment, which also
|
// The new passphrase is asked for even when SB_UNLOCK_PASSPHRASE is
|
||||||
// unlocks the current passphrase unlocker, else the one entered here
|
// set, and the variable unlocks the vault. With no terminal to ask on,
|
||||||
passphraseBuffer := cli.UnlockPassphrase
|
// the new unlocker takes the passphrase in the variable.
|
||||||
if passphraseBuffer == nil {
|
fromVariable := false
|
||||||
// Use secure passphrase input with confirmation
|
|
||||||
passphraseBuffer, err = readSecurePassphrase("Enter passphrase for unlocker: ")
|
passphraseBuffer, err := readSecurePassphrase("Enter passphrase for unlocker: ")
|
||||||
if err != nil {
|
|
||||||
return err
|
switch {
|
||||||
}
|
case errors.Is(err, secret.ErrNoTerminal) && cli.UnlockPassphrase != nil:
|
||||||
|
passphraseBuffer = cli.UnlockPassphrase
|
||||||
|
fromVariable = true
|
||||||
|
case err != nil:
|
||||||
|
return err
|
||||||
|
default:
|
||||||
defer passphraseBuffer.Destroy()
|
defer passphraseBuffer.Destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -460,6 +465,12 @@ func (cli *Instance) addPassphraseUnlocker(cmd *cobra.Command) error {
|
|||||||
|
|
||||||
cmd.Printf("Created passphrase unlocker: %s\n", passphraseUnlocker.GetID())
|
cmd.Printf("Created passphrase unlocker: %s\n", passphraseUnlocker.GetID())
|
||||||
|
|
||||||
|
if fromVariable {
|
||||||
|
cmd.Printf("The new unlocker takes its passphrase from %s, since "+
|
||||||
|
"there is no terminal to ask for a new one on\n",
|
||||||
|
secret.EnvUnlockPassphrase)
|
||||||
|
}
|
||||||
|
|
||||||
// CreatePassphraseUnlocker has already made it the current unlocker
|
// CreatePassphraseUnlocker has already made it the current unlocker
|
||||||
cmd.Printf("Automatically selected as current unlocker\n")
|
cmd.Printf("Automatically selected as current unlocker\n")
|
||||||
|
|
||||||
|
|||||||
@@ -15,10 +15,13 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
errNilPassphraseBuffer = errors.New("passphrase buffer is nil")
|
errNilPassphraseBuffer = errors.New("passphrase buffer is nil")
|
||||||
errNoTerminal = errors.New("there is no terminal to ask on")
|
|
||||||
errNothingEntered = errors.New("nothing was entered")
|
errNothingEntered = errors.New("nothing was entered")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ErrNoTerminal is wrapped in the error of ReadPassphrase and ReadMnemonic
|
||||||
|
// when the process has no controlling terminal to ask on.
|
||||||
|
var ErrNoTerminal = errors.New("there is no terminal to ask on")
|
||||||
|
|
||||||
// ErrMnemonicNotRead is wrapped in every error of ReadMnemonic: there is no
|
// ErrMnemonicNotRead is wrapped in every error of ReadMnemonic: there is no
|
||||||
// terminal to read the mnemonic from, reading it failed, or it was empty.
|
// terminal to read the mnemonic from, reading it failed, or it was empty.
|
||||||
var ErrMnemonicNotRead = errors.New("failed to read mnemonic")
|
var ErrMnemonicNotRead = errors.New("failed to read mnemonic")
|
||||||
@@ -203,7 +206,7 @@ func readFromTerminal(
|
|||||||
|
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf(
|
||||||
"%w: %w. Please set the %s environment variable or run interactively",
|
"%w: %w. Please set the %s environment variable or run interactively",
|
||||||
notRead, errNoTerminal, envVar)
|
notRead, ErrNoTerminal, envVar)
|
||||||
}
|
}
|
||||||
defer func() { _ = terminal.Close() }()
|
defer func() { _ = terminal.Close() }()
|
||||||
|
|
||||||
|
|||||||
@@ -388,7 +388,8 @@ func ResolveGPGKeyFingerprint(keyID string) (string, error) {
|
|||||||
return "", fmt.Errorf("%w: %s", ErrGPGKeyNotFound, keyID)
|
return "", fmt.Errorf("%w: %s", ErrGPGKeyNotFound, keyID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", fmt.Errorf("failed to resolve GPG key fingerprint: %w", err)
|
return "", fmt.Errorf(
|
||||||
|
"failed to resolve GPG key fingerprint: %w", withGPGStderr(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the output to extract the fingerprint
|
// Parse the output to extract the fingerprint
|
||||||
@@ -418,6 +419,24 @@ func checkGPGAvailable() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// withGPGStderr appends gpg's stderr, which says why gpg failed, to err
|
||||||
|
// from cmd.Output. cmd.Output keeps that stderr in the *exec.ExitError.
|
||||||
|
// err is returned unchanged when gpg wrote nothing to stderr.
|
||||||
|
func withGPGStderr(err error) error {
|
||||||
|
var exitErr *exec.ExitError
|
||||||
|
|
||||||
|
if !errors.As(err, &exitErr) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
stderr := strings.TrimSpace(string(exitErr.Stderr))
|
||||||
|
if stderr == "" {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("%w: %s", err, stderr)
|
||||||
|
}
|
||||||
|
|
||||||
// gpgEncryptDefault is the default implementation of GPG encryption
|
// gpgEncryptDefault is the default implementation of GPG encryption
|
||||||
func gpgEncryptDefault(data *memguard.LockedBuffer, keyID string) ([]byte, error) {
|
func gpgEncryptDefault(data *memguard.LockedBuffer, keyID string) ([]byte, error) {
|
||||||
if data == nil {
|
if data == nil {
|
||||||
@@ -437,7 +456,7 @@ func gpgEncryptDefault(data *memguard.LockedBuffer, keyID string) ([]byte, error
|
|||||||
|
|
||||||
output, err := cmd.Output()
|
output, err := cmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("GPG encryption failed: %w", err)
|
return nil, fmt.Errorf("GPG encryption failed: %w", withGPGStderr(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
return output, nil
|
return output, nil
|
||||||
@@ -450,7 +469,7 @@ func gpgDecryptDefault(encryptedData []byte) (*memguard.LockedBuffer, error) {
|
|||||||
|
|
||||||
output, err := cmd.Output()
|
output, err := cmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("GPG decryption failed: %w", err)
|
return nil, fmt.Errorf("GPG decryption failed: %w", withGPGStderr(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a secure buffer for the decrypted data
|
// Create a secure buffer for the decrypted data
|
||||||
|
|||||||
@@ -102,3 +102,27 @@ func TestPGPUnlockerAddedTwiceKeepsFirst(t *testing.T) {
|
|||||||
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()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestGPGDecryptErrorHasGPGReason runs the real gpg on data that is not an
|
||||||
|
// OpenPGP message and checks that the error carries the reason gpg writes
|
||||||
|
// to stderr.
|
||||||
|
func TestGPGDecryptErrorHasGPGReason(t *testing.T) {
|
||||||
|
// The empty GNUPGHOME keeps gpg away from the user's own keyring;
|
||||||
|
// LC_ALL=C keeps gpg's message in English.
|
||||||
|
t.Setenv("GNUPGHOME", t.TempDir())
|
||||||
|
t.Setenv("LC_ALL", "C")
|
||||||
|
|
||||||
|
_, err := secret.GPGDecryptFunc([]byte("not an OpenPGP message"))
|
||||||
|
require.ErrorContains(t, err, "gpg: no valid OpenPGP data found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGPGDecryptErrorWithoutGPGStderr checks that when gpg fails without
|
||||||
|
// writing to stderr, the error ends with the exit status and nothing after it.
|
||||||
|
//
|
||||||
|
//nolint:paralleltest // installFakeGPG uses t.Setenv
|
||||||
|
func TestGPGDecryptErrorWithoutGPGStderr(t *testing.T) {
|
||||||
|
installFakeGPG(t)
|
||||||
|
|
||||||
|
_, err := secret.GPGDecryptFunc([]byte("not an OpenPGP message"))
|
||||||
|
require.EqualError(t, err, "GPG decryption failed: exit status 1")
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user