forked from sneak/secret
575 lines
15 KiB
Go
575 lines
15 KiB
Go
package secret
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"filippo.io/age"
|
|
"git.eeqj.de/sneak/secret/pkg/agehd"
|
|
"github.com/spf13/afero"
|
|
)
|
|
|
|
const testMnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
|
|
|
// setupTestEnvironment sets up the test environment with mock filesystem and environment variables
|
|
func setupTestEnvironment(t *testing.T) (afero.Fs, func()) {
|
|
// Create mock filesystem
|
|
fs := afero.NewMemMapFs()
|
|
|
|
// Save original environment variables
|
|
oldMnemonic := os.Getenv(EnvMnemonic)
|
|
oldPassphrase := os.Getenv(EnvUnlockPassphrase)
|
|
oldStateDir := os.Getenv(EnvStateDir)
|
|
|
|
// Create a real temporary directory for the state directory
|
|
// This is needed because GetStateDir checks the real filesystem
|
|
realTempDir, err := os.MkdirTemp("", "secret-test-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create real temp directory: %v", err)
|
|
}
|
|
|
|
// Set test environment variables
|
|
os.Setenv(EnvMnemonic, testMnemonic)
|
|
os.Setenv(EnvUnlockPassphrase, "test-passphrase")
|
|
os.Setenv(EnvStateDir, realTempDir)
|
|
|
|
// Also create the directory structure in the mock filesystem
|
|
err = fs.MkdirAll(realTempDir, 0700)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create test state directory in mock fs: %v", err)
|
|
}
|
|
|
|
// Create vaults.d directory in both filesystems
|
|
vaultsDir := filepath.Join(realTempDir, "vaults.d")
|
|
err = os.MkdirAll(vaultsDir, 0700)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create real vaults directory: %v", err)
|
|
}
|
|
err = fs.MkdirAll(vaultsDir, 0700)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create mock vaults directory: %v", err)
|
|
}
|
|
|
|
// Return cleanup function
|
|
cleanup := func() {
|
|
// Clean up real temporary directory
|
|
os.RemoveAll(realTempDir)
|
|
|
|
// Restore environment variables
|
|
if oldMnemonic == "" {
|
|
os.Unsetenv(EnvMnemonic)
|
|
} else {
|
|
os.Setenv(EnvMnemonic, oldMnemonic)
|
|
}
|
|
if oldPassphrase == "" {
|
|
os.Unsetenv(EnvUnlockPassphrase)
|
|
} else {
|
|
os.Setenv(EnvUnlockPassphrase, oldPassphrase)
|
|
}
|
|
if oldStateDir == "" {
|
|
os.Unsetenv(EnvStateDir)
|
|
} else {
|
|
os.Setenv(EnvStateDir, oldStateDir)
|
|
}
|
|
}
|
|
|
|
return fs, cleanup
|
|
}
|
|
|
|
func TestCreateVault(t *testing.T) {
|
|
fs, cleanup := setupTestEnvironment(t)
|
|
defer cleanup()
|
|
|
|
stateDir := "/test-secret-state"
|
|
|
|
// Test creating a new vault
|
|
vault, err := CreateVault(fs, stateDir, "test-vault")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create vault: %v", err)
|
|
}
|
|
|
|
if vault.Name != "test-vault" {
|
|
t.Errorf("Expected vault name 'test-vault', got '%s'", vault.Name)
|
|
}
|
|
|
|
// Check that vault directory was created
|
|
vaultDir, err := vault.GetDirectory()
|
|
if err != nil {
|
|
t.Fatalf("Failed to get vault directory: %v", err)
|
|
}
|
|
|
|
exists, err := afero.DirExists(fs, vaultDir)
|
|
if err != nil {
|
|
t.Fatalf("Error checking vault directory: %v", err)
|
|
}
|
|
if !exists {
|
|
t.Errorf("Vault directory was not created")
|
|
}
|
|
|
|
// Check that subdirectories were created
|
|
secretsDir := filepath.Join(vaultDir, "secrets.d")
|
|
exists, err = afero.DirExists(fs, secretsDir)
|
|
if err != nil {
|
|
t.Fatalf("Error checking secrets directory: %v", err)
|
|
}
|
|
if !exists {
|
|
t.Errorf("Secrets directory was not created")
|
|
}
|
|
|
|
unlockKeysDir := filepath.Join(vaultDir, "unlock.d")
|
|
exists, err = afero.DirExists(fs, unlockKeysDir)
|
|
if err != nil {
|
|
t.Fatalf("Error checking unlock keys directory: %v", err)
|
|
}
|
|
if !exists {
|
|
t.Errorf("Unlock keys directory was not created")
|
|
}
|
|
|
|
// Test creating a vault that already exists
|
|
_, err = CreateVault(fs, stateDir, "test-vault")
|
|
if err == nil {
|
|
t.Errorf("Expected error when creating vault that already exists")
|
|
}
|
|
}
|
|
|
|
func TestSelectVault(t *testing.T) {
|
|
fs, cleanup := setupTestEnvironment(t)
|
|
defer cleanup()
|
|
|
|
stateDir := "/test-secret-state"
|
|
|
|
// Create a vault first
|
|
_, err := CreateVault(fs, stateDir, "test-vault")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create vault: %v", err)
|
|
}
|
|
|
|
// Test selecting the vault
|
|
err = SelectVault(fs, stateDir, "test-vault")
|
|
if err != nil {
|
|
t.Fatalf("Failed to select vault: %v", err)
|
|
}
|
|
|
|
// Check that currentvault symlink was created with correct target
|
|
currentVaultPath := filepath.Join(stateDir, "currentvault")
|
|
|
|
content, err := afero.ReadFile(fs, currentVaultPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to read currentvault symlink: %v", err)
|
|
}
|
|
|
|
expectedPath := filepath.Join(stateDir, "vaults.d", "test-vault")
|
|
if string(content) != expectedPath {
|
|
t.Errorf("Expected currentvault to point to '%s', got '%s'", expectedPath, string(content))
|
|
}
|
|
|
|
// Test selecting a vault that doesn't exist
|
|
err = SelectVault(fs, stateDir, "nonexistent-vault")
|
|
if err == nil {
|
|
t.Errorf("Expected error when selecting nonexistent vault")
|
|
}
|
|
}
|
|
|
|
func TestGetCurrentVault(t *testing.T) {
|
|
fs, cleanup := setupTestEnvironment(t)
|
|
defer cleanup()
|
|
|
|
stateDir := "/test-secret-state"
|
|
|
|
// Create and select a vault
|
|
_, err := CreateVault(fs, stateDir, "test-vault")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create vault: %v", err)
|
|
}
|
|
|
|
err = SelectVault(fs, stateDir, "test-vault")
|
|
if err != nil {
|
|
t.Fatalf("Failed to select vault: %v", err)
|
|
}
|
|
|
|
// Test getting current vault
|
|
vault, err := GetCurrentVault(fs, stateDir)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get current vault: %v", err)
|
|
}
|
|
|
|
if vault.Name != "test-vault" {
|
|
t.Errorf("Expected current vault name 'test-vault', got '%s'", vault.Name)
|
|
}
|
|
}
|
|
|
|
func TestListVaults(t *testing.T) {
|
|
fs, cleanup := setupTestEnvironment(t)
|
|
defer cleanup()
|
|
|
|
stateDir := "/test-secret-state"
|
|
|
|
// Initially no vaults
|
|
vaults, err := ListVaults(fs, stateDir)
|
|
if err != nil {
|
|
t.Fatalf("Failed to list vaults: %v", err)
|
|
}
|
|
if len(vaults) != 0 {
|
|
t.Errorf("Expected no vaults initially, got %d", len(vaults))
|
|
}
|
|
|
|
// Create multiple vaults
|
|
vaultNames := []string{"vault1", "vault2", "vault3"}
|
|
for _, name := range vaultNames {
|
|
_, err := CreateVault(fs, stateDir, name)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create vault %s: %v", name, err)
|
|
}
|
|
}
|
|
|
|
// List vaults
|
|
vaults, err = ListVaults(fs, stateDir)
|
|
if err != nil {
|
|
t.Fatalf("Failed to list vaults: %v", err)
|
|
}
|
|
|
|
if len(vaults) != len(vaultNames) {
|
|
t.Errorf("Expected %d vaults, got %d", len(vaultNames), len(vaults))
|
|
}
|
|
|
|
// Check that all created vaults are in the list
|
|
vaultMap := make(map[string]bool)
|
|
for _, vault := range vaults {
|
|
vaultMap[vault] = true
|
|
}
|
|
|
|
for _, name := range vaultNames {
|
|
if !vaultMap[name] {
|
|
t.Errorf("Expected vault '%s' in list", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestVaultGetDirectory(t *testing.T) {
|
|
fs, cleanup := setupTestEnvironment(t)
|
|
defer cleanup()
|
|
|
|
stateDir := "/test-secret-state"
|
|
|
|
vault := NewVault(fs, "test-vault", stateDir)
|
|
|
|
dir, err := vault.GetDirectory()
|
|
if err != nil {
|
|
t.Fatalf("Failed to get vault directory: %v", err)
|
|
}
|
|
|
|
expectedDir := "/test-secret-state/vaults.d/test-vault"
|
|
if dir != expectedDir {
|
|
t.Errorf("Expected directory '%s', got '%s'", expectedDir, dir)
|
|
}
|
|
}
|
|
|
|
func TestAddSecret(t *testing.T) {
|
|
fs, cleanup := setupTestEnvironment(t)
|
|
defer cleanup()
|
|
|
|
stateDir := "/test-secret-state"
|
|
|
|
// Create vault and set up long-term key
|
|
vault, err := CreateVault(fs, stateDir, "test-vault")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create vault: %v", err)
|
|
}
|
|
|
|
// We need to create a long-term public key for the vault
|
|
// This simulates what happens during vault initialization
|
|
err = setupVaultWithLongTermKey(fs, vault)
|
|
if err != nil {
|
|
t.Fatalf("Failed to setup vault with long-term key: %v", err)
|
|
}
|
|
|
|
// Test adding a secret
|
|
secretName := "test-secret"
|
|
secretValue := []byte("super secret value")
|
|
|
|
err = vault.AddSecret(secretName, secretValue, false)
|
|
if err != nil {
|
|
t.Fatalf("Failed to add secret: %v", err)
|
|
}
|
|
|
|
// Check that secret directory was created
|
|
vaultDir, _ := vault.GetDirectory()
|
|
secretDir := filepath.Join(vaultDir, "secrets.d", secretName)
|
|
exists, err := afero.DirExists(fs, secretDir)
|
|
if err != nil {
|
|
t.Fatalf("Error checking secret directory: %v", err)
|
|
}
|
|
if !exists {
|
|
t.Errorf("Secret directory was not created")
|
|
}
|
|
|
|
// Check that encrypted secret file exists
|
|
secretFile := filepath.Join(secretDir, "value.age")
|
|
exists, err = afero.Exists(fs, secretFile)
|
|
if err != nil {
|
|
t.Fatalf("Error checking secret file: %v", err)
|
|
}
|
|
if !exists {
|
|
t.Errorf("Secret file was not created")
|
|
}
|
|
|
|
// Check that metadata file exists
|
|
metadataFile := filepath.Join(secretDir, "secret-metadata.json")
|
|
exists, err = afero.Exists(fs, metadataFile)
|
|
if err != nil {
|
|
t.Fatalf("Error checking metadata file: %v", err)
|
|
}
|
|
if !exists {
|
|
t.Errorf("Metadata file was not created")
|
|
}
|
|
|
|
// Test adding a duplicate secret without force flag
|
|
err = vault.AddSecret(secretName, secretValue, false)
|
|
if err == nil {
|
|
t.Errorf("Expected error when adding duplicate secret without force flag")
|
|
}
|
|
|
|
// Test adding a duplicate secret with force flag
|
|
err = vault.AddSecret(secretName, []byte("new value"), true)
|
|
if err != nil {
|
|
t.Errorf("Failed to overwrite secret with force flag: %v", err)
|
|
}
|
|
|
|
// Test adding secret with slash in name (should be encoded)
|
|
err = vault.AddSecret("path/to/secret", []byte("value"), false)
|
|
if err != nil {
|
|
t.Fatalf("Failed to add secret with slash in name: %v", err)
|
|
}
|
|
|
|
// Check that the slash was encoded as percent
|
|
encodedSecretDir := filepath.Join(vaultDir, "secrets.d", "path%to%secret")
|
|
exists, err = afero.DirExists(fs, encodedSecretDir)
|
|
if err != nil {
|
|
t.Fatalf("Error checking encoded secret directory: %v", err)
|
|
}
|
|
if !exists {
|
|
t.Errorf("Encoded secret directory was not created")
|
|
}
|
|
}
|
|
|
|
func TestGetSecret(t *testing.T) {
|
|
fs, cleanup := setupTestEnvironment(t)
|
|
defer cleanup()
|
|
|
|
stateDir := "/test-secret-state"
|
|
|
|
// Create vault and set up long-term key
|
|
vault, err := CreateVault(fs, stateDir, "test-vault")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create vault: %v", err)
|
|
}
|
|
|
|
err = setupVaultWithLongTermKey(fs, vault)
|
|
if err != nil {
|
|
t.Fatalf("Failed to setup vault with long-term key: %v", err)
|
|
}
|
|
|
|
// Add a secret
|
|
secretName := "test-secret"
|
|
secretValue := []byte("super secret value")
|
|
err = vault.AddSecret(secretName, secretValue, false)
|
|
if err != nil {
|
|
t.Fatalf("Failed to add secret: %v", err)
|
|
}
|
|
|
|
// Test getting the secret (using mnemonic environment variable)
|
|
retrievedValue, err := vault.GetSecret(secretName)
|
|
if err != nil {
|
|
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))
|
|
}
|
|
|
|
// Test getting a nonexistent secret
|
|
_, err = vault.GetSecret("nonexistent-secret")
|
|
if err == nil {
|
|
t.Errorf("Expected error when getting nonexistent secret")
|
|
}
|
|
|
|
// Test getting secret with encoded name
|
|
encodedSecretName := "path/to/secret"
|
|
encodedSecretValue := []byte("encoded secret value")
|
|
err = vault.AddSecret(encodedSecretName, encodedSecretValue, false)
|
|
if err != nil {
|
|
t.Fatalf("Failed to add encoded secret: %v", err)
|
|
}
|
|
|
|
retrievedEncodedValue, err := vault.GetSecret(encodedSecretName)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get encoded secret: %v", err)
|
|
}
|
|
|
|
if string(retrievedEncodedValue) != string(encodedSecretValue) {
|
|
t.Errorf("Expected encoded secret value '%s', got '%s'", string(encodedSecretValue), string(retrievedEncodedValue))
|
|
}
|
|
}
|
|
|
|
func TestListSecrets(t *testing.T) {
|
|
fs, cleanup := setupTestEnvironment(t)
|
|
defer cleanup()
|
|
|
|
stateDir := "/test-secret-state"
|
|
|
|
// Create vault and set up long-term key
|
|
vault, err := CreateVault(fs, stateDir, "test-vault")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create vault: %v", err)
|
|
}
|
|
|
|
err = setupVaultWithLongTermKey(fs, vault)
|
|
if err != nil {
|
|
t.Fatalf("Failed to setup vault with long-term key: %v", err)
|
|
}
|
|
|
|
// Initially no secrets
|
|
secrets, err := vault.ListSecrets()
|
|
if err != nil {
|
|
t.Fatalf("Failed to list secrets: %v", err)
|
|
}
|
|
if len(secrets) != 0 {
|
|
t.Errorf("Expected no secrets initially, got %d", len(secrets))
|
|
}
|
|
|
|
// Add multiple secrets
|
|
secretNames := []string{"secret1", "secret2", "path/to/secret3"}
|
|
for _, name := range secretNames {
|
|
err := vault.AddSecret(name, []byte("value for "+name), false)
|
|
if err != nil {
|
|
t.Fatalf("Failed to add secret %s: %v", name, err)
|
|
}
|
|
}
|
|
|
|
// List secrets
|
|
secrets, err = vault.ListSecrets()
|
|
if err != nil {
|
|
t.Fatalf("Failed to list secrets: %v", err)
|
|
}
|
|
|
|
if len(secrets) != len(secretNames) {
|
|
t.Errorf("Expected %d secrets, got %d", len(secretNames), len(secrets))
|
|
}
|
|
|
|
// Check that all added secrets are in the list (names should be decoded)
|
|
secretMap := make(map[string]bool)
|
|
for _, secret := range secrets {
|
|
secretMap[secret] = true
|
|
}
|
|
|
|
for _, name := range secretNames {
|
|
if !secretMap[name] {
|
|
t.Errorf("Expected secret '%s' in list", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGetSecretMetadata(t *testing.T) {
|
|
fs, cleanup := setupTestEnvironment(t)
|
|
defer cleanup()
|
|
|
|
stateDir := "/test-secret-state"
|
|
|
|
// Create vault and set up long-term key
|
|
vault, err := CreateVault(fs, stateDir, "test-vault")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create vault: %v", err)
|
|
}
|
|
|
|
err = setupVaultWithLongTermKey(fs, vault)
|
|
if err != nil {
|
|
t.Fatalf("Failed to setup vault with long-term key: %v", err)
|
|
}
|
|
|
|
// Add a secret
|
|
secretName := "test-secret"
|
|
secretValue := []byte("super secret value")
|
|
beforeAdd := time.Now()
|
|
err = vault.AddSecret(secretName, secretValue, false)
|
|
if err != nil {
|
|
t.Fatalf("Failed to add secret: %v", err)
|
|
}
|
|
afterAdd := time.Now()
|
|
|
|
// Get secret object and its metadata
|
|
secretObj, err := vault.GetSecretObject(secretName)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get secret object: %v", err)
|
|
}
|
|
|
|
metadata := secretObj.GetMetadata()
|
|
|
|
if metadata.Name != secretName {
|
|
t.Errorf("Expected metadata name '%s', got '%s'", secretName, metadata.Name)
|
|
}
|
|
|
|
// Check that timestamps are reasonable
|
|
if metadata.CreatedAt.Before(beforeAdd) || metadata.CreatedAt.After(afterAdd) {
|
|
t.Errorf("CreatedAt timestamp is out of expected range")
|
|
}
|
|
|
|
if metadata.UpdatedAt.Before(beforeAdd) || metadata.UpdatedAt.After(afterAdd) {
|
|
t.Errorf("UpdatedAt timestamp is out of expected range")
|
|
}
|
|
|
|
// Test getting metadata for nonexistent secret
|
|
_, err = vault.GetSecretObject("nonexistent-secret")
|
|
if err == nil {
|
|
t.Errorf("Expected error when getting secret object for nonexistent secret")
|
|
}
|
|
}
|
|
|
|
func TestListUnlockKeys(t *testing.T) {
|
|
fs, cleanup := setupTestEnvironment(t)
|
|
defer cleanup()
|
|
|
|
stateDir := "/test-secret-state"
|
|
|
|
// Create vault
|
|
vault, err := CreateVault(fs, stateDir, "test-vault")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create vault: %v", err)
|
|
}
|
|
|
|
// Initially no unlock keys
|
|
keys, err := vault.ListUnlockKeys()
|
|
if err != nil {
|
|
t.Fatalf("Failed to list unlock keys: %v", err)
|
|
}
|
|
if len(keys) != 0 {
|
|
t.Errorf("Expected no unlock keys initially, got %d", len(keys))
|
|
}
|
|
}
|
|
|
|
// setupVaultWithLongTermKey sets up a vault with a long-term public key for testing
|
|
func setupVaultWithLongTermKey(fs afero.Fs, vault *Vault) error {
|
|
// This simulates what happens during vault initialization
|
|
// We derive a long-term keypair from the test mnemonic
|
|
ltIdentity, err := vault.deriveLongTermIdentity()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Store the long-term public key in the vault
|
|
vaultDir, err := vault.GetDirectory()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ltPubKey := ltIdentity.Recipient().String()
|
|
return afero.WriteFile(fs, filepath.Join(vaultDir, "pub.age"), []byte(ltPubKey), 0600)
|
|
}
|
|
|
|
// deriveLongTermIdentity is a helper method to derive the long-term identity for testing
|
|
func (v *Vault) deriveLongTermIdentity() (*age.X25519Identity, error) {
|
|
// Use agehd.DeriveIdentity with the test mnemonic
|
|
return agehd.DeriveIdentity(testMnemonic, 0)
|
|
}
|