WIP: refactor to use memguard for secure memory handling

- Add memguard dependency
- Update ReadPassphrase to return LockedBuffer
- Update EncryptWithPassphrase/DecryptWithPassphrase to accept LockedBuffer
- Remove string wrapper functions
- Update all callers to create LockedBuffers at entry points
- Update interfaces and mock implementations
This commit is contained in:
2025-07-15 07:22:41 +02:00
parent f9938135c6
commit c9774e89e0
18 changed files with 194 additions and 65 deletions

View File

@@ -7,6 +7,7 @@ import (
"path/filepath"
"filippo.io/age"
"github.com/awnumar/memguard"
"github.com/spf13/afero"
)
@@ -15,16 +16,17 @@ type PassphraseUnlocker struct {
Directory string
Metadata UnlockerMetadata
fs afero.Fs
Passphrase string
Passphrase *memguard.LockedBuffer // Secure buffer for passphrase
}
// getPassphrase retrieves the passphrase from memory, environment, or user input
func (p *PassphraseUnlocker) getPassphrase() (string, error) {
// Returns a LockedBuffer for secure memory handling
func (p *PassphraseUnlocker) getPassphrase() (*memguard.LockedBuffer, error) {
// First check if we already have the passphrase
if p.Passphrase != "" {
if p.Passphrase != nil && p.Passphrase.IsAlive() {
Debug("Using in-memory passphrase", "unlocker_id", p.GetID())
return p.Passphrase, nil
// Return a copy of the passphrase buffer
return memguard.NewBufferFromBytes(p.Passphrase.Bytes()), nil
}
Debug("No passphrase in memory, checking environment")
@@ -32,20 +34,22 @@ func (p *PassphraseUnlocker) getPassphrase() (string, error) {
passphraseStr := os.Getenv(EnvUnlockPassphrase)
if passphraseStr != "" {
Debug("Using passphrase from environment", "unlocker_id", p.GetID())
return passphraseStr, nil
// Convert to secure buffer
secureBuffer := memguard.NewBufferFromBytes([]byte(passphraseStr))
return secureBuffer, nil
}
Debug("No passphrase in environment, prompting user")
// Prompt for passphrase
passphraseStr, err := ReadPassphrase("Enter unlock passphrase: ")
secureBuffer, err := ReadPassphrase("Enter unlock passphrase: ")
if err != nil {
Debug("Failed to read passphrase", "error", err, "unlocker_id", p.GetID())
return "", fmt.Errorf("failed to read passphrase: %w", err)
return nil, fmt.Errorf("failed to read passphrase: %w", err)
}
return passphraseStr, nil
return secureBuffer, nil
}
// GetIdentity implements Unlocker interface for passphrase-based unlockers
@@ -55,10 +59,11 @@ func (p *PassphraseUnlocker) GetIdentity() (*age.X25519Identity, error) {
slog.String("unlocker_type", p.GetType()),
)
passphraseStr, err := p.getPassphrase()
passphraseBuffer, err := p.getPassphrase()
if err != nil {
return nil, err
}
defer passphraseBuffer.Destroy()
// Read encrypted private key of unlocker
unlockerPrivPath := filepath.Join(p.Directory, "priv.age")
@@ -79,7 +84,7 @@ func (p *PassphraseUnlocker) GetIdentity() (*age.X25519Identity, error) {
Debug("Decrypting unlocker private key with passphrase", "unlocker_id", p.GetID())
// Decrypt the unlocker private key with passphrase
privKeyData, err := DecryptWithPassphrase(encryptedPrivKeyData, passphraseStr)
privKeyData, err := DecryptWithPassphrase(encryptedPrivKeyData, passphraseBuffer)
if err != nil {
Debug("Failed to decrypt unlocker private key", "error", err, "unlocker_id", p.GetID())
@@ -93,7 +98,17 @@ func (p *PassphraseUnlocker) GetIdentity() (*age.X25519Identity, error) {
// Parse the decrypted private key
Debug("Parsing decrypted unlocker identity", "unlocker_id", p.GetID())
identity, err := age.ParseX25519Identity(string(privKeyData))
// Create a secure buffer for the private key data
privKeyBuffer := memguard.NewBufferFromBytes(privKeyData)
defer privKeyBuffer.Destroy()
// Clear the original private key data
for i := range privKeyData {
privKeyData[i] = 0
}
identity, err := age.ParseX25519Identity(privKeyBuffer.String())
if err != nil {
Debug("Failed to parse unlocker private key", "error", err, "unlocker_id", p.GetID())
@@ -133,6 +148,11 @@ func (p *PassphraseUnlocker) GetID() string {
// Remove implements Unlocker interface - removes the passphrase unlocker
func (p *PassphraseUnlocker) Remove() error {
// Clean up the passphrase from memory if it exists
if p.Passphrase != nil && p.Passphrase.IsAlive() {
p.Passphrase.Destroy()
}
// For passphrase unlockers, we just need to remove the directory
// No external resources (like keychain items) to clean up
if err := p.fs.RemoveAll(p.Directory); err != nil {
@@ -152,7 +172,8 @@ func NewPassphraseUnlocker(fs afero.Fs, directory string, metadata UnlockerMetad
}
// CreatePassphraseUnlocker creates a new passphrase-protected unlocker
func CreatePassphraseUnlocker(fs afero.Fs, stateDir string, passphrase string) (*PassphraseUnlocker, error) {
// The passphrase must be provided as a LockedBuffer for security
func CreatePassphraseUnlocker(fs afero.Fs, stateDir string, passphrase *memguard.LockedBuffer) (*PassphraseUnlocker, error) {
// Get current vault
currentVault, err := GetCurrentVault(fs, stateDir)
if err != nil {