diff --git a/Makefile b/Makefile index e451bb8..a988e2e 100644 --- a/Makefile +++ b/Makefile @@ -1,86 +1,55 @@ -# Makefile for Secret Manager macOS App with Code Signing +# Makefile for Secret Manager - Simple Go CLI Tool -# Configuration - Update these with your Apple Developer details -DEVELOPER_ID_DEV = "Apple Development: YOUR_NAME (TEAM_ID)" -DEVELOPER_ID_DIST = "Developer ID Application: YOUR_NAME (TEAM_ID)" -ENTITLEMENTS = entitlements.plist +# Configuration BINARY_NAME = secret -# Build directories -BUILD_DIR = build -DIST_DIR = dist +default: build -default: test - -# Development build with code signing -build-dev: clean - @echo "Building development version..." +# Simple build (no code signing needed) +build: clean + @echo "Building secret manager..." go build -o $(BINARY_NAME) cmd/secret/main.go - @echo "Code signing for development..." - codesign --sign $(DEVELOPER_ID_DEV) \ - --entitlements $(ENTITLEMENTS) \ - --options runtime \ - --force \ - --verbose \ - ./$(BINARY_NAME) - @echo "Development build complete: ./$(BINARY_NAME)" + @echo "Build complete: ./$(BINARY_NAME)" -# Production build with code signing -build-prod: clean - @echo "Building production version..." - go build -ldflags="-s -w" -o $(BINARY_NAME) cmd/secret/main.go - @echo "Code signing for distribution..." - codesign --sign $(DEVELOPER_ID_DIST) \ - --entitlements $(ENTITLEMENTS) \ - --options runtime \ - --force \ - --verbose \ - ./$(BINARY_NAME) - @echo "Production build complete: ./$(BINARY_NAME)" +# Build with verbose output +build-verbose: clean + @echo "Building with verbose output..." + go build -v -o $(BINARY_NAME) cmd/secret/main.go + @echo "Build complete: ./$(BINARY_NAME)" -# Build without code signing (for testing compilation) -build-unsigned: clean - @echo "Building unsigned version..." - go build -o $(BINARY_NAME) cmd/secret/main.go - @echo "Unsigned build complete: ./$(BINARY_NAME)" +# Vet the code +vet: + @echo "Running go vet..." + go vet ./... -# Verify code signing -verify: - @echo "Verifying code signature..." - codesign -dv --verbose=4 ./$(BINARY_NAME) - @echo "\nVerifying entitlements..." - codesign -d --entitlements :- ./$(BINARY_NAME) - -# Check certificates and provisioning profiles -check-signing: - @echo "Available code signing identities:" - security find-identity -v -p codesigning - @echo "\nInstalled provisioning profiles:" - ls -la ~/Library/MobileDevice/Provisioning\ Profiles/ 2>/dev/null || echo "No provisioning profiles found" - -# Test with linting -test: lint +# Test with linting and vetting +test: vet lint + @echo "Running go tests..." go test -v ./... +# Run comprehensive test script +test-comprehensive: build + @echo "Running comprehensive test script..." + @chmod +x test_secret_manager.sh + @./test_secret_manager.sh + +# Run all tests (unit tests + comprehensive tests) +test-all: test test-comprehensive + # Lint the code lint: + @echo "Running linter..." golangci-lint run --timeout 5m +# Check all code quality (build + vet + lint + unit tests) +check: build vet lint test + # Clean build artifacts clean: rm -f ./$(BINARY_NAME) - rm -rf $(BUILD_DIR) $(DIST_DIR) -# Create app bundle structure (for future app store distribution) -bundle: build-prod - @echo "Creating app bundle..." - mkdir -p $(DIST_DIR)/Secret.app/Contents/MacOS - mkdir -p $(DIST_DIR)/Secret.app/Contents/Resources - cp $(BINARY_NAME) $(DIST_DIR)/Secret.app/Contents/MacOS/ - @echo "App bundle created in $(DIST_DIR)/Secret.app" - -# Install to /usr/local/bin (development) -install-dev: build-dev +# Install to /usr/local/bin +install: build @echo "Installing to /usr/local/bin..." sudo cp $(BINARY_NAME) /usr/local/bin/ @echo "Installed to /usr/local/bin/$(BINARY_NAME)" @@ -91,23 +60,34 @@ uninstall: sudo rm -f /usr/local/bin/$(BINARY_NAME) @echo "Uninstalled $(BINARY_NAME)" +# Test keychain functionality +test-keychain: + @echo "Testing keychain functionality..." + @./$(BINARY_NAME) --help > /dev/null 2>&1 && echo "Binary runs successfully" || echo "Binary failed to run" + # Help target help: - @echo "Available targets:" - @echo " build-dev - Build and sign for development" - @echo " build-prod - Build and sign for production/distribution" - @echo " build-unsigned - Build without code signing (testing only)" - @echo " verify - Verify code signature and entitlements" - @echo " check-signing - Show available certificates and profiles" - @echo " test - Run tests with linting" - @echo " lint - Run linter only" - @echo " clean - Remove build artifacts" - @echo " bundle - Create macOS app bundle" - @echo " install-dev - Install development build to /usr/local/bin" - @echo " uninstall - Remove from /usr/local/bin" - @echo " help - Show this help" + @echo "Secret Manager - Simple Go CLI Tool" + @echo "====================================" @echo "" - @echo "Before using build-dev or build-prod, update the DEVELOPER_ID variables" - @echo "in this Makefile with your Apple Developer certificate names." + @echo "Available targets:" + @echo " build - Build the secret manager (default)" + @echo " build-verbose - Build with verbose output" + @echo " vet - Run go vet" + @echo " lint - Run linter only" + @echo " test - Run unit tests with vet and lint" + @echo " test-comprehensive - Run comprehensive test script" + @echo " test-all - Run both unit tests and comprehensive tests" + @echo " check - Run all code quality checks" + @echo " clean - Remove build artifacts" + @echo " install - Install to /usr/local/bin" + @echo " uninstall - Remove from /usr/local/bin" + @echo " test-keychain - Test basic functionality" + @echo " help - Show this help" + @echo "" + @echo "Usage:" + @echo " make build && ./secret --help" + @echo " make test-all # Run all tests" + @echo " make check # Run all quality checks" -.PHONY: default build-dev build-prod build-unsigned verify check-signing test lint clean bundle install-dev uninstall help +.PHONY: default build build-verbose vet test test-comprehensive test-all lint check clean install uninstall test-keychain help diff --git a/PROVISIONING_GUIDE.md b/PROVISIONING_GUIDE.md deleted file mode 100644 index f84f3ad..0000000 --- a/PROVISIONING_GUIDE.md +++ /dev/null @@ -1,182 +0,0 @@ -# Provisioning Profile Setup for macOS Biometric Authentication - -## Prerequisites - -1. Apple Developer Account (paid membership required) -2. macOS development machine -3. Xcode installed (for code signing tools) - -## Step 1: Log into Apple Developer Portal - -1. Go to [developer.apple.com](https://developer.apple.com) -2. Sign in with your Apple Developer account -3. Navigate to "Certificates, Identifiers & Profiles" - -## Step 2: Create App ID - -1. Click "Identifiers" in the sidebar -2. Click the "+" button to create a new identifier -3. Select "App IDs" and click "Continue" -4. Choose "App" (not App Clip) and click "Continue" -5. Fill in the details: - - **Description**: `Secret Manager macOS App` - - **Bundle ID**: Select "Explicit" and enter `berlin.sneak.pkg.secret` -6. In the "Capabilities" section, enable: - - **Keychain Sharing** (this is essential for keychain access) - - Leave other capabilities unchecked unless specifically needed -7. Click "Continue" and then "Register" - -## Step 3: Create/Verify Development Certificate - -1. Click "Certificates" in the sidebar -2. Click the "+" button if you need a new certificate -3. Under "Development", select "Mac Development" -4. Follow the instructions to generate a Certificate Signing Request (CSR): - - Open Keychain Access on your Mac - - Go to Keychain Access → Certificate Assistant → Request a Certificate from a Certificate Authority - - Enter your email address and name - - Select "Saved to disk" and "Let me specify key pair information" - - Click "Continue" and save the CSR file -5. Upload the CSR file and download the certificate -6. Double-click the downloaded certificate to install it in Keychain Access - -## Step 4: Register Development Device - -1. Click "Devices" in the sidebar -2. Click the "+" button to register a new device -3. Select "macOS" as the platform -4. Get your Mac's hardware UUID: - ```bash - system_profiler SPHardwareDataType | grep "Hardware UUID" - ``` -5. Enter: - - **Device Name**: Your Mac's name (e.g., "John's MacBook Pro") - - **Device ID (UUID)**: The hardware UUID from step 4 -6. Click "Continue" and then "Register" - -## Step 5: Create Development Provisioning Profile - -1. Click "Profiles" in the sidebar -2. Click the "+" button to create a new profile -3. Under "Development", select "Mac App Development" -4. Click "Continue" -5. Select your App ID: `berlin.sneak.pkg.secret` -6. Click "Continue" -7. Select your development certificate -8. Click "Continue" -9. Select your registered Mac device -10. Click "Continue" -11. Enter a profile name: `Secret Manager macOS Development` -12. Click "Generate" -13. Download the provisioning profile - -## Step 6: Install Provisioning Profile - -1. Double-click the downloaded `.provisionprofile` file to install it -2. Or manually copy it to: `~/Library/MobileDevice/Provisioning Profiles/` - -## Step 7: Code Signing Setup - -### Option A: Manual Code Signing - -Add these flags when building your Go binary: - -```bash -# Build the binary -go build -o secret cmd/secret/main.go - -# Sign the binary -codesign --sign "Apple Development: YOUR_NAME (TEAM_ID)" \ - --entitlements entitlements.plist \ - --options runtime \ - --force \ - ./secret -``` - -### Option B: Using Makefile - -Update your Makefile to include code signing: - -```makefile -DEVELOPER_ID = "Apple Development: YOUR_NAME (TEAM_ID)" -ENTITLEMENTS = entitlements.plist - -secret: - go build -o secret cmd/secret/main.go - codesign --sign $(DEVELOPER_ID) \ - --entitlements $(ENTITLEMENTS) \ - --options runtime \ - --force \ - ./secret - -.PHONY: secret -``` - -## Step 8: Verify Code Signing - -Check that your binary is properly signed: - -```bash -# Check code signature -codesign -dv --verbose=4 ./secret - -# Check entitlements -codesign -d --entitlements :- ./secret -``` - -## Step 9: Test Biometric Authentication - -Run your app and verify that: -1. Touch ID/Face ID prompts appear when accessing keychain -2. No entitlement errors occur -3. Keychain operations work correctly - -## Troubleshooting - -### Common Issues: - -1. **errSecMissingEntitlement (-34018)** - - Ensure your provisioning profile includes keychain access - - Verify code signing is applied correctly - - Check that bundle ID matches exactly - -2. **No biometric prompt appears** - - Verify access control flags in your Security Framework calls - - Ensure device has biometric authentication enabled - - Check system preferences for app permissions - -3. **Code signing failures** - - Ensure certificate is installed in Keychain Access - - Verify team ID matches between certificate and provisioning profile - - Check that provisioning profile is installed - -### Debug Commands: - -```bash -# List installed certificates -security find-identity -v -p codesigning - -# List provisioning profiles -ls ~/Library/MobileDevice/Provisioning\ Profiles/ - -# Check provisioning profile contents -security cms -D -i ~/Library/MobileDevice/Provisioning\ Profiles/YOUR_PROFILE.provisionprofile -``` - -## Production Distribution - -For production distribution, you'll need to: - -1. Create a "Developer ID Application" certificate -2. Create a "Developer ID" provisioning profile -3. Notarize your app with Apple -4. Staple the notarization ticket - -This allows distribution outside the Mac App Store while maintaining system trust. - -## Important Notes - -- Keychain access groups are automatically included for explicit App IDs -- Biometric authentication requires proper access controls in your Security Framework calls -- The `com.apple.security.cs.disable-library-validation` entitlement may be needed for Go binaries -- Test thoroughly on a clean system to ensure all entitlements work correctly \ No newline at end of file diff --git a/README.md b/README.md index 7a08369..96ed043 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ Creates a new unlock key of the specified type: **Types:** - `passphrase`: Password-protected unlock key -- `macos-sep`: macOS Secure Enclave unlock key (Touch ID/Face ID) +- `keychain`: macOS Keychain unlock key (Touch ID/Face ID) - `pgp`: GPG/PGP key unlock key **Options:** @@ -121,11 +121,14 @@ Selects an unlock key as the current default for operations. ### Import Operations -#### `secret import [vault-name]` +#### `secret import --source ` +Imports a secret from a file and stores it in the current vault under the given name. + +#### `secret vault import [vault-name]` Imports a mnemonic phrase into the specified vault (defaults to "default"). #### `secret enroll` -Enrolls a macOS Secure Enclave unlock key for biometric authentication. +Enrolls a macOS Keychain unlock key for biometric authentication. ### Encryption Operations @@ -154,7 +157,7 @@ $BASE/ # ~/.config/berlin.sneak.pkg.secret │ │ │ ├── pub.age # Unlock key public key │ │ │ ├── priv.age # Unlock key private key (encrypted) │ │ │ └── longterm.age # Long-term private key (encrypted to this unlock key) - │ │ ├── sep/ # Secure Enclave unlock key + │ │ ├── keychain/ # Keychain unlock key │ │ │ ├── unlock-metadata.json │ │ │ ├── pub.age │ │ │ ├── priv.age @@ -193,8 +196,8 @@ Unlock keys provide different authentication methods to access the long-term key - Private key encrypted using a user-provided passphrase - Stored as encrypted Age identity in `priv.age` -2. **macOS Secure Enclave Keys**: - - Private key stored in the Secure Enclave +2. **macOS Keychain Keys**: + - Private key stored in the macOS Keychain - Requires biometric authentication (Touch ID/Face ID) - Provides hardware-backed security @@ -232,7 +235,7 @@ Unlock keys provide different authentication methods to access the long-term key - Long-term keys protected by multiple unlock key layers ### Hardware Integration -- macOS Secure Enclave support for biometric authentication +- macOS Keychain support for biometric authentication - Hardware token support via PGP/GPG integration ## Examples @@ -263,7 +266,7 @@ secret vault create personal # Work with work vault secret vault select work echo "work-db-pass" | secret add database/password -secret keys add macos-sep # Add Touch ID authentication +secret keys add keychain # Add Touch ID authentication # Switch to personal vault secret vault select personal @@ -277,7 +280,7 @@ secret vault list ```bash # Add multiple unlock methods secret keys add passphrase # Password-based -secret keys add macos-sep # Touch ID (macOS only) +secret keys add keychain # Touch ID (macOS only) secret keys add pgp --keyid ABCD1234 # GPG key # List unlock keys @@ -313,8 +316,8 @@ secret decrypt encryption/mykey --input document.txt.age --output document.txt - **Configuration**: JSON configuration files ### Cross-Platform Support -- **macOS**: Full support including Secure Enclave integration -- **Linux**: Full support (excluding Secure Enclave features) +- **macOS**: Full support including Keychain integration +- **Linux**: Full support (excluding Keychain features) - **Windows**: Basic support (filesystem operations only) ## Security Considerations @@ -326,7 +329,7 @@ secret decrypt encryption/mykey --input document.txt.age --output document.txt ### Best Practices 1. Use strong, unique passphrases for unlock keys -2. Enable hardware authentication (Secure Enclave, hardware tokens) when available +2. Enable hardware authentication (Keychain, hardware tokens) when available 3. Regularly audit unlock keys and remove unused ones 4. Keep mnemonic phrases securely backed up offline 5. Use separate vaults for different security contexts diff --git a/TODO.md b/TODO.md index c52a83d..a32c554 100644 --- a/TODO.md +++ b/TODO.md @@ -76,7 +76,7 @@ This document outlines the bugs, issues, and improvements that need to be addres ### Cross-Platform Issues -- [ ] **macOS Secure Enclave error handling**: Better error messages when biometric authentication fails or isn't available. +- [ ] **macOS Keychain error handling**: Better error messages when biometric authentication fails or isn't available. - [ ] **Windows path handling**: File paths may not work correctly on Windows systems. @@ -122,7 +122,7 @@ This document outlines the bugs, issues, and improvements that need to be addres - [ ] **Audit logging**: Log all secret access and modifications. -- [ ] **Integration tests for hardware features**: Automated testing of Secure Enclave and GPG functionality. +- [ ] **Integration tests for hardware features**: Automated testing of Keychain and GPG functionality. ### Documentation diff --git a/entitlements.plist b/entitlements.plist deleted file mode 100644 index 059daf4..0000000 --- a/entitlements.plist +++ /dev/null @@ -1,24 +0,0 @@ - - - - - keychain-access-groups - - $(AppIdentifierPrefix)berlin.sneak.pkg.secret - - com.apple.application-identifier - $(AppIdentifierPrefix)berlin.sneak.pkg.secret - com.apple.developer.team-identifier - $(AppIdentifierPrefix) - get-task-allow - - com.apple.security.cs.allow-jit - - com.apple.security.cs.allow-unsigned-executable-memory - - com.apple.security.cs.allow-dyld-environment-variables - - com.apple.security.cs.disable-library-validation - - - \ No newline at end of file diff --git a/internal/secret/cli.go b/internal/secret/cli.go index 8b00ceb..d25b0b5 100644 --- a/internal/secret/cli.go +++ b/internal/secret/cli.go @@ -210,6 +210,7 @@ func newVaultCmd() *cobra.Command { cmd.AddCommand(newVaultListCmd()) cmd.AddCommand(newVaultCreateCmd()) cmd.AddCommand(newVaultSelectCmd()) + cmd.AddCommand(newVaultImportCmd()) return cmd } @@ -254,6 +255,24 @@ func newVaultSelectCmd() *cobra.Command { } } +func newVaultImportCmd() *cobra.Command { + return &cobra.Command{ + Use: "import ", + Short: "Import a mnemonic into a vault", + Long: `Import a BIP39 mnemonic phrase into the specified vault (default if not specified).`, + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + vaultName := "default" + if len(args) > 0 { + vaultName = args[0] + } + + cli := NewCLIInstance() + return cli.Import(vaultName) + }, + } +} + func newAddCmd() *cobra.Command { cmd := &cobra.Command{ Use: "add ", @@ -342,7 +361,7 @@ func newKeysAddCmd() *cobra.Command { cmd := &cobra.Command{ Use: "add ", Short: "Add a new unlock key", - Long: `Add a new unlock key of the specified type (passphrase, macos-sep, pgp).`, + Long: `Add a new unlock key of the specified type (passphrase, keychain, pgp).`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { cli := NewCLIInstance() @@ -391,28 +410,31 @@ func newKeySelectSubCmd() *cobra.Command { } func newImportCmd() *cobra.Command { - return &cobra.Command{ - Use: "import [vault-name]", - Short: "Import a mnemonic into a vault", - Long: `Import a BIP39 mnemonic phrase into the specified vault (default if not specified).`, - Args: cobra.MaximumNArgs(1), + cmd := &cobra.Command{ + Use: "import ", + Short: "Import a secret from a file", + Long: `Import a secret from a file and store it in the current vault under the given name.`, + Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - vaultName := "default" - if len(args) > 0 { - vaultName = args[0] - } + sourceFile, _ := cmd.Flags().GetString("source") + force, _ := cmd.Flags().GetBool("force") cli := NewCLIInstance() - return cli.Import(vaultName) + return cli.ImportSecret(args[0], sourceFile, force) }, } + + cmd.Flags().StringP("source", "s", "", "Source file to import from (required)") + cmd.Flags().BoolP("force", "f", false, "Overwrite existing secret") + _ = cmd.MarkFlagRequired("source") + return cmd } func newEnrollCmd() *cobra.Command { return &cobra.Command{ Use: "enroll", - Short: "Enroll a macOS Secure Enclave unlock key", - Long: `Enroll a macOS Secure Enclave unlock key that uses Touch ID/Face ID for biometric authentication.`, + Short: "Enroll a macOS Keychain unlock key", + Long: `Enroll a macOS Keychain unlock key that uses Touch ID/Face ID for biometric authentication.`, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { cli := NewCLIInstance() @@ -934,8 +956,8 @@ func (cli *CLIInstance) KeysList(jsonOutput bool) error { switch metadata.Type { case "passphrase": unlockKey = NewPassphraseUnlockKey(cli.fs, keyDir, metadata) - case "macos-sep": - unlockKey = NewSEPUnlockKey(cli.fs, keyDir, metadata) + case "keychain": + unlockKey = NewKeychainUnlockKey(cli.fs, keyDir, metadata) case "pgp": unlockKey = NewPGPUnlockKey(cli.fs, keyDir, metadata) } @@ -1026,8 +1048,8 @@ func (cli *CLIInstance) KeysAdd(keyType string, cmd *cobra.Command) error { cmd.Printf("Created passphrase unlock key: %s\n", passphraseKey.GetMetadata().ID) return nil - case "macos-sep": - return fmt.Errorf("macOS Secure Enclave unlock keys should be created using 'secret enroll' command") + case "keychain": + return fmt.Errorf("macOS Keychain unlock keys should be created using 'secret enroll' command") case "pgp": // Get GPG key ID from flag or environment variable @@ -1050,7 +1072,7 @@ func (cli *CLIInstance) KeysAdd(keyType string, cmd *cobra.Command) error { return nil default: - return fmt.Errorf("unsupported key type: %s (supported: passphrase, macos-sep, pgp)", keyType) + return fmt.Errorf("unsupported key type: %s (supported: passphrase, keychain, pgp)", keyType) } } @@ -1106,17 +1128,17 @@ func (cli *CLIInstance) Import(vaultName string) error { // Enroll enrolls a hardware security module func (cli *CLIInstance) Enroll() error { - sepKey, err := CreateSEPUnlockKey(cli.fs, cli.stateDir) + keychainKey, err := CreateKeychainUnlockKey(cli.fs, cli.stateDir) if err != nil { - return fmt.Errorf("failed to enroll macOS SEP unlock key: %w", err) + return fmt.Errorf("failed to enroll macOS Keychain unlock key: %w", err) } - fmt.Printf("macOS SEP unlock key enrolled successfully!\n") - fmt.Printf("Key ID: %s\n", sepKey.GetMetadata().ID) - fmt.Printf("Directory: %s\n", sepKey.GetDirectory()) + fmt.Printf("macOS Keychain unlock key enrolled successfully!\n") + fmt.Printf("Key ID: %s\n", keychainKey.GetMetadata().ID) + fmt.Printf("Directory: %s\n", keychainKey.GetDirectory()) // Load the key name to show the keychain key name - if keyName, err := sepKey.GetKeyName(); err == nil { + if keyName, err := keychainKey.GetKeyName(); err == nil { fmt.Printf("Keychain Key Name: %s\n", keyName) } @@ -1497,3 +1519,26 @@ func determineStateDir(customConfigDir string) string { } return filepath.Join(configDir, AppID) } + +// ImportSecret imports a secret from a file +func (cli *CLIInstance) ImportSecret(secretName, sourceFile string, force bool) error { + // Get current vault + vault, err := GetCurrentVault(cli.fs, cli.stateDir) + if err != nil { + return err + } + + // Read secret value from the source file + value, err := afero.ReadFile(cli.fs, sourceFile) + if err != nil { + return fmt.Errorf("failed to read secret from file %s: %w", sourceFile, err) + } + + // Store the secret in the vault + if err := vault.AddSecret(secretName, value, force); err != nil { + return err + } + + fmt.Printf("Successfully imported secret '%s' from file '%s'\n", secretName, sourceFile) + return nil +} diff --git a/test_secret_manager.sh b/test_secret_manager.sh index 9820d69..7089b9b 100755 --- a/test_secret_manager.sh +++ b/test_secret_manager.sh @@ -82,11 +82,9 @@ cleanup() { # Set cleanup trap trap cleanup EXIT -# Build the secret binary if it doesn't exist +# Check that the secret binary exists if [ ! -f "$SECRET_BINARY" ]; then - print_step "0" "Building secret binary" - go build -o "$SECRET_BINARY" ./cmd/secret/ - print_success "Built secret binary" + print_error "Secret binary not found at $SECRET_BINARY. Please run 'make build' first." fi # Test 1: Set up environment variables @@ -714,6 +712,274 @@ fi # Re-enable mnemonic for final tests export SB_SECRET_MNEMONIC="$TEST_MNEMONIC" +# Test 17: Architecture Refactoring - Separation of Concerns +print_step "17" "Testing refactored architecture - separation of concerns" + +echo "Testing that secrets handle their own data access..." +# Create a test secret first +if echo "test-self-access" | $SECRET_BINARY add "test/self-access" > /dev/null 2>&1; then + print_success "Created test secret for self-access testing" + + # Try to retrieve it (this tests that Secret.GetEncryptedData() works) + if $SECRET_BINARY get "test/self-access" > /dev/null 2>&1; then + print_success "Secret correctly handles its own data access" + else + print_error "Secret failed to handle its own data access" + fi +else + print_error "Failed to create test secret" +fi + +echo "Testing unlock key delegation pattern..." +# Test that vault delegates to unlock keys for decryption +# This is tested implicitly by all our secret retrieval operations +if $SECRET_BINARY get "database/password" > /dev/null 2>&1; then + print_success "Vault correctly delegates to unlock keys for decryption" +else + print_error "Vault delegation pattern failed" +fi + +# Test 18: Interface Method Compliance +print_step "18" "Testing interface method compliance" + +echo "Verifying all unlock key types implement required methods..." + +# Create different types of unlock keys to test interface compliance +echo "Testing PassphraseUnlockKey interface compliance..." +if echo "interface-test-pass" | $SECRET_BINARY keys add passphrase > /dev/null 2>&1; then + print_success "PassphraseUnlockKey created successfully" + + # Test that we can use it (this verifies GetIdentity and DecryptSecret work) + if echo "interface-test-secret" | $SECRET_BINARY add "interface/test" > /dev/null 2>&1; then + if $SECRET_BINARY get "interface/test" > /dev/null 2>&1; then + print_success "PassphraseUnlockKey interface methods working" + else + print_error "PassphraseUnlockKey interface methods failed" + fi + else + print_error "Failed to test PassphraseUnlockKey interface" + fi +else + print_warning "Could not create PassphraseUnlockKey for interface testing" +fi + +# Test Secure Enclave on macOS (if available) +if [[ "$OSTYPE" == "darwin"* ]]; then + echo "Testing SEPUnlockKey interface compliance on macOS..." + if $SECRET_BINARY enroll sep > /dev/null 2>&1; then + print_success "SEPUnlockKey created successfully" + + # Test that we can use it + if echo "sep-test-secret" | $SECRET_BINARY add "sep/test" > /dev/null 2>&1; then + if $SECRET_BINARY get "sep/test" > /dev/null 2>&1; then + print_success "SEPUnlockKey interface methods working" + else + print_error "SEPUnlockKey interface methods failed" + fi + else + print_error "Failed to test SEPUnlockKey interface" + fi + else + print_warning "SEPUnlockKey creation not available for interface testing" + fi +else + print_warning "SEPUnlockKey only available on macOS" +fi + +# Test 19: Long-term Key Management Separation +print_step "19" "Testing long-term key management separation" + +echo "Testing that unlock keys manage their own long-term keys..." + +# Switch between different unlock methods to verify each handles its own long-term keys +echo "Testing mnemonic-based long-term key management..." +export SB_SECRET_MNEMONIC="$TEST_MNEMONIC" +if echo "mnemonic-longterm-test" | $SECRET_BINARY add "longterm/mnemonic" > /dev/null 2>&1; then + if $SECRET_BINARY get "longterm/mnemonic" > /dev/null 2>&1; then + print_success "Mnemonic-based long-term key management working" + else + print_error "Mnemonic-based long-term key management failed" + fi +else + print_error "Failed to test mnemonic-based long-term key management" +fi + +echo "Testing passphrase-based long-term key management..." +unset SB_SECRET_MNEMONIC +if echo "passphrase-longterm-test" | $SECRET_BINARY add "longterm/passphrase" > /dev/null 2>&1; then + if $SECRET_BINARY get "longterm/passphrase" > /dev/null 2>&1; then + print_success "Passphrase-based long-term key management working" + else + print_error "Passphrase-based long-term key management failed" + fi +else + print_error "Failed to test passphrase-based long-term key management" +fi + +# Re-enable mnemonic +export SB_SECRET_MNEMONIC="$TEST_MNEMONIC" + +# Test 20: Directory Structure and File Access Patterns +print_step "20" "Testing directory structure and file access patterns" + +echo "Verifying secrets access their own directory structure..." + +# Check that secret directories contain the expected structure +SECRET_NAME="structure/test" +if echo "structure-test-value" | $SECRET_BINARY add "$SECRET_NAME" > /dev/null 2>&1; then + print_success "Created secret for structure testing" + + # Convert secret name to directory name (URL encoding) + ENCODED_NAME=$(echo "$SECRET_NAME" | sed 's|/|%|g') + SECRET_DIR="$TEMP_DIR/vaults.d/default/secrets.d/$ENCODED_NAME" + + if [ -d "$SECRET_DIR" ]; then + print_success "Secret directory structure created correctly" + + # Verify secret can access its own encrypted data + if $SECRET_BINARY get "$SECRET_NAME" > /dev/null 2>&1; then + print_success "Secret correctly accesses its own encrypted data" + else + print_error "Secret failed to access its own encrypted data" + fi + else + print_error "Secret directory structure not found" + fi +else + print_error "Failed to create secret for structure testing" +fi + +echo "Verifying unlock keys manage their own key files..." + +# Check unlock key directory structure +UNLOCK_KEYS_DIR="$TEMP_DIR/vaults.d/default/unlock-keys.d" +if [ -d "$UNLOCK_KEYS_DIR" ]; then + print_success "Unlock keys directory exists" + + # Check for passphrase unlock key files + for keydir in "$UNLOCK_KEYS_DIR"/*; do + if [ -d "$keydir" ] && [ -f "$keydir/metadata.json" ]; then + print_success "Found unlock key with proper structure: $(basename "$keydir")" + + # Check for required files + if [ -f "$keydir/pub.age" ] && [ -f "$keydir/priv.age" ]; then + print_success "Unlock key has required key files" + + # Check for long-term key management files + if [ -f "$keydir/longterm.age" ]; then + print_success "Unlock key has long-term key file" + else + print_warning "Unlock key missing long-term key file (may be mnemonic-only)" + fi + else + print_error "Unlock key missing required key files" + fi + fi + done +else + print_error "Unlock keys directory not found" +fi + +# Test 21: Error Handling in Refactored Architecture +print_step "21" "Testing error handling in refactored architecture" + +echo "Testing secret error handling..." + +# Test non-existent secret +if $SECRET_BINARY get "nonexistent/secret" > /dev/null 2>&1; then + print_error "Should have failed for non-existent secret" +else + print_success "Correctly handled non-existent secret" +fi + +echo "Testing unlock key error handling..." + +# Test with corrupted state (temporarily rename a key file) +UNLOCK_KEYS_DIR="$TEMP_DIR/vaults.d/default/unlock-keys.d" +FIRST_KEY_DIR=$(find "$UNLOCK_KEYS_DIR" -type d -name "*" | head -n1) +if [ -d "$FIRST_KEY_DIR" ] && [ -f "$FIRST_KEY_DIR/priv.age" ]; then + # Temporarily corrupt the key + mv "$FIRST_KEY_DIR/priv.age" "$FIRST_KEY_DIR/priv.age.backup" + + # Temporarily disable mnemonic to force unlock key usage + unset SB_SECRET_MNEMONIC + + if $SECRET_BINARY get "database/password" > /dev/null 2>&1; then + print_warning "Expected failure with corrupted unlock key, but succeeded (may have fallback)" + else + print_success "Correctly handled corrupted unlock key" + fi + + # Restore the key + mv "$FIRST_KEY_DIR/priv.age.backup" "$FIRST_KEY_DIR/priv.age" + + # Re-enable mnemonic + export SB_SECRET_MNEMONIC="$TEST_MNEMONIC" +else + print_warning "Could not test unlock key error handling - no key found" +fi + +# Test 22: Cross-Component Integration +print_step "22" "Testing cross-component integration" + +echo "Testing vault-secret-unlock key integration..." + +# Create a secret in one vault, switch vaults, create another secret, switch back +if $SECRET_BINARY vault create integration-test > /dev/null 2>&1; then + print_success "Created integration test vault" + + # Add secret to default vault + if echo "default-vault-secret" | $SECRET_BINARY add "integration/default" > /dev/null 2>&1; then + print_success "Added secret to default vault" + + # Switch to integration-test vault + if $SECRET_BINARY vault select integration-test > /dev/null 2>&1; then + print_success "Switched to integration-test vault" + + # Create unlock key in new vault + if echo "integration-passphrase" | $SECRET_BINARY keys add passphrase > /dev/null 2>&1; then + print_success "Created unlock key in integration-test vault" + + # Add secret to integration-test vault + if echo "integration-vault-secret" | $SECRET_BINARY add "integration/test" > /dev/null 2>&1; then + print_success "Added secret to integration-test vault" + + # Verify secret retrieval works + if $SECRET_BINARY get "integration/test" > /dev/null 2>&1; then + print_success "Cross-component integration working" + else + print_error "Cross-component integration failed" + fi + else + print_error "Failed to add secret to integration-test vault" + fi + else + print_error "Failed to create unlock key in integration-test vault" + fi + + # Switch back to default vault + if $SECRET_BINARY vault select default > /dev/null 2>&1; then + print_success "Switched back to default vault" + + # Verify we can still access default vault secrets + if $SECRET_BINARY get "integration/default" > /dev/null 2>&1; then + print_success "Can still access default vault secrets" + else + print_error "Cannot access default vault secrets after switching" + fi + else + print_error "Failed to switch back to default vault" + fi + else + print_error "Failed to switch to integration-test vault" + fi + else + print_error "Failed to add secret to default vault" + fi +else + print_error "Failed to create integration test vault" +fi + # Final summary echo -e "\n${GREEN}=== Test Summary ===${NC}" echo -e "${GREEN}✓ Environment variable support (SB_SECRET_STATE_DIR, SB_SECRET_MNEMONIC)${NC}" @@ -731,8 +997,14 @@ echo -e "${GREEN}✓ Per-secret key file structure${NC}" echo -e "${GREEN}✓ Unlock key removal${NC}" echo -e "${GREEN}✓ Mixed approach compatibility${NC}" echo -e "${GREEN}✓ Error handling${NC}" +echo -e "${GREEN}✓ Refactored architecture - separation of concerns${NC}" +echo -e "${GREEN}✓ Interface method compliance${NC}" +echo -e "${GREEN}✓ Long-term key management separation${NC}" +echo -e "${GREEN}✓ Directory structure and file access patterns${NC}" +echo -e "${GREEN}✓ Error handling in refactored architecture${NC}" +echo -e "${GREEN}✓ Cross-component integration${NC}" -echo -e "\n${GREEN}🎉 Comprehensive test completed!${NC}" +echo -e "\n${GREEN}🎉 Comprehensive test completed with architecture verification!${NC}" # Show usage examples for all implemented functionality echo -e "\n${BLUE}=== Complete Usage Examples ===${NC}"