uses protected memory buffers now for all secrets in ram

This commit is contained in:
2025-07-15 08:32:33 +02:00
parent d3ca006886
commit 7596049828
22 changed files with 786 additions and 133 deletions

View File

@@ -8,6 +8,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"
)
@@ -107,8 +108,13 @@ func TestVaultWithRealFilesystem(t *testing.T) {
// Create a secret with a deeply nested path
deepPath := "api/credentials/production/database/primary"
secretValue := []byte("supersecretdbpassword")
expectedValue := make([]byte, len(secretValue))
copy(expectedValue, secretValue)
err = vlt.AddSecret(deepPath, secretValue, false)
secretBuffer := memguard.NewBufferFromBytes(secretValue)
defer secretBuffer.Destroy()
err = vlt.AddSecret(deepPath, secretBuffer, false)
if err != nil {
t.Fatalf("Failed to add secret with deep path: %v", err)
}
@@ -137,9 +143,9 @@ func TestVaultWithRealFilesystem(t *testing.T) {
t.Fatalf("Failed to retrieve deep path secret: %v", err)
}
if string(retrievedValue) != string(secretValue) {
if string(retrievedValue) != string(expectedValue) {
t.Errorf("Retrieved value doesn't match. Expected %q, got %q",
string(secretValue), string(retrievedValue))
string(expectedValue), string(retrievedValue))
}
})
@@ -368,7 +374,11 @@ func TestVaultWithRealFilesystem(t *testing.T) {
// Add a secret to vault1
secretName := "test-secret"
secretValue := []byte("secret in vault1")
if err := vault1.AddSecret(secretName, secretValue, false); err != nil {
secretBuffer := memguard.NewBufferFromBytes(secretValue)
defer secretBuffer.Destroy()
if err := vault1.AddSecret(secretName, secretBuffer, false); err != nil {
t.Fatalf("Failed to add secret to vault1: %v", err)
}

View File

@@ -29,11 +29,21 @@ import (
"git.eeqj.de/sneak/secret/internal/secret"
"git.eeqj.de/sneak/secret/pkg/agehd"
"github.com/awnumar/memguard"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Helper function to add a secret to vault with proper buffer protection
func addTestSecret(t *testing.T, vault *Vault, name string, value []byte, force bool) {
t.Helper()
buffer := memguard.NewBufferFromBytes(value)
defer buffer.Destroy()
err := vault.AddSecret(name, buffer, force)
require.NoError(t, err)
}
// TestVersionIntegrationWorkflow tests the complete version workflow
func TestVersionIntegrationWorkflow(t *testing.T) {
fs := afero.NewMemMapFs()
@@ -66,8 +76,7 @@ func TestVersionIntegrationWorkflow(t *testing.T) {
// Step 1: Create initial version
t.Run("create_initial_version", func(t *testing.T) {
err := vault.AddSecret(secretName, []byte("version-1-data"), false)
require.NoError(t, err)
addTestSecret(t, vault, secretName, []byte("version-1-data"), false)
// Verify secret can be retrieved
value, err := vault.GetSecret(secretName)
@@ -108,8 +117,7 @@ func TestVersionIntegrationWorkflow(t *testing.T) {
firstVersionName = versions[0]
// Create second version
err = vault.AddSecret(secretName, []byte("version-2-data"), true)
require.NoError(t, err)
addTestSecret(t, vault, secretName, []byte("version-2-data"), true)
// Verify new value is current
value, err := vault.GetSecret(secretName)
@@ -142,8 +150,7 @@ func TestVersionIntegrationWorkflow(t *testing.T) {
t.Run("create_third_version", func(t *testing.T) {
time.Sleep(10 * time.Millisecond)
err := vault.AddSecret(secretName, []byte("version-3-data"), true)
require.NoError(t, err)
addTestSecret(t, vault, secretName, []byte("version-3-data"), true)
// Verify we now have three versions
secretDir := filepath.Join(vaultDir, "secrets.d", "integration%test")
@@ -214,8 +221,7 @@ func TestVersionIntegrationWorkflow(t *testing.T) {
secretDir := filepath.Join(vaultDir, "secrets.d", "limit%test", "versions")
// Create 998 versions (we already have one from the first AddSecret)
err := vault.AddSecret(limitSecretName, []byte("initial"), false)
require.NoError(t, err)
addTestSecret(t, vault, limitSecretName, []byte("initial"), false)
// Get today's date for consistent version names
today := time.Now().Format("20060102")
@@ -255,7 +261,9 @@ func TestVersionIntegrationWorkflow(t *testing.T) {
assert.Error(t, err)
// Try to add secret without force when it exists
err = vault.AddSecret(secretName, []byte("should-fail"), false)
failBuffer := memguard.NewBufferFromBytes([]byte("should-fail"))
defer failBuffer.Destroy()
err = vault.AddSecret(secretName, failBuffer, false)
assert.Error(t, err)
assert.Contains(t, err.Error(), "already exists")
})
@@ -272,8 +280,7 @@ func TestVersionConcurrency(t *testing.T) {
secretName := "concurrent/test"
// Create initial version
err := vault.AddSecret(secretName, []byte("initial"), false)
require.NoError(t, err)
addTestSecret(t, vault, secretName, []byte("initial"), false)
// Test concurrent reads
t.Run("concurrent_reads", func(t *testing.T) {
@@ -326,8 +333,10 @@ func TestVersionCompatibility(t *testing.T) {
// Create old-style encrypted value directly in secret directory
testValue := []byte("legacy-value")
testValueBuffer := memguard.NewBufferFromBytes(testValue)
defer testValueBuffer.Destroy()
ltRecipient := ltIdentity.Recipient()
encrypted, err := secret.EncryptToRecipient(testValue, ltRecipient)
encrypted, err := secret.EncryptToRecipient(testValueBuffer, ltRecipient)
require.NoError(t, err)
valuePath := filepath.Join(secretDir, "value.age")

View File

@@ -11,6 +11,7 @@ import (
"filippo.io/age"
"git.eeqj.de/sneak/secret/internal/secret"
"github.com/awnumar/memguard"
"github.com/spf13/afero"
)
@@ -98,11 +99,15 @@ func isValidSecretName(name string) bool {
}
// AddSecret adds a secret to this vault
func (v *Vault) AddSecret(name string, value []byte, force bool) error {
func (v *Vault) AddSecret(name string, value *memguard.LockedBuffer, force bool) error {
if value == nil {
return fmt.Errorf("value buffer is nil")
}
secret.DebugWith("Adding secret to vault",
slog.String("vault_name", v.Name),
slog.String("secret_name", name),
slog.Int("value_length", len(value)),
slog.Int("value_length", value.Size()),
slog.Bool("force", force),
)
@@ -195,7 +200,7 @@ func (v *Vault) AddSecret(name string, value []byte, force bool) error {
// We'll update the previous version's notAfter after we save the new version
}
// Save the new version
// Save the new version - pass the LockedBuffer directly
if err := newVersion.Save(value); err != nil {
secret.Debug("Failed to save new version", "error", err, "version", versionName)
@@ -272,7 +277,10 @@ func updateVersionMetadata(fs afero.Fs, version *secret.Version, ltIdentity *age
}
// Encrypt metadata to the version's public key
encryptedMetadata, err := secret.EncryptToRecipient(metadataBytes, versionIdentity.Recipient())
metadataBuffer := memguard.NewBufferFromBytes(metadataBytes)
defer metadataBuffer.Destroy()
encryptedMetadata, err := secret.EncryptToRecipient(metadataBuffer, versionIdentity.Recipient())
if err != nil {
return fmt.Errorf("failed to encrypt version metadata: %w", err)
}

View File

@@ -24,11 +24,21 @@ import (
"git.eeqj.de/sneak/secret/internal/secret"
"git.eeqj.de/sneak/secret/pkg/agehd"
"github.com/awnumar/memguard"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Helper function to add a secret to vault with proper buffer protection
func addTestSecretToVault(t *testing.T, vault *Vault, name string, value []byte, force bool) {
t.Helper()
buffer := memguard.NewBufferFromBytes(value)
defer buffer.Destroy()
err := vault.AddSecret(name, buffer, force)
require.NoError(t, err)
}
// Helper function to create a vault with long-term key set up
func createTestVaultWithKey(t *testing.T, fs afero.Fs, stateDir, vaultName string) *Vault {
// Set mnemonic for testing
@@ -65,9 +75,10 @@ func TestVaultAddSecretCreatesVersion(t *testing.T) {
// Add a secret
secretName := "test/secret"
secretValue := []byte("initial-value")
expectedValue := make([]byte, len(secretValue))
copy(expectedValue, secretValue)
err := vault.AddSecret(secretName, secretValue, false)
require.NoError(t, err)
addTestSecretToVault(t, vault, secretName, secretValue, false)
// Check that version directory was created
vaultDir, _ := vault.GetDirectory()
@@ -88,7 +99,7 @@ func TestVaultAddSecretCreatesVersion(t *testing.T) {
// Get the secret value
retrievedValue, err := vault.GetSecret(secretName)
require.NoError(t, err)
assert.Equal(t, secretValue, retrievedValue)
assert.Equal(t, expectedValue, retrievedValue)
}
func TestVaultAddSecretMultipleVersions(t *testing.T) {
@@ -101,17 +112,17 @@ func TestVaultAddSecretMultipleVersions(t *testing.T) {
secretName := "test/secret"
// Add first version
err := vault.AddSecret(secretName, []byte("version-1"), false)
require.NoError(t, err)
addTestSecretToVault(t, vault, secretName, []byte("version-1"), false)
// Try to add again without force - should fail
err = vault.AddSecret(secretName, []byte("version-2"), false)
failBuffer := memguard.NewBufferFromBytes([]byte("version-2"))
defer failBuffer.Destroy()
err := vault.AddSecret(secretName, failBuffer, false)
assert.Error(t, err)
assert.Contains(t, err.Error(), "already exists")
// Add with force - should create new version
err = vault.AddSecret(secretName, []byte("version-2"), true)
require.NoError(t, err)
addTestSecretToVault(t, vault, secretName, []byte("version-2"), true)
// Check that we have two versions
vaultDir, _ := vault.GetDirectory()
@@ -136,14 +147,12 @@ func TestVaultGetSecretVersion(t *testing.T) {
secretName := "test/secret"
// Add multiple versions
err := vault.AddSecret(secretName, []byte("version-1"), false)
require.NoError(t, err)
addTestSecretToVault(t, vault, secretName, []byte("version-1"), false)
// Small delay to ensure different version names
time.Sleep(10 * time.Millisecond)
err = vault.AddSecret(secretName, []byte("version-2"), true)
require.NoError(t, err)
addTestSecretToVault(t, vault, secretName, []byte("version-2"), true)
// Get versions list
vaultDir, _ := vault.GetDirectory()
@@ -185,7 +194,9 @@ func TestVaultVersionTimestamps(t *testing.T) {
// Add first version
beforeFirst := time.Now()
err = vault.AddSecret(secretName, []byte("version-1"), false)
v1Buffer := memguard.NewBufferFromBytes([]byte("version-1"))
defer v1Buffer.Destroy()
err = vault.AddSecret(secretName, v1Buffer, false)
require.NoError(t, err)
afterFirst := time.Now()
@@ -212,8 +223,7 @@ func TestVaultVersionTimestamps(t *testing.T) {
// Add second version
time.Sleep(10 * time.Millisecond)
beforeSecond := time.Now()
err = vault.AddSecret(secretName, []byte("version-2"), true)
require.NoError(t, err)
addTestSecretToVault(t, vault, secretName, []byte("version-2"), true)
afterSecond := time.Now()
// Get updated versions
@@ -249,11 +259,10 @@ func TestVaultGetNonExistentVersion(t *testing.T) {
vault := createTestVaultWithKey(t, fs, stateDir, "test")
// Add a secret
err := vault.AddSecret("test/secret", []byte("value"), false)
require.NoError(t, err)
addTestSecretToVault(t, vault, "test/secret", []byte("value"), false)
// Try to get non-existent version
_, err = vault.GetSecretVersion("test/secret", "20991231.999")
_, err := vault.GetSecretVersion("test/secret", "20991231.999")
assert.Error(t, err)
assert.Contains(t, err.Error(), "not found")
}
@@ -281,7 +290,9 @@ func TestUpdateVersionMetadata(t *testing.T) {
version.Metadata.NotAfter = nil
// Save version
err = version.Save([]byte("test-value"))
testBuffer := memguard.NewBufferFromBytes([]byte("test-value"))
defer testBuffer.Destroy()
err = version.Save(testBuffer)
require.NoError(t, err)
// Update metadata

View File

@@ -346,10 +346,7 @@ func (v *Vault) CreatePassphraseUnlocker(passphrase *memguard.LockedBuffer) (*se
// Encrypt private key with passphrase
privKeyStr := unlockerIdentity.String()
privKeyBuffer := memguard.NewBufferFromBytes([]byte(privKeyStr))
defer privKeyBuffer.Destroy()
encryptedPrivKey, err := secret.EncryptWithPassphrase(privKeyBuffer.Bytes(), passphrase)
encryptedPrivKey, err := secret.EncryptWithPassphrase([]byte(privKeyStr), passphrase)
if err != nil {
return nil, fmt.Errorf("failed to encrypt unlocker private key: %w", err)
}
@@ -385,8 +382,10 @@ func (v *Vault) CreatePassphraseUnlocker(passphrase *memguard.LockedBuffer) (*se
return nil, fmt.Errorf("failed to get long-term key: %w", err)
}
ltPrivKey := []byte(ltIdentity.String())
encryptedLtPrivKey, err := secret.EncryptToRecipient(ltPrivKey, unlockerIdentity.Recipient())
ltPrivKeyBuffer := memguard.NewBufferFromBytes([]byte(ltIdentity.String()))
defer ltPrivKeyBuffer.Destroy()
encryptedLtPrivKey, err := secret.EncryptToRecipient(ltPrivKeyBuffer, unlockerIdentity.Recipient())
if err != nil {
return nil, fmt.Errorf("failed to encrypt long-term private key: %w", err)
}

View File

@@ -122,8 +122,13 @@ func TestVaultOperations(t *testing.T) {
// Now add a secret
secretName := "test/secret"
secretValue := []byte("test-secret-value")
expectedValue := make([]byte, len(secretValue))
copy(expectedValue, secretValue)
err = vlt.AddSecret(secretName, secretValue, false)
secretBuffer := memguard.NewBufferFromBytes(secretValue)
defer secretBuffer.Destroy()
err = vlt.AddSecret(secretName, secretBuffer, false)
if err != nil {
t.Fatalf("Failed to add secret: %v", err)
}
@@ -152,8 +157,8 @@ func TestVaultOperations(t *testing.T) {
t.Fatalf("Failed to get secret: %v", err)
}
if string(retrievedValue) != string(secretValue) {
t.Errorf("Expected secret value '%s', got '%s'", string(secretValue), string(retrievedValue))
if string(retrievedValue) != string(expectedValue) {
t.Errorf("Expected secret value '%s', got '%s'", string(expectedValue), string(retrievedValue))
}
})