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:
parent
bb82d10f91
commit
659b5ba508
142
Makefile
142
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
|
||||
|
@ -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
|
27
README.md
27
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 <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").
|
||||
|
||||
#### `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
|
||||
|
4
TODO.md
4
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
|
||||
|
||||
|
@ -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>
|
@ -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 <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 {
|
||||
cmd := &cobra.Command{
|
||||
Use: "add <secret-name>",
|
||||
@ -342,7 +361,7 @@ func newKeysAddCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "add <type>",
|
||||
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 <secret-name>",
|
||||
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
|
||||
}
|
||||
|
@ -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}"
|
||||
|
Loading…
Reference in New Issue
Block a user