check / check (push) Failing after 2s
secret init and secret vault create read the mnemonic with the new secret.ReadMnemonic, whose every error wraps the new secret.ErrMnemonicNotRead. It shares the terminal read with ReadPassphrase, whose errors still wrap ErrPassphraseNotRead. Without a terminal the error names the environment variable that gives the value instead: SB_SECRET_MNEMONIC for the mnemonic, SB_UNLOCK_PASSPHRASE for the passphrase. A test pins the message of init without a terminal. Model: opus-5-5
229 lines
6.9 KiB
Go
229 lines
6.9 KiB
Go
package secret
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"syscall"
|
|
"unsafe"
|
|
|
|
"filippo.io/age"
|
|
"github.com/awnumar/memguard"
|
|
"golang.org/x/term"
|
|
)
|
|
|
|
var (
|
|
errNilPassphraseBuffer = errors.New("passphrase buffer is nil")
|
|
errStdinNotTerminal = errors.New(
|
|
"stdin is not a terminal (piped input or script)")
|
|
errStderrNotTerminal = errors.New(
|
|
"stderr is not a terminal (running in non-interactive mode)")
|
|
errNothingEntered = errors.New("nothing was entered")
|
|
errEmptyPassphrase = errors.New("passphrase cannot be empty")
|
|
)
|
|
|
|
// ErrMnemonicNotRead is wrapped in every error of ReadMnemonic: there is no
|
|
// terminal to read the mnemonic from, reading it failed, or it was empty.
|
|
var ErrMnemonicNotRead = errors.New("failed to read mnemonic")
|
|
|
|
// EncryptToRecipient encrypts data to a recipient using age
|
|
// The data parameter should be a LockedBuffer for secure memory handling
|
|
func EncryptToRecipient(
|
|
data *memguard.LockedBuffer, recipient age.Recipient,
|
|
) ([]byte, error) {
|
|
if data == nil {
|
|
return nil, errNilDataBuffer
|
|
}
|
|
|
|
Debug("EncryptToRecipient starting", "data_length", data.Size())
|
|
|
|
var buf bytes.Buffer
|
|
|
|
Debug("Creating age encryptor")
|
|
|
|
w, err := age.Encrypt(&buf, recipient)
|
|
if err != nil {
|
|
Debug("Failed to create encryptor", "error", err)
|
|
|
|
return nil, fmt.Errorf("failed to create encryptor: %w", err)
|
|
}
|
|
|
|
Debug("Created age encryptor successfully")
|
|
Debug("Writing data to encryptor")
|
|
|
|
_, err = w.Write(data.Bytes())
|
|
if err != nil {
|
|
Debug("Failed to write data to encryptor", "error", err)
|
|
|
|
return nil, fmt.Errorf("failed to write data: %w", err)
|
|
}
|
|
|
|
Debug("Wrote data to encryptor successfully")
|
|
Debug("Closing encryptor")
|
|
|
|
err = w.Close()
|
|
if err != nil {
|
|
Debug("Failed to close encryptor", "error", err)
|
|
|
|
return nil, fmt.Errorf("failed to close encryptor: %w", err)
|
|
}
|
|
|
|
Debug("Closed encryptor successfully")
|
|
|
|
result := buf.Bytes()
|
|
Debug("EncryptToRecipient completed successfully", "result_length", len(result))
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// DecryptWithIdentity decrypts data with an identity using age
|
|
func DecryptWithIdentity(
|
|
data []byte, identity age.Identity,
|
|
) (*memguard.LockedBuffer, error) {
|
|
r, err := age.Decrypt(bytes.NewReader(data), identity)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create decryptor: %w", err)
|
|
}
|
|
|
|
result, err := io.ReadAll(r)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read decrypted data: %w", err)
|
|
}
|
|
|
|
// Create a secure buffer for the decrypted data
|
|
resultBuffer := memguard.NewBufferFromBytes(result)
|
|
|
|
// Zero out the original slice to prevent plaintext from lingering
|
|
// in unprotected memory
|
|
for i := range result {
|
|
result[i] = 0
|
|
}
|
|
|
|
return resultBuffer, nil
|
|
}
|
|
|
|
// IdentityToLockedBuffer returns the private key of id, in age's text form, in
|
|
// a new locked buffer. The caller must destroy it.
|
|
//
|
|
// This is best effort. age gives the key only as a string in ordinary memory.
|
|
// The bytes of that string are moved into the buffer, which overwrites them,
|
|
// although Go otherwise never changes a string; nothing else holds this one.
|
|
// The copies age makes while building the string are left in ordinary memory.
|
|
// Avoiding those would mean encoding the key here, straight into the buffer.
|
|
func IdentityToLockedBuffer(id *age.X25519Identity) *memguard.LockedBuffer {
|
|
key := id.String()
|
|
|
|
//nolint:gosec // G103: the string's own bytes, which NewBufferFromBytes wipes
|
|
keyBytes := unsafe.Slice(unsafe.StringData(key), len(key))
|
|
|
|
return memguard.NewBufferFromBytes(keyBytes)
|
|
}
|
|
|
|
// EncryptWithPassphrase encrypts data using a passphrase with age's
|
|
// scrypt-based encryption. Both data and passphrase parameters should
|
|
// be LockedBuffers for secure memory handling
|
|
func EncryptWithPassphrase(
|
|
data *memguard.LockedBuffer, passphrase *memguard.LockedBuffer,
|
|
) ([]byte, error) {
|
|
if data == nil {
|
|
return nil, errNilDataBuffer
|
|
}
|
|
|
|
if passphrase == nil {
|
|
return nil, errNilPassphraseBuffer
|
|
}
|
|
|
|
// Create recipient directly from passphrase - unavoidable string
|
|
// conversion due to age API
|
|
recipient, err := age.NewScryptRecipient(passphrase.String())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create scrypt recipient: %w", err)
|
|
}
|
|
|
|
return EncryptToRecipient(data, recipient)
|
|
}
|
|
|
|
// DecryptWithPassphrase decrypts data using a passphrase with age's
|
|
// scrypt-based decryption. The passphrase parameter should be a
|
|
// LockedBuffer for secure memory handling
|
|
func DecryptWithPassphrase(
|
|
encryptedData []byte, passphrase *memguard.LockedBuffer,
|
|
) (*memguard.LockedBuffer, error) {
|
|
if passphrase == nil {
|
|
return nil, errNilPassphraseBuffer
|
|
}
|
|
|
|
// Create identity directly from passphrase - unavoidable string
|
|
// conversion due to age API
|
|
identity, err := age.NewScryptIdentity(passphrase.String())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create scrypt identity: %w", err)
|
|
}
|
|
|
|
return DecryptWithIdentity(encryptedData, identity)
|
|
}
|
|
|
|
// ReadPassphrase reads a passphrase securely from the terminal without echoing
|
|
// This version is for unlocking and doesn't require confirmation
|
|
// Returns a LockedBuffer containing the passphrase for secure memory handling.
|
|
// Every error it returns wraps ErrPassphraseNotRead.
|
|
func ReadPassphrase(prompt string) (*memguard.LockedBuffer, error) {
|
|
return readFromTerminal(prompt, ErrPassphraseNotRead, EnvUnlockPassphrase)
|
|
}
|
|
|
|
// ReadMnemonic reads a mnemonic from the terminal as ReadPassphrase reads a
|
|
// passphrase. Every error it returns wraps ErrMnemonicNotRead.
|
|
func ReadMnemonic(prompt string) (*memguard.LockedBuffer, error) {
|
|
return readFromTerminal(prompt, ErrMnemonicNotRead, EnvMnemonic)
|
|
}
|
|
|
|
// readFromTerminal reads input from the terminal without echoing it. Every
|
|
// error it returns wraps notRead; without a terminal, the error says to set
|
|
// envVar instead.
|
|
func readFromTerminal(
|
|
prompt string, notRead error, envVar string,
|
|
) (*memguard.LockedBuffer, error) {
|
|
// Check if stdin is a terminal
|
|
if !term.IsTerminal(syscall.Stdin) {
|
|
// Not a terminal - never read secrets from piped input
|
|
// for security reasons
|
|
return nil, fmt.Errorf(
|
|
"%w: %w. Please set the %s environment variable or run interactively",
|
|
notRead, errStdinNotTerminal, envVar)
|
|
}
|
|
|
|
// stdin is a terminal, check if stderr is also a terminal for
|
|
// interactive prompting
|
|
if !term.IsTerminal(syscall.Stderr) {
|
|
return nil, fmt.Errorf("%w: %w. Please set the %s environment variable",
|
|
notRead, errStderrNotTerminal, envVar)
|
|
}
|
|
|
|
// Both stdin and stderr are terminals - use secure password reading
|
|
fmt.Fprint(os.Stderr, prompt) // Write prompt to stderr, not stdout
|
|
|
|
input, err := term.ReadPassword(syscall.Stdin)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: %w", notRead, err)
|
|
}
|
|
|
|
// Print newline to stderr since ReadPassword doesn't echo
|
|
fmt.Fprintln(os.Stderr)
|
|
|
|
if len(input) == 0 {
|
|
return nil, fmt.Errorf("%w: %w", notRead, errNothingEntered)
|
|
}
|
|
|
|
// Create a secure buffer and copy the input
|
|
secureBuffer := memguard.NewBufferFromBytes(input)
|
|
|
|
// Clear the original input slice
|
|
for i := range input {
|
|
input[i] = 0
|
|
}
|
|
|
|
return secureBuffer, nil
|
|
}
|