Fix integration tests: correct vault derivation index and debug test failures
This commit is contained in:
parent
e036d280c0
commit
02be4b2a55
1
Makefile
1
Makefile
@ -9,7 +9,6 @@ vet:
|
||||
|
||||
test:
|
||||
go test -v ./...
|
||||
bash test_secret_manager.sh
|
||||
|
||||
lint:
|
||||
golangci-lint run --timeout 5m
|
||||
|
18
README.md
18
README.md
@ -175,13 +175,16 @@ Decrypts data using an Age key stored as a secret.
|
||||
│ │ │ └── database%password/ # Secret: database/password
|
||||
│ │ │ ├── versions/
|
||||
│ │ │ └── current -> versions/20231215.001
|
||||
│ │ ├── vault-metadata.json # Vault metadata
|
||||
│ │ ├── pub.age # Long-term public key
|
||||
│ │ └── current-unlocker -> ../unlockers.d/passphrase
|
||||
│ └── work/
|
||||
│ ├── unlockers.d/
|
||||
│ ├── secrets.d/
|
||||
│ ├── vault-metadata.json
|
||||
│ ├── pub.age
|
||||
│ └── current-unlocker
|
||||
├── currentvault -> vaults.d/default
|
||||
└── configuration.json
|
||||
└── currentvault -> vaults.d/default
|
||||
```
|
||||
|
||||
### Key Management and Encryption Flow
|
||||
@ -309,11 +312,17 @@ secret decrypt encryption/mykey --input document.txt.age --output document.txt
|
||||
- **Encryption**: Age (X25519 + ChaCha20-Poly1305)
|
||||
- **Key Exchange**: X25519 elliptic curve Diffie-Hellman
|
||||
- **Authentication**: Poly1305 MAC
|
||||
- **Hashing**: Double SHA-256 for public key identification
|
||||
|
||||
### File Formats
|
||||
- **Age Files**: Standard Age encryption format (.age extension)
|
||||
- **Metadata**: JSON format with timestamps and type information
|
||||
- **Configuration**: JSON configuration files
|
||||
- **Vault Metadata**: JSON containing vault name, creation time, derivation index, and public key hash
|
||||
|
||||
### Vault Management
|
||||
- **Derivation Index**: Each vault uses a unique derivation index from the mnemonic
|
||||
- **Public Key Hash**: Double SHA-256 hash of the index-0 public key identifies vaults from the same mnemonic
|
||||
- **Automatic Key Derivation**: When creating vaults with a mnemonic, keys are automatically derived
|
||||
|
||||
### Cross-Platform Support
|
||||
- **macOS**: Full support including Keychain integration
|
||||
@ -351,8 +360,9 @@ make lint # Run linter
|
||||
### Testing
|
||||
The project includes comprehensive tests:
|
||||
```bash
|
||||
./test_secret_manager.sh # Full integration test suite
|
||||
make test # Run all tests
|
||||
go test ./... # Unit tests
|
||||
go test -tags=integration -v ./internal/cli # Integration tests
|
||||
```
|
||||
|
||||
## Features
|
||||
|
@ -1,148 +0,0 @@
|
||||
# Version Support Test Suite Documentation
|
||||
|
||||
This document describes the comprehensive test suite created for the versioned secrets functionality in the Secret Manager.
|
||||
|
||||
## Test Files Created
|
||||
|
||||
### 1. `internal/secret/version_test.go`
|
||||
Core unit tests for version functionality:
|
||||
|
||||
- **TestGenerateVersionName**: Tests version name generation with date and serial format
|
||||
- **TestGenerateVersionNameMaxSerial**: Tests the 999 versions per day limit
|
||||
- **TestNewSecretVersion**: Tests secret version object creation
|
||||
- **TestSecretVersionSave**: Tests saving a version with encryption
|
||||
- **TestSecretVersionLoadMetadata**: Tests loading and decrypting version metadata
|
||||
- **TestSecretVersionGetValue**: Tests retrieving and decrypting version values
|
||||
- **TestListVersions**: Tests listing versions in reverse chronological order
|
||||
- **TestGetCurrentVersion**: Tests retrieving the current version via symlink
|
||||
- **TestSetCurrentVersion**: Tests updating the current version symlink
|
||||
- **TestVersionMetadataTimestamps**: Tests timestamp pointer consistency
|
||||
|
||||
### 2. `internal/vault/secrets_version_test.go`
|
||||
Integration tests for vault-level version operations:
|
||||
|
||||
- **TestVaultAddSecretCreatesVersion**: Tests that AddSecret creates proper version structure
|
||||
- **TestVaultAddSecretMultipleVersions**: Tests creating multiple versions with force flag
|
||||
- **TestVaultGetSecretVersion**: Tests retrieving specific versions and current version
|
||||
- **TestVaultVersionTimestamps**: Tests timestamp logic (notBefore/notAfter) across versions
|
||||
- **TestVaultGetNonExistentVersion**: Tests error handling for invalid versions
|
||||
- **TestUpdateVersionMetadata**: Tests metadata update functionality
|
||||
|
||||
### 3. `internal/cli/version_test.go`
|
||||
CLI command tests:
|
||||
|
||||
- **TestListVersionsCommand**: Tests `secret version list` command output
|
||||
- **TestListVersionsNonExistentSecret**: Tests error handling for missing secrets
|
||||
- **TestPromoteVersionCommand**: Tests `secret version promote` command
|
||||
- **TestPromoteNonExistentVersion**: Tests error handling for invalid promotion
|
||||
- **TestGetSecretWithVersion**: Tests `secret get --version` flag functionality
|
||||
- **TestVersionCommandStructure**: Tests command structure and help text
|
||||
- **TestListVersionsEmptyOutput**: Tests edge case with no versions
|
||||
|
||||
### 4. `internal/vault/integration_version_test.go`
|
||||
Comprehensive integration tests:
|
||||
|
||||
- **TestVersionIntegrationWorkflow**: End-to-end workflow testing
|
||||
- Creating initial version with proper metadata
|
||||
- Creating multiple versions with timestamp updates
|
||||
- Retrieving specific versions by name
|
||||
- Promoting old versions to current
|
||||
- Testing version serial number limits (999/day)
|
||||
- Error cases and edge conditions
|
||||
|
||||
- **TestVersionConcurrency**: Tests concurrent read operations
|
||||
|
||||
- **TestVersionCompatibility**: Tests handling of legacy non-versioned secrets
|
||||
|
||||
## Key Test Scenarios Covered
|
||||
|
||||
### Version Creation
|
||||
- First version gets `notBefore = epoch + 1 second`
|
||||
- Subsequent versions update previous version's `notAfter` timestamp
|
||||
- New version's `notBefore` equals previous version's `notAfter`
|
||||
- Version names follow `YYYYMMDD.NNN` format
|
||||
- Maximum 999 versions per day enforced
|
||||
|
||||
### Version Retrieval
|
||||
- Get current version via symlink
|
||||
- Get specific version by name
|
||||
- Empty version parameter returns current
|
||||
- Non-existent versions return appropriate errors
|
||||
|
||||
### Version Management
|
||||
- List versions in reverse chronological order
|
||||
- Promote any version to current
|
||||
- Promotion doesn't modify timestamps
|
||||
- Metadata remains encrypted and intact
|
||||
|
||||
### Data Integrity
|
||||
- Each version has independent encryption keys
|
||||
- Metadata encryption protects version history
|
||||
- Long-term key required for all operations
|
||||
- Concurrent reads handled safely
|
||||
|
||||
### Backward Compatibility
|
||||
- Legacy secrets without versions detected
|
||||
- Appropriate error messages for incompatible operations
|
||||
|
||||
## Test Utilities Created
|
||||
|
||||
### Helper Functions
|
||||
- `createTestVaultWithKey()`: Sets up vault with long-term key for testing
|
||||
- `setupTestVault()`: CLI test helper for vault initialization
|
||||
- Mock implementations for isolated testing
|
||||
|
||||
### Test Environment
|
||||
- Uses in-memory filesystem (afero.MemMapFs)
|
||||
- Consistent test mnemonic for reproducible keys
|
||||
- Proper cleanup and isolation between tests
|
||||
|
||||
## Running the Tests
|
||||
|
||||
Run all version-related tests:
|
||||
```bash
|
||||
go test ./internal/... -run "Test.*Version.*" -v
|
||||
```
|
||||
|
||||
Run specific test suites:
|
||||
```bash
|
||||
# Core version tests
|
||||
go test ./internal/secret -run "Test.*Version.*" -v
|
||||
|
||||
# Vault integration tests
|
||||
go test ./internal/vault -run "Test.*Version.*" -v
|
||||
|
||||
# CLI tests
|
||||
go test ./internal/cli -run "Test.*Version.*" -v
|
||||
```
|
||||
|
||||
Run the comprehensive integration test:
|
||||
```bash
|
||||
go test ./internal/vault -run TestVersionIntegrationWorkflow -v
|
||||
```
|
||||
|
||||
## Test Coverage Areas
|
||||
|
||||
1. **Functional Coverage**
|
||||
- Version CRUD operations
|
||||
- Timestamp management
|
||||
- Encryption/decryption
|
||||
- Symlink handling
|
||||
- Error conditions
|
||||
|
||||
2. **Integration Coverage**
|
||||
- Vault-secret interaction
|
||||
- CLI-vault interaction
|
||||
- End-to-end workflows
|
||||
|
||||
3. **Edge Cases**
|
||||
- Maximum versions per day
|
||||
- Empty version directories
|
||||
- Missing symlinks
|
||||
- Concurrent access
|
||||
- Legacy compatibility
|
||||
|
||||
4. **Security Coverage**
|
||||
- Encrypted metadata
|
||||
- Key isolation per version
|
||||
- Long-term key requirements
|
@ -6,7 +6,6 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"filippo.io/age"
|
||||
"git.eeqj.de/sneak/secret/internal/secret"
|
||||
@ -75,31 +74,18 @@ func (cli *CLIInstance) Init(cmd *cobra.Command) error {
|
||||
return fmt.Errorf("invalid BIP39 mnemonic phrase\nRun 'secret generate mnemonic' to create a valid mnemonic")
|
||||
}
|
||||
|
||||
// Calculate mnemonic hash for index tracking
|
||||
mnemonicHash := vault.ComputeDoubleSHA256([]byte(mnemonicStr))
|
||||
secret.DebugWith("Calculated mnemonic hash", slog.String("hash", mnemonicHash))
|
||||
|
||||
// Get the next available derivation index for this mnemonic
|
||||
derivationIndex, err := vault.GetNextDerivationIndex(cli.fs, cli.stateDir, mnemonicHash)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to get next derivation index", "error", err)
|
||||
return fmt.Errorf("failed to get next derivation index: %w", err)
|
||||
// Set mnemonic in environment for CreateVault to use
|
||||
originalMnemonic := os.Getenv(secret.EnvMnemonic)
|
||||
os.Setenv(secret.EnvMnemonic, mnemonicStr)
|
||||
defer func() {
|
||||
if originalMnemonic != "" {
|
||||
os.Setenv(secret.EnvMnemonic, originalMnemonic)
|
||||
} else {
|
||||
os.Unsetenv(secret.EnvMnemonic)
|
||||
}
|
||||
secret.DebugWith("Using derivation index", slog.Uint64("index", uint64(derivationIndex)))
|
||||
}()
|
||||
|
||||
// Derive long-term keypair from mnemonic with the appropriate index
|
||||
secret.DebugWith("Deriving long-term key from mnemonic", slog.Uint64("index", uint64(derivationIndex)))
|
||||
ltIdentity, err := agehd.DeriveIdentity(mnemonicStr, derivationIndex)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to derive long-term key", "error", err)
|
||||
return fmt.Errorf("failed to derive long-term key from mnemonic: %w", err)
|
||||
}
|
||||
|
||||
// Calculate the long-term key hash
|
||||
ltKeyHash := vault.ComputeDoubleSHA256([]byte(ltIdentity.String()))
|
||||
secret.DebugWith("Calculated long-term key hash", slog.String("hash", ltKeyHash))
|
||||
|
||||
// Create the default vault
|
||||
// Create the default vault - it will handle key derivation internally
|
||||
secret.Debug("Creating default vault")
|
||||
vlt, err := vault.CreateVault(cli.fs, cli.stateDir, "default")
|
||||
if err != nil {
|
||||
@ -107,35 +93,21 @@ func (cli *CLIInstance) Init(cmd *cobra.Command) error {
|
||||
return fmt.Errorf("failed to create default vault: %w", err)
|
||||
}
|
||||
|
||||
// Set as current vault
|
||||
secret.Debug("Setting default vault as current")
|
||||
if err := vault.SelectVault(cli.fs, cli.stateDir, "default"); err != nil {
|
||||
secret.Debug("Failed to select default vault", "error", err)
|
||||
return fmt.Errorf("failed to select default vault: %w", err)
|
||||
}
|
||||
|
||||
// Store long-term public key in vault
|
||||
// Get the vault metadata to retrieve the derivation index
|
||||
vaultDir := filepath.Join(stateDir, "vaults.d", "default")
|
||||
ltPubKey := ltIdentity.Recipient().String()
|
||||
secret.DebugWith("Storing long-term public key", slog.String("pubkey", ltPubKey), slog.String("vault_dir", vaultDir))
|
||||
if err := afero.WriteFile(cli.fs, filepath.Join(vaultDir, "pub.age"), []byte(ltPubKey), secret.FilePerms); err != nil {
|
||||
secret.Debug("Failed to write long-term public key", "error", err)
|
||||
return fmt.Errorf("failed to write long-term public key: %w", err)
|
||||
metadata, err := vault.LoadVaultMetadata(cli.fs, vaultDir)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to load vault metadata", "error", err)
|
||||
return fmt.Errorf("failed to load vault metadata: %w", err)
|
||||
}
|
||||
|
||||
// Save vault metadata
|
||||
metadata := &vault.VaultMetadata{
|
||||
Name: "default",
|
||||
CreatedAt: time.Now(),
|
||||
DerivationIndex: derivationIndex,
|
||||
LongTermKeyHash: ltKeyHash,
|
||||
MnemonicHash: mnemonicHash,
|
||||
// Derive the long-term key using the same index that CreateVault used
|
||||
ltIdentity, err := agehd.DeriveIdentity(mnemonicStr, metadata.DerivationIndex)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to derive long-term key", "error", err)
|
||||
return fmt.Errorf("failed to derive long-term key from mnemonic: %w", err)
|
||||
}
|
||||
if err := vault.SaveVaultMetadata(cli.fs, vaultDir, metadata); err != nil {
|
||||
secret.Debug("Failed to save vault metadata", "error", err)
|
||||
return fmt.Errorf("failed to save vault metadata: %w", err)
|
||||
}
|
||||
secret.Debug("Saved vault metadata with derivation index and key hash")
|
||||
ltPubKey := ltIdentity.Recipient().String()
|
||||
|
||||
// Unlock the vault with the derived long-term key
|
||||
vlt.Unlock(ltIdentity)
|
||||
|
1947
internal/cli/integration_test.go
Normal file
1947
internal/cli/integration_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@ -240,13 +240,8 @@ func (cli *CLIInstance) UnlockersAdd(unlockerType string, cmd *cobra.Command) er
|
||||
return fmt.Errorf("failed to get current vault: %w", err)
|
||||
}
|
||||
|
||||
// Try to unlock the vault if not already unlocked
|
||||
if vlt.Locked() {
|
||||
_, err := vlt.UnlockVault()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unlock vault: %w", err)
|
||||
}
|
||||
}
|
||||
// For passphrase unlockers, we don't need the vault to be unlocked
|
||||
// The CreatePassphraseUnlocker method will handle getting the long-term key
|
||||
|
||||
// Check if passphrase is set in environment variable
|
||||
var passphraseStr string
|
||||
|
@ -181,6 +181,12 @@ func (cli *CLIInstance) VaultImport(vaultName string) error {
|
||||
return fmt.Errorf("vault '%s' does not exist", vaultName)
|
||||
}
|
||||
|
||||
// Check if vault already has a public key
|
||||
pubKeyPath := fmt.Sprintf("%s/pub.age", vaultDir)
|
||||
if _, err := cli.fs.Stat(pubKeyPath); err == nil {
|
||||
return fmt.Errorf("vault '%s' already has a long-term key configured", vaultName)
|
||||
}
|
||||
|
||||
// Get mnemonic from environment
|
||||
mnemonic := os.Getenv(secret.EnvMnemonic)
|
||||
if mnemonic == "" {
|
||||
@ -194,12 +200,8 @@ func (cli *CLIInstance) VaultImport(vaultName string) error {
|
||||
return fmt.Errorf("invalid BIP39 mnemonic")
|
||||
}
|
||||
|
||||
// Calculate mnemonic hash for index tracking
|
||||
mnemonicHash := vault.ComputeDoubleSHA256([]byte(mnemonic))
|
||||
secret.Debug("Calculated mnemonic hash", "hash", mnemonicHash)
|
||||
|
||||
// Get the next available derivation index for this mnemonic
|
||||
derivationIndex, err := vault.GetNextDerivationIndex(cli.fs, cli.stateDir, mnemonicHash)
|
||||
derivationIndex, err := vault.GetNextDerivationIndex(cli.fs, cli.stateDir, mnemonic)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to get next derivation index", "error", err)
|
||||
return fmt.Errorf("failed to get next derivation index: %w", err)
|
||||
@ -213,32 +215,36 @@ func (cli *CLIInstance) VaultImport(vaultName string) error {
|
||||
return fmt.Errorf("failed to derive long-term key: %w", err)
|
||||
}
|
||||
|
||||
// Calculate the long-term key hash
|
||||
ltKeyHash := vault.ComputeDoubleSHA256([]byte(ltIdentity.String()))
|
||||
secret.Debug("Calculated long-term key hash", "hash", ltKeyHash)
|
||||
|
||||
// Store long-term public key in vault
|
||||
ltPublicKey := ltIdentity.Recipient().String()
|
||||
secret.Debug("Storing long-term public key", "pubkey", ltPublicKey, "vault_dir", vaultDir)
|
||||
|
||||
pubKeyPath := fmt.Sprintf("%s/pub.age", vaultDir)
|
||||
if err := afero.WriteFile(cli.fs, pubKeyPath, []byte(ltPublicKey), 0600); err != nil {
|
||||
return fmt.Errorf("failed to store long-term public key: %w", err)
|
||||
}
|
||||
|
||||
// Save vault metadata
|
||||
metadata := &vault.VaultMetadata{
|
||||
// Calculate public key hash
|
||||
publicKeyHash := vault.ComputeDoubleSHA256([]byte(ltPublicKey))
|
||||
|
||||
// Load existing metadata
|
||||
existingMetadata, err := vault.LoadVaultMetadata(cli.fs, vaultDir)
|
||||
if err != nil {
|
||||
// If metadata doesn't exist, create new
|
||||
existingMetadata = &vault.VaultMetadata{
|
||||
Name: vaultName,
|
||||
CreatedAt: time.Now(),
|
||||
DerivationIndex: derivationIndex,
|
||||
LongTermKeyHash: ltKeyHash,
|
||||
MnemonicHash: mnemonicHash,
|
||||
}
|
||||
if err := vault.SaveVaultMetadata(cli.fs, vaultDir, metadata); err != nil {
|
||||
}
|
||||
|
||||
// Update metadata with new derivation info
|
||||
existingMetadata.DerivationIndex = derivationIndex
|
||||
existingMetadata.PublicKeyHash = publicKeyHash
|
||||
|
||||
if err := vault.SaveVaultMetadata(cli.fs, vaultDir, existingMetadata); err != nil {
|
||||
secret.Debug("Failed to save vault metadata", "error", err)
|
||||
return fmt.Errorf("failed to save vault metadata: %w", err)
|
||||
}
|
||||
secret.Debug("Saved vault metadata with derivation index and key hash")
|
||||
secret.Debug("Saved vault metadata with derivation index and public key hash")
|
||||
|
||||
// Get passphrase from environment variable
|
||||
passphraseStr := os.Getenv(secret.EnvUnlockPassphrase)
|
||||
|
@ -1,3 +1,19 @@
|
||||
// Version CLI Command Tests
|
||||
//
|
||||
// Tests for version-related CLI commands:
|
||||
//
|
||||
// - TestListVersionsCommand: Tests `secret version list` command output
|
||||
// - TestListVersionsNonExistentSecret: Tests error handling for missing secrets
|
||||
// - TestPromoteVersionCommand: Tests `secret version promote` command
|
||||
// - TestPromoteNonExistentVersion: Tests error handling for invalid promotion
|
||||
// - TestGetSecretWithVersion: Tests `secret get --version` flag functionality
|
||||
// - TestVersionCommandStructure: Tests command structure and help text
|
||||
// - TestListVersionsEmptyOutput: Tests edge case with no versions
|
||||
//
|
||||
// Test Utilities:
|
||||
// - setupTestVault(): CLI test helper for vault initialization
|
||||
// - Uses consistent test mnemonic for reproducible testing
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
|
@ -10,8 +10,7 @@ type VaultMetadata struct {
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
Description string `json:"description,omitempty"`
|
||||
DerivationIndex uint32 `json:"derivation_index"`
|
||||
LongTermKeyHash string `json:"long_term_key_hash"` // Double SHA256 hash of derived long-term private key
|
||||
MnemonicHash string `json:"mnemonic_hash"` // Double SHA256 hash of mnemonic for index tracking
|
||||
PublicKeyHash string `json:"public_key_hash,omitempty"` // Double SHA256 hash of the long-term public key
|
||||
}
|
||||
|
||||
// UnlockerMetadata contains information about an unlocker
|
||||
|
@ -1,3 +1,37 @@
|
||||
// Version Support Test Suite Documentation
|
||||
//
|
||||
// This file contains core unit tests for version functionality:
|
||||
//
|
||||
// - TestGenerateVersionName: Tests version name generation with date and serial format
|
||||
// - TestGenerateVersionNameMaxSerial: Tests the 999 versions per day limit
|
||||
// - TestNewSecretVersion: Tests secret version object creation
|
||||
// - TestSecretVersionSave: Tests saving a version with encryption
|
||||
// - TestSecretVersionLoadMetadata: Tests loading and decrypting version metadata
|
||||
// - TestSecretVersionGetValue: Tests retrieving and decrypting version values
|
||||
// - TestListVersions: Tests listing versions in reverse chronological order
|
||||
// - TestGetCurrentVersion: Tests retrieving the current version via symlink
|
||||
// - TestSetCurrentVersion: Tests updating the current version symlink
|
||||
// - TestVersionMetadataTimestamps: Tests timestamp pointer consistency
|
||||
//
|
||||
// Key Test Scenarios:
|
||||
// - Version Creation: First version gets notBefore = epoch + 1 second
|
||||
// - Subsequent versions update previous version's notAfter timestamp
|
||||
// - New version's notBefore equals previous version's notAfter
|
||||
// - Version names follow YYYYMMDD.NNN format
|
||||
// - Maximum 999 versions per day enforced
|
||||
//
|
||||
// Version Retrieval:
|
||||
// - Get current version via symlink
|
||||
// - Get specific version by name
|
||||
// - Empty version parameter returns current
|
||||
// - Non-existent versions return appropriate errors
|
||||
//
|
||||
// Data Integrity:
|
||||
// - Each version has independent encryption keys
|
||||
// - Metadata encryption protects version history
|
||||
// - Long-term key required for all operations
|
||||
// - Concurrent reads handled safely
|
||||
|
||||
package secret
|
||||
|
||||
import (
|
||||
|
@ -102,7 +102,7 @@ func TestVaultWithRealFilesystem(t *testing.T) {
|
||||
t.Fatalf("Failed to create state dir: %v", err)
|
||||
}
|
||||
|
||||
// Create a test vault
|
||||
// Create a test vault - CreateVault now handles public key when mnemonic is in env
|
||||
vlt, err := vault.CreateVault(fs, stateDir, "test-vault")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create vault: %v", err)
|
||||
@ -114,19 +114,6 @@ func TestVaultWithRealFilesystem(t *testing.T) {
|
||||
t.Fatalf("Failed to derive long-term key: %v", err)
|
||||
}
|
||||
|
||||
// Get the vault directory
|
||||
vaultDir, err := vlt.GetDirectory()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get vault directory: %v", err)
|
||||
}
|
||||
|
||||
// Write long-term public key
|
||||
ltPubKeyPath := filepath.Join(vaultDir, "pub.age")
|
||||
pubKey := ltIdentity.Recipient().String()
|
||||
if err := afero.WriteFile(fs, ltPubKeyPath, []byte(pubKey), secret.FilePerms); err != nil {
|
||||
t.Fatalf("Failed to write long-term public key: %v", err)
|
||||
}
|
||||
|
||||
// Unlock the vault
|
||||
vlt.Unlock(ltIdentity)
|
||||
|
||||
@ -176,31 +163,18 @@ func TestVaultWithRealFilesystem(t *testing.T) {
|
||||
t.Fatalf("Failed to create state dir: %v", err)
|
||||
}
|
||||
|
||||
// Create a test vault
|
||||
// Create a test vault - CreateVault now handles public key when mnemonic is in env
|
||||
vlt, err := vault.CreateVault(fs, stateDir, "test-vault")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create vault: %v", err)
|
||||
}
|
||||
|
||||
// Derive long-term key from mnemonic
|
||||
// Derive long-term key from mnemonic for verification
|
||||
ltIdentity, err := agehd.DeriveIdentity(testMnemonic, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to derive long-term key: %v", err)
|
||||
}
|
||||
|
||||
// Get the vault directory
|
||||
vaultDir, err := vlt.GetDirectory()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get vault directory: %v", err)
|
||||
}
|
||||
|
||||
// Write long-term public key
|
||||
ltPubKeyPath := filepath.Join(vaultDir, "pub.age")
|
||||
pubKey := ltIdentity.Recipient().String()
|
||||
if err := afero.WriteFile(fs, ltPubKeyPath, []byte(pubKey), secret.FilePerms); err != nil {
|
||||
t.Fatalf("Failed to write long-term public key: %v", err)
|
||||
}
|
||||
|
||||
// Verify the vault is locked initially
|
||||
if !vlt.Locked() {
|
||||
t.Errorf("Vault should be locked initially")
|
||||
@ -346,7 +320,7 @@ func TestVaultWithRealFilesystem(t *testing.T) {
|
||||
t.Fatalf("Failed to create state dir: %v", err)
|
||||
}
|
||||
|
||||
// Create two vaults
|
||||
// Create two vaults - CreateVault now handles public key when mnemonic is in env
|
||||
vault1, err := vault.CreateVault(fs, stateDir, "vault1")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create vault1: %v", err)
|
||||
@ -358,26 +332,20 @@ func TestVaultWithRealFilesystem(t *testing.T) {
|
||||
}
|
||||
|
||||
// Derive long-term key from mnemonic
|
||||
ltIdentity, err := agehd.DeriveIdentity(testMnemonic, 0)
|
||||
// Note: Both vaults will have different derivation indexes due to GetNextDerivationIndex
|
||||
ltIdentity1, err := agehd.DeriveIdentity(testMnemonic, 0) // vault1 gets index 0
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to derive long-term key: %v", err)
|
||||
t.Fatalf("Failed to derive long-term key for vault1: %v", err)
|
||||
}
|
||||
|
||||
// Setup both vaults with the same long-term key
|
||||
for _, vlt := range []*vault.Vault{vault1, vault2} {
|
||||
vaultDir, err := vlt.GetDirectory()
|
||||
ltIdentity2, err := agehd.DeriveIdentity(testMnemonic, 1) // vault2 gets index 1
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get vault directory: %v", err)
|
||||
t.Fatalf("Failed to derive long-term key for vault2: %v", err)
|
||||
}
|
||||
|
||||
ltPubKeyPath := filepath.Join(vaultDir, "pub.age")
|
||||
pubKey := ltIdentity.Recipient().String()
|
||||
if err := afero.WriteFile(fs, ltPubKeyPath, []byte(pubKey), secret.FilePerms); err != nil {
|
||||
t.Fatalf("Failed to write long-term public key: %v", err)
|
||||
}
|
||||
|
||||
vlt.Unlock(ltIdentity)
|
||||
}
|
||||
// Unlock the vaults with their respective keys
|
||||
vault1.Unlock(ltIdentity1)
|
||||
vault2.Unlock(ltIdentity2)
|
||||
|
||||
// Add a secret to vault1
|
||||
secretName := "test-secret"
|
||||
|
@ -1,3 +1,24 @@
|
||||
// Version Support Integration Tests
|
||||
//
|
||||
// Comprehensive integration tests for version functionality:
|
||||
//
|
||||
// - TestVersionIntegrationWorkflow: End-to-end workflow testing
|
||||
// - Creating initial version with proper metadata
|
||||
// - Creating multiple versions with timestamp updates
|
||||
// - Retrieving specific versions by name
|
||||
// - Promoting old versions to current
|
||||
// - Testing version serial number limits (999/day)
|
||||
// - Error cases and edge conditions
|
||||
//
|
||||
// - TestVersionConcurrency: Tests concurrent read operations
|
||||
//
|
||||
// - TestVersionCompatibility: Tests handling of legacy non-versioned secrets
|
||||
//
|
||||
// Test Environment:
|
||||
// - Uses in-memory filesystem (afero.MemMapFs)
|
||||
// - Consistent test mnemonic for reproducible keys
|
||||
// - Proper cleanup and isolation between tests
|
||||
|
||||
package vault
|
||||
|
||||
import (
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.eeqj.de/sneak/secret/internal/secret"
|
||||
"git.eeqj.de/sneak/secret/pkg/agehd"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
@ -202,13 +203,53 @@ func CreateVault(fs afero.Fs, stateDir string, name string) (*Vault, error) {
|
||||
return nil, fmt.Errorf("failed to create unlockers directory: %w", err)
|
||||
}
|
||||
|
||||
// Save initial vault metadata (without derivation info until a mnemonic is imported)
|
||||
// Check if mnemonic is available in environment
|
||||
mnemonic := os.Getenv(secret.EnvMnemonic)
|
||||
var derivationIndex uint32
|
||||
var publicKeyHash string
|
||||
|
||||
if mnemonic != "" {
|
||||
secret.Debug("Mnemonic found in environment, deriving long-term key", "vault", name)
|
||||
|
||||
// Get the next available derivation index for this mnemonic
|
||||
var err error
|
||||
derivationIndex, err = GetNextDerivationIndex(fs, stateDir, mnemonic)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get next derivation index: %w", err)
|
||||
}
|
||||
|
||||
// Derive the long-term key
|
||||
ltIdentity, err := agehd.DeriveIdentity(mnemonic, derivationIndex)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to derive long-term key: %w", err)
|
||||
}
|
||||
|
||||
// Write the public key
|
||||
ltPubKey := ltIdentity.Recipient().String()
|
||||
ltPubKeyPath := filepath.Join(vaultDir, "pub.age")
|
||||
if err := afero.WriteFile(fs, ltPubKeyPath, []byte(ltPubKey), secret.FilePerms); err != nil {
|
||||
return nil, fmt.Errorf("failed to write long-term public key: %w", err)
|
||||
}
|
||||
secret.Debug("Wrote long-term public key", "path", ltPubKeyPath)
|
||||
|
||||
// Compute public key hash from index 0 (same for all vaults with this mnemonic)
|
||||
identity0, err := agehd.DeriveIdentity(mnemonic, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to derive identity for index 0: %w", err)
|
||||
}
|
||||
publicKeyHash = ComputeDoubleSHA256([]byte(identity0.Recipient().String()))
|
||||
} else {
|
||||
secret.Debug("No mnemonic in environment, vault created without long-term key", "vault", name)
|
||||
// Use 0 for derivation index when no mnemonic is provided
|
||||
derivationIndex = 0
|
||||
}
|
||||
|
||||
// Save vault metadata
|
||||
metadata := &VaultMetadata{
|
||||
Name: name,
|
||||
CreatedAt: time.Now(),
|
||||
DerivationIndex: 0,
|
||||
LongTermKeyHash: "", // Will be set when mnemonic is imported
|
||||
MnemonicHash: "", // Will be set when mnemonic is imported
|
||||
DerivationIndex: derivationIndex,
|
||||
PublicKeyHash: publicKeyHash,
|
||||
}
|
||||
if err := SaveVaultMetadata(fs, vaultDir, metadata); err != nil {
|
||||
return nil, fmt.Errorf("failed to save vault metadata: %w", err)
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"git.eeqj.de/sneak/secret/internal/secret"
|
||||
"git.eeqj.de/sneak/secret/pkg/agehd"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
@ -24,8 +25,16 @@ func ComputeDoubleSHA256(data []byte) string {
|
||||
return hex.EncodeToString(secondHash[:])
|
||||
}
|
||||
|
||||
// GetNextDerivationIndex finds the next available derivation index for a given mnemonic hash
|
||||
func GetNextDerivationIndex(fs afero.Fs, stateDir string, mnemonicHash string) (uint32, error) {
|
||||
// GetNextDerivationIndex finds the next available derivation index for a given mnemonic
|
||||
// by deriving the public key for index 0 and using its hash to identify related vaults
|
||||
func GetNextDerivationIndex(fs afero.Fs, stateDir string, mnemonic string) (uint32, error) {
|
||||
// First, derive the public key for index 0 to get our identifier
|
||||
identity0, err := agehd.DeriveIdentity(mnemonic, 0)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to derive identity for index 0: %w", err)
|
||||
}
|
||||
pubKeyHash := ComputeDoubleSHA256([]byte(identity0.Recipient().String()))
|
||||
|
||||
vaultsDir := filepath.Join(stateDir, "vaults.d")
|
||||
|
||||
// Check if vaults directory exists
|
||||
@ -44,9 +53,8 @@ func GetNextDerivationIndex(fs afero.Fs, stateDir string, mnemonicHash string) (
|
||||
return 0, fmt.Errorf("failed to read vaults directory: %w", err)
|
||||
}
|
||||
|
||||
// Track the highest index for this mnemonic
|
||||
var highestIndex uint32 = 0
|
||||
foundMatch := false
|
||||
// Track which indices are in use for this mnemonic
|
||||
usedIndices := make(map[uint32]bool)
|
||||
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
@ -67,22 +75,19 @@ func GetNextDerivationIndex(fs afero.Fs, stateDir string, mnemonicHash string) (
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this vault uses the same mnemonic
|
||||
if metadata.MnemonicHash == mnemonicHash {
|
||||
foundMatch = true
|
||||
if metadata.DerivationIndex >= highestIndex {
|
||||
highestIndex = metadata.DerivationIndex
|
||||
}
|
||||
// Check if this vault uses the same mnemonic by comparing public key hashes
|
||||
if metadata.PublicKeyHash == pubKeyHash {
|
||||
usedIndices[metadata.DerivationIndex] = true
|
||||
}
|
||||
}
|
||||
|
||||
// If we found a match, use the next index
|
||||
if foundMatch {
|
||||
return highestIndex + 1, nil
|
||||
// Find the first available index
|
||||
var index uint32 = 0
|
||||
for usedIndices[index] {
|
||||
index++
|
||||
}
|
||||
|
||||
// No existing vault with this mnemonic, start at 0
|
||||
return 0, nil
|
||||
return index, nil
|
||||
}
|
||||
|
||||
// SaveVaultMetadata saves vault metadata to the vault directory
|
||||
|
@ -13,6 +13,9 @@ func TestVaultMetadata(t *testing.T) {
|
||||
fs := afero.NewMemMapFs()
|
||||
stateDir := "/test/state"
|
||||
|
||||
// Test mnemonic for consistent testing
|
||||
testMnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||
|
||||
t.Run("ComputeDoubleSHA256", func(t *testing.T) {
|
||||
// Test data
|
||||
data := []byte("test data")
|
||||
@ -38,7 +41,7 @@ func TestVaultMetadata(t *testing.T) {
|
||||
|
||||
t.Run("GetNextDerivationIndex", func(t *testing.T) {
|
||||
// Test with no existing vaults
|
||||
index, err := GetNextDerivationIndex(fs, stateDir, "mnemonic-hash-1")
|
||||
index, err := GetNextDerivationIndex(fs, stateDir, testMnemonic)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get derivation index: %v", err)
|
||||
}
|
||||
@ -46,24 +49,36 @@ func TestVaultMetadata(t *testing.T) {
|
||||
t.Errorf("Expected index 0 for first vault, got %d", index)
|
||||
}
|
||||
|
||||
// Create a vault with metadata
|
||||
// Create a vault with metadata and matching public key
|
||||
vaultDir := filepath.Join(stateDir, "vaults.d", "vault1")
|
||||
if err := fs.MkdirAll(vaultDir, 0700); err != nil {
|
||||
t.Fatalf("Failed to create vault directory: %v", err)
|
||||
}
|
||||
|
||||
// Derive identity for index 0
|
||||
identity0, err := agehd.DeriveIdentity(testMnemonic, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to derive identity: %v", err)
|
||||
}
|
||||
pubKey0 := identity0.Recipient().String()
|
||||
pubKeyHash0 := ComputeDoubleSHA256([]byte(pubKey0))
|
||||
|
||||
// Write public key
|
||||
if err := afero.WriteFile(fs, filepath.Join(vaultDir, "pub.age"), []byte(pubKey0), 0600); err != nil {
|
||||
t.Fatalf("Failed to write public key: %v", err)
|
||||
}
|
||||
|
||||
metadata1 := &VaultMetadata{
|
||||
Name: "vault1",
|
||||
DerivationIndex: 0,
|
||||
MnemonicHash: "mnemonic-hash-1",
|
||||
LongTermKeyHash: "key-hash-1",
|
||||
PublicKeyHash: pubKeyHash0,
|
||||
}
|
||||
if err := SaveVaultMetadata(fs, vaultDir, metadata1); err != nil {
|
||||
t.Fatalf("Failed to save metadata: %v", err)
|
||||
}
|
||||
|
||||
// Next index for same mnemonic should be 1
|
||||
index, err = GetNextDerivationIndex(fs, stateDir, "mnemonic-hash-1")
|
||||
index, err = GetNextDerivationIndex(fs, stateDir, testMnemonic)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get derivation index: %v", err)
|
||||
}
|
||||
@ -72,7 +87,8 @@ func TestVaultMetadata(t *testing.T) {
|
||||
}
|
||||
|
||||
// Different mnemonic should start at 0
|
||||
index, err = GetNextDerivationIndex(fs, stateDir, "mnemonic-hash-2")
|
||||
differentMnemonic := "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong"
|
||||
index, err = GetNextDerivationIndex(fs, stateDir, differentMnemonic)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get derivation index: %v", err)
|
||||
}
|
||||
@ -86,23 +102,34 @@ func TestVaultMetadata(t *testing.T) {
|
||||
t.Fatalf("Failed to create vault directory: %v", err)
|
||||
}
|
||||
|
||||
// Derive identity for index 5
|
||||
identity5, err := agehd.DeriveIdentity(testMnemonic, 5)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to derive identity: %v", err)
|
||||
}
|
||||
pubKey5 := identity5.Recipient().String()
|
||||
|
||||
// Write public key
|
||||
if err := afero.WriteFile(fs, filepath.Join(vaultDir2, "pub.age"), []byte(pubKey5), 0600); err != nil {
|
||||
t.Fatalf("Failed to write public key: %v", err)
|
||||
}
|
||||
|
||||
metadata2 := &VaultMetadata{
|
||||
Name: "vault2",
|
||||
DerivationIndex: 5,
|
||||
MnemonicHash: "mnemonic-hash-1",
|
||||
LongTermKeyHash: "key-hash-2",
|
||||
PublicKeyHash: pubKeyHash0, // Same hash since it's from the same mnemonic
|
||||
}
|
||||
if err := SaveVaultMetadata(fs, vaultDir2, metadata2); err != nil {
|
||||
t.Fatalf("Failed to save metadata: %v", err)
|
||||
}
|
||||
|
||||
// Next index should be 6
|
||||
index, err = GetNextDerivationIndex(fs, stateDir, "mnemonic-hash-1")
|
||||
// Next index should be 1 (not 6) because we look for the first available slot
|
||||
index, err = GetNextDerivationIndex(fs, stateDir, testMnemonic)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get derivation index: %v", err)
|
||||
}
|
||||
if index != 6 {
|
||||
t.Errorf("Expected index 6 after vault with index 5, got %d", index)
|
||||
if index != 1 {
|
||||
t.Errorf("Expected index 1 (first available), got %d", index)
|
||||
}
|
||||
})
|
||||
|
||||
@ -116,8 +143,7 @@ func TestVaultMetadata(t *testing.T) {
|
||||
metadata := &VaultMetadata{
|
||||
Name: "test-vault",
|
||||
DerivationIndex: 3,
|
||||
MnemonicHash: "test-mnemonic-hash",
|
||||
LongTermKeyHash: "test-key-hash",
|
||||
PublicKeyHash: "test-public-key-hash",
|
||||
}
|
||||
|
||||
if err := SaveVaultMetadata(fs, vaultDir, metadata); err != nil {
|
||||
@ -136,17 +162,12 @@ func TestVaultMetadata(t *testing.T) {
|
||||
if loaded.DerivationIndex != metadata.DerivationIndex {
|
||||
t.Errorf("DerivationIndex mismatch: expected %d, got %d", metadata.DerivationIndex, loaded.DerivationIndex)
|
||||
}
|
||||
if loaded.MnemonicHash != metadata.MnemonicHash {
|
||||
t.Errorf("MnemonicHash mismatch: expected %s, got %s", metadata.MnemonicHash, loaded.MnemonicHash)
|
||||
}
|
||||
if loaded.LongTermKeyHash != metadata.LongTermKeyHash {
|
||||
t.Errorf("LongTermKeyHash mismatch: expected %s, got %s", metadata.LongTermKeyHash, loaded.LongTermKeyHash)
|
||||
if loaded.PublicKeyHash != metadata.PublicKeyHash {
|
||||
t.Errorf("PublicKeyHash mismatch: expected %s, got %s", metadata.PublicKeyHash, loaded.PublicKeyHash)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("DifferentKeysForDifferentIndices", func(t *testing.T) {
|
||||
testMnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||
|
||||
// Derive keys with different indices
|
||||
identity0, err := agehd.DeriveIdentity(testMnemonic, 0)
|
||||
if err != nil {
|
||||
@ -158,18 +179,24 @@ func TestVaultMetadata(t *testing.T) {
|
||||
t.Fatalf("Failed to derive identity with index 1: %v", err)
|
||||
}
|
||||
|
||||
// Compute hashes
|
||||
hash0 := ComputeDoubleSHA256([]byte(identity0.String()))
|
||||
hash1 := ComputeDoubleSHA256([]byte(identity1.String()))
|
||||
// Compute public key hashes
|
||||
pubKey0 := identity0.Recipient().String()
|
||||
pubKey1 := identity1.Recipient().String()
|
||||
hash0 := ComputeDoubleSHA256([]byte(pubKey0))
|
||||
|
||||
// Verify different indices produce different keys
|
||||
if hash0 == hash1 {
|
||||
t.Errorf("Different derivation indices should produce different keys")
|
||||
// Verify different indices produce different public keys
|
||||
if pubKey0 == pubKey1 {
|
||||
t.Errorf("Different derivation indices should produce different public keys")
|
||||
}
|
||||
|
||||
// Verify public keys are also different
|
||||
if identity0.Recipient().String() == identity1.Recipient().String() {
|
||||
t.Errorf("Different derivation indices should produce different public keys")
|
||||
// But the hash of index 0's public key should be the same for the same mnemonic
|
||||
// This is what we use as the identifier
|
||||
identity0Again, _ := agehd.DeriveIdentity(testMnemonic, 0)
|
||||
pubKey0Again := identity0Again.Recipient().String()
|
||||
hash0Again := ComputeDoubleSHA256([]byte(pubKey0Again))
|
||||
|
||||
if hash0 != hash0Again {
|
||||
t.Errorf("Same mnemonic should produce same public key hash for index 0")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -63,10 +63,31 @@ func (v *Vault) ListSecrets() ([]string, error) {
|
||||
}
|
||||
|
||||
// isValidSecretName validates secret names according to the format [a-z0-9\.\-\_\/]+
|
||||
// but with additional restrictions:
|
||||
// - No leading or trailing slashes
|
||||
// - No double slashes
|
||||
// - No names starting with dots
|
||||
func isValidSecretName(name string) bool {
|
||||
if name == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for leading/trailing slashes
|
||||
if strings.HasPrefix(name, "/") || strings.HasSuffix(name, "/") {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for double slashes
|
||||
if strings.Contains(name, "//") {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for names starting with dot
|
||||
if strings.HasPrefix(name, ".") {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check the basic pattern
|
||||
matched, _ := regexp.MatchString(`^[a-z0-9\.\-\_\/]+$`, name)
|
||||
return matched
|
||||
}
|
||||
|
@ -1,3 +1,20 @@
|
||||
// Vault-Level Version Operation Tests
|
||||
//
|
||||
// Integration tests for vault-level version operations:
|
||||
//
|
||||
// - TestVaultAddSecretCreatesVersion: Tests that AddSecret creates proper version structure
|
||||
// - TestVaultAddSecretMultipleVersions: Tests creating multiple versions with force flag
|
||||
// - TestVaultGetSecretVersion: Tests retrieving specific versions and current version
|
||||
// - TestVaultVersionTimestamps: Tests timestamp logic (notBefore/notAfter) across versions
|
||||
// - TestVaultGetNonExistentVersion: Tests error handling for invalid versions
|
||||
// - TestUpdateVersionMetadata: Tests metadata update functionality
|
||||
//
|
||||
// Version Management:
|
||||
// - List versions in reverse chronological order
|
||||
// - Promote any version to current
|
||||
// - Promotion doesn't modify timestamps
|
||||
// - Metadata remains encrypted and intact
|
||||
|
||||
package vault
|
||||
|
||||
import (
|
||||
|
@ -350,9 +350,14 @@ func (v *Vault) CreatePassphraseUnlocker(passphrase string) (*secret.PassphraseU
|
||||
return nil, fmt.Errorf("failed to write unlocker metadata: %w", err)
|
||||
}
|
||||
|
||||
// Encrypt long-term private key to this unlocker if vault is unlocked
|
||||
if !v.Locked() {
|
||||
ltPrivKey := []byte(v.GetLongTermKey().String())
|
||||
// Encrypt long-term private key to this unlocker
|
||||
// We need to get the long-term key (either from memory if unlocked, or derive it)
|
||||
ltIdentity, err := v.GetOrDeriveLongTermKey()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get long-term key: %w", err)
|
||||
}
|
||||
|
||||
ltPrivKey := []byte(ltIdentity.String())
|
||||
encryptedLtPrivKey, err := secret.EncryptToRecipient(ltPrivKey, unlockerIdentity.Recipient())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encrypt long-term private key: %w", err)
|
||||
@ -362,7 +367,6 @@ func (v *Vault) CreatePassphraseUnlocker(passphrase string) (*secret.PassphraseU
|
||||
if err := afero.WriteFile(v.fs, ltPrivKeyPath, encryptedLtPrivKey, secret.FilePerms); err != nil {
|
||||
return nil, fmt.Errorf("failed to write encrypted long-term private key: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Select this unlocker as current
|
||||
if err := v.SelectUnlocker(unlockerID); err != nil {
|
||||
|
@ -65,7 +65,20 @@ func (v *Vault) GetOrDeriveLongTermKey() (*age.X25519Identity, error) {
|
||||
// Try to derive from environment mnemonic first
|
||||
if envMnemonic := os.Getenv(secret.EnvMnemonic); envMnemonic != "" {
|
||||
secret.Debug("Using mnemonic from environment for long-term key derivation", "vault_name", v.Name)
|
||||
ltIdentity, err := agehd.DeriveIdentity(envMnemonic, 0)
|
||||
|
||||
// Load vault metadata to get the derivation index
|
||||
vaultDir, err := v.GetDirectory()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get vault directory: %w", err)
|
||||
}
|
||||
|
||||
metadata, err := LoadVaultMetadata(v.fs, vaultDir)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to load vault metadata", "error", err, "vault_name", v.Name)
|
||||
return nil, fmt.Errorf("failed to load vault metadata: %w", err)
|
||||
}
|
||||
|
||||
ltIdentity, err := agehd.DeriveIdentity(envMnemonic, metadata.DerivationIndex)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to derive long-term key from mnemonic", "error", err, "vault_name", v.Name)
|
||||
return nil, fmt.Errorf("failed to derive long-term key from mnemonic: %w", err)
|
||||
@ -74,6 +87,7 @@ func (v *Vault) GetOrDeriveLongTermKey() (*age.X25519Identity, error) {
|
||||
secret.DebugWith("Successfully derived long-term key from mnemonic",
|
||||
slog.String("vault_name", v.Name),
|
||||
slog.String("public_key", ltIdentity.Recipient().String()),
|
||||
slog.Uint64("derivation_index", uint64(metadata.DerivationIndex)),
|
||||
)
|
||||
|
||||
// Cache the derived key by unlocking the vault
|
||||
|
173
test_output.log
Normal file
173
test_output.log
Normal file
@ -0,0 +1,173 @@
|
||||
=== RUN TestSecretManagerIntegration
|
||||
=== RUN TestSecretManagerIntegration/01_Initialize
|
||||
=== RUN TestSecretManagerIntegration/02_ListVaults
|
||||
=== RUN TestSecretManagerIntegration/03_CreateVault
|
||||
=== RUN TestSecretManagerIntegration/04_ImportMnemonic
|
||||
=== RUN TestSecretManagerIntegration/05_AddSecret
|
||||
=== RUN TestSecretManagerIntegration/06_GetSecret
|
||||
=== RUN TestSecretManagerIntegration/07_AddSecretVersion
|
||||
=== RUN TestSecretManagerIntegration/08_ListVersions
|
||||
=== RUN TestSecretManagerIntegration/09_GetSpecificVersion
|
||||
=== RUN TestSecretManagerIntegration/10_PromoteVersion
|
||||
=== RUN TestSecretManagerIntegration/11_ListSecrets
|
||||
integration_test.go:710: JSON output: {
|
||||
"secrets": [
|
||||
{
|
||||
"created_at": "2025-06-09T04:52:08.170719-07:00",
|
||||
"name": "api/key",
|
||||
"updated_at": "2025-06-09T04:52:08.170719-07:00"
|
||||
},
|
||||
{
|
||||
"created_at": "2025-06-09T04:52:08.170725-07:00",
|
||||
"name": "config/database.yaml",
|
||||
"updated_at": "2025-06-09T04:52:08.170725-07:00"
|
||||
},
|
||||
{
|
||||
"created_at": "2025-06-09T04:52:08.170731-07:00",
|
||||
"name": "database/password",
|
||||
"updated_at": "2025-06-09T04:52:08.170731-07:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
=== RUN TestSecretManagerIntegration/12_SecretNameFormats
|
||||
=== RUN TestSecretManagerIntegration/12_SecretNameFormats/api/keys/production
|
||||
=== RUN TestSecretManagerIntegration/12_SecretNameFormats/config.yaml
|
||||
=== RUN TestSecretManagerIntegration/12_SecretNameFormats/ssh_private_key
|
||||
=== RUN TestSecretManagerIntegration/12_SecretNameFormats/deeply/nested/path/to/secret
|
||||
=== RUN TestSecretManagerIntegration/12_SecretNameFormats/test-with-dash
|
||||
=== RUN TestSecretManagerIntegration/12_SecretNameFormats/test.with.dots
|
||||
=== RUN TestSecretManagerIntegration/12_SecretNameFormats/test_with_underscore
|
||||
=== RUN TestSecretManagerIntegration/12_SecretNameFormats/mixed/test.name_format-123
|
||||
=== RUN TestSecretManagerIntegration/12_SecretNameFormats/invalid_empty
|
||||
=== RUN TestSecretManagerIntegration/12_SecretNameFormats/invalid_UPPERCASE
|
||||
=== RUN TestSecretManagerIntegration/12_SecretNameFormats/invalid_with_space_space
|
||||
=== RUN TestSecretManagerIntegration/12_SecretNameFormats/invalid_with@symbol
|
||||
=== RUN TestSecretManagerIntegration/12_SecretNameFormats/invalid_with#hash
|
||||
=== RUN TestSecretManagerIntegration/12_SecretNameFormats/invalid_with$dollar
|
||||
=== RUN TestSecretManagerIntegration/12_SecretNameFormats/invalid__slash_leading-slash
|
||||
integration_test.go:854: add '/leading-slash' result: err=<nil>, output=
|
||||
=== RUN TestSecretManagerIntegration/12_SecretNameFormats/invalid_trailing-slash_slash_
|
||||
integration_test.go:854: add 'trailing-slash/' result: err=<nil>, output=
|
||||
=== RUN TestSecretManagerIntegration/12_SecretNameFormats/invalid_double_slash__slash_slash
|
||||
integration_test.go:854: add 'double//slash' result: err=<nil>, output=
|
||||
=== RUN TestSecretManagerIntegration/12_SecretNameFormats/invalid_.hidden
|
||||
integration_test.go:854: add '.hidden' result: err=<nil>, output=
|
||||
=== RUN TestSecretManagerIntegration/13_UnlockerManagement
|
||||
integration_test.go:883: Error adding passphrase unlocker: exit status 1, output: Error: failed to unlock vault: failed to get long-term key: failed to get unlocker identity: failed to decrypt unlocker private key: failed to create decryptor: no identity matched any of the recipients
|
||||
Usage:
|
||||
secret unlockers add <type> [flags]
|
||||
|
||||
Flags:
|
||||
-h, --help help for add
|
||||
--keyid string GPG key ID for PGP unlockers
|
||||
|
||||
integration_test.go:885:
|
||||
Error Trace: /Users/user/dev/secret/internal/cli/integration_test.go:885
|
||||
Error: Received unexpected error:
|
||||
exit status 1
|
||||
Test: TestSecretManagerIntegration/13_UnlockerManagement
|
||||
Messages: add passphrase unlocker should succeed
|
||||
=== RUN TestSecretManagerIntegration/14_SwitchVault
|
||||
=== RUN TestSecretManagerIntegration/15_VaultIsolation
|
||||
=== RUN TestSecretManagerIntegration/16_GenerateSecret
|
||||
=== RUN TestSecretManagerIntegration/17_ImportFromFile
|
||||
=== RUN TestSecretManagerIntegration/18_AgeKeyOperations
|
||||
=== RUN TestSecretManagerIntegration/19_DisasterRecovery
|
||||
integration_test.go:1233: Long-term public key from vault: age1gel0je3w796uqpp7k47w65agnrhe85ee3xz550us6kdpgy5nr3rq7mkfqs
|
||||
integration_test.go:1239: Note: Long-term private key can be derived from mnemonic
|
||||
integration_test.go:1259: === Disaster Recovery Chain ===
|
||||
integration_test.go:1260: 1. Secret value is encrypted to version public key: /var/folders/w9/_481zfb94wx2yq562f5h68vw0000gn/T/TestSecretManagerIntegration3789343214/001/vaults.d/default/secrets.d/test%disaster-recovery/versions/20250609.001/pub.age
|
||||
integration_test.go:1261: 2. Version private key is encrypted to long-term public key: /var/folders/w9/_481zfb94wx2yq562f5h68vw0000gn/T/TestSecretManagerIntegration3789343214/001/vaults.d/default/pub.age
|
||||
integration_test.go:1262: 3. Long-term private key is derived from mnemonic
|
||||
integration_test.go:1282: === Disaster Recovery Test Complete ===
|
||||
integration_test.go:1283: The vault structure is compatible with standard age encryption.
|
||||
integration_test.go:1284: In a real disaster scenario:
|
||||
integration_test.go:1285: 1. Derive long-term private key from mnemonic using BIP32/BIP39
|
||||
integration_test.go:1286: 2. Use 'age -d' to decrypt version private keys
|
||||
integration_test.go:1287: 3. Use 'age -d' to decrypt secret values
|
||||
integration_test.go:1288: No proprietary tools needed - just mnemonic + age CLI
|
||||
=== RUN TestSecretManagerIntegration/20_VersionTimestamps
|
||||
=== RUN TestSecretManagerIntegration/21_MaxVersionsPerDay
|
||||
integration_test.go:1353: Skipping max versions test - would take too long
|
||||
=== RUN TestSecretManagerIntegration/22_JSONOutput
|
||||
integration_test.go:1380: JSON output formats verified for vault list, secret list, and unlockers list
|
||||
=== RUN TestSecretManagerIntegration/23_ErrorHandling
|
||||
=== RUN TestSecretManagerIntegration/24_EnvironmentVariables
|
||||
=== RUN TestSecretManagerIntegration/25_ConcurrentOperations
|
||||
=== RUN TestSecretManagerIntegration/26_LargeSecrets
|
||||
=== RUN TestSecretManagerIntegration/27_SpecialCharacters
|
||||
=== RUN TestSecretManagerIntegration/28_VaultMetadata
|
||||
integration_test.go:1700:
|
||||
Error Trace: /Users/user/dev/secret/internal/cli/integration_test.go:1700
|
||||
Error: Not equal:
|
||||
expected: "992552b00b3879dfae461fab9a084b47784a032771c7a9accaebdde05ec7a7d1"
|
||||
actual : "e34a2f500e395d8934a90a99ee9311edcfffd68cb701079575e50cbac7bb9417"
|
||||
|
||||
Diff:
|
||||
--- Expected
|
||||
+++ Actual
|
||||
@@ -1 +1 @@
|
||||
-992552b00b3879dfae461fab9a084b47784a032771c7a9accaebdde05ec7a7d1
|
||||
+e34a2f500e395d8934a90a99ee9311edcfffd68cb701079575e50cbac7bb9417
|
||||
Test: TestSecretManagerIntegration/28_VaultMetadata
|
||||
Messages: vaults from same mnemonic should have same public_key_hash
|
||||
=== RUN TestSecretManagerIntegration/29_SymlinkHandling
|
||||
=== RUN TestSecretManagerIntegration/30_BackupRestore
|
||||
integration_test.go:1838:
|
||||
Error Trace: /Users/user/dev/secret/internal/cli/integration_test.go:1838
|
||||
Error: Received unexpected error:
|
||||
exit status 1
|
||||
Test: TestSecretManagerIntegration/30_BackupRestore
|
||||
Messages: get restored secret should succeed
|
||||
--- FAIL: TestSecretManagerIntegration (4.96s)
|
||||
--- PASS: TestSecretManagerIntegration/01_Initialize (0.79s)
|
||||
--- PASS: TestSecretManagerIntegration/02_ListVaults (0.02s)
|
||||
--- PASS: TestSecretManagerIntegration/03_CreateVault (0.02s)
|
||||
--- PASS: TestSecretManagerIntegration/04_ImportMnemonic (0.75s)
|
||||
--- PASS: TestSecretManagerIntegration/05_AddSecret (0.04s)
|
||||
--- PASS: TestSecretManagerIntegration/06_GetSecret (0.04s)
|
||||
--- PASS: TestSecretManagerIntegration/07_AddSecretVersion (0.06s)
|
||||
--- PASS: TestSecretManagerIntegration/08_ListVersions (0.03s)
|
||||
--- PASS: TestSecretManagerIntegration/09_GetSpecificVersion (0.06s)
|
||||
--- PASS: TestSecretManagerIntegration/10_PromoteVersion (0.04s)
|
||||
--- PASS: TestSecretManagerIntegration/11_ListSecrets (0.06s)
|
||||
--- PASS: TestSecretManagerIntegration/12_SecretNameFormats (0.37s)
|
||||
--- PASS: TestSecretManagerIntegration/12_SecretNameFormats/api/keys/production (0.03s)
|
||||
--- PASS: TestSecretManagerIntegration/12_SecretNameFormats/config.yaml (0.03s)
|
||||
--- PASS: TestSecretManagerIntegration/12_SecretNameFormats/ssh_private_key (0.03s)
|
||||
--- PASS: TestSecretManagerIntegration/12_SecretNameFormats/deeply/nested/path/to/secret (0.03s)
|
||||
--- PASS: TestSecretManagerIntegration/12_SecretNameFormats/test-with-dash (0.03s)
|
||||
--- PASS: TestSecretManagerIntegration/12_SecretNameFormats/test.with.dots (0.03s)
|
||||
--- PASS: TestSecretManagerIntegration/12_SecretNameFormats/test_with_underscore (0.03s)
|
||||
--- PASS: TestSecretManagerIntegration/12_SecretNameFormats/mixed/test.name_format-123 (0.03s)
|
||||
--- PASS: TestSecretManagerIntegration/12_SecretNameFormats/invalid_empty (0.01s)
|
||||
--- PASS: TestSecretManagerIntegration/12_SecretNameFormats/invalid_UPPERCASE (0.01s)
|
||||
--- PASS: TestSecretManagerIntegration/12_SecretNameFormats/invalid_with_space_space (0.01s)
|
||||
--- PASS: TestSecretManagerIntegration/12_SecretNameFormats/invalid_with@symbol (0.01s)
|
||||
--- PASS: TestSecretManagerIntegration/12_SecretNameFormats/invalid_with#hash (0.01s)
|
||||
--- PASS: TestSecretManagerIntegration/12_SecretNameFormats/invalid_with$dollar (0.01s)
|
||||
--- PASS: TestSecretManagerIntegration/12_SecretNameFormats/invalid__slash_leading-slash (0.01s)
|
||||
--- PASS: TestSecretManagerIntegration/12_SecretNameFormats/invalid_trailing-slash_slash_ (0.01s)
|
||||
--- PASS: TestSecretManagerIntegration/12_SecretNameFormats/invalid_double_slash__slash_slash (0.01s)
|
||||
--- PASS: TestSecretManagerIntegration/12_SecretNameFormats/invalid_.hidden (0.01s)
|
||||
--- FAIL: TestSecretManagerIntegration/13_UnlockerManagement (0.75s)
|
||||
--- PASS: TestSecretManagerIntegration/14_SwitchVault (0.03s)
|
||||
--- PASS: TestSecretManagerIntegration/15_VaultIsolation (0.08s)
|
||||
--- PASS: TestSecretManagerIntegration/16_GenerateSecret (0.10s)
|
||||
--- PASS: TestSecretManagerIntegration/17_ImportFromFile (0.06s)
|
||||
--- PASS: TestSecretManagerIntegration/18_AgeKeyOperations (0.09s)
|
||||
--- PASS: TestSecretManagerIntegration/19_DisasterRecovery (0.04s)
|
||||
--- PASS: TestSecretManagerIntegration/20_VersionTimestamps (0.17s)
|
||||
--- SKIP: TestSecretManagerIntegration/21_MaxVersionsPerDay (0.00s)
|
||||
--- PASS: TestSecretManagerIntegration/22_JSONOutput (0.02s)
|
||||
--- PASS: TestSecretManagerIntegration/23_ErrorHandling (0.06s)
|
||||
--- PASS: TestSecretManagerIntegration/24_EnvironmentVariables (0.79s)
|
||||
--- PASS: TestSecretManagerIntegration/25_ConcurrentOperations (0.03s)
|
||||
--- PASS: TestSecretManagerIntegration/26_LargeSecrets (0.07s)
|
||||
--- PASS: TestSecretManagerIntegration/27_SpecialCharacters (0.10s)
|
||||
--- FAIL: TestSecretManagerIntegration/28_VaultMetadata (0.00s)
|
||||
--- PASS: TestSecretManagerIntegration/29_SymlinkHandling (0.03s)
|
||||
--- FAIL: TestSecretManagerIntegration/30_BackupRestore (0.18s)
|
||||
FAIL
|
||||
FAIL git.eeqj.de/sneak/secret/internal/cli 5.283s
|
||||
FAIL
|
@ -1,851 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e # Exit on any error
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Test configuration
|
||||
TEST_MNEMONIC="abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||
TEST_PASSPHRASE="test-passphrase-123"
|
||||
TEMP_DIR="$(mktemp -d)"
|
||||
SECRET_BINARY="./secret"
|
||||
|
||||
# Enable debug output from the secret program
|
||||
export GODEBUG="berlin.sneak.pkg.secret"
|
||||
|
||||
echo -e "${BLUE}=== Secret Manager Comprehensive Test Script ===${NC}"
|
||||
echo -e "${YELLOW}Using temporary directory: $TEMP_DIR${NC}"
|
||||
echo -e "${YELLOW}Debug output enabled: GODEBUG=$GODEBUG${NC}"
|
||||
echo -e "${YELLOW}Note: All tests use environment variables (no manual input)${NC}"
|
||||
|
||||
# Function to print test steps
|
||||
print_step() {
|
||||
echo -e "\n${BLUE}Step $1: $2${NC}"
|
||||
}
|
||||
|
||||
# Function to print success
|
||||
print_success() {
|
||||
echo -e "${GREEN}✓ $1${NC}"
|
||||
}
|
||||
|
||||
# Function to print error and exit
|
||||
print_error() {
|
||||
echo -e "${RED}✗ $1${NC}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Function to print warning (for expected failures)
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}⚠ $1${NC}"
|
||||
}
|
||||
|
||||
# Function to clear state directory and reset environment
|
||||
reset_state() {
|
||||
echo -e "${YELLOW}Resetting state directory...${NC}"
|
||||
|
||||
# Safety checks before removing anything
|
||||
if [ -z "$TEMP_DIR" ]; then
|
||||
print_error "TEMP_DIR is not set, cannot reset state safely"
|
||||
fi
|
||||
|
||||
if [ ! -d "$TEMP_DIR" ]; then
|
||||
print_error "TEMP_DIR ($TEMP_DIR) is not a directory, cannot reset state safely"
|
||||
fi
|
||||
|
||||
# Additional safety: ensure TEMP_DIR looks like a temp directory
|
||||
case "$TEMP_DIR" in
|
||||
/tmp/* | /var/folders/* | */tmp/*)
|
||||
# Looks like a reasonable temp directory path
|
||||
;;
|
||||
*)
|
||||
print_error "TEMP_DIR ($TEMP_DIR) does not look like a safe temporary directory path"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Now it's safe to remove contents - use find to avoid glob expansion issues
|
||||
find "${TEMP_DIR:?}" -mindepth 1 -delete 2>/dev/null || true
|
||||
unset SB_SECRET_MNEMONIC
|
||||
unset SB_UNLOCK_PASSPHRASE
|
||||
export SB_SECRET_STATE_DIR="$TEMP_DIR"
|
||||
}
|
||||
|
||||
# Cleanup function
|
||||
cleanup() {
|
||||
echo -e "\n${YELLOW}Cleaning up...${NC}"
|
||||
rm -rf "$TEMP_DIR"
|
||||
unset SB_SECRET_STATE_DIR
|
||||
unset SB_SECRET_MNEMONIC
|
||||
unset SB_UNLOCK_PASSPHRASE
|
||||
unset GODEBUG
|
||||
echo -e "${GREEN}Cleanup complete${NC}"
|
||||
}
|
||||
|
||||
# Set cleanup trap
|
||||
trap cleanup EXIT
|
||||
|
||||
# Check that the secret binary exists
|
||||
if [ ! -f "$SECRET_BINARY" ]; then
|
||||
print_error "Secret binary not found at $SECRET_BINARY. Please run 'make build' first."
|
||||
fi
|
||||
|
||||
# Test 1: Set up environment variables
|
||||
print_step "1" "Setting up environment variables"
|
||||
export SB_SECRET_STATE_DIR="$TEMP_DIR"
|
||||
export SB_SECRET_MNEMONIC="$TEST_MNEMONIC"
|
||||
print_success "Environment variables set"
|
||||
echo " SB_SECRET_STATE_DIR=$SB_SECRET_STATE_DIR"
|
||||
echo " SB_SECRET_MNEMONIC=$TEST_MNEMONIC"
|
||||
|
||||
# Test 2: Initialize the secret manager (should create default vault)
|
||||
print_step "2" "Initializing secret manager (creates default vault)"
|
||||
export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
|
||||
echo " SB_UNLOCK_PASSPHRASE=$SB_UNLOCK_PASSPHRASE"
|
||||
|
||||
# Verify environment variables are exported and visible to subprocesses
|
||||
echo "Verifying environment variables are exported:"
|
||||
env | grep -E "^SB_" || true
|
||||
|
||||
echo "Running: $SECRET_BINARY init"
|
||||
# Run with explicit environment to ensure variables are passed
|
||||
if SB_SECRET_STATE_DIR="$SB_SECRET_STATE_DIR" \
|
||||
SB_SECRET_MNEMONIC="$SB_SECRET_MNEMONIC" \
|
||||
SB_UNLOCK_PASSPHRASE="$SB_UNLOCK_PASSPHRASE" \
|
||||
GODEBUG="$GODEBUG" \
|
||||
$SECRET_BINARY init </dev/null; then
|
||||
print_success "Secret manager initialized with default vault"
|
||||
else
|
||||
print_error "Failed to initialize secret manager"
|
||||
fi
|
||||
unset SB_UNLOCK_PASSPHRASE
|
||||
|
||||
# Verify directory structure was created
|
||||
if [ -d "$TEMP_DIR" ]; then
|
||||
print_success "State directory created: $TEMP_DIR"
|
||||
else
|
||||
print_error "State directory was not created"
|
||||
fi
|
||||
|
||||
# Test 3: Vault management
|
||||
print_step "3" "Testing vault management"
|
||||
|
||||
# List vaults (should show default)
|
||||
echo "Listing vaults..."
|
||||
echo "Running: $SECRET_BINARY vault list"
|
||||
if $SECRET_BINARY vault list; then
|
||||
VAULTS=$($SECRET_BINARY vault list)
|
||||
echo "Available vaults: $VAULTS"
|
||||
print_success "Listed vaults successfully"
|
||||
else
|
||||
print_error "Failed to list vaults"
|
||||
fi
|
||||
|
||||
# Create a new vault
|
||||
echo "Creating new vault 'work'..."
|
||||
echo "Running: $SECRET_BINARY vault create work"
|
||||
if $SECRET_BINARY vault create work; then
|
||||
print_success "Created vault 'work'"
|
||||
else
|
||||
print_error "Failed to create vault 'work'"
|
||||
fi
|
||||
|
||||
# Create another vault
|
||||
echo "Creating new vault 'personal'..."
|
||||
echo "Running: $SECRET_BINARY vault create personal"
|
||||
if $SECRET_BINARY vault create personal; then
|
||||
print_success "Created vault 'personal'"
|
||||
else
|
||||
print_error "Failed to create vault 'personal'"
|
||||
fi
|
||||
|
||||
# List vaults again (should show default, work, personal)
|
||||
echo "Listing vaults after creation..."
|
||||
echo "Running: $SECRET_BINARY vault list"
|
||||
if $SECRET_BINARY vault list; then
|
||||
VAULTS=$($SECRET_BINARY vault list)
|
||||
echo "Available vaults: $VAULTS"
|
||||
print_success "Listed vaults after creation"
|
||||
else
|
||||
print_error "Failed to list vaults after creation"
|
||||
fi
|
||||
|
||||
# Switch to work vault
|
||||
echo "Switching to 'work' vault..."
|
||||
echo "Running: $SECRET_BINARY vault select work"
|
||||
if $SECRET_BINARY vault select work; then
|
||||
print_success "Switched to 'work' vault"
|
||||
else
|
||||
print_error "Failed to switch to 'work' vault"
|
||||
fi
|
||||
|
||||
# Test 4: Import functionality with environment variable combinations
|
||||
print_step "4" "Testing import functionality with environment variable combinations"
|
||||
|
||||
# Test 4a: Import with both env vars set (typical usage)
|
||||
echo -e "\n${YELLOW}Test 4a: Import with both SB_SECRET_MNEMONIC and SB_UNLOCK_PASSPHRASE set${NC}"
|
||||
reset_state
|
||||
export SB_SECRET_MNEMONIC="$TEST_MNEMONIC"
|
||||
export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
|
||||
|
||||
# Create a vault first
|
||||
echo "Running: $SECRET_BINARY vault create test-vault"
|
||||
if $SECRET_BINARY vault create test-vault; then
|
||||
print_success "Created test-vault for import testing"
|
||||
else
|
||||
print_error "Failed to create test-vault"
|
||||
fi
|
||||
|
||||
# Import should work without prompts
|
||||
echo "Importing with both env vars set (automated)..."
|
||||
echo "Running: $SECRET_BINARY vault import test-vault"
|
||||
if $SECRET_BINARY vault import test-vault; then
|
||||
print_success "Import succeeded with both env vars (automated)"
|
||||
else
|
||||
print_error "Import failed with both env vars"
|
||||
fi
|
||||
|
||||
# Test 4b: Import into non-existent vault (should fail)
|
||||
echo -e "\n${YELLOW}Test 4b: Import into non-existent vault (should fail)${NC}"
|
||||
echo "Importing into non-existent vault (should fail)..."
|
||||
if $SECRET_BINARY vault import nonexistent-vault; then
|
||||
print_error "Import should have failed for non-existent vault"
|
||||
else
|
||||
print_success "Import correctly failed for non-existent vault"
|
||||
fi
|
||||
|
||||
# Test 4c: Import with invalid mnemonic (should fail)
|
||||
echo -e "\n${YELLOW}Test 4c: Import with invalid mnemonic (should fail)${NC}"
|
||||
export SB_SECRET_MNEMONIC="invalid mnemonic phrase that should not work"
|
||||
|
||||
# Create a vault first
|
||||
echo "Running: $SECRET_BINARY vault create test-vault2"
|
||||
if $SECRET_BINARY vault create test-vault2; then
|
||||
print_success "Created test-vault2 for invalid mnemonic testing"
|
||||
else
|
||||
print_error "Failed to create test-vault2"
|
||||
fi
|
||||
|
||||
echo "Importing with invalid mnemonic (should fail)..."
|
||||
if $SECRET_BINARY vault import test-vault2; then
|
||||
print_error "Import should have failed with invalid mnemonic"
|
||||
else
|
||||
print_success "Import correctly failed with invalid mnemonic"
|
||||
fi
|
||||
|
||||
# Reset state for remaining tests
|
||||
reset_state
|
||||
export SB_SECRET_MNEMONIC="$TEST_MNEMONIC"
|
||||
|
||||
# Test 5: Unlocker management
|
||||
print_step "5" "Testing unlocker management"
|
||||
|
||||
# Initialize with mnemonic and passphrase
|
||||
export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
|
||||
echo "Running: $SECRET_BINARY init (with SB_SECRET_MNEMONIC and SB_UNLOCK_PASSPHRASE set)"
|
||||
if $SECRET_BINARY init; then
|
||||
print_success "Initialized for unlocker testing"
|
||||
else
|
||||
print_error "Failed to initialize for unlocker testing"
|
||||
fi
|
||||
|
||||
# Create passphrase-protected unlocker
|
||||
echo "Creating passphrase-protected unlocker..."
|
||||
echo "Running: $SECRET_BINARY unlockers add passphrase (with SB_UNLOCK_PASSPHRASE set)"
|
||||
if $SECRET_BINARY unlockers add passphrase; then
|
||||
print_success "Created passphrase-protected unlocker"
|
||||
else
|
||||
print_error "Failed to create passphrase-protected unlocker"
|
||||
exit 1
|
||||
fi
|
||||
unset SB_UNLOCK_PASSPHRASE
|
||||
|
||||
# List unlockers
|
||||
echo "Listing unlockers..."
|
||||
echo "Running: $SECRET_BINARY unlockers list"
|
||||
if $SECRET_BINARY unlockers list; then
|
||||
UNLOCKERS=$($SECRET_BINARY unlockers list)
|
||||
echo "Available unlockers: $UNLOCKERS"
|
||||
print_success "Listed unlockers"
|
||||
else
|
||||
print_error "Failed to list unlockers"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 6: Secret management with mnemonic (keyless operation)
|
||||
print_step "6" "Testing mnemonic-based secret operations (keyless)"
|
||||
|
||||
# Add secrets using mnemonic (no unlocker required)
|
||||
echo "Adding secrets using mnemonic-based long-term key..."
|
||||
|
||||
# Test secret 1
|
||||
echo "Running: echo \"my-super-secret-password\" | $SECRET_BINARY add \"database/password\""
|
||||
if echo "my-super-secret-password" | $SECRET_BINARY add "database/password"; then
|
||||
print_success "Added secret: database/password"
|
||||
else
|
||||
print_error "Failed to add secret: database/password"
|
||||
fi
|
||||
|
||||
# Test secret 2
|
||||
echo "Running: echo \"api-key-12345\" | $SECRET_BINARY add \"api/key\""
|
||||
if echo "api-key-12345" | $SECRET_BINARY add "api/key"; then
|
||||
print_success "Added secret: api/key"
|
||||
else
|
||||
print_error "Failed to add secret: api/key"
|
||||
fi
|
||||
|
||||
# Test secret 3 (with path)
|
||||
echo "Running: echo \"ssh-private-key-content\" | $SECRET_BINARY add \"ssh/private-key\""
|
||||
if echo "ssh-private-key-content" | $SECRET_BINARY add "ssh/private-key"; then
|
||||
print_success "Added secret: ssh/private-key"
|
||||
else
|
||||
print_error "Failed to add secret: ssh/private-key"
|
||||
fi
|
||||
|
||||
# Test secret 4 (with dots and underscores)
|
||||
echo "Running: echo \"jwt-secret-token\" | $SECRET_BINARY add \"app.config_jwt_secret\""
|
||||
if echo "jwt-secret-token" | $SECRET_BINARY add "app.config_jwt_secret"; then
|
||||
print_success "Added secret: app.config_jwt_secret"
|
||||
else
|
||||
print_error "Failed to add secret: app.config_jwt_secret"
|
||||
fi
|
||||
|
||||
# Retrieve secrets using mnemonic
|
||||
echo "Retrieving secrets using mnemonic-based long-term key..."
|
||||
|
||||
# Retrieve and verify secret 1
|
||||
RETRIEVED_SECRET1=$($SECRET_BINARY get "database/password" 2>/dev/null)
|
||||
if [ "$RETRIEVED_SECRET1" = "my-super-secret-password" ]; then
|
||||
print_success "Retrieved and verified secret: database/password"
|
||||
else
|
||||
print_error "Failed to retrieve or verify secret: database/password"
|
||||
fi
|
||||
|
||||
# Retrieve and verify secret 2
|
||||
RETRIEVED_SECRET2=$($SECRET_BINARY get "api/key" 2>/dev/null)
|
||||
if [ "$RETRIEVED_SECRET2" = "api-key-12345" ]; then
|
||||
print_success "Retrieved and verified secret: api/key"
|
||||
else
|
||||
print_error "Failed to retrieve or verify secret: api/key"
|
||||
fi
|
||||
|
||||
# Retrieve and verify secret 3
|
||||
RETRIEVED_SECRET3=$($SECRET_BINARY get "ssh/private-key" 2>/dev/null)
|
||||
if [ "$RETRIEVED_SECRET3" = "ssh-private-key-content" ]; then
|
||||
print_success "Retrieved and verified secret: ssh/private-key"
|
||||
else
|
||||
print_error "Failed to retrieve or verify secret: ssh/private-key"
|
||||
fi
|
||||
|
||||
# List all secrets
|
||||
echo "Listing all secrets..."
|
||||
echo "Running: $SECRET_BINARY list"
|
||||
if $SECRET_BINARY list; then
|
||||
SECRETS=$($SECRET_BINARY list)
|
||||
echo "Secrets in current vault:"
|
||||
echo "$SECRETS" | while read -r secret; do
|
||||
echo " - $secret"
|
||||
done
|
||||
print_success "Listed all secrets"
|
||||
else
|
||||
print_error "Failed to list secrets"
|
||||
fi
|
||||
|
||||
# Test 7: Testing vault operations with different unlockers
|
||||
print_step "7" "Testing vault operations with passphrase unlocker"
|
||||
|
||||
# Create a new vault for unlocker testing
|
||||
echo "Running: $SECRET_BINARY vault create traditional"
|
||||
$SECRET_BINARY vault create traditional
|
||||
|
||||
# Import mnemonic into the traditional vault (required for versioned secrets)
|
||||
echo "Importing mnemonic into traditional vault..."
|
||||
export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
|
||||
echo "Running: $SECRET_BINARY vault import traditional"
|
||||
if $SECRET_BINARY vault import traditional; then
|
||||
print_success "Imported mnemonic into traditional vault"
|
||||
else
|
||||
print_error "Failed to import mnemonic into traditional vault"
|
||||
fi
|
||||
unset SB_UNLOCK_PASSPHRASE
|
||||
|
||||
# Now add a secret using the vault with unlocker
|
||||
echo "Adding secret to vault with unlocker..."
|
||||
echo "Running: echo 'traditional-secret' | $SECRET_BINARY add traditional/secret"
|
||||
if echo "traditional-secret" | $SECRET_BINARY add traditional/secret; then
|
||||
print_success "Added secret to vault with unlocker"
|
||||
else
|
||||
print_error "Failed to add secret to vault with unlocker"
|
||||
fi
|
||||
|
||||
# Retrieve secret using passphrase (temporarily unset mnemonic to test unlocker)
|
||||
echo "Retrieving secret from vault with unlocker..."
|
||||
TEMP_MNEMONIC="$SB_SECRET_MNEMONIC"
|
||||
unset SB_SECRET_MNEMONIC
|
||||
export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
|
||||
echo "Running: $SECRET_BINARY get traditional/secret (using passphrase unlocker)"
|
||||
if RETRIEVED=$($SECRET_BINARY get traditional/secret 2>&1); then
|
||||
print_success "Retrieved: $RETRIEVED"
|
||||
else
|
||||
print_error "Failed to retrieve secret from vault with unlocker"
|
||||
fi
|
||||
unset SB_UNLOCK_PASSPHRASE
|
||||
export SB_SECRET_MNEMONIC="$TEMP_MNEMONIC"
|
||||
|
||||
# Test 8: Advanced unlocker management
|
||||
print_step "8" "Testing advanced unlocker management"
|
||||
|
||||
if [ "$PLATFORM" = "darwin" ]; then
|
||||
# macOS only: Test Secure Enclave
|
||||
echo "Testing Secure Enclave unlocker creation..."
|
||||
if $SECRET_BINARY unlockers add sep; then
|
||||
print_success "Created Secure Enclave unlocker"
|
||||
else
|
||||
print_warning "Secure Enclave unlocker creation not yet implemented"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Get current unlocker ID for testing
|
||||
echo "Getting current unlocker for testing..."
|
||||
echo "Running: $SECRET_BINARY unlockers list"
|
||||
if $SECRET_BINARY unlockers list; then
|
||||
CURRENT_UNLOCKER_ID=$($SECRET_BINARY unlockers list | head -n1 | awk '{print $1}')
|
||||
if [ -n "$CURRENT_UNLOCKER_ID" ]; then
|
||||
print_success "Found unlocker ID: $CURRENT_UNLOCKER_ID"
|
||||
|
||||
# Test unlocker selection
|
||||
echo "Testing unlocker selection..."
|
||||
echo "Running: $SECRET_BINARY unlocker select $CURRENT_UNLOCKER_ID"
|
||||
if $SECRET_BINARY unlocker select "$CURRENT_UNLOCKER_ID"; then
|
||||
print_success "Selected unlocker: $CURRENT_UNLOCKER_ID"
|
||||
else
|
||||
print_warning "Unlocker selection not yet implemented"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Test 9: Secret name validation and edge cases
|
||||
print_step "9" "Testing secret name validation and edge cases"
|
||||
|
||||
# Switch back to default vault for name validation tests
|
||||
echo "Switching back to default vault..."
|
||||
$SECRET_BINARY vault select default
|
||||
|
||||
# Test valid names
|
||||
VALID_NAMES=("valid-name" "valid.name" "valid_name" "valid/path/name" "123valid" "a" "very-long-name-with-many-parts/and/paths")
|
||||
for name in "${VALID_NAMES[@]}"; do
|
||||
echo "Running: echo \"test-value\" | $SECRET_BINARY add $name --force"
|
||||
if echo "test-value" | $SECRET_BINARY add "$name" --force; then
|
||||
print_success "Valid name accepted: $name"
|
||||
else
|
||||
print_error "Valid name rejected: $name"
|
||||
fi
|
||||
done
|
||||
|
||||
# Test invalid names (these should fail)
|
||||
echo "Testing invalid names (should fail)..."
|
||||
INVALID_NAMES=("Invalid-Name" "invalid name" "invalid@name" "invalid#name" "invalid%name" "")
|
||||
for name in "${INVALID_NAMES[@]}"; do
|
||||
echo "Running: echo \"test-value\" | $SECRET_BINARY add $name"
|
||||
if echo "test-value" | $SECRET_BINARY add "$name"; then
|
||||
print_error "Invalid name accepted (should have been rejected): '$name'"
|
||||
else
|
||||
print_success "Invalid name correctly rejected: '$name'"
|
||||
fi
|
||||
done
|
||||
|
||||
# Test 10: Overwrite protection and force flag
|
||||
print_step "10" "Testing overwrite protection and force flag"
|
||||
|
||||
# Try to add existing secret without --force (should fail)
|
||||
echo "Running: echo \"new-value\" | $SECRET_BINARY add \"database/password\""
|
||||
if echo "new-value" | $SECRET_BINARY add "database/password"; then
|
||||
print_error "Overwrite protection failed - secret was overwritten without --force"
|
||||
else
|
||||
print_success "Overwrite protection working - secret not overwritten without --force"
|
||||
fi
|
||||
|
||||
# Try to add existing secret with --force (should succeed)
|
||||
echo "Running: echo \"new-password-value\" | $SECRET_BINARY add \"database/password\" --force"
|
||||
if echo "new-password-value" | $SECRET_BINARY add "database/password" --force; then
|
||||
print_success "Force overwrite working - secret overwritten with --force"
|
||||
|
||||
# Verify the new value
|
||||
RETRIEVED_NEW=$($SECRET_BINARY get "database/password" 2>/dev/null)
|
||||
if [ "$RETRIEVED_NEW" = "new-password-value" ]; then
|
||||
print_success "Overwritten secret has correct new value"
|
||||
else
|
||||
print_error "Overwritten secret has incorrect value"
|
||||
fi
|
||||
else
|
||||
print_error "Force overwrite failed - secret not overwritten with --force"
|
||||
fi
|
||||
|
||||
# Test 11: Cross-vault operations
|
||||
print_step "11" "Testing cross-vault operations"
|
||||
|
||||
# First create and import mnemonic into work vault since it was destroyed by reset_state
|
||||
echo "Creating work vault for cross-vault testing..."
|
||||
echo "Running: $SECRET_BINARY vault create work"
|
||||
if $SECRET_BINARY vault create work; then
|
||||
print_success "Created work vault for cross-vault testing"
|
||||
else
|
||||
print_error "Failed to create work vault for cross-vault testing"
|
||||
fi
|
||||
|
||||
# Import mnemonic into work vault so it can store secrets
|
||||
echo "Importing mnemonic into work vault..."
|
||||
export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
|
||||
echo "Running: $SECRET_BINARY vault import work"
|
||||
if $SECRET_BINARY vault import work; then
|
||||
print_success "Imported mnemonic into work vault"
|
||||
else
|
||||
print_error "Failed to import mnemonic into work vault"
|
||||
fi
|
||||
unset SB_UNLOCK_PASSPHRASE
|
||||
|
||||
# Switch to work vault and add secrets there
|
||||
echo "Switching to 'work' vault for cross-vault testing..."
|
||||
echo "Running: $SECRET_BINARY vault select work"
|
||||
if $SECRET_BINARY vault select work; then
|
||||
print_success "Switched to 'work' vault"
|
||||
|
||||
# Add work-specific secrets
|
||||
echo "Running: echo \"work-database-password\" | $SECRET_BINARY add \"work/database\""
|
||||
if echo "work-database-password" | $SECRET_BINARY add "work/database"; then
|
||||
print_success "Added work-specific secret"
|
||||
else
|
||||
print_error "Failed to add work-specific secret"
|
||||
fi
|
||||
|
||||
# List secrets in work vault
|
||||
echo "Running: $SECRET_BINARY list"
|
||||
if $SECRET_BINARY list; then
|
||||
WORK_SECRETS=$($SECRET_BINARY list)
|
||||
echo "Secrets in work vault: $WORK_SECRETS"
|
||||
print_success "Listed work vault secrets"
|
||||
else
|
||||
print_error "Failed to list work vault secrets"
|
||||
fi
|
||||
else
|
||||
print_error "Failed to switch to 'work' vault"
|
||||
fi
|
||||
|
||||
# Switch back to default vault
|
||||
echo "Switching back to 'default' vault..."
|
||||
echo "Running: $SECRET_BINARY vault select default"
|
||||
if $SECRET_BINARY vault select default; then
|
||||
print_success "Switched back to 'default' vault"
|
||||
|
||||
# Verify default vault secrets are still there
|
||||
echo "Running: $SECRET_BINARY get \"database/password\""
|
||||
if $SECRET_BINARY get "database/password"; then
|
||||
print_success "Default vault secrets still accessible"
|
||||
else
|
||||
print_error "Default vault secrets not accessible"
|
||||
fi
|
||||
else
|
||||
print_error "Failed to switch back to 'default' vault"
|
||||
fi
|
||||
|
||||
# Test 12: File structure verification
|
||||
print_step "12" "Verifying file structure"
|
||||
|
||||
echo "Checking file structure in $TEMP_DIR..."
|
||||
if [ -d "$TEMP_DIR/vaults.d/default/secrets.d" ]; then
|
||||
print_success "Default vault structure exists"
|
||||
|
||||
# Check a specific secret's file structure
|
||||
SECRET_DIR="$TEMP_DIR/vaults.d/default/secrets.d/database%password"
|
||||
if [ -d "$SECRET_DIR" ]; then
|
||||
print_success "Secret directory exists: database%password"
|
||||
|
||||
# Check for versions directory and current symlink
|
||||
if [ -d "$SECRET_DIR/versions" ]; then
|
||||
print_success "Versions directory exists"
|
||||
else
|
||||
print_error "Versions directory missing"
|
||||
fi
|
||||
|
||||
if [ -L "$SECRET_DIR/current" ] || [ -f "$SECRET_DIR/current" ]; then
|
||||
print_success "Current version symlink exists"
|
||||
else
|
||||
print_error "Current version symlink missing"
|
||||
fi
|
||||
|
||||
# Check version directory structure
|
||||
LATEST_VERSION=$(ls -1 "$SECRET_DIR/versions" 2>/dev/null | sort -r | head -n1)
|
||||
if [ -n "$LATEST_VERSION" ]; then
|
||||
VERSION_DIR="$SECRET_DIR/versions/$LATEST_VERSION"
|
||||
print_success "Found version directory: $LATEST_VERSION"
|
||||
|
||||
# Check required files in version directory
|
||||
VERSION_FILES=("value.age" "pub.age" "priv.age" "metadata.age")
|
||||
for file in "${VERSION_FILES[@]}"; do
|
||||
if [ -f "$VERSION_DIR/$file" ]; then
|
||||
print_success "Version file exists: $file"
|
||||
else
|
||||
print_error "Version file missing: $file"
|
||||
fi
|
||||
done
|
||||
else
|
||||
print_error "No version directories found"
|
||||
fi
|
||||
else
|
||||
print_error "Secret directory not found"
|
||||
fi
|
||||
else
|
||||
print_error "Default vault structure not found"
|
||||
fi
|
||||
|
||||
# Check work vault structure
|
||||
if [ -d "$TEMP_DIR/vaults.d/work" ]; then
|
||||
print_success "Work vault structure exists"
|
||||
else
|
||||
print_error "Work vault structure not found"
|
||||
fi
|
||||
|
||||
# Check configuration files
|
||||
if [ -f "$TEMP_DIR/configuration.json" ]; then
|
||||
print_success "Global configuration file exists"
|
||||
else
|
||||
print_warning "Global configuration file not found (may not be implemented yet)"
|
||||
fi
|
||||
|
||||
# Check current vault symlink
|
||||
if [ -L "$TEMP_DIR/currentvault" ] || [ -f "$TEMP_DIR/currentvault" ]; then
|
||||
print_success "Current vault link exists"
|
||||
else
|
||||
print_error "Current vault link not found"
|
||||
fi
|
||||
|
||||
# Test 13: Environment variable error handling
|
||||
print_step "13" "Testing environment variable error handling"
|
||||
|
||||
# Test with non-existent state directory
|
||||
export SB_SECRET_STATE_DIR="$TEMP_DIR/nonexistent/directory"
|
||||
echo "Running: $SECRET_BINARY get \"database/password\""
|
||||
if $SECRET_BINARY get "database/password"; then
|
||||
print_error "Should have failed with non-existent state directory"
|
||||
else
|
||||
print_success "Correctly failed with non-existent state directory"
|
||||
fi
|
||||
|
||||
# Test init with non-existent directory (should work)
|
||||
echo "Running: $SECRET_BINARY init (with SB_UNLOCK_PASSPHRASE set)"
|
||||
export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
|
||||
if $SECRET_BINARY init; then
|
||||
print_success "Init works with non-existent state directory"
|
||||
else
|
||||
print_error "Init should work with non-existent state directory"
|
||||
fi
|
||||
unset SB_UNLOCK_PASSPHRASE
|
||||
|
||||
# Reset to working directory
|
||||
export SB_SECRET_STATE_DIR="$TEMP_DIR"
|
||||
|
||||
# Test 14: Mixed approach compatibility
|
||||
print_step "14" "Testing mixed approach compatibility"
|
||||
|
||||
# Switch to traditional vault and test access with passphrase
|
||||
echo "Switching to traditional vault..."
|
||||
$SECRET_BINARY vault select traditional
|
||||
|
||||
# Verify passphrase can access traditional vault secrets
|
||||
unset SB_SECRET_MNEMONIC
|
||||
export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
|
||||
RETRIEVED_MIXED=$($SECRET_BINARY get "traditional/secret" 2>/dev/null)
|
||||
unset SB_UNLOCK_PASSPHRASE
|
||||
export SB_SECRET_MNEMONIC="$TEST_MNEMONIC"
|
||||
|
||||
if [ "$RETRIEVED_MIXED" = "traditional-secret" ]; then
|
||||
print_success "Passphrase unlocker can access vault secrets"
|
||||
else
|
||||
print_error "Failed to access secret from traditional vault (expected: traditional-secret, got: $RETRIEVED_MIXED)"
|
||||
fi
|
||||
|
||||
# Switch back to default vault
|
||||
$SECRET_BINARY vault select default
|
||||
|
||||
# Test without mnemonic but with unlocker
|
||||
echo "Testing mnemonic-created vault access..."
|
||||
echo "Testing traditional unlocker access to mnemonic-created secrets..."
|
||||
echo "Running: $SECRET_BINARY get test/seed (with mnemonic set)"
|
||||
if RETRIEVED=$($SECRET_BINARY get test/seed 2>&1); then
|
||||
print_success "Traditional unlocker can access mnemonic-created secrets"
|
||||
else
|
||||
print_warning "Traditional unlocker cannot access mnemonic-created secrets (may need implementation)"
|
||||
fi
|
||||
|
||||
# Re-enable mnemonic for final tests
|
||||
export SB_SECRET_MNEMONIC="$TEST_MNEMONIC"
|
||||
|
||||
# Test 15: Version management
|
||||
print_step "15" "Testing version management"
|
||||
|
||||
# Switch back to default vault for version testing
|
||||
echo "Switching to default vault for version testing..."
|
||||
echo "Running: $SECRET_BINARY vault select default"
|
||||
$SECRET_BINARY vault select default
|
||||
|
||||
# Test listing versions of a secret
|
||||
echo "Listing versions of database/password..."
|
||||
echo "Running: $SECRET_BINARY version list \"database/password\""
|
||||
if $SECRET_BINARY version list "database/password"; then
|
||||
print_success "Listed versions of database/password"
|
||||
else
|
||||
print_error "Failed to list versions of database/password"
|
||||
fi
|
||||
|
||||
# Add a new version of an existing secret
|
||||
echo "Adding new version of database/password..."
|
||||
echo "Running: echo \"version-2-password\" | $SECRET_BINARY add \"database/password\" --force"
|
||||
if echo "version-2-password" | $SECRET_BINARY add "database/password" --force; then
|
||||
print_success "Added new version of database/password"
|
||||
|
||||
# List versions again to see both
|
||||
echo "Running: $SECRET_BINARY version list \"database/password\""
|
||||
if $SECRET_BINARY version list "database/password"; then
|
||||
print_success "Listed versions after adding new version"
|
||||
else
|
||||
print_error "Failed to list versions after adding new version"
|
||||
fi
|
||||
else
|
||||
print_error "Failed to add new version of database/password"
|
||||
fi
|
||||
|
||||
# Get current version (should be the latest)
|
||||
echo "Getting current version of database/password..."
|
||||
CURRENT_VALUE=$($SECRET_BINARY get "database/password" 2>/dev/null)
|
||||
if [ "$CURRENT_VALUE" = "version-2-password" ]; then
|
||||
print_success "Current version has correct value"
|
||||
else
|
||||
print_error "Current version has incorrect value"
|
||||
fi
|
||||
|
||||
# Get specific version by capturing version from list output
|
||||
echo "Getting specific version of database/password..."
|
||||
VERSIONS=$($SECRET_BINARY version list "database/password" | grep -E '^[0-9]{8}\.[0-9]{3}' | awk '{print $1}')
|
||||
FIRST_VERSION=$(echo "$VERSIONS" | tail -n1)
|
||||
if [ -n "$FIRST_VERSION" ]; then
|
||||
echo "Running: $SECRET_BINARY get --version $FIRST_VERSION \"database/password\""
|
||||
VERSIONED_VALUE=$($SECRET_BINARY get --version "$FIRST_VERSION" "database/password" 2>/dev/null)
|
||||
if [ "$VERSIONED_VALUE" = "my-super-secret-password" ]; then
|
||||
print_success "Retrieved correct value from specific version"
|
||||
else
|
||||
print_error "Retrieved incorrect value from specific version (expected: my-super-secret-password, got: $VERSIONED_VALUE)"
|
||||
fi
|
||||
else
|
||||
print_error "Could not determine version to test"
|
||||
fi
|
||||
|
||||
# Test version promotion
|
||||
echo "Testing version promotion..."
|
||||
if [ -n "$FIRST_VERSION" ]; then
|
||||
echo "Running: $SECRET_BINARY version promote \"database/password\" $FIRST_VERSION"
|
||||
if $SECRET_BINARY version promote "database/password" "$FIRST_VERSION"; then
|
||||
print_success "Promoted older version to current"
|
||||
|
||||
# Verify the promoted version is now current
|
||||
PROMOTED_VALUE=$($SECRET_BINARY get "database/password" 2>/dev/null)
|
||||
if [ "$PROMOTED_VALUE" = "my-super-secret-password" ]; then
|
||||
print_success "Promoted version is now current"
|
||||
else
|
||||
print_error "Promoted version value is incorrect (expected: my-super-secret-password, got: $PROMOTED_VALUE)"
|
||||
fi
|
||||
else
|
||||
print_error "Failed to promote version"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check version directory structure
|
||||
echo "Checking version directory structure..."
|
||||
VERSION_DIR="$TEMP_DIR/vaults.d/default/secrets.d/database%password/versions"
|
||||
if [ -d "$VERSION_DIR" ]; then
|
||||
print_success "Versions directory exists"
|
||||
|
||||
# Count version directories
|
||||
VERSION_COUNT=$(find "$VERSION_DIR" -mindepth 1 -maxdepth 1 -type d | wc -l)
|
||||
if [ "$VERSION_COUNT" -ge 2 ]; then
|
||||
print_success "Multiple version directories found: $VERSION_COUNT"
|
||||
else
|
||||
print_error "Expected multiple version directories, found: $VERSION_COUNT"
|
||||
fi
|
||||
|
||||
# Check for current symlink
|
||||
CURRENT_LINK="$TEMP_DIR/vaults.d/default/secrets.d/database%password/current"
|
||||
if [ -L "$CURRENT_LINK" ] || [ -f "$CURRENT_LINK" ]; then
|
||||
print_success "Current version symlink exists"
|
||||
else
|
||||
print_error "Current version symlink not found"
|
||||
fi
|
||||
else
|
||||
print_error "Versions directory not found"
|
||||
fi
|
||||
|
||||
# Final summary
|
||||
echo -e "\n${GREEN}=== Test Summary ===${NC}"
|
||||
echo -e "${GREEN}✓ Environment variable support (SB_SECRET_STATE_DIR, SB_SECRET_MNEMONIC)${NC}"
|
||||
echo -e "${GREEN}✓ Secret manager initialization${NC}"
|
||||
echo -e "${GREEN}✓ Vault management (create, list, select)${NC}"
|
||||
echo -e "${GREEN}✓ Import functionality with environment variable combinations${NC}"
|
||||
echo -e "${GREEN}✓ Import error handling (non-existent vault, invalid mnemonic)${NC}"
|
||||
echo -e "${GREEN}✓ Unlocker management (passphrase, PGP, SEP)${NC}"
|
||||
echo -e "${GREEN}✓ Secret generation and storage${NC}"
|
||||
echo -e "${GREEN}✓ Vault operations with passphrase unlocker${NC}"
|
||||
echo -e "${GREEN}✓ Secret name validation${NC}"
|
||||
echo -e "${GREEN}✓ Overwrite protection and force flag${NC}"
|
||||
echo -e "${GREEN}✓ Cross-vault operations${NC}"
|
||||
echo -e "${GREEN}✓ Per-secret key file structure${NC}"
|
||||
echo -e "${GREEN}✓ Mixed approach compatibility${NC}"
|
||||
echo -e "${GREEN}✓ Error handling${NC}"
|
||||
echo -e "${GREEN}✓ Version management (list, get, promote)${NC}"
|
||||
|
||||
echo -e "\n${GREEN}🎉 Comprehensive test completed with environment variable automation!${NC}"
|
||||
|
||||
# Show usage examples for all implemented functionality
|
||||
echo -e "\n${BLUE}=== Complete Usage Examples ===${NC}"
|
||||
echo -e "${YELLOW}# Environment setup:${NC}"
|
||||
echo "export SB_SECRET_STATE_DIR=\"/path/to/your/secrets\""
|
||||
echo "export SB_SECRET_MNEMONIC=\"your twelve word mnemonic phrase here\""
|
||||
echo ""
|
||||
echo -e "${YELLOW}# Initialization:${NC}"
|
||||
echo "secret init"
|
||||
echo ""
|
||||
echo -e "${YELLOW}# Vault management:${NC}"
|
||||
echo "secret vault list"
|
||||
echo "secret vault create work"
|
||||
echo "secret vault select work"
|
||||
echo ""
|
||||
echo -e "${YELLOW}# Import mnemonic (automated with environment variables):${NC}"
|
||||
echo "export SB_SECRET_MNEMONIC=\"abandon abandon...\""
|
||||
echo "export SB_UNLOCK_PASSPHRASE=\"passphrase\""
|
||||
echo "secret vault import work"
|
||||
echo ""
|
||||
echo -e "${YELLOW}# Unlocker management:${NC}"
|
||||
echo "$SECRET_BINARY unlockers add <type> # Add unlocker (passphrase, pgp, keychain)"
|
||||
echo "$SECRET_BINARY unlockers add passphrase"
|
||||
echo "$SECRET_BINARY unlockers add pgp <gpg-key-id>"
|
||||
echo "$SECRET_BINARY unlockers add keychain # macOS only"
|
||||
echo "$SECRET_BINARY unlockers list # List all unlockers"
|
||||
echo "$SECRET_BINARY unlocker select <unlocker-id> # Select current unlocker"
|
||||
echo "$SECRET_BINARY unlockers rm <unlocker-id> # Remove unlocker"
|
||||
echo ""
|
||||
echo -e "${YELLOW}# Secret management:${NC}"
|
||||
echo "echo \"my-secret\" | secret add \"app/password\""
|
||||
echo "echo \"my-secret\" | secret add \"app/password\" --force"
|
||||
echo "secret get \"app/password\""
|
||||
echo "secret get --version 20231215.001 \"app/password\""
|
||||
echo "secret list"
|
||||
echo ""
|
||||
echo -e "${YELLOW}# Version management:${NC}"
|
||||
echo "secret version list \"app/password\""
|
||||
echo "secret version promote \"app/password\" 20231215.001"
|
||||
echo ""
|
||||
echo -e "${YELLOW}# Cross-vault operations:${NC}"
|
||||
echo "secret vault select work"
|
||||
echo "echo \"work-secret\" | secret add \"work/database\""
|
||||
echo "secret vault select default"
|
Loading…
Reference in New Issue
Block a user