check / check (push) Failing after 3s
secret.IdentityToLockedBuffer replaces the eight places that converted an age identity's String() to bytes for a locked buffer and left the string, which holds the private key, in ordinary memory. It moves the string's own bytes into the buffer, which overwrites them. The copies age makes while encoding the key remain; the function's comment says so. TODO.md drops these places from the 1.0 memory-security entry, along with its stale version.go reference. Model: opus-5-5
209 lines
6.0 KiB
Go
209 lines
6.0 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(
|
|
"cannot read passphrase from non-terminal stdin " +
|
|
"(piped input or script). Please set the SB_UNLOCK_PASSPHRASE " +
|
|
"environment variable or run interactively")
|
|
errStderrNotTerminal = errors.New(
|
|
"cannot prompt for passphrase: stderr is not a terminal " +
|
|
"(running in non-interactive mode). Please set the " +
|
|
"SB_UNLOCK_PASSPHRASE environment variable")
|
|
errEmptyPassphrase = errors.New("passphrase cannot be empty")
|
|
)
|
|
|
|
// 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
|
|
func ReadPassphrase(prompt string) (*memguard.LockedBuffer, error) {
|
|
// Check if stdin is a terminal
|
|
if !term.IsTerminal(syscall.Stdin) {
|
|
// Not a terminal - never read passphrases from piped input
|
|
// for security reasons
|
|
return nil, errStdinNotTerminal
|
|
}
|
|
|
|
// stdin is a terminal, check if stderr is also a terminal for
|
|
// interactive prompting
|
|
if !term.IsTerminal(syscall.Stderr) {
|
|
return nil, errStderrNotTerminal
|
|
}
|
|
|
|
// Both stdin and stderr are terminals - use secure password reading
|
|
fmt.Fprint(os.Stderr, prompt) // Write prompt to stderr, not stdout
|
|
|
|
passphrase, err := term.ReadPassword(syscall.Stdin)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read passphrase: %w", err)
|
|
}
|
|
|
|
// Print newline to stderr since ReadPassword doesn't echo
|
|
fmt.Fprintln(os.Stderr)
|
|
|
|
if len(passphrase) == 0 {
|
|
return nil, errEmptyPassphrase
|
|
}
|
|
|
|
// Create a secure buffer and copy the passphrase
|
|
secureBuffer := memguard.NewBufferFromBytes(passphrase)
|
|
|
|
// Clear the original passphrase slice
|
|
for i := range passphrase {
|
|
passphrase[i] = 0
|
|
}
|
|
|
|
return secureBuffer, nil
|
|
}
|