uses protected memory buffers now for all secrets in ram
This commit is contained in:
@@ -13,8 +13,13 @@ import (
|
||||
)
|
||||
|
||||
// EncryptToRecipient encrypts data to a recipient using age
|
||||
func EncryptToRecipient(data []byte, recipient age.Recipient) ([]byte, error) {
|
||||
Debug("EncryptToRecipient starting", "data_length", len(data))
|
||||
// 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")
|
||||
@@ -27,7 +32,7 @@ func EncryptToRecipient(data []byte, recipient age.Recipient) ([]byte, error) {
|
||||
Debug("Created age encryptor successfully")
|
||||
|
||||
Debug("Writing data to encryptor")
|
||||
if _, err := w.Write(data); err != nil {
|
||||
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)
|
||||
@@ -77,7 +82,11 @@ func EncryptWithPassphrase(data []byte, passphrase *memguard.LockedBuffer) ([]by
|
||||
return nil, fmt.Errorf("failed to create scrypt recipient: %w", err)
|
||||
}
|
||||
|
||||
return EncryptToRecipient(data, recipient)
|
||||
// Create a secure buffer for the data
|
||||
dataBuffer := memguard.NewBufferFromBytes(data)
|
||||
defer dataBuffer.Destroy()
|
||||
|
||||
return EncryptToRecipient(dataBuffer, recipient)
|
||||
}
|
||||
|
||||
// DecryptWithPassphrase decrypts data using a passphrase with age's scrypt-based decryption
|
||||
@@ -138,4 +147,3 @@ func ReadPassphrase(prompt string) (*memguard.LockedBuffer, error) {
|
||||
|
||||
return secureBuffer, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ func (k *KeychainUnlocker) GetIdentity() (*age.X25519Identity, error) {
|
||||
// Create secure buffer for the keychain passphrase
|
||||
passphraseBuffer := memguard.NewBufferFromBytes([]byte(keychainData.AgePrivKeyPassphrase))
|
||||
defer passphraseBuffer.Destroy()
|
||||
|
||||
|
||||
agePrivKeyData, err := DecryptWithPassphrase(encryptedAgePrivKeyData, passphraseBuffer)
|
||||
if err != nil {
|
||||
Debug("Failed to decrypt age private key with keychain passphrase", "error", err, "unlocker_id", k.GetID())
|
||||
@@ -245,7 +245,8 @@ func generateKeychainUnlockerName(vaultName string) (string, error) {
|
||||
}
|
||||
|
||||
// getLongTermPrivateKey retrieves the long-term private key either from environment or current unlocker
|
||||
func getLongTermPrivateKey(fs afero.Fs, vault VaultInterface) ([]byte, error) {
|
||||
// Returns a LockedBuffer to ensure the private key is protected in memory
|
||||
func getLongTermPrivateKey(fs afero.Fs, vault VaultInterface) (*memguard.LockedBuffer, error) {
|
||||
// Check if mnemonic is available in environment variable
|
||||
envMnemonic := os.Getenv(EnvMnemonic)
|
||||
if envMnemonic != "" {
|
||||
@@ -255,7 +256,8 @@ func getLongTermPrivateKey(fs afero.Fs, vault VaultInterface) ([]byte, error) {
|
||||
return nil, fmt.Errorf("failed to derive long-term key from mnemonic: %w", err)
|
||||
}
|
||||
|
||||
return []byte(ltIdentity.String()), nil
|
||||
// Return the private key in a secure buffer
|
||||
return memguard.NewBufferFromBytes([]byte(ltIdentity.String())), nil
|
||||
}
|
||||
|
||||
// Get the vault to access current unlocker
|
||||
@@ -304,7 +306,8 @@ func getLongTermPrivateKey(fs afero.Fs, vault VaultInterface) ([]byte, error) {
|
||||
return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err)
|
||||
}
|
||||
|
||||
return ltPrivKeyData, nil
|
||||
// Return the decrypted key in a secure buffer
|
||||
return memguard.NewBufferFromBytes(ltPrivKeyData), nil
|
||||
}
|
||||
|
||||
// CreateKeychainUnlocker creates a new keychain unlocker and stores it in the vault
|
||||
@@ -361,7 +364,7 @@ func CreateKeychainUnlocker(fs afero.Fs, stateDir string) (*KeychainUnlocker, er
|
||||
agePrivKeyStr := ageIdentity.String()
|
||||
agePrivKeyBuffer := memguard.NewBufferFromBytes([]byte(agePrivKeyStr))
|
||||
defer agePrivKeyBuffer.Destroy()
|
||||
|
||||
|
||||
passphraseBuffer := memguard.NewBufferFromBytes([]byte(agePrivKeyPassphrase))
|
||||
defer passphraseBuffer.Destroy()
|
||||
|
||||
@@ -380,6 +383,7 @@ func CreateKeychainUnlocker(fs afero.Fs, stateDir string) (*KeychainUnlocker, er
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer ltPrivKeyData.Destroy()
|
||||
|
||||
// Step 6: Encrypt long-term private key to the new age unlocker
|
||||
encryptedLtPrivKeyToAge, err := EncryptToRecipient(ltPrivKeyData, ageIdentity.Recipient())
|
||||
|
||||
@@ -113,8 +113,9 @@ func TestPassphraseUnlockerWithRealFS(t *testing.T) {
|
||||
t.Fatalf("Failed to parse recipient: %v", err)
|
||||
}
|
||||
|
||||
ltPrivKeyData := []byte(ltIdentity.String())
|
||||
encryptedLtPrivKey, err := secret.EncryptToRecipient(ltPrivKeyData, recipient)
|
||||
ltPrivKeyBuffer := memguard.NewBufferFromBytes([]byte(ltIdentity.String()))
|
||||
defer ltPrivKeyBuffer.Destroy()
|
||||
encryptedLtPrivKey, err := secret.EncryptToRecipient(ltPrivKeyBuffer, recipient)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to encrypt long-term private key: %v", err)
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ func (p *PassphraseUnlocker) getPassphrase() (*memguard.LockedBuffer, error) {
|
||||
Debug("Using passphrase from environment", "unlocker_id", p.GetID())
|
||||
// Convert to secure buffer
|
||||
secureBuffer := memguard.NewBufferFromBytes([]byte(passphraseStr))
|
||||
|
||||
|
||||
return secureBuffer, nil
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ func (p *PassphraseUnlocker) getPassphrase() (*memguard.LockedBuffer, error) {
|
||||
secureBuffer, err := ReadPassphrase("Enter unlock passphrase: ")
|
||||
if err != nil {
|
||||
Debug("Failed to read passphrase", "error", err, "unlocker_id", p.GetID())
|
||||
|
||||
|
||||
return nil, fmt.Errorf("failed to read passphrase: %w", err)
|
||||
}
|
||||
|
||||
@@ -173,7 +173,11 @@ func NewPassphraseUnlocker(fs afero.Fs, directory string, metadata UnlockerMetad
|
||||
|
||||
// CreatePassphraseUnlocker creates a new passphrase-protected unlocker
|
||||
// The passphrase must be provided as a LockedBuffer for security
|
||||
func CreatePassphraseUnlocker(fs afero.Fs, stateDir string, passphrase *memguard.LockedBuffer) (*PassphraseUnlocker, error) {
|
||||
func CreatePassphraseUnlocker(
|
||||
fs afero.Fs,
|
||||
stateDir string,
|
||||
passphrase *memguard.LockedBuffer,
|
||||
) (*PassphraseUnlocker, error) {
|
||||
// Get current vault
|
||||
currentVault, err := GetCurrentVault(fs, stateDir)
|
||||
if err != nil {
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
"filippo.io/age"
|
||||
"github.com/awnumar/memguard"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
@@ -233,6 +234,7 @@ func CreatePGPUnlocker(fs afero.Fs, stateDir string, gpgKeyID string) (*PGPUnloc
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer ltPrivKeyData.Destroy()
|
||||
|
||||
// Step 7: Encrypt long-term private key to the new age unlocker
|
||||
encryptedLtPrivKeyToAge, err := EncryptToRecipient(ltPrivKeyData, ageIdentity.Recipient())
|
||||
@@ -247,8 +249,11 @@ func CreatePGPUnlocker(fs afero.Fs, stateDir string, gpgKeyID string) (*PGPUnloc
|
||||
}
|
||||
|
||||
// Step 8: Encrypt age private key to the GPG key ID
|
||||
agePrivateKeyBytes := []byte(ageIdentity.String())
|
||||
encryptedAgePrivKey, err := GPGEncryptFunc(agePrivateKeyBytes, gpgKeyID)
|
||||
// Use memguard to protect the private key in memory
|
||||
agePrivateKeyBuffer := memguard.NewBufferFromBytes([]byte(ageIdentity.String()))
|
||||
defer agePrivateKeyBuffer.Destroy()
|
||||
|
||||
encryptedAgePrivKey, err := GPGEncryptFunc(agePrivateKeyBuffer.Bytes(), gpgKeyID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encrypt age private key with GPG: %w", err)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
// VaultInterface defines the interface that vault implementations must satisfy
|
||||
type VaultInterface interface {
|
||||
GetDirectory() (string, error)
|
||||
AddSecret(name string, value []byte, force bool) error
|
||||
AddSecret(name string, value *memguard.LockedBuffer, force bool) error
|
||||
GetName() string
|
||||
GetFilesystem() afero.Fs
|
||||
GetCurrentUnlocker() (Unlocker, error)
|
||||
@@ -72,7 +72,12 @@ func (s *Secret) Save(value []byte, force bool) error {
|
||||
slog.Bool("force", force),
|
||||
)
|
||||
|
||||
err := s.vault.AddSecret(s.Name, value, force)
|
||||
// Create a secure buffer for the value - note that the caller
|
||||
// should ideally pass a LockedBuffer directly to vault.AddSecret
|
||||
valueBuffer := memguard.NewBufferFromBytes(value)
|
||||
defer valueBuffer.Destroy()
|
||||
|
||||
err := s.vault.AddSecret(s.Name, valueBuffer, force)
|
||||
if err != nil {
|
||||
Debug("Failed to save secret", "error", err, "secret_name", s.Name)
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ func (m *MockVault) GetDirectory() (string, error) {
|
||||
return m.directory, nil
|
||||
}
|
||||
|
||||
func (m *MockVault) AddSecret(name string, value []byte, _ bool) error {
|
||||
func (m *MockVault) AddSecret(name string, value *memguard.LockedBuffer, _ bool) error {
|
||||
// Create secret directory with proper storage name conversion
|
||||
storageName := strings.ReplaceAll(name, "/", "%")
|
||||
secretDir := filepath.Join(m.directory, "secrets.d", storageName)
|
||||
@@ -75,7 +75,7 @@ func (m *MockVault) AddSecret(name string, value []byte, _ bool) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Encrypt value to version's public key
|
||||
// Encrypt value to version's public key (value is already a LockedBuffer)
|
||||
encryptedValue, err := EncryptToRecipient(value, versionIdentity.Recipient())
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -88,7 +88,9 @@ func (m *MockVault) AddSecret(name string, value []byte, _ bool) error {
|
||||
}
|
||||
|
||||
// Encrypt version private key to long-term public key
|
||||
encryptedPrivKey, err := EncryptToRecipient([]byte(versionIdentity.String()), ltIdentity.Recipient())
|
||||
versionPrivKeyBuffer := memguard.NewBufferFromBytes([]byte(versionIdentity.String()))
|
||||
defer versionPrivKeyBuffer.Destroy()
|
||||
encryptedPrivKey, err := EncryptToRecipient(versionPrivKeyBuffer, ltIdentity.Recipient())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -180,9 +182,13 @@ func TestPerSecretKeyFunctionality(t *testing.T) {
|
||||
secretName := "test-secret"
|
||||
secretValue := []byte("this is a test secret value")
|
||||
|
||||
// Create a secure buffer for the test value
|
||||
valueBuffer := memguard.NewBufferFromBytes(secretValue)
|
||||
defer valueBuffer.Destroy()
|
||||
|
||||
// Test AddSecret
|
||||
t.Run("AddSecret", func(t *testing.T) {
|
||||
err := vault.AddSecret(secretName, secretValue, false)
|
||||
err := vault.AddSecret(secretName, valueBuffer, false)
|
||||
if err != nil {
|
||||
t.Fatalf("AddSecret failed: %v", err)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"filippo.io/age"
|
||||
"github.com/awnumar/memguard"
|
||||
"github.com/oklog/ulid/v2"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
@@ -120,11 +121,15 @@ func GenerateVersionName(fs afero.Fs, secretDir string) (string, error) {
|
||||
}
|
||||
|
||||
// Save saves the version metadata and value
|
||||
func (sv *Version) Save(value []byte) error {
|
||||
func (sv *Version) Save(value *memguard.LockedBuffer) error {
|
||||
if value == nil {
|
||||
return fmt.Errorf("value buffer is nil")
|
||||
}
|
||||
|
||||
DebugWith("Saving secret version",
|
||||
slog.String("secret_name", sv.SecretName),
|
||||
slog.String("version", sv.Version),
|
||||
slog.Int("value_length", len(value)),
|
||||
slog.Int("value_length", value.Size()),
|
||||
)
|
||||
|
||||
fs := sv.vault.GetFilesystem()
|
||||
@@ -146,7 +151,9 @@ func (sv *Version) Save(value []byte) error {
|
||||
}
|
||||
|
||||
versionPublicKey := versionIdentity.Recipient().String()
|
||||
versionPrivateKey := versionIdentity.String()
|
||||
// Store private key in memguard buffer immediately
|
||||
versionPrivateKeyBuffer := memguard.NewBufferFromBytes([]byte(versionIdentity.String()))
|
||||
defer versionPrivateKeyBuffer.Destroy()
|
||||
|
||||
DebugWith("Generated version keypair",
|
||||
slog.String("version", sv.Version),
|
||||
@@ -202,7 +209,7 @@ func (sv *Version) Save(value []byte) error {
|
||||
|
||||
// Step 6: Encrypt the version's private key to the long-term public key
|
||||
Debug("Encrypting version private key to long-term public key", "version", sv.Version)
|
||||
encryptedPrivKey, err := EncryptToRecipient([]byte(versionPrivateKey), ltRecipient)
|
||||
encryptedPrivKey, err := EncryptToRecipient(versionPrivateKeyBuffer, ltRecipient)
|
||||
if err != nil {
|
||||
Debug("Failed to encrypt version private key", "error", err, "version", sv.Version)
|
||||
|
||||
@@ -228,7 +235,10 @@ func (sv *Version) Save(value []byte) error {
|
||||
}
|
||||
|
||||
// Encrypt metadata to the version's public key
|
||||
encryptedMetadata, err := EncryptToRecipient(metadataBytes, versionIdentity.Recipient())
|
||||
metadataBuffer := memguard.NewBufferFromBytes(metadataBytes)
|
||||
defer metadataBuffer.Destroy()
|
||||
|
||||
encryptedMetadata, err := EncryptToRecipient(metadataBuffer, versionIdentity.Recipient())
|
||||
if err != nil {
|
||||
Debug("Failed to encrypt version metadata", "error", err, "version", sv.Version)
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ func (m *MockVersionVault) GetDirectory() (string, error) {
|
||||
return filepath.Join(m.stateDir, "vaults.d", m.Name), nil
|
||||
}
|
||||
|
||||
func (m *MockVersionVault) AddSecret(_ string, _ []byte, _ bool) error {
|
||||
func (m *MockVersionVault) AddSecret(_ string, _ *memguard.LockedBuffer, _ bool) error {
|
||||
return fmt.Errorf("not implemented in mock")
|
||||
}
|
||||
|
||||
@@ -165,7 +165,9 @@ func TestSecretVersionSave(t *testing.T) {
|
||||
sv := NewVersion(vault, "test/secret", "20231215.001")
|
||||
testValue := []byte("test-secret-value")
|
||||
|
||||
err = sv.Save(testValue)
|
||||
testBuffer := memguard.NewBufferFromBytes(testValue)
|
||||
defer testBuffer.Destroy()
|
||||
err = sv.Save(testBuffer)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify files were created
|
||||
@@ -203,7 +205,9 @@ func TestSecretVersionLoadMetadata(t *testing.T) {
|
||||
sv.Metadata.NotBefore = &epochPlusOne
|
||||
sv.Metadata.NotAfter = &now
|
||||
|
||||
err = sv.Save([]byte("test-value"))
|
||||
testBuffer := memguard.NewBufferFromBytes([]byte("test-value"))
|
||||
defer testBuffer.Destroy()
|
||||
err = sv.Save(testBuffer)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create new version object and load metadata
|
||||
@@ -242,15 +246,19 @@ func TestSecretVersionGetValue(t *testing.T) {
|
||||
// Create and save a version
|
||||
sv := NewVersion(vault, "test/secret", "20231215.001")
|
||||
originalValue := []byte("test-secret-value-12345")
|
||||
expectedValue := make([]byte, len(originalValue))
|
||||
copy(expectedValue, originalValue)
|
||||
|
||||
err = sv.Save(originalValue)
|
||||
originalBuffer := memguard.NewBufferFromBytes(originalValue)
|
||||
defer originalBuffer.Destroy()
|
||||
err = sv.Save(originalBuffer)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Retrieve the value
|
||||
retrievedValue, err := sv.GetValue(ltIdentity)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, originalValue, retrievedValue)
|
||||
assert.Equal(t, expectedValue, retrievedValue)
|
||||
}
|
||||
|
||||
func TestListVersions(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user