refactor: rename SEP to Keychain and reorganize import commands - Renamed sepunlock.go to keychainunlock.go - Changed all SEP types to Keychain types (SEPUnlockKey -> KeychainUnlockKey) - Updated type string from 'macos-sep' to 'keychain' - Moved 'secret import' to 'secret vault import' for mnemonic imports - Added new 'secret import <secret-name> --source <filename>' for file imports - Updated README to replace all 'Secure Enclave' references with 'macOS Keychain' - Updated directory structure diagrams and examples - Fixed linter error in MarkFlagRequired call - All tests passing, linter clean

This commit is contained in:
Jeffrey Paul 2025-05-29 06:07:15 -07:00
parent bb82d10f91
commit 659b5ba508
7 changed files with 424 additions and 330 deletions

132
Makefile
View File

@ -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 # Configuration
DEVELOPER_ID_DEV = "Apple Development: YOUR_NAME (TEAM_ID)"
DEVELOPER_ID_DIST = "Developer ID Application: YOUR_NAME (TEAM_ID)"
ENTITLEMENTS = entitlements.plist
BINARY_NAME = secret BINARY_NAME = secret
# Build directories default: build
BUILD_DIR = build
DIST_DIR = dist
default: test # Simple build (no code signing needed)
build: clean
# Development build with code signing @echo "Building secret manager..."
build-dev: clean
@echo "Building development version..."
go build -o $(BINARY_NAME) cmd/secret/main.go go build -o $(BINARY_NAME) cmd/secret/main.go
@echo "Code signing for development..." @echo "Build complete: ./$(BINARY_NAME)"
codesign --sign $(DEVELOPER_ID_DEV) \
--entitlements $(ENTITLEMENTS) \
--options runtime \
--force \
--verbose \
./$(BINARY_NAME)
@echo "Development build complete: ./$(BINARY_NAME)"
# Production build with code signing # Build with verbose output
build-prod: clean build-verbose: clean
@echo "Building production version..." @echo "Building with verbose output..."
go build -ldflags="-s -w" -o $(BINARY_NAME) cmd/secret/main.go go build -v -o $(BINARY_NAME) cmd/secret/main.go
@echo "Code signing for distribution..." @echo "Build complete: ./$(BINARY_NAME)"
codesign --sign $(DEVELOPER_ID_DIST) \
--entitlements $(ENTITLEMENTS) \
--options runtime \
--force \
--verbose \
./$(BINARY_NAME)
@echo "Production build complete: ./$(BINARY_NAME)"
# Build without code signing (for testing compilation) # Vet the code
build-unsigned: clean vet:
@echo "Building unsigned version..." @echo "Running go vet..."
go build -o $(BINARY_NAME) cmd/secret/main.go go vet ./...
@echo "Unsigned build complete: ./$(BINARY_NAME)"
# Verify code signing # Test with linting and vetting
verify: test: vet lint
@echo "Verifying code signature..." @echo "Running go tests..."
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
go test -v ./... 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 the code
lint: lint:
@echo "Running linter..."
golangci-lint run --timeout 5m golangci-lint run --timeout 5m
# Check all code quality (build + vet + lint + unit tests)
check: build vet lint test
# Clean build artifacts # Clean build artifacts
clean: clean:
rm -f ./$(BINARY_NAME) rm -f ./$(BINARY_NAME)
rm -rf $(BUILD_DIR) $(DIST_DIR)
# Create app bundle structure (for future app store distribution) # Install to /usr/local/bin
bundle: build-prod install: build
@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
@echo "Installing to /usr/local/bin..." @echo "Installing to /usr/local/bin..."
sudo cp $(BINARY_NAME) /usr/local/bin/ sudo cp $(BINARY_NAME) /usr/local/bin/
@echo "Installed to /usr/local/bin/$(BINARY_NAME)" @echo "Installed to /usr/local/bin/$(BINARY_NAME)"
@ -91,23 +60,34 @@ uninstall:
sudo rm -f /usr/local/bin/$(BINARY_NAME) sudo rm -f /usr/local/bin/$(BINARY_NAME)
@echo "Uninstalled $(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 target
help: help:
@echo "Secret Manager - Simple Go CLI Tool"
@echo "===================================="
@echo ""
@echo "Available targets:" @echo "Available targets:"
@echo " build-dev - Build and sign for development" @echo " build - Build the secret manager (default)"
@echo " build-prod - Build and sign for production/distribution" @echo " build-verbose - Build with verbose output"
@echo " build-unsigned - Build without code signing (testing only)" @echo " vet - Run go vet"
@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 " 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 " clean - Remove build artifacts"
@echo " bundle - Create macOS app bundle" @echo " install - Install to /usr/local/bin"
@echo " install-dev - Install development build to /usr/local/bin"
@echo " uninstall - Remove from /usr/local/bin" @echo " uninstall - Remove from /usr/local/bin"
@echo " test-keychain - Test basic functionality"
@echo " help - Show this help" @echo " help - Show this help"
@echo "" @echo ""
@echo "Before using build-dev or build-prod, update the DEVELOPER_ID variables" @echo "Usage:"
@echo "in this Makefile with your Apple Developer certificate names." @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

