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

@@ -8,6 +8,7 @@ import (
"filippo.io/age"
"git.eeqj.de/sneak/secret/internal/secret"
"git.eeqj.de/sneak/secret/internal/vault"
"github.com/awnumar/memguard"
"github.com/spf13/cobra"
)
@@ -83,8 +84,11 @@ func (cli *Instance) Encrypt(secretName, inputFile, outputFile string) error {
ageSecretKey = identity.String()
// Store the generated key as a secret
err = vlt.AddSecret(secretName, []byte(ageSecretKey), false)
// Store the generated key as a secret using secure buffer
secureBuffer := memguard.NewBufferFromBytes([]byte(ageSecretKey))
defer secureBuffer.Destroy()
err = vlt.AddSecret(secretName, secureBuffer.Bytes(), false)
if err != nil {
return fmt.Errorf("failed to store age key: %w", err)
}
@@ -95,7 +99,16 @@ func (cli *Instance) Encrypt(secretName, inputFile, outputFile string) error {
return fmt.Errorf("failed to get secret value: %w", err)
}
ageSecretKey = string(secretValue)
// Create secure buffer for the secret value
secureBuffer := memguard.NewBufferFromBytes(secretValue)
defer secureBuffer.Destroy()
// Clear the original secret value
for i := range secretValue {
secretValue[i] = 0
}
ageSecretKey = secureBuffer.String()
// Validate that it's a valid age secret key
if !isValidAgeSecretKey(ageSecretKey) {
@@ -103,8 +116,11 @@ func (cli *Instance) Encrypt(secretName, inputFile, outputFile string) error {
}
}
// Parse the secret key
identity, err := age.ParseX25519Identity(ageSecretKey)
// Parse the secret key using secure buffer
finalSecureBuffer := memguard.NewBufferFromBytes([]byte(ageSecretKey))
defer finalSecureBuffer.Destroy()
identity, err := age.ParseX25519Identity(finalSecureBuffer.String())
if err != nil {
return fmt.Errorf("failed to parse age secret key: %w", err)
}
@@ -185,15 +201,22 @@ func (cli *Instance) Decrypt(secretName, inputFile, outputFile string) error {
return fmt.Errorf("failed to get secret value: %w", err)
}
ageSecretKey := string(secretValue)
// Create secure buffer for the secret value
secureBuffer := memguard.NewBufferFromBytes(secretValue)
defer secureBuffer.Destroy()
// Clear the original secret value
for i := range secretValue {
secretValue[i] = 0
}
// Validate that it's a valid age secret key
if !isValidAgeSecretKey(ageSecretKey) {
if !isValidAgeSecretKey(secureBuffer.String()) {
return fmt.Errorf("secret '%s' does not contain a valid age secret key", secretName)
}
// Parse the age secret key to get the identity
identity, err := age.ParseX25519Identity(ageSecretKey)
identity, err := age.ParseX25519Identity(secureBuffer.String())
if err != nil {
return fmt.Errorf("failed to parse age secret key: %w", err)
}

View File

@@ -11,6 +11,7 @@ import (
"git.eeqj.de/sneak/secret/internal/secret"
"git.eeqj.de/sneak/secret/internal/vault"
"git.eeqj.de/sneak/secret/pkg/agehd"
"github.com/awnumar/memguard"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"github.com/tyler-smith/go-bip39"
@@ -125,24 +126,25 @@ func (cli *Instance) Init(cmd *cobra.Command) error {
vlt.Unlock(ltIdentity)
// Prompt for passphrase for unlocker
var passphraseStr string
var passphraseBuffer *memguard.LockedBuffer
if envPassphrase := os.Getenv(secret.EnvUnlockPassphrase); envPassphrase != "" {
secret.Debug("Using unlock passphrase from environment variable")
passphraseStr = envPassphrase
passphraseBuffer = memguard.NewBufferFromBytes([]byte(envPassphrase))
} else {
secret.Debug("Prompting user for unlock passphrase")
// Use secure passphrase input with confirmation
passphraseStr, err = readSecurePassphrase("Enter passphrase for unlocker: ")
passphraseBuffer, err = readSecurePassphrase("Enter passphrase for unlocker: ")
if err != nil {
secret.Debug("Failed to read unlock passphrase", "error", err)
return fmt.Errorf("failed to read passphrase: %w", err)
}
}
defer passphraseBuffer.Destroy()
// Create passphrase-protected unlocker
secret.Debug("Creating passphrase-protected unlocker")
passphraseUnlocker, err := vlt.CreatePassphraseUnlocker(passphraseStr)
passphraseUnlocker, err := vlt.CreatePassphraseUnlocker(passphraseBuffer)
if err != nil {
secret.Debug("Failed to create unlocker", "error", err)
@@ -190,23 +192,27 @@ func (cli *Instance) Init(cmd *cobra.Command) error {
// readSecurePassphrase reads a passphrase securely from the terminal without echoing
// This version adds confirmation (read twice) for creating new unlockers
func readSecurePassphrase(prompt string) (string, error) {
// Returns a LockedBuffer containing the passphrase
func readSecurePassphrase(prompt string) (*memguard.LockedBuffer, error) {
// Get the first passphrase
passphrase1, err := secret.ReadPassphrase(prompt)
passphraseBuffer1, err := secret.ReadPassphrase(prompt)
if err != nil {
return "", err
return nil, err
}
defer passphraseBuffer1.Destroy()
// Read confirmation passphrase
passphrase2, err := secret.ReadPassphrase("Confirm passphrase: ")
passphraseBuffer2, err := secret.ReadPassphrase("Confirm passphrase: ")
if err != nil {
return "", fmt.Errorf("failed to read passphrase confirmation: %w", err)
return nil, fmt.Errorf("failed to read passphrase confirmation: %w", err)
}
defer passphraseBuffer2.Destroy()
// Compare passphrases
if passphrase1 != passphrase2 {
return "", fmt.Errorf("passphrases do not match")
if passphraseBuffer1.String() != passphraseBuffer2.String() {
return nil, fmt.Errorf("passphrases do not match")
}
return passphrase1, nil
// Create a new buffer with the confirmed passphrase
return memguard.NewBufferFromBytes(passphraseBuffer1.Bytes()), nil
}

View File

@@ -10,6 +10,7 @@ import (
"git.eeqj.de/sneak/secret/internal/secret"
"git.eeqj.de/sneak/secret/internal/vault"
"github.com/awnumar/memguard"
"github.com/spf13/afero"
"github.com/spf13/cobra"
)
@@ -254,18 +255,19 @@ func (cli *Instance) UnlockersAdd(unlockerType string, cmd *cobra.Command) error
// The CreatePassphraseUnlocker method will handle getting the long-term key
// Check if passphrase is set in environment variable
var passphraseStr string
var passphraseBuffer *memguard.LockedBuffer
if envPassphrase := os.Getenv(secret.EnvUnlockPassphrase); envPassphrase != "" {
passphraseStr = envPassphrase
passphraseBuffer = memguard.NewBufferFromBytes([]byte(envPassphrase))
} else {
// Use secure passphrase input with confirmation
passphraseStr, err = readSecurePassphrase("Enter passphrase for unlocker: ")
passphraseBuffer, err = readSecurePassphrase("Enter passphrase for unlocker: ")
if err != nil {
return fmt.Errorf("failed to read passphrase: %w", err)
}
}
defer passphraseBuffer.Destroy()
passphraseUnlocker, err := vlt.CreatePassphraseUnlocker(passphraseStr)
passphraseUnlocker, err := vlt.CreatePassphraseUnlocker(passphraseBuffer)
if err != nil {
return err
}

View File

@@ -10,6 +10,7 @@ import (
"git.eeqj.de/sneak/secret/internal/secret"
"git.eeqj.de/sneak/secret/internal/vault"
"git.eeqj.de/sneak/secret/pkg/agehd"
"github.com/awnumar/memguard"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"github.com/tyler-smith/go-bip39"
@@ -272,12 +273,16 @@ func (cli *Instance) VaultImport(cmd *cobra.Command, vaultName string) error {
secret.Debug("Using unlock passphrase from environment variable")
// Create secure buffer for passphrase
passphraseBuffer := memguard.NewBufferFromBytes([]byte(passphraseStr))
defer passphraseBuffer.Destroy()
// Unlock the vault with the derived long-term key
vlt.Unlock(ltIdentity)
// Create passphrase-protected unlocker
secret.Debug("Creating passphrase-protected unlocker")
passphraseUnlocker, err := vlt.CreatePassphraseUnlocker(passphraseStr)
passphraseUnlocker, err := vlt.CreatePassphraseUnlocker(passphraseBuffer)
if err != nil {
secret.Debug("Failed to create unlocker", "error", err)