package secret import ( "bytes" "fmt" "io" "os" "syscall" "filippo.io/age" "github.com/awnumar/memguard" "golang.org/x/term" ) // 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, fmt.Errorf("data buffer is nil") } 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") if _, err := w.Write(data.Bytes()); 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") if err := w.Close(); 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 } // 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, fmt.Errorf("data buffer is nil") } if passphrase == nil { return nil, fmt.Errorf("passphrase buffer is nil") } // 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, fmt.Errorf("passphrase buffer is nil") } // 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, fmt.Errorf("cannot read passphrase from non-terminal stdin " + "(piped input or script). Please set the SB_UNLOCK_PASSPHRASE " + "environment variable or run interactively") } // stdin is a terminal, check if stderr is also a terminal for interactive prompting if !term.IsTerminal(syscall.Stderr) { return nil, fmt.Errorf("cannot prompt for passphrase: stderr is not a terminal " + "(running in non-interactive mode). Please set the SB_UNLOCK_PASSPHRASE " + "environment variable") } // 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) } fmt.Fprintln(os.Stderr) // Print newline to stderr since ReadPassword doesn't echo if len(passphrase) == 0 { return nil, fmt.Errorf("passphrase cannot be empty") } // 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 }