View File

@ -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

View File

@ -107,7 +107,7 @@ Creates a new unlock key of the specified type:
**Types:** **Types:**
- `passphrase`: Password-protected unlock key - `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 - `pgp`: GPG/PGP key unlock key
**Options:** **Options:**
@ -121,11 +121,14 @@ Selects an unlock key as the current default for operations.
### Import Operations ### Import Operations
#### `secret import [vault-name]` #### `secret import <secret-name> --source <filename>`
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"). Imports a mnemonic phrase into the specified vault (defaults to "default").
#### `secret enroll` #### `secret enroll`
Enrolls a macOS Secure Enclave unlock key for biometric authentication. Enrolls a macOS Keychain unlock key for biometric authentication.
### Encryption Operations ### Encryption Operations
@ -154,7 +157,7 @@ $BASE/ # ~/.config/berlin.sneak.pkg.secret
│ │ │ ├── pub.age # Unlock key public key │ │ │ ├── pub.age # Unlock key public key
│ │ │ ├── priv.age # Unlock key private key (encrypted) │ │ │ ├── priv.age # Unlock key private key (encrypted)
│ │ │ └── longterm.age # Long-term private key (encrypted to this unlock key) │ │ │ └── longterm.age # Long-term private key (encrypted to this unlock key)
│ │ ├── sep/ # Secure Enclave unlock key │ │ ├── keychain/ # Keychain unlock key
│ │ │ ├── unlock-metadata.json │ │ │ ├── unlock-metadata.json
│ │ │ ├── pub.age │ │ │ ├── pub.age
│ │ │ ├── priv.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 - Private key encrypted using a user-provided passphrase
- Stored as encrypted Age identity in `priv.age` - Stored as encrypted Age identity in `priv.age`
2. **macOS Secure Enclave Keys**: 2. **macOS Keychain Keys**:
- Private key stored in the Secure Enclave - Private key stored in the macOS Keychain
- Requires biometric authentication (Touch ID/Face ID) - Requires biometric authentication (Touch ID/Face ID)
- Provides hardware-backed security - 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 - Long-term keys protected by multiple unlock key layers
### Hardware Integration ### Hardware Integration
- macOS Secure Enclave support for biometric authentication - macOS Keychain support for biometric authentication
- Hardware token support via PGP/GPG integration - Hardware token support via PGP/GPG integration
## Examples ## Examples
@ -263,7 +266,7 @@ secret vault create personal
# Work with work vault # Work with work vault
secret vault select work secret vault select work
echo "work-db-pass" | secret add database/password 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 # Switch to personal vault
secret vault select personal secret vault select personal
@ -277,7 +280,7 @@ secret vault list
```bash ```bash
# Add multiple unlock methods # Add multiple unlock methods
secret keys add passphrase # Password-based 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 secret keys add pgp --keyid ABCD1234 # GPG key
# List unlock keys # List unlock keys
@ -313,8 +316,8 @@ secret decrypt encryption/mykey --input document.txt.age --output document.txt
- **Configuration**: JSON configuration files - **Configuration**: JSON configuration files
### Cross-Platform Support ### Cross-Platform Support
- **macOS**: Full support including Secure Enclave integration - **macOS**: Full support including Keychain integration
- **Linux**: Full support (excluding Secure Enclave features) - **Linux**: Full support (excluding Keychain features)
- **Windows**: Basic support (filesystem operations only) - **Windows**: Basic support (filesystem operations only)
## Security Considerations ## Security Considerations
@ -326,7 +329,7 @@ secret decrypt encryption/mykey --input document.txt.age --output document.txt
### Best Practices ### Best Practices
1. Use strong, unique passphrases for unlock keys 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 3. Regularly audit unlock keys and remove unused ones
4. Keep mnemonic phrases securely backed up offline 4. Keep mnemonic phrases securely backed up offline
5. Use separate vaults for different security contexts 5. Use separate vaults for different security contexts

View File

@ -76,7 +76,7 @@ This document outlines the bugs, issues, and improvements that need to be addres
### Cross-Platform Issues ### 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. - [ ] **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. - [ ] **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 ### Documentation

View File

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)berlin.sneak.pkg.secret</string>
</array>
<key>com.apple.application-identifier</key>
<string>$(AppIdentifierPrefix)berlin.sneak.pkg.secret</string>
<key>com.apple.developer.team-identifier</key>
<string>$(AppIdentifierPrefix)</string>
<key>get-task-allow</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<false/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<false/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<false/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>

View File

@ -210,6 +210,7 @@ func newVaultCmd() *cobra.Command {
cmd.AddCommand(newVaultListCmd()) cmd.AddCommand(newVaultListCmd())
cmd.AddCommand(newVaultCreateCmd()) cmd.AddCommand(newVaultCreateCmd())
cmd.AddCommand(newVaultSelectCmd()) cmd.AddCommand(newVaultSelectCmd())
cmd.AddCommand(newVaultImportCmd())
return cmd return cmd
} }
@ -254,6 +255,24 @@ func newVaultSelectCmd() *cobra.Command {
} }
} }
func newVaultImportCmd() *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),
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 { func newAddCmd() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "add <secret-name>", Use: "add <secret-name>",
@ -342,7 +361,7 @@ func newKeysAddCmd() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "add <type>", Use: "add <type>",
Short: "Add a new unlock key", 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), Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
cli := NewCLIInstance() cli := NewCLIInstance()
@ -391,28 +410,31 @@ func newKeySelectSubCmd() *cobra.Command {
} }
func newImportCmd() *cobra.Command { func newImportCmd() *cobra.Command {
return &cobra.Command{ cmd := &cobra.Command{
Use: "import [vault-name]", Use: "import <secret-name>",
Short: "Import a mnemonic into a vault", Short: "Import a secret from a file",
Long: `Import a BIP39 mnemonic phrase into the specified vault (default if not specified).`, Long: `Import a secret from a file and store it in the current vault under the given name.`,
Args: cobra.MaximumNArgs(1), Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
vaultName := "default" sourceFile, _ := cmd.Flags().GetString("source")
if len(args) > 0 { force, _ := cmd.Flags().GetBool("force")
vaultName = args[0]
}
cli := NewCLIInstance() 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 { func newEnrollCmd() *cobra.Command {
return &cobra.Command{ return &cobra.Command{
Use: "enroll", Use: "enroll",
Short: "Enroll a macOS Secure Enclave unlock key", Short: "Enroll a macOS Keychain unlock key",
Long: `Enroll a macOS Secure Enclave unlock key that uses Touch ID/Face ID for biometric authentication.`, Long: `Enroll a macOS Keychain unlock key that uses Touch ID/Face ID for biometric authentication.`,
Args: cobra.NoArgs, Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
cli := NewCLIInstance() cli := NewCLIInstance()
@ -934,8 +956,8 @@ func (cli *CLIInstance) KeysList(jsonOutput bool) error {
switch metadata.Type { switch metadata.Type {
case "passphrase": case "passphrase":
unlockKey = NewPassphraseUnlockKey(cli.fs, keyDir, metadata) unlockKey = NewPassphraseUnlockKey(cli.fs, keyDir, metadata)
case "macos-sep": case "keychain":
unlockKey = NewSEPUnlockKey(cli.fs, keyDir, metadata) unlockKey = NewKeychainUnlockKey(cli.fs, keyDir, metadata)
case "pgp": case "pgp":
unlockKey = NewPGPUnlockKey(cli.fs, keyDir, metadata) 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) cmd.Printf("Created passphrase unlock key: %s\n", passphraseKey.GetMetadata().ID)
return nil return nil
case "macos-sep": case "keychain":
return fmt.Errorf("macOS Secure Enclave unlock keys should be created using 'secret enroll' command") return fmt.Errorf("macOS Keychain unlock keys should be created using 'secret enroll' command")
case "pgp": case "pgp":
// Get GPG key ID from flag or environment variable // 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 return nil
default: 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 // Enroll enrolls a hardware security module
func (cli *CLIInstance) Enroll() error { func (cli *CLIInstance) Enroll() error {
sepKey, err := CreateSEPUnlockKey(cli.fs, cli.stateDir) keychainKey, err := CreateKeychainUnlockKey(cli.fs, cli.stateDir)
if err != nil { 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("macOS Keychain unlock key enrolled successfully!\n")
fmt.Printf("Key ID: %s\n", sepKey.GetMetadata().ID) fmt.Printf("Key ID: %s\n", keychainKey.GetMetadata().ID)
fmt.Printf("Directory: %s\n", sepKey.GetDirectory()) fmt.Printf("Directory: %s\n", keychainKey.GetDirectory())
// Load the key name to show the keychain key name // 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) fmt.Printf("Keychain Key Name: %s\n", keyName)
} }
@ -1497,3 +1519,26 @@ func determineStateDir(customConfigDir string) string {
} }
return filepath.Join(configDir, AppID) 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
}

View File

@ -82,11 +82,9 @@ cleanup() {
# Set cleanup trap # Set cleanup trap
trap cleanup EXIT trap cleanup EXIT
# Build the secret binary if it doesn't exist # Check that the secret binary exists
if [ ! -f "$SECRET_BINARY" ]; then if [ ! -f "$SECRET_BINARY" ]; then
print_step "0" "Building secret binary" print_error "Secret binary not found at $SECRET_BINARY. Please run 'make build' first."
go build -o "$SECRET_BINARY" ./cmd/secret/
print_success "Built secret binary"
fi fi
# Test 1: Set up environment variables # Test 1: Set up environment variables
@ -714,6 +712,274 @@ fi
# Re-enable mnemonic for final tests # Re-enable mnemonic for final tests
export SB_SECRET_MNEMONIC="$TEST_MNEMONIC" 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 # Final summary
echo -e "\n${GREEN}=== Test Summary ===${NC}" echo -e "\n${GREEN}=== Test Summary ===${NC}"
echo -e "${GREEN}✓ Environment variable support (SB_SECRET_STATE_DIR, SB_SECRET_MNEMONIC)${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}✓ Unlock key removal${NC}"
echo -e "${GREEN}✓ Mixed approach compatibility${NC}" echo -e "${GREEN}✓ Mixed approach compatibility${NC}"
echo -e "${GREEN}✓ Error handling${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 # Show usage examples for all implemented functionality
echo -e "\n${BLUE}=== Complete Usage Examples ===${NC}" echo -e "\n${BLUE}=== Complete Usage Examples ===${NC}"