#!/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) 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 # Add unlocker (passphrase, pgp, keychain)" echo "$SECRET_BINARY unlockers add passphrase" echo "$SECRET_BINARY unlockers add pgp " echo "$SECRET_BINARY unlockers add keychain # macOS only" echo "$SECRET_BINARY unlockers list # List all unlockers" echo "$SECRET_BINARY unlocker select # Select current unlocker" echo "$SECRET_BINARY unlockers rm # 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"