Compare commits
	
		
			2 Commits
		
	
	
		
			ee49ace397
			...
			3d90388b5b
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 3d90388b5b | |||
| 8c08c2e748 | 
							
								
								
									
										31
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								README.md
									
									
									
									
									
								
							@ -22,7 +22,7 @@ Build from source:
 | 
				
			|||||||
```bash
 | 
					```bash
 | 
				
			||||||
git clone <repository>
 | 
					git clone <repository>
 | 
				
			||||||
cd secret
 | 
					cd secret
 | 
				
			||||||
go build -o secret ./cmd/secret
 | 
					make build
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Quick Start
 | 
					## Quick Start
 | 
				
			||||||
@ -107,7 +107,6 @@ Creates a new unlock key of the specified type:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
**Types:**
 | 
					**Types:**
 | 
				
			||||||
- `passphrase`: Traditional passphrase-protected unlock key
 | 
					- `passphrase`: Traditional passphrase-protected unlock key
 | 
				
			||||||
- `keychain`: macOS Keychain-protected unlock key (macOS only)
 | 
					 | 
				
			||||||
- `pgp`: Uses an existing GPG key for encryption/decryption
 | 
					- `pgp`: Uses an existing GPG key for encryption/decryption
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**Options:**
 | 
					**Options:**
 | 
				
			||||||
@ -145,7 +144,6 @@ Decrypts data using an Age key stored as a secret.
 | 
				
			|||||||
│   ├── default/
 | 
					│   ├── default/
 | 
				
			||||||
│   │   ├── unlock-keys.d/
 | 
					│   │   ├── unlock-keys.d/
 | 
				
			||||||
│   │   │   ├── passphrase/              # Passphrase unlock key
 | 
					│   │   │   ├── passphrase/              # Passphrase unlock key
 | 
				
			||||||
│   │   │   ├── keychain/                # Keychain unlock key (macOS)
 | 
					 | 
				
			||||||
│   │   │   └── pgp/                     # PGP unlock key
 | 
					│   │   │   └── pgp/                     # PGP unlock key
 | 
				
			||||||
│   │   ├── secrets.d/
 | 
					│   │   ├── secrets.d/
 | 
				
			||||||
│   │   │   ├── api%key/                 # Secret: api/key
 | 
					│   │   │   ├── api%key/                 # Secret: api/key
 | 
				
			||||||
@ -174,12 +172,7 @@ Unlock keys provide different authentication methods to access the long-term key
 | 
				
			|||||||
   - Stored as encrypted Age keys
 | 
					   - Stored as encrypted Age keys
 | 
				
			||||||
   - Cross-platform compatible
 | 
					   - Cross-platform compatible
 | 
				
			||||||
 | 
					
 | 
				
			||||||
2. **Keychain Keys** (macOS only):
 | 
					2. **PGP Keys**:
 | 
				
			||||||
   - Uses macOS Keychain for secure storage
 | 
					 | 
				
			||||||
   - Provides seamless authentication on macOS systems
 | 
					 | 
				
			||||||
   - Age private key encrypted with random passphrase stored in Keychain
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
3. **PGP Keys**:
 | 
					 | 
				
			||||||
   - Uses existing GPG key infrastructure
 | 
					   - Uses existing GPG key infrastructure
 | 
				
			||||||
   - Leverages existing key management workflows
 | 
					   - Leverages existing key management workflows
 | 
				
			||||||
   - Strong authentication through GPG
 | 
					   - Strong authentication through GPG
 | 
				
			||||||
@ -214,9 +207,8 @@ Each vault maintains its own set of unlock keys and one long-term key. The long-
 | 
				
			|||||||
- Per-secret encryption keys limit exposure if compromised
 | 
					- Per-secret encryption keys limit exposure if compromised
 | 
				
			||||||
- Long-term keys protected by multiple unlock key layers
 | 
					- Long-term keys protected by multiple unlock key layers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Platform Integration
 | 
					### Hardware Integration
 | 
				
			||||||
- macOS Keychain integration for seamless authentication
 | 
					- Hardware token support via PGP/GPG integration
 | 
				
			||||||
- GPG integration for existing key management workflows
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Examples
 | 
					## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -260,7 +252,6 @@ 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 keychain                # macOS Keychain (macOS only)
 | 
					 | 
				
			||||||
secret keys add pgp --keyid ABCD1234    # GPG key
 | 
					secret keys add pgp --keyid ABCD1234    # GPG key
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# List unlock keys
 | 
					# List unlock keys
 | 
				
			||||||
@ -305,11 +296,11 @@ secret decrypt encryption/mykey --input document.txt.age --output document.txt
 | 
				
			|||||||
### Threat Model
 | 
					### Threat Model
 | 
				
			||||||
- Protects against unauthorized access to secret values
 | 
					- Protects against unauthorized access to secret values
 | 
				
			||||||
- Provides defense against compromise of individual components
 | 
					- Provides defense against compromise of individual components
 | 
				
			||||||
- Supports platform-specific authentication where available
 | 
					- Supports hardware-backed authentication where available
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Best Practices
 | 
					### Best Practices
 | 
				
			||||||
1. Use strong, unique passphrases for unlock keys
 | 
					1. Use strong, unique passphrases for unlock keys
 | 
				
			||||||
2. Enable platform-specific authentication (Keychain) 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
 | 
				
			||||||
@ -317,15 +308,15 @@ secret decrypt encryption/mykey --input document.txt.age --output document.txt
 | 
				
			|||||||
### Limitations
 | 
					### Limitations
 | 
				
			||||||
- Requires access to unlock keys for secret retrieval
 | 
					- Requires access to unlock keys for secret retrieval
 | 
				
			||||||
- Mnemonic phrases must be securely stored and backed up
 | 
					- Mnemonic phrases must be securely stored and backed up
 | 
				
			||||||
- Platform-specific features limited to supported platforms
 | 
					- Hardware features limited to supported platforms
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Development
 | 
					## Development
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Building
 | 
					### Building
 | 
				
			||||||
```bash
 | 
					```bash
 | 
				
			||||||
go build -o secret ./cmd/secret  # Build binary
 | 
					make build    # Build binary
 | 
				
			||||||
go test ./...                    # Run tests
 | 
					make test     # Run tests
 | 
				
			||||||
go vet ./...                     # Run static analysis
 | 
					make lint     # Run linter
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Testing
 | 
					### Testing
 | 
				
			||||||
@ -337,7 +328,7 @@ go test ./...             # Unit tests
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
## Features
 | 
					## Features
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- **Multiple Authentication Methods**: Supports passphrase-based, keychain-based (macOS), and PGP-based unlock keys
 | 
					- **Multiple Authentication Methods**: Supports passphrase-based and PGP-based unlock keys
 | 
				
			||||||
- **Vault Isolation**: Complete separation between different vaults
 | 
					- **Vault Isolation**: Complete separation between different vaults
 | 
				
			||||||
- **Per-Secret Encryption**: Each secret has its own encryption key
 | 
					- **Per-Secret Encryption**: Each secret has its own encryption key
 | 
				
			||||||
- **BIP39 Mnemonic Support**: Keyless operation using mnemonic phrases
 | 
					- **BIP39 Mnemonic Support**: Keyless operation using mnemonic phrases
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,7 @@
 | 
				
			|||||||
package main
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import "git.eeqj.de/sneak/secret/internal/secret"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func main() {
 | 
					func main() {
 | 
				
			||||||
	CLIEntry()
 | 
						secret.CLIEntry()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -93,20 +93,3 @@ func readPassphrase(prompt string) (string, error) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	return string(passphrase), nil
 | 
						return string(passphrase), nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
// decryptSecretWithLongTermKey is a helper that parses a long-term private key and uses it to decrypt secret data
 | 
					 | 
				
			||||||
func decryptSecretWithLongTermKey(ltPrivKeyData []byte, encryptedData []byte) ([]byte, error) {
 | 
					 | 
				
			||||||
	// Parse long-term private key
 | 
					 | 
				
			||||||
	ltIdentity, err := age.ParseX25519Identity(string(ltPrivKeyData))
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return nil, fmt.Errorf("failed to parse long-term private key: %w", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Decrypt secret data using long-term key
 | 
					 | 
				
			||||||
	decryptedData, err := decryptWithIdentity(encryptedData, ltIdentity)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return nil, fmt.Errorf("failed to decrypt secret: %w", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return decryptedData, nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										133
									
								
								internal/secret/debug.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								internal/secret/debug.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,133 @@
 | 
				
			|||||||
 | 
					package secret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"log/slog"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"syscall"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"golang.org/x/term"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						debugEnabled bool
 | 
				
			||||||
 | 
						debugLogger  *slog.Logger
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func init() {
 | 
				
			||||||
 | 
						initDebugLogging()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// initDebugLogging initializes the debug logging system based on GODEBUG environment variable
 | 
				
			||||||
 | 
					func initDebugLogging() {
 | 
				
			||||||
 | 
						godebug := os.Getenv("GODEBUG")
 | 
				
			||||||
 | 
						debugEnabled = strings.Contains(godebug, "berlin.sneak.pkg.secret")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !debugEnabled {
 | 
				
			||||||
 | 
							// Create a no-op logger that discards all output
 | 
				
			||||||
 | 
							debugLogger = slog.New(slog.NewTextHandler(io.Discard, nil))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check if STDERR is a TTY
 | 
				
			||||||
 | 
						isTTY := term.IsTerminal(int(syscall.Stderr))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var handler slog.Handler
 | 
				
			||||||
 | 
						if isTTY {
 | 
				
			||||||
 | 
							// TTY output: colorized structured format
 | 
				
			||||||
 | 
							handler = newColorizedHandler(os.Stderr)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							// Non-TTY output: JSON Lines format
 | 
				
			||||||
 | 
							handler = slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
 | 
				
			||||||
 | 
								Level: slog.LevelDebug,
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						debugLogger = slog.New(handler)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IsDebugEnabled returns true if debug logging is enabled
 | 
				
			||||||
 | 
					func IsDebugEnabled() bool {
 | 
				
			||||||
 | 
						return debugEnabled
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Debug logs a debug message with optional attributes
 | 
				
			||||||
 | 
					func Debug(msg string, args ...any) {
 | 
				
			||||||
 | 
						if !debugEnabled {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						debugLogger.Debug(msg, args...)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// DebugF logs a formatted debug message with optional attributes
 | 
				
			||||||
 | 
					func DebugF(format string, args ...any) {
 | 
				
			||||||
 | 
						if !debugEnabled {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						debugLogger.Debug(fmt.Sprintf(format, args...))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// DebugWith logs a debug message with structured attributes
 | 
				
			||||||
 | 
					func DebugWith(msg string, attrs ...slog.Attr) {
 | 
				
			||||||
 | 
						if !debugEnabled {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						debugLogger.LogAttrs(context.Background(), slog.LevelDebug, msg, attrs...)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// colorizedHandler implements a TTY-friendly structured log handler
 | 
				
			||||||
 | 
					type colorizedHandler struct {
 | 
				
			||||||
 | 
						output io.Writer
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func newColorizedHandler(output io.Writer) slog.Handler {
 | 
				
			||||||
 | 
						return &colorizedHandler{output: output}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (h *colorizedHandler) Enabled(_ context.Context, level slog.Level) bool {
 | 
				
			||||||
 | 
						// Explicitly check that debug is enabled AND the level is DEBUG or higher
 | 
				
			||||||
 | 
						// This ensures we don't default to INFO level when debug is enabled
 | 
				
			||||||
 | 
						return debugEnabled && level >= slog.LevelDebug
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (h *colorizedHandler) Handle(_ context.Context, record slog.Record) error {
 | 
				
			||||||
 | 
						if !debugEnabled {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Format: [DEBUG] message {key=value, key2=value2}
 | 
				
			||||||
 | 
						output := fmt.Sprintf("\033[36m[DEBUG]\033[0m \033[1m%s\033[0m", record.Message)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if record.NumAttrs() > 0 {
 | 
				
			||||||
 | 
							output += " \033[33m{"
 | 
				
			||||||
 | 
							first := true
 | 
				
			||||||
 | 
							record.Attrs(func(attr slog.Attr) bool {
 | 
				
			||||||
 | 
								if !first {
 | 
				
			||||||
 | 
									output += ", "
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								first = false
 | 
				
			||||||
 | 
								output += fmt.Sprintf("%s=%#v", attr.Key, attr.Value.Any())
 | 
				
			||||||
 | 
								return true
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							output += "}\033[0m"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						output += "\n"
 | 
				
			||||||
 | 
						_, err := h.output.Write([]byte(output))
 | 
				
			||||||
 | 
						return err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (h *colorizedHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
 | 
				
			||||||
 | 
						// For simplicity, return the same handler
 | 
				
			||||||
 | 
						// In a more complex implementation, we'd create a new handler with the attrs
 | 
				
			||||||
 | 
						return h
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (h *colorizedHandler) WithGroup(name string) slog.Handler {
 | 
				
			||||||
 | 
						// For simplicity, return the same handler
 | 
				
			||||||
 | 
						// In a more complex implementation, we'd create a new handler with the group
 | 
				
			||||||
 | 
						return h
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										555
									
								
								internal/secret/keychainunlock.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										555
									
								
								internal/secret/keychainunlock.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,555 @@
 | 
				
			|||||||
 | 
					package secret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"encoding/hex"
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"log/slog"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"os/exec"
 | 
				
			||||||
 | 
						"path/filepath"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"filippo.io/age"
 | 
				
			||||||
 | 
						"git.eeqj.de/sneak/secret/pkg/agehd"
 | 
				
			||||||
 | 
						"github.com/spf13/afero"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// KeychainUnlockKeyMetadata extends UnlockKeyMetadata with keychain-specific data
 | 
				
			||||||
 | 
					type KeychainUnlockKeyMetadata struct {
 | 
				
			||||||
 | 
						UnlockKeyMetadata
 | 
				
			||||||
 | 
						// Age keypair information
 | 
				
			||||||
 | 
						AgePublicKey string `json:"age_public_key"`
 | 
				
			||||||
 | 
						// Keychain item name
 | 
				
			||||||
 | 
						KeychainItemName string `json:"keychain_item_name"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// KeychainUnlockKey represents a macOS Keychain-protected unlock key
 | 
				
			||||||
 | 
					type KeychainUnlockKey struct {
 | 
				
			||||||
 | 
						Directory string
 | 
				
			||||||
 | 
						Metadata  UnlockKeyMetadata
 | 
				
			||||||
 | 
						fs        afero.Fs
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// KeychainData represents the data stored in the macOS keychain
 | 
				
			||||||
 | 
					type KeychainData struct {
 | 
				
			||||||
 | 
						AgePublicKey         string `json:"age_public_key"`
 | 
				
			||||||
 | 
						AgePrivKeyPassphrase string `json:"age_priv_key_passphrase"`
 | 
				
			||||||
 | 
						EncryptedLongtermKey string `json:"encrypted_longterm_key"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetIdentity implements UnlockKey interface for Keychain-based unlock keys
 | 
				
			||||||
 | 
					func (k *KeychainUnlockKey) GetIdentity() (*age.X25519Identity, error) {
 | 
				
			||||||
 | 
						DebugWith("Getting keychain unlock key identity",
 | 
				
			||||||
 | 
							slog.String("key_id", k.GetID()),
 | 
				
			||||||
 | 
							slog.String("key_type", k.GetType()),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Step 1: Get keychain item name
 | 
				
			||||||
 | 
						keychainItemName, err := k.GetKeychainItemName()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to get keychain item name", "error", err, "key_id", k.GetID())
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to get keychain item name: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Step 2: Retrieve data from keychain
 | 
				
			||||||
 | 
						Debug("Retrieving data from macOS keychain", "keychain_item", keychainItemName)
 | 
				
			||||||
 | 
						keychainDataBytes, err := retrieveFromKeychain(keychainItemName)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to retrieve data from keychain", "error", err, "keychain_item", keychainItemName)
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to retrieve data from keychain: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						DebugWith("Retrieved data from keychain",
 | 
				
			||||||
 | 
							slog.String("key_id", k.GetID()),
 | 
				
			||||||
 | 
							slog.Int("data_length", len(keychainDataBytes)),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Step 3: Parse keychain data
 | 
				
			||||||
 | 
						var keychainData KeychainData
 | 
				
			||||||
 | 
						if err := json.Unmarshal(keychainDataBytes, &keychainData); err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to parse keychain data", "error", err, "key_id", k.GetID())
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to parse keychain data: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						Debug("Parsed keychain data successfully", "key_id", k.GetID())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Step 4: Read the encrypted age private key from filesystem
 | 
				
			||||||
 | 
						agePrivKeyPath := filepath.Join(k.Directory, "priv.age")
 | 
				
			||||||
 | 
						Debug("Reading encrypted age private key", "path", agePrivKeyPath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						encryptedAgePrivKeyData, err := afero.ReadFile(k.fs, agePrivKeyPath)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to read encrypted age private key", "error", err, "path", agePrivKeyPath)
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to read encrypted age private key: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						DebugWith("Read encrypted age private key",
 | 
				
			||||||
 | 
							slog.String("key_id", k.GetID()),
 | 
				
			||||||
 | 
							slog.Int("encrypted_length", len(encryptedAgePrivKeyData)),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Step 5: Decrypt the age private key using the passphrase from keychain
 | 
				
			||||||
 | 
						Debug("Decrypting age private key with keychain passphrase", "key_id", k.GetID())
 | 
				
			||||||
 | 
						agePrivKeyData, err := decryptWithPassphrase(encryptedAgePrivKeyData, keychainData.AgePrivKeyPassphrase)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to decrypt age private key with keychain passphrase", "error", err, "key_id", k.GetID())
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to decrypt age private key with keychain passphrase: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						DebugWith("Successfully decrypted age private key with keychain passphrase",
 | 
				
			||||||
 | 
							slog.String("key_id", k.GetID()),
 | 
				
			||||||
 | 
							slog.Int("decrypted_length", len(agePrivKeyData)),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Step 6: Parse the decrypted age private key
 | 
				
			||||||
 | 
						Debug("Parsing decrypted age private key", "key_id", k.GetID())
 | 
				
			||||||
 | 
						ageIdentity, err := age.ParseX25519Identity(string(agePrivKeyData))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to parse age private key", "error", err, "key_id", k.GetID())
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to parse age private key: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						DebugWith("Successfully parsed keychain age identity",
 | 
				
			||||||
 | 
							slog.String("key_id", k.GetID()),
 | 
				
			||||||
 | 
							slog.String("public_key", ageIdentity.Recipient().String()),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return ageIdentity, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetType implements UnlockKey interface
 | 
				
			||||||
 | 
					func (k *KeychainUnlockKey) GetType() string {
 | 
				
			||||||
 | 
						return "keychain"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetMetadata implements UnlockKey interface
 | 
				
			||||||
 | 
					func (k *KeychainUnlockKey) GetMetadata() UnlockKeyMetadata {
 | 
				
			||||||
 | 
						return k.Metadata
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetDirectory implements UnlockKey interface
 | 
				
			||||||
 | 
					func (k *KeychainUnlockKey) GetDirectory() string {
 | 
				
			||||||
 | 
						return k.Directory
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetID implements UnlockKey interface
 | 
				
			||||||
 | 
					func (k *KeychainUnlockKey) GetID() string {
 | 
				
			||||||
 | 
						return k.Metadata.ID
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ID implements UnlockKey interface - generates ID from keychain item name
 | 
				
			||||||
 | 
					func (k *KeychainUnlockKey) ID() string {
 | 
				
			||||||
 | 
						// Generate ID using keychain item name
 | 
				
			||||||
 | 
						keychainItemName, err := k.GetKeychainItemName()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							// Fallback to metadata ID if we can't read the keychain item name
 | 
				
			||||||
 | 
							return k.Metadata.ID
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return fmt.Sprintf("%s-keychain", keychainItemName)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Remove implements UnlockKey interface - removes the keychain unlock key
 | 
				
			||||||
 | 
					func (k *KeychainUnlockKey) Remove() error {
 | 
				
			||||||
 | 
						// Step 1: Get keychain item name
 | 
				
			||||||
 | 
						keychainItemName, err := k.GetKeychainItemName()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to get keychain item name during removal", "error", err, "key_id", k.GetID())
 | 
				
			||||||
 | 
							return fmt.Errorf("failed to get keychain item name: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Step 2: Remove from keychain
 | 
				
			||||||
 | 
						Debug("Removing keychain item", "keychain_item", keychainItemName)
 | 
				
			||||||
 | 
						if err := deleteFromKeychain(keychainItemName); err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to remove keychain item", "error", err, "keychain_item", keychainItemName)
 | 
				
			||||||
 | 
							return fmt.Errorf("failed to remove keychain item: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Step 3: Remove directory
 | 
				
			||||||
 | 
						Debug("Removing keychain unlock key directory", "directory", k.Directory)
 | 
				
			||||||
 | 
						if err := k.fs.RemoveAll(k.Directory); err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to remove keychain unlock key directory", "error", err, "directory", k.Directory)
 | 
				
			||||||
 | 
							return fmt.Errorf("failed to remove keychain unlock key directory: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						Debug("Successfully removed keychain unlock key", "key_id", k.GetID(), "keychain_item", keychainItemName)
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// DecryptSecret decrypts a secret using this keychain unlock key's long-term key management
 | 
				
			||||||
 | 
					func (k *KeychainUnlockKey) DecryptSecret(secret *Secret) ([]byte, error) {
 | 
				
			||||||
 | 
						DebugWith("Decrypting secret with keychain unlock key",
 | 
				
			||||||
 | 
							slog.String("secret_name", secret.Name),
 | 
				
			||||||
 | 
							slog.String("key_id", k.GetID()),
 | 
				
			||||||
 | 
							slog.String("key_type", k.GetType()),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Let the secret read its own encrypted data
 | 
				
			||||||
 | 
						encryptedData, err := secret.GetEncryptedData()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to get encrypted secret data for keychain decryption", "error", err, "secret_name", secret.Name)
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to get encrypted secret data: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						DebugWith("Retrieved encrypted secret data for keychain decryption",
 | 
				
			||||||
 | 
							slog.String("secret_name", secret.Name),
 | 
				
			||||||
 | 
							slog.String("key_id", k.GetID()),
 | 
				
			||||||
 | 
							slog.Int("encrypted_length", len(encryptedData)),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Get or derive the long-term private key
 | 
				
			||||||
 | 
						var ltPrivKeyData []byte
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check if mnemonic is available in environment variable
 | 
				
			||||||
 | 
						if envMnemonic := os.Getenv(EnvMnemonic); envMnemonic != "" {
 | 
				
			||||||
 | 
							// Use mnemonic directly to derive long-term key
 | 
				
			||||||
 | 
							ltIdentity, err := agehd.DeriveIdentity(envMnemonic, 0)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("failed to derive long-term key from mnemonic: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							ltPrivKeyData = []byte(ltIdentity.String())
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							// Get keychain item name and retrieve data
 | 
				
			||||||
 | 
							keychainItemName, err := k.GetKeychainItemName()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("failed to get keychain item name: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							keychainDataBytes, err := retrieveFromKeychain(keychainItemName)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("failed to retrieve data from keychain: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							var keychainData KeychainData
 | 
				
			||||||
 | 
							if err := json.Unmarshal(keychainDataBytes, &keychainData); err != nil {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("failed to parse keychain data: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Decrypt the long-term private key using the encrypted data from keychain
 | 
				
			||||||
 | 
							encryptedLtPrivKey, err := hex.DecodeString(keychainData.EncryptedLongtermKey)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("failed to decode encrypted long-term key: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Get our unlock key identity to decrypt the long-term key
 | 
				
			||||||
 | 
							unlockIdentity, err := k.GetIdentity()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("failed to get unlock identity: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Decrypt long-term private key using our unlock key
 | 
				
			||||||
 | 
							ltPrivKeyData, err = decryptWithIdentity(encryptedLtPrivKey, unlockIdentity)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Parse long-term private key
 | 
				
			||||||
 | 
						Debug("Parsing long-term private key", "key_id", k.GetID())
 | 
				
			||||||
 | 
						ltIdentity, err := age.ParseX25519Identity(string(ltPrivKeyData))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to parse long-term private key", "error", err, "key_id", k.GetID())
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to parse long-term private key: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						DebugWith("Successfully parsed long-term identity",
 | 
				
			||||||
 | 
							slog.String("key_id", k.GetID()),
 | 
				
			||||||
 | 
							slog.String("public_key", ltIdentity.Recipient().String()),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Decrypt secret data using long-term key
 | 
				
			||||||
 | 
						Debug("Decrypting secret data with long-term key", "secret_name", secret.Name, "key_id", k.GetID())
 | 
				
			||||||
 | 
						decryptedData, err := decryptWithIdentity(encryptedData, ltIdentity)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to decrypt secret with long-term key", "error", err, "secret_name", secret.Name, "key_id", k.GetID())
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to decrypt secret: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						DebugWith("Successfully decrypted secret with keychain unlock key",
 | 
				
			||||||
 | 
							slog.String("secret_name", secret.Name),
 | 
				
			||||||
 | 
							slog.String("key_id", k.GetID()),
 | 
				
			||||||
 | 
							slog.Int("decrypted_length", len(decryptedData)),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return decryptedData, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewKeychainUnlockKey creates a new KeychainUnlockKey instance
 | 
				
			||||||
 | 
					func NewKeychainUnlockKey(fs afero.Fs, directory string, metadata UnlockKeyMetadata) *KeychainUnlockKey {
 | 
				
			||||||
 | 
						return &KeychainUnlockKey{
 | 
				
			||||||
 | 
							Directory: directory,
 | 
				
			||||||
 | 
							Metadata:  metadata,
 | 
				
			||||||
 | 
							fs:        fs,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetKeychainItemName returns the keychain item name from metadata
 | 
				
			||||||
 | 
					func (k *KeychainUnlockKey) GetKeychainItemName() (string, error) {
 | 
				
			||||||
 | 
						// Load the metadata
 | 
				
			||||||
 | 
						metadataPath := filepath.Join(k.Directory, "unlock-metadata.json")
 | 
				
			||||||
 | 
						metadataData, err := afero.ReadFile(k.fs, metadataPath)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", fmt.Errorf("failed to read keychain metadata: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var keychainMetadata KeychainUnlockKeyMetadata
 | 
				
			||||||
 | 
						if err := json.Unmarshal(metadataData, &keychainMetadata); err != nil {
 | 
				
			||||||
 | 
							return "", fmt.Errorf("failed to parse keychain metadata: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return keychainMetadata.KeychainItemName, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// generateKeychainUnlockKeyName generates a unique name for the keychain unlock key
 | 
				
			||||||
 | 
					func generateKeychainUnlockKeyName(vaultName string) (string, error) {
 | 
				
			||||||
 | 
						hostname, err := os.Hostname()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", fmt.Errorf("failed to get hostname: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Format: secret-<vault>-<hostname>-<date>
 | 
				
			||||||
 | 
						enrollmentDate := time.Now().Format("2006-01-02")
 | 
				
			||||||
 | 
						return fmt.Sprintf("secret-%s-%s-%s", vaultName, hostname, enrollmentDate), nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreateKeychainUnlockKey creates a new keychain unlock key and stores it in the vault
 | 
				
			||||||
 | 
					func CreateKeychainUnlockKey(fs afero.Fs, stateDir string) (*KeychainUnlockKey, error) {
 | 
				
			||||||
 | 
						// Check if we're on macOS
 | 
				
			||||||
 | 
						if err := checkMacOSAvailable(); err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Get current vault
 | 
				
			||||||
 | 
						vault, err := GetCurrentVault(fs, stateDir)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to get current vault: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Generate the keychain item name
 | 
				
			||||||
 | 
						keychainItemName, err := generateKeychainUnlockKeyName(vault.Name)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to generate keychain item name: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create unlock key directory using the keychain item name as the directory name
 | 
				
			||||||
 | 
						vaultDir, err := vault.GetDirectory()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to get vault directory: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						unlockKeyDir := filepath.Join(vaultDir, "unlock.d", keychainItemName)
 | 
				
			||||||
 | 
						if err := fs.MkdirAll(unlockKeyDir, 0700); err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to create unlock key directory: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Step 1: Generate a new age keypair for the keychain unlock key
 | 
				
			||||||
 | 
						ageIdentity, err := age.GenerateX25519Identity()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to generate age keypair: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Step 2: Generate a random passphrase for encrypting the age private key
 | 
				
			||||||
 | 
						agePrivKeyPassphrase, err := generateRandomPassphrase(64)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to generate age private key passphrase: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Step 3: Store age public key as plaintext
 | 
				
			||||||
 | 
						agePublicKeyString := ageIdentity.Recipient().String()
 | 
				
			||||||
 | 
						agePubKeyPath := filepath.Join(unlockKeyDir, "pub.age")
 | 
				
			||||||
 | 
						if err := afero.WriteFile(fs, agePubKeyPath, []byte(agePublicKeyString), 0600); err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to write age public key: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Step 4: Encrypt age private key with the generated passphrase and store on disk
 | 
				
			||||||
 | 
						agePrivateKeyBytes := []byte(ageIdentity.String())
 | 
				
			||||||
 | 
						encryptedAgePrivKey, err := encryptWithPassphrase(agePrivateKeyBytes, agePrivKeyPassphrase)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to encrypt age private key with passphrase: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						agePrivKeyPath := filepath.Join(unlockKeyDir, "priv.age")
 | 
				
			||||||
 | 
						if err := afero.WriteFile(fs, agePrivKeyPath, encryptedAgePrivKey, 0600); err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to write encrypted age private key: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Step 5: Get or derive the long-term private key
 | 
				
			||||||
 | 
						var ltPrivKeyData []byte
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check if mnemonic is available in environment variable
 | 
				
			||||||
 | 
						if envMnemonic := os.Getenv(EnvMnemonic); envMnemonic != "" {
 | 
				
			||||||
 | 
							// Use mnemonic directly to derive long-term key
 | 
				
			||||||
 | 
							ltIdentity, err := agehd.DeriveIdentity(envMnemonic, 0)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("failed to derive long-term key from mnemonic: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							ltPrivKeyData = []byte(ltIdentity.String())
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							// Get the vault to access current unlock key
 | 
				
			||||||
 | 
							currentUnlockKey, err := vault.GetCurrentUnlockKey()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("failed to get current unlock key: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Get the current unlock key identity
 | 
				
			||||||
 | 
							currentUnlockIdentity, err := currentUnlockKey.GetIdentity()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("failed to get current unlock key identity: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Get encrypted long-term key from current unlock key, handling different types
 | 
				
			||||||
 | 
							var encryptedLtPrivKey []byte
 | 
				
			||||||
 | 
							switch currentUnlockKey := currentUnlockKey.(type) {
 | 
				
			||||||
 | 
							case *PassphraseUnlockKey:
 | 
				
			||||||
 | 
								// Read the encrypted long-term private key from passphrase unlock key
 | 
				
			||||||
 | 
								encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlockKey.GetDirectory(), "longterm.age"))
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return nil, fmt.Errorf("failed to read encrypted long-term key from current passphrase unlock key: %w", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							case *PGPUnlockKey:
 | 
				
			||||||
 | 
								// Read the encrypted long-term private key from PGP unlock key
 | 
				
			||||||
 | 
								encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlockKey.GetDirectory(), "longterm.age"))
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return nil, fmt.Errorf("failed to read encrypted long-term key from current PGP unlock key: %w", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							case *KeychainUnlockKey:
 | 
				
			||||||
 | 
								// Read the encrypted long-term private key from another keychain unlock key
 | 
				
			||||||
 | 
								encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlockKey.GetDirectory(), "longterm.age"))
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return nil, fmt.Errorf("failed to read encrypted long-term key from current keychain unlock key: %w", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							default:
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("unsupported current unlock key type for keychain unlock key creation")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Decrypt long-term private key using current unlock key
 | 
				
			||||||
 | 
							ltPrivKeyData, err = decryptWithIdentity(encryptedLtPrivKey, currentUnlockIdentity)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Step 6: Encrypt long-term private key to the new age unlock key
 | 
				
			||||||
 | 
						encryptedLtPrivKeyToAge, err := encryptToRecipient(ltPrivKeyData, ageIdentity.Recipient())
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to encrypt long-term private key to age unlock key: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Write encrypted long-term private key
 | 
				
			||||||
 | 
						ltPrivKeyPath := filepath.Join(unlockKeyDir, "longterm.age")
 | 
				
			||||||
 | 
						if err := afero.WriteFile(fs, ltPrivKeyPath, encryptedLtPrivKeyToAge, 0600); err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to write encrypted long-term private key: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Step 7: Prepare keychain data
 | 
				
			||||||
 | 
						keychainData := KeychainData{
 | 
				
			||||||
 | 
							AgePublicKey:         agePublicKeyString,
 | 
				
			||||||
 | 
							AgePrivKeyPassphrase: agePrivKeyPassphrase,
 | 
				
			||||||
 | 
							EncryptedLongtermKey: hex.EncodeToString(encryptedLtPrivKeyToAge),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						keychainDataBytes, err := json.Marshal(keychainData)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to marshal keychain data: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Step 8: Store data in keychain
 | 
				
			||||||
 | 
						if err := storeInKeychain(keychainItemName, keychainDataBytes); err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to store data in keychain: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Step 9: Create and write enhanced metadata
 | 
				
			||||||
 | 
						// Generate the key ID directly using the keychain item name
 | 
				
			||||||
 | 
						keyID := fmt.Sprintf("%s-keychain", keychainItemName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						keychainMetadata := KeychainUnlockKeyMetadata{
 | 
				
			||||||
 | 
							UnlockKeyMetadata: UnlockKeyMetadata{
 | 
				
			||||||
 | 
								ID:        keyID,
 | 
				
			||||||
 | 
								Type:      "keychain",
 | 
				
			||||||
 | 
								CreatedAt: time.Now(),
 | 
				
			||||||
 | 
								Flags:     []string{"keychain", "macos"},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							AgePublicKey:     agePublicKeyString,
 | 
				
			||||||
 | 
							KeychainItemName: keychainItemName,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						metadataBytes, err := json.MarshalIndent(keychainMetadata, "", "  ")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to marshal unlock key metadata: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := afero.WriteFile(fs, filepath.Join(unlockKeyDir, "unlock-metadata.json"), metadataBytes, 0600); err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to write unlock key metadata: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return &KeychainUnlockKey{
 | 
				
			||||||
 | 
							Directory: unlockKeyDir,
 | 
				
			||||||
 | 
							Metadata:  keychainMetadata.UnlockKeyMetadata,
 | 
				
			||||||
 | 
							fs:        fs,
 | 
				
			||||||
 | 
						}, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// checkMacOSAvailable verifies that we're running on macOS and security command is available
 | 
				
			||||||
 | 
					func checkMacOSAvailable() error {
 | 
				
			||||||
 | 
						cmd := exec.Command("security", "help")
 | 
				
			||||||
 | 
						if err := cmd.Run(); err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("macOS security command not available: %w (keychain unlock keys are only supported on macOS)", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// storeInKeychain stores data in the macOS keychain using the security command
 | 
				
			||||||
 | 
					func storeInKeychain(itemName string, data []byte) error {
 | 
				
			||||||
 | 
						cmd := exec.Command("security", "add-generic-password",
 | 
				
			||||||
 | 
							"-a", itemName,
 | 
				
			||||||
 | 
							"-s", itemName,
 | 
				
			||||||
 | 
							"-w", string(data),
 | 
				
			||||||
 | 
							"-U") // Update if exists
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := cmd.Run(); err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("failed to store item in keychain: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// retrieveFromKeychain retrieves data from the macOS keychain using the security command
 | 
				
			||||||
 | 
					func retrieveFromKeychain(itemName string) ([]byte, error) {
 | 
				
			||||||
 | 
						cmd := exec.Command("security", "find-generic-password",
 | 
				
			||||||
 | 
							"-a", itemName,
 | 
				
			||||||
 | 
							"-s", itemName,
 | 
				
			||||||
 | 
							"-w") // Return password only
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						output, err := cmd.Output()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to retrieve item from keychain: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Remove trailing newline if present
 | 
				
			||||||
 | 
						if len(output) > 0 && output[len(output)-1] == '\n' {
 | 
				
			||||||
 | 
							output = output[:len(output)-1]
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return output, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// deleteFromKeychain removes an item from the macOS keychain using the security command
 | 
				
			||||||
 | 
					func deleteFromKeychain(itemName string) error {
 | 
				
			||||||
 | 
						cmd := exec.Command("security", "delete-generic-password",
 | 
				
			||||||
 | 
							"-a", itemName,
 | 
				
			||||||
 | 
							"-s", itemName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := cmd.Run(); err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("failed to delete item from keychain: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// generateRandomPassphrase generates a random passphrase for encrypting the age private key
 | 
				
			||||||
 | 
					func generateRandomPassphrase(length int) (string, error) {
 | 
				
			||||||
 | 
						return generateRandomString(length, "0123456789abcdef")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -141,17 +141,32 @@ func CreatePassphraseKey(fs afero.Fs, stateDir string, passphrase string) (*Pass
 | 
				
			|||||||
	return currentVault.CreatePassphraseKey(passphrase)
 | 
						return currentVault.CreatePassphraseKey(passphrase)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// DecryptLongTermKey decrypts and returns the long-term private key for this vault
 | 
					// DecryptSecret decrypts a secret using this passphrase unlock key's long-term key management
 | 
				
			||||||
func (p *PassphraseUnlockKey) DecryptLongTermKey() ([]byte, error) {
 | 
					func (p *PassphraseUnlockKey) DecryptSecret(secret *Secret) ([]byte, error) {
 | 
				
			||||||
	DebugWith("Decrypting long-term key with passphrase unlock key",
 | 
						DebugWith("Decrypting secret with passphrase unlock key",
 | 
				
			||||||
 | 
							slog.String("secret_name", secret.Name),
 | 
				
			||||||
		slog.String("key_id", p.GetID()),
 | 
							slog.String("key_id", p.GetID()),
 | 
				
			||||||
		slog.String("key_type", p.GetType()),
 | 
							slog.String("key_type", p.GetType()),
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Get our unlock key identity
 | 
						// Get our unlock key encrypted data
 | 
				
			||||||
 | 
						encryptedData, err := secret.GetEncryptedData()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to get encrypted secret data for passphrase decryption", "error", err, "secret_name", secret.Name)
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to get encrypted secret data: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						DebugWith("Retrieved encrypted secret data for passphrase decryption",
 | 
				
			||||||
 | 
							slog.String("secret_name", secret.Name),
 | 
				
			||||||
 | 
							slog.String("key_id", p.GetID()),
 | 
				
			||||||
 | 
							slog.Int("encrypted_length", len(encryptedData)),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Get our age identity
 | 
				
			||||||
 | 
						Debug("Getting passphrase unlock key identity for secret decryption", "key_id", p.GetID())
 | 
				
			||||||
	unlockIdentity, err := p.GetIdentity()
 | 
						unlockIdentity, err := p.GetIdentity()
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		Debug("Failed to get passphrase unlock identity for long-term decryption", "error", err, "key_id", p.GetID())
 | 
							Debug("Failed to get passphrase unlock identity", "error", err, "key_id", p.GetID())
 | 
				
			||||||
		return nil, fmt.Errorf("failed to get unlock identity: %w", err)
 | 
							return nil, fmt.Errorf("failed to get unlock identity: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -183,42 +198,25 @@ func (p *PassphraseUnlockKey) DecryptLongTermKey() ([]byte, error) {
 | 
				
			|||||||
		slog.Int("decrypted_length", len(ltPrivKeyData)),
 | 
							slog.Int("decrypted_length", len(ltPrivKeyData)),
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return ltPrivKeyData, nil
 | 
						// Parse long-term private key
 | 
				
			||||||
}
 | 
						Debug("Parsing long-term private key", "key_id", p.GetID())
 | 
				
			||||||
 | 
						ltIdentity, err := age.ParseX25519Identity(string(ltPrivKeyData))
 | 
				
			||||||
// DecryptSecret decrypts a secret using this passphrase unlock key's long-term key management
 | 
					 | 
				
			||||||
func (p *PassphraseUnlockKey) DecryptSecret(secret *Secret) ([]byte, error) {
 | 
					 | 
				
			||||||
	DebugWith("Decrypting secret with passphrase unlock key",
 | 
					 | 
				
			||||||
		slog.String("secret_name", secret.Name),
 | 
					 | 
				
			||||||
		slog.String("key_id", p.GetID()),
 | 
					 | 
				
			||||||
		slog.String("key_type", p.GetType()),
 | 
					 | 
				
			||||||
	)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Get encrypted secret data
 | 
					 | 
				
			||||||
	encryptedData, err := secret.GetEncryptedData()
 | 
					 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		Debug("Failed to get encrypted secret data for passphrase decryption", "error", err, "secret_name", secret.Name)
 | 
							Debug("Failed to parse long-term private key", "error", err, "key_id", p.GetID())
 | 
				
			||||||
		return nil, fmt.Errorf("failed to get encrypted secret data: %w", err)
 | 
							return nil, fmt.Errorf("failed to parse long-term private key: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	DebugWith("Retrieved encrypted secret data for passphrase decryption",
 | 
						DebugWith("Successfully parsed long-term identity",
 | 
				
			||||||
		slog.String("secret_name", secret.Name),
 | 
					 | 
				
			||||||
		slog.String("key_id", p.GetID()),
 | 
							slog.String("key_id", p.GetID()),
 | 
				
			||||||
		slog.Int("encrypted_length", len(encryptedData)),
 | 
							slog.String("public_key", ltIdentity.Recipient().String()),
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Decrypt long-term private key using our unlock key
 | 
						// Decrypt secret data using long-term key
 | 
				
			||||||
	ltPrivKeyData, err := p.DecryptLongTermKey()
 | 
						Debug("Decrypting secret data with long-term key", "secret_name", secret.Name, "key_id", p.GetID())
 | 
				
			||||||
	if err != nil {
 | 
						decryptedData, err := decryptWithIdentity(encryptedData, ltIdentity)
 | 
				
			||||||
		Debug("Failed to decrypt long-term private key for secret decryption", "error", err, "key_id", p.GetID())
 | 
					 | 
				
			||||||
		return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Use helper to parse long-term key and decrypt secret
 | 
					 | 
				
			||||||
	decryptedData, err := decryptSecretWithLongTermKey(ltPrivKeyData, encryptedData)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		Debug("Failed to decrypt secret with long-term key", "error", err, "secret_name", secret.Name, "key_id", p.GetID())
 | 
							Debug("Failed to decrypt secret with long-term key", "error", err, "secret_name", secret.Name, "key_id", p.GetID())
 | 
				
			||||||
		return nil, err
 | 
							return nil, fmt.Errorf("failed to decrypt secret: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	DebugWith("Successfully decrypted secret with passphrase unlock key",
 | 
						DebugWith("Successfully decrypted secret with passphrase unlock key",
 | 
				
			||||||
 | 
				
			|||||||
@ -1 +1,467 @@
 | 
				
			|||||||
 | 
					package secret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"log/slog"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"os/exec"
 | 
				
			||||||
 | 
						"path/filepath"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"filippo.io/age"
 | 
				
			||||||
 | 
						"git.eeqj.de/sneak/secret/pkg/agehd"
 | 
				
			||||||
 | 
						"github.com/spf13/afero"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PGPUnlockKeyMetadata extends UnlockKeyMetadata with PGP-specific data
 | 
				
			||||||
 | 
					type PGPUnlockKeyMetadata struct {
 | 
				
			||||||
 | 
						UnlockKeyMetadata
 | 
				
			||||||
 | 
						// GPG key ID used for encryption
 | 
				
			||||||
 | 
						GPGKeyID string `json:"gpg_key_id"`
 | 
				
			||||||
 | 
						// Age keypair information
 | 
				
			||||||
 | 
						AgePublicKey string `json:"age_public_key"`
 | 
				
			||||||
 | 
						AgeRecipient string `json:"age_recipient"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PGPUnlockKey represents a PGP-protected unlock key
 | 
				
			||||||
 | 
					type PGPUnlockKey struct {
 | 
				
			||||||
 | 
						Directory string
 | 
				
			||||||
 | 
						Metadata  UnlockKeyMetadata
 | 
				
			||||||
 | 
						fs        afero.Fs
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetIdentity implements UnlockKey interface for PGP-based unlock keys
 | 
				
			||||||
 | 
					func (p *PGPUnlockKey) GetIdentity() (*age.X25519Identity, error) {
 | 
				
			||||||
 | 
						DebugWith("Getting PGP unlock key identity",
 | 
				
			||||||
 | 
							slog.String("key_id", p.GetID()),
 | 
				
			||||||
 | 
							slog.String("key_type", p.GetType()),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Step 1: Read the encrypted age private key from filesystem
 | 
				
			||||||
 | 
						agePrivKeyPath := filepath.Join(p.Directory, "priv.age.gpg")
 | 
				
			||||||
 | 
						Debug("Reading PGP-encrypted age private key", "path", agePrivKeyPath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						encryptedAgePrivKeyData, err := afero.ReadFile(p.fs, agePrivKeyPath)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to read PGP-encrypted age private key", "error", err, "path", agePrivKeyPath)
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to read encrypted age private key: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						DebugWith("Read PGP-encrypted age private key",
 | 
				
			||||||
 | 
							slog.String("key_id", p.GetID()),
 | 
				
			||||||
 | 
							slog.Int("encrypted_length", len(encryptedAgePrivKeyData)),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Step 2: Decrypt the age private key using GPG
 | 
				
			||||||
 | 
						Debug("Decrypting age private key with GPG", "key_id", p.GetID())
 | 
				
			||||||
 | 
						agePrivKeyData, err := gpgDecrypt(encryptedAgePrivKeyData)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to decrypt age private key with GPG", "error", err, "key_id", p.GetID())
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to decrypt age private key with GPG: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						DebugWith("Successfully decrypted age private key with GPG",
 | 
				
			||||||
 | 
							slog.String("key_id", p.GetID()),
 | 
				
			||||||
 | 
							slog.Int("decrypted_length", len(agePrivKeyData)),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Step 3: Parse the decrypted age private key
 | 
				
			||||||
 | 
						Debug("Parsing decrypted age private key", "key_id", p.GetID())
 | 
				
			||||||
 | 
						ageIdentity, err := age.ParseX25519Identity(string(agePrivKeyData))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to parse age private key", "error", err, "key_id", p.GetID())
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to parse age private key: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						DebugWith("Successfully parsed PGP age identity",
 | 
				
			||||||
 | 
							slog.String("key_id", p.GetID()),
 | 
				
			||||||
 | 
							slog.String("public_key", ageIdentity.Recipient().String()),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return ageIdentity, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetType implements UnlockKey interface
 | 
				
			||||||
 | 
					func (p *PGPUnlockKey) GetType() string {
 | 
				
			||||||
 | 
						return "pgp"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetMetadata implements UnlockKey interface
 | 
				
			||||||
 | 
					func (p *PGPUnlockKey) GetMetadata() UnlockKeyMetadata {
 | 
				
			||||||
 | 
						return p.Metadata
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetDirectory implements UnlockKey interface
 | 
				
			||||||
 | 
					func (p *PGPUnlockKey) GetDirectory() string {
 | 
				
			||||||
 | 
						return p.Directory
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetID implements UnlockKey interface
 | 
				
			||||||
 | 
					func (p *PGPUnlockKey) GetID() string {
 | 
				
			||||||
 | 
						return p.Metadata.ID
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ID implements UnlockKey interface - generates ID from GPG key ID
 | 
				
			||||||
 | 
					func (p *PGPUnlockKey) ID() string {
 | 
				
			||||||
 | 
						// Generate ID using GPG key ID: <keyid>-pgp
 | 
				
			||||||
 | 
						gpgKeyID, err := p.GetGPGKeyID()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							// Fallback to metadata ID if we can't read the GPG key ID
 | 
				
			||||||
 | 
							return p.Metadata.ID
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return fmt.Sprintf("%s-pgp", gpgKeyID)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Remove implements UnlockKey interface - removes the PGP unlock key
 | 
				
			||||||
 | 
					func (p *PGPUnlockKey) Remove() error {
 | 
				
			||||||
 | 
						// For PGP keys, we just need to remove the directory
 | 
				
			||||||
 | 
						// No external resources (like keychain items) to clean up
 | 
				
			||||||
 | 
						if err := p.fs.RemoveAll(p.Directory); err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("failed to remove PGP unlock key directory: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// DecryptSecret decrypts a secret using this PGP unlock key's long-term key management
 | 
				
			||||||
 | 
					func (p *PGPUnlockKey) DecryptSecret(secret *Secret) ([]byte, error) {
 | 
				
			||||||
 | 
						DebugWith("Decrypting secret with PGP unlock key",
 | 
				
			||||||
 | 
							slog.String("secret_name", secret.Name),
 | 
				
			||||||
 | 
							slog.String("key_id", p.GetID()),
 | 
				
			||||||
 | 
							slog.String("key_type", p.GetType()),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Let the secret read its own encrypted data
 | 
				
			||||||
 | 
						encryptedData, err := secret.GetEncryptedData()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to get encrypted secret data for PGP decryption", "error", err, "secret_name", secret.Name)
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to get encrypted secret data: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						DebugWith("Retrieved encrypted secret data for PGP decryption",
 | 
				
			||||||
 | 
							slog.String("secret_name", secret.Name),
 | 
				
			||||||
 | 
							slog.String("key_id", p.GetID()),
 | 
				
			||||||
 | 
							slog.Int("encrypted_length", len(encryptedData)),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Get our age identity
 | 
				
			||||||
 | 
						Debug("Getting PGP unlock key identity for secret decryption", "key_id", p.GetID())
 | 
				
			||||||
 | 
						_, err = p.GetIdentity()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to get PGP unlock identity", "error", err, "key_id", p.GetID())
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to get unlock identity: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Get or derive the long-term private key
 | 
				
			||||||
 | 
						var ltPrivKeyData []byte
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check if mnemonic is available in environment variable
 | 
				
			||||||
 | 
						if envMnemonic := os.Getenv(EnvMnemonic); envMnemonic != "" {
 | 
				
			||||||
 | 
							// Use mnemonic directly to derive long-term key
 | 
				
			||||||
 | 
							ltIdentity, err := agehd.DeriveIdentity(envMnemonic, 0)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("failed to derive long-term key from mnemonic: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							ltPrivKeyData = []byte(ltIdentity.String())
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							// Get the vault to access current unlock key
 | 
				
			||||||
 | 
							stateDir := filepath.Dir(filepath.Dir(filepath.Dir(p.Directory)))
 | 
				
			||||||
 | 
							vault, err := GetCurrentVault(p.fs, stateDir)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("failed to get vault: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Get current unlock key
 | 
				
			||||||
 | 
							currentUnlockKey, err := vault.GetCurrentUnlockKey()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("failed to get current unlock key: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Get the current unlock key identity
 | 
				
			||||||
 | 
							currentUnlockIdentity, err := currentUnlockKey.GetIdentity()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("failed to get current unlock key identity: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Get encrypted long-term key from current unlock key, handling different types
 | 
				
			||||||
 | 
							var encryptedLtPrivKey []byte
 | 
				
			||||||
 | 
							switch currentUnlockKey := currentUnlockKey.(type) {
 | 
				
			||||||
 | 
							case *PassphraseUnlockKey:
 | 
				
			||||||
 | 
								// Read the encrypted long-term private key from passphrase unlock key
 | 
				
			||||||
 | 
								encryptedLtPrivKey, err = afero.ReadFile(p.fs, filepath.Join(currentUnlockKey.GetDirectory(), "longterm.age"))
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return nil, fmt.Errorf("failed to read encrypted long-term key from current passphrase unlock key: %w", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							case *PGPUnlockKey:
 | 
				
			||||||
 | 
								// Read the encrypted long-term private key from PGP unlock key
 | 
				
			||||||
 | 
								encryptedLtPrivKey, err = afero.ReadFile(p.fs, filepath.Join(currentUnlockKey.GetDirectory(), "longterm.age"))
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return nil, fmt.Errorf("failed to read encrypted long-term key from current PGP unlock key: %w", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							default:
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("unsupported current unlock key type for PGP unlock key creation")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Decrypt long-term private key using current unlock key
 | 
				
			||||||
 | 
							ltPrivKeyData, err = decryptWithIdentity(encryptedLtPrivKey, currentUnlockIdentity)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Parse long-term private key
 | 
				
			||||||
 | 
						Debug("Parsing long-term private key", "key_id", p.GetID())
 | 
				
			||||||
 | 
						ltIdentity, err := age.ParseX25519Identity(string(ltPrivKeyData))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to parse long-term private key", "error", err, "key_id", p.GetID())
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to parse long-term private key: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						DebugWith("Successfully parsed long-term identity",
 | 
				
			||||||
 | 
							slog.String("key_id", p.GetID()),
 | 
				
			||||||
 | 
							slog.String("public_key", ltIdentity.Recipient().String()),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Decrypt secret data using long-term key
 | 
				
			||||||
 | 
						Debug("Decrypting secret data with long-term key", "secret_name", secret.Name, "key_id", p.GetID())
 | 
				
			||||||
 | 
						decryptedData, err := decryptWithIdentity(encryptedData, ltIdentity)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to decrypt secret with long-term key", "error", err, "secret_name", secret.Name, "key_id", p.GetID())
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to decrypt secret: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						DebugWith("Successfully decrypted secret with PGP unlock key",
 | 
				
			||||||
 | 
							slog.String("secret_name", secret.Name),
 | 
				
			||||||
 | 
							slog.String("key_id", p.GetID()),
 | 
				
			||||||
 | 
							slog.Int("decrypted_length", len(decryptedData)),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return decryptedData, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewPGPUnlockKey creates a new PGPUnlockKey instance
 | 
				
			||||||
 | 
					func NewPGPUnlockKey(fs afero.Fs, directory string, metadata UnlockKeyMetadata) *PGPUnlockKey {
 | 
				
			||||||
 | 
						return &PGPUnlockKey{
 | 
				
			||||||
 | 
							Directory: directory,
 | 
				
			||||||
 | 
							Metadata:  metadata,
 | 
				
			||||||
 | 
							fs:        fs,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetGPGKeyID returns the GPG key ID from metadata
 | 
				
			||||||
 | 
					func (p *PGPUnlockKey) GetGPGKeyID() (string, error) {
 | 
				
			||||||
 | 
						// Load the metadata
 | 
				
			||||||
 | 
						metadataPath := filepath.Join(p.Directory, "unlock-metadata.json")
 | 
				
			||||||
 | 
						metadataData, err := afero.ReadFile(p.fs, metadataPath)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", fmt.Errorf("failed to read PGP metadata: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var pgpMetadata PGPUnlockKeyMetadata
 | 
				
			||||||
 | 
						if err := json.Unmarshal(metadataData, &pgpMetadata); err != nil {
 | 
				
			||||||
 | 
							return "", fmt.Errorf("failed to parse PGP metadata: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return pgpMetadata.GPGKeyID, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// generatePGPUnlockKeyName generates a unique name for the PGP unlock key based on hostname and date
 | 
				
			||||||
 | 
					func generatePGPUnlockKeyName() (string, error) {
 | 
				
			||||||
 | 
						hostname, err := os.Hostname()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", fmt.Errorf("failed to get hostname: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Format: hostname-pgp-YYYY-MM-DD
 | 
				
			||||||
 | 
						enrollmentDate := time.Now().Format("2006-01-02")
 | 
				
			||||||
 | 
						return fmt.Sprintf("%s-pgp-%s", hostname, enrollmentDate), nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreatePGPUnlockKey creates a new PGP unlock key and stores it in the vault
 | 
				
			||||||
 | 
					func CreatePGPUnlockKey(fs afero.Fs, stateDir string, gpgKeyID string) (*PGPUnlockKey, error) {
 | 
				
			||||||
 | 
						// Check if GPG is available
 | 
				
			||||||
 | 
						if err := checkGPGAvailable(); err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Get current vault
 | 
				
			||||||
 | 
						vault, err := GetCurrentVault(fs, stateDir)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to get current vault: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Generate the unlock key name based on hostname and date
 | 
				
			||||||
 | 
						unlockKeyName, err := generatePGPUnlockKeyName()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to generate unlock key name: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create unlock key directory using the generated name
 | 
				
			||||||
 | 
						vaultDir, err := vault.GetDirectory()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to get vault directory: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						unlockKeyDir := filepath.Join(vaultDir, "unlock.d", unlockKeyName)
 | 
				
			||||||
 | 
						if err := fs.MkdirAll(unlockKeyDir, 0700); err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to create unlock key directory: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Step 1: Generate a new age keypair for the PGP unlock key
 | 
				
			||||||
 | 
						ageIdentity, err := age.GenerateX25519Identity()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to generate age keypair: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Step 2: Store age public key as plaintext
 | 
				
			||||||
 | 
						agePublicKeyString := ageIdentity.Recipient().String()
 | 
				
			||||||
 | 
						agePubKeyPath := filepath.Join(unlockKeyDir, "pub.age")
 | 
				
			||||||
 | 
						if err := afero.WriteFile(fs, agePubKeyPath, []byte(agePublicKeyString), 0600); err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to write age public key: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Step 3: Get or derive the long-term private key
 | 
				
			||||||
 | 
						var ltPrivKeyData []byte
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check if mnemonic is available in environment variable
 | 
				
			||||||
 | 
						if envMnemonic := os.Getenv(EnvMnemonic); envMnemonic != "" {
 | 
				
			||||||
 | 
							// Use mnemonic directly to derive long-term key
 | 
				
			||||||
 | 
							ltIdentity, err := agehd.DeriveIdentity(envMnemonic, 0)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("failed to derive long-term key from mnemonic: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							ltPrivKeyData = []byte(ltIdentity.String())
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							// Get the vault to access current unlock key
 | 
				
			||||||
 | 
							currentUnlockKey, err := vault.GetCurrentUnlockKey()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("failed to get current unlock key: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Get the current unlock key identity
 | 
				
			||||||
 | 
							currentUnlockIdentity, err := currentUnlockKey.GetIdentity()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("failed to get current unlock key identity: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Get encrypted long-term key from current unlock key, handling different types
 | 
				
			||||||
 | 
							var encryptedLtPrivKey []byte
 | 
				
			||||||
 | 
							switch currentUnlockKey := currentUnlockKey.(type) {
 | 
				
			||||||
 | 
							case *PassphraseUnlockKey:
 | 
				
			||||||
 | 
								// Read the encrypted long-term private key from passphrase unlock key
 | 
				
			||||||
 | 
								encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlockKey.GetDirectory(), "longterm.age"))
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return nil, fmt.Errorf("failed to read encrypted long-term key from current passphrase unlock key: %w", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							case *PGPUnlockKey:
 | 
				
			||||||
 | 
								// Read the encrypted long-term private key from PGP unlock key
 | 
				
			||||||
 | 
								encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlockKey.GetDirectory(), "longterm.age"))
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return nil, fmt.Errorf("failed to read encrypted long-term key from current PGP unlock key: %w", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							default:
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("unsupported current unlock key type for PGP unlock key creation")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Decrypt long-term private key using current unlock key
 | 
				
			||||||
 | 
							ltPrivKeyData, err = decryptWithIdentity(encryptedLtPrivKey, currentUnlockIdentity)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Step 4: Encrypt long-term private key to the new age unlock key
 | 
				
			||||||
 | 
						encryptedLtPrivKeyToAge, err := encryptToRecipient(ltPrivKeyData, ageIdentity.Recipient())
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to encrypt long-term private key to age unlock key: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Write encrypted long-term private key
 | 
				
			||||||
 | 
						ltPrivKeyPath := filepath.Join(unlockKeyDir, "longterm.age")
 | 
				
			||||||
 | 
						if err := afero.WriteFile(fs, ltPrivKeyPath, encryptedLtPrivKeyToAge, 0600); err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to write encrypted long-term private key: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Step 5: Encrypt age private key to the GPG key ID
 | 
				
			||||||
 | 
						agePrivateKeyBytes := []byte(ageIdentity.String())
 | 
				
			||||||
 | 
						encryptedAgePrivKey, err := gpgEncrypt(agePrivateKeyBytes, gpgKeyID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to encrypt age private key with GPG: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						agePrivKeyPath := filepath.Join(unlockKeyDir, "priv.age.gpg")
 | 
				
			||||||
 | 
						if err := afero.WriteFile(fs, agePrivKeyPath, encryptedAgePrivKey, 0600); err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to write encrypted age private key: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Step 6: Create and write enhanced metadata
 | 
				
			||||||
 | 
						// Generate the key ID directly using the GPG key ID
 | 
				
			||||||
 | 
						keyID := fmt.Sprintf("%s-pgp", gpgKeyID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						pgpMetadata := PGPUnlockKeyMetadata{
 | 
				
			||||||
 | 
							UnlockKeyMetadata: UnlockKeyMetadata{
 | 
				
			||||||
 | 
								ID:        keyID,
 | 
				
			||||||
 | 
								Type:      "pgp",
 | 
				
			||||||
 | 
								CreatedAt: time.Now(),
 | 
				
			||||||
 | 
								Flags:     []string{"gpg", "encrypted"},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							GPGKeyID:     gpgKeyID,
 | 
				
			||||||
 | 
							AgePublicKey: agePublicKeyString,
 | 
				
			||||||
 | 
							AgeRecipient: ageIdentity.Recipient().String(),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						metadataBytes, err := json.MarshalIndent(pgpMetadata, "", "  ")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to marshal unlock key metadata: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := afero.WriteFile(fs, filepath.Join(unlockKeyDir, "unlock-metadata.json"), metadataBytes, 0600); err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to write unlock key metadata: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return &PGPUnlockKey{
 | 
				
			||||||
 | 
							Directory: unlockKeyDir,
 | 
				
			||||||
 | 
							Metadata:  pgpMetadata.UnlockKeyMetadata,
 | 
				
			||||||
 | 
							fs:        fs,
 | 
				
			||||||
 | 
						}, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// checkGPGAvailable verifies that GPG is available
 | 
				
			||||||
 | 
					func checkGPGAvailable() error {
 | 
				
			||||||
 | 
						cmd := exec.Command("gpg", "--version")
 | 
				
			||||||
 | 
						if err := cmd.Run(); err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("GPG not available: %w (make sure 'gpg' command is installed and in PATH)", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// gpgEncrypt encrypts data to the specified GPG key ID
 | 
				
			||||||
 | 
					func gpgEncrypt(data []byte, keyID string) ([]byte, error) {
 | 
				
			||||||
 | 
						cmd := exec.Command("gpg", "--trust-model", "always", "--armor", "--encrypt", "-r", keyID)
 | 
				
			||||||
 | 
						cmd.Stdin = strings.NewReader(string(data))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						output, err := cmd.Output()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("GPG encryption failed: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return output, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// gpgDecrypt decrypts GPG-encrypted data
 | 
				
			||||||
 | 
					func gpgDecrypt(encryptedData []byte) ([]byte, error) {
 | 
				
			||||||
 | 
						cmd := exec.Command("gpg", "--quiet", "--decrypt")
 | 
				
			||||||
 | 
						cmd.Stdin = strings.NewReader(string(encryptedData))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						output, err := cmd.Output()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("GPG decryption failed: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return output, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										269
									
								
								internal/secret/secret.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										269
									
								
								internal/secret/secret.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,269 @@
 | 
				
			|||||||
 | 
					package secret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"log/slog"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"path/filepath"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.eeqj.de/sneak/secret/pkg/agehd"
 | 
				
			||||||
 | 
						"github.com/spf13/afero"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Secret represents a secret in a vault
 | 
				
			||||||
 | 
					type Secret struct {
 | 
				
			||||||
 | 
						Name      string
 | 
				
			||||||
 | 
						Directory string
 | 
				
			||||||
 | 
						Metadata  SecretMetadata
 | 
				
			||||||
 | 
						vault     *Vault
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewSecret creates a new Secret instance
 | 
				
			||||||
 | 
					func NewSecret(vault *Vault, name string) *Secret {
 | 
				
			||||||
 | 
						DebugWith("Creating new secret instance",
 | 
				
			||||||
 | 
							slog.String("secret_name", name),
 | 
				
			||||||
 | 
							slog.String("vault_name", vault.Name),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Convert slashes to percent signs for storage directory name
 | 
				
			||||||
 | 
						storageName := strings.ReplaceAll(name, "/", "%")
 | 
				
			||||||
 | 
						vaultDir, _ := vault.GetDirectory()
 | 
				
			||||||
 | 
						secretDir := filepath.Join(vaultDir, "secrets.d", storageName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						DebugWith("Secret storage details",
 | 
				
			||||||
 | 
							slog.String("secret_name", name),
 | 
				
			||||||
 | 
							slog.String("storage_name", storageName),
 | 
				
			||||||
 | 
							slog.String("secret_dir", secretDir),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return &Secret{
 | 
				
			||||||
 | 
							Name:      name,
 | 
				
			||||||
 | 
							Directory: secretDir,
 | 
				
			||||||
 | 
							vault:     vault,
 | 
				
			||||||
 | 
							Metadata: SecretMetadata{
 | 
				
			||||||
 | 
								Name:      name,
 | 
				
			||||||
 | 
								CreatedAt: time.Now(),
 | 
				
			||||||
 | 
								UpdatedAt: time.Now(),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Save saves a secret value to the vault
 | 
				
			||||||
 | 
					func (s *Secret) Save(value []byte, force bool) error {
 | 
				
			||||||
 | 
						DebugWith("Saving secret",
 | 
				
			||||||
 | 
							slog.String("secret_name", s.Name),
 | 
				
			||||||
 | 
							slog.String("vault_name", s.vault.Name),
 | 
				
			||||||
 | 
							slog.Int("value_length", len(value)),
 | 
				
			||||||
 | 
							slog.Bool("force", force),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err := s.vault.AddSecret(s.Name, value, force)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to save secret", "error", err, "secret_name", s.Name)
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						Debug("Successfully saved secret", "secret_name", s.Name)
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetValue retrieves and decrypts the secret value using the provided unlock key
 | 
				
			||||||
 | 
					func (s *Secret) GetValue(unlockKey UnlockKey) ([]byte, error) {
 | 
				
			||||||
 | 
						DebugWith("Getting secret value",
 | 
				
			||||||
 | 
							slog.String("secret_name", s.Name),
 | 
				
			||||||
 | 
							slog.String("vault_name", s.vault.Name),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check if secret exists
 | 
				
			||||||
 | 
						exists, err := s.Exists()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to check if secret exists during GetValue", "error", err, "secret_name", s.Name)
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to check if secret exists: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if !exists {
 | 
				
			||||||
 | 
							Debug("Secret not found during GetValue", "secret_name", s.Name, "vault_name", s.vault.Name)
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("secret %s not found", s.Name)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						Debug("Secret exists, proceeding with decryption", "secret_name", s.Name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check if we have SB_SECRET_MNEMONIC environment variable for direct decryption
 | 
				
			||||||
 | 
						if envMnemonic := os.Getenv(EnvMnemonic); envMnemonic != "" {
 | 
				
			||||||
 | 
							Debug("Using mnemonic from environment for secret decryption", "secret_name", s.Name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Use mnemonic directly to derive long-term key
 | 
				
			||||||
 | 
							ltIdentity, err := agehd.DeriveIdentity(envMnemonic, 0)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								Debug("Failed to derive long-term key from mnemonic for secret", "error", err, "secret_name", s.Name)
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("failed to derive long-term key from mnemonic: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							Debug("Successfully derived long-term key from mnemonic", "secret_name", s.Name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Read our own encrypted data
 | 
				
			||||||
 | 
							encryptedData, err := s.GetEncryptedData()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								Debug("Failed to get encrypted data for mnemonic decryption", "error", err, "secret_name", s.Name)
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							DebugWith("Retrieved encrypted data for mnemonic decryption",
 | 
				
			||||||
 | 
								slog.String("secret_name", s.Name),
 | 
				
			||||||
 | 
								slog.Int("encrypted_length", len(encryptedData)),
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Decrypt secret data
 | 
				
			||||||
 | 
							Debug("Decrypting secret with long-term key from mnemonic", "secret_name", s.Name)
 | 
				
			||||||
 | 
							decryptedData, err := decryptWithIdentity(encryptedData, ltIdentity)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								Debug("Failed to decrypt secret with mnemonic", "error", err, "secret_name", s.Name)
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("failed to decrypt secret: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							DebugWith("Successfully decrypted secret with mnemonic",
 | 
				
			||||||
 | 
								slog.String("secret_name", s.Name),
 | 
				
			||||||
 | 
								slog.Int("decrypted_length", len(decryptedData)),
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return decryptedData, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						Debug("Using unlock key for secret decryption", "secret_name", s.Name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Use the provided unlock key to decrypt the secret
 | 
				
			||||||
 | 
						if unlockKey == nil {
 | 
				
			||||||
 | 
							Debug("No unlock key provided for secret decryption", "secret_name", s.Name)
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("unlock key required to decrypt secret")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						DebugWith("Delegating secret decryption to unlock key",
 | 
				
			||||||
 | 
							slog.String("secret_name", s.Name),
 | 
				
			||||||
 | 
							slog.String("unlock_key_type", unlockKey.GetType()),
 | 
				
			||||||
 | 
							slog.String("unlock_key_id", unlockKey.GetID()),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Delegate decryption to the unlock key implementation
 | 
				
			||||||
 | 
						decryptedData, err := unlockKey.DecryptSecret(s)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Unlock key failed to decrypt secret", "error", err, "secret_name", s.Name, "unlock_key_type", unlockKey.GetType())
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						DebugWith("Successfully decrypted secret via unlock key",
 | 
				
			||||||
 | 
							slog.String("secret_name", s.Name),
 | 
				
			||||||
 | 
							slog.String("unlock_key_type", unlockKey.GetType()),
 | 
				
			||||||
 | 
							slog.Int("decrypted_length", len(decryptedData)),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return decryptedData, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// LoadMetadata loads the secret metadata from disk
 | 
				
			||||||
 | 
					func (s *Secret) LoadMetadata() error {
 | 
				
			||||||
 | 
						DebugWith("Loading secret metadata",
 | 
				
			||||||
 | 
							slog.String("secret_name", s.Name),
 | 
				
			||||||
 | 
							slog.String("vault_name", s.vault.Name),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						vaultDir, err := s.vault.GetDirectory()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to get vault directory for metadata loading", "error", err, "secret_name", s.Name)
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Convert slashes to percent signs for storage
 | 
				
			||||||
 | 
						storageName := strings.ReplaceAll(s.Name, "/", "%")
 | 
				
			||||||
 | 
						metadataPath := filepath.Join(vaultDir, "secrets.d", storageName, "secret-metadata.json")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						DebugWith("Reading secret metadata",
 | 
				
			||||||
 | 
							slog.String("secret_name", s.Name),
 | 
				
			||||||
 | 
							slog.String("metadata_path", metadataPath),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Read metadata file
 | 
				
			||||||
 | 
						metadataBytes, err := afero.ReadFile(s.vault.fs, metadataPath)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to read secret metadata file", "error", err, "metadata_path", metadataPath)
 | 
				
			||||||
 | 
							return fmt.Errorf("failed to read metadata: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						DebugWith("Read secret metadata file",
 | 
				
			||||||
 | 
							slog.String("secret_name", s.Name),
 | 
				
			||||||
 | 
							slog.Int("metadata_size", len(metadataBytes)),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var metadata SecretMetadata
 | 
				
			||||||
 | 
						if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to parse secret metadata JSON", "error", err, "secret_name", s.Name)
 | 
				
			||||||
 | 
							return fmt.Errorf("failed to parse metadata: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						DebugWith("Parsed secret metadata",
 | 
				
			||||||
 | 
							slog.String("secret_name", metadata.Name),
 | 
				
			||||||
 | 
							slog.Time("created_at", metadata.CreatedAt),
 | 
				
			||||||
 | 
							slog.Time("updated_at", metadata.UpdatedAt),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						s.Metadata = metadata
 | 
				
			||||||
 | 
						Debug("Successfully loaded secret metadata", "secret_name", s.Name)
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetMetadata returns the secret metadata
 | 
				
			||||||
 | 
					func (s *Secret) GetMetadata() SecretMetadata {
 | 
				
			||||||
 | 
						Debug("Returning secret metadata", "secret_name", s.Name)
 | 
				
			||||||
 | 
						return s.Metadata
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetEncryptedData reads and returns the encrypted secret data
 | 
				
			||||||
 | 
					func (s *Secret) GetEncryptedData() ([]byte, error) {
 | 
				
			||||||
 | 
						DebugWith("Getting encrypted secret data",
 | 
				
			||||||
 | 
							slog.String("secret_name", s.Name),
 | 
				
			||||||
 | 
							slog.String("vault_name", s.vault.Name),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						secretPath := filepath.Join(s.Directory, "secret.age")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						Debug("Reading encrypted secret file", "secret_path", secretPath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						encryptedData, err := afero.ReadFile(s.vault.fs, secretPath)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to read encrypted secret file", "error", err, "secret_path", secretPath)
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to read encrypted secret: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						DebugWith("Successfully read encrypted secret data",
 | 
				
			||||||
 | 
							slog.String("secret_name", s.Name),
 | 
				
			||||||
 | 
							slog.Int("encrypted_length", len(encryptedData)),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return encryptedData, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Exists checks if the secret exists on disk
 | 
				
			||||||
 | 
					func (s *Secret) Exists() (bool, error) {
 | 
				
			||||||
 | 
						DebugWith("Checking if secret exists",
 | 
				
			||||||
 | 
							slog.String("secret_name", s.Name),
 | 
				
			||||||
 | 
							slog.String("vault_name", s.vault.Name),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						secretPath := filepath.Join(s.Directory, "secret.age")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						Debug("Checking secret file existence", "secret_path", secretPath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						exists, err := afero.Exists(s.vault.fs, secretPath)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to check secret file existence", "error", err, "secret_path", secretPath)
 | 
				
			||||||
 | 
							return false, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						DebugWith("Secret existence check result",
 | 
				
			||||||
 | 
							slog.String("secret_name", s.Name),
 | 
				
			||||||
 | 
							slog.Bool("exists", exists),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return exists, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -14,9 +14,6 @@ type UnlockKey interface {
 | 
				
			|||||||
	ID() string    // Generate ID from the key's public key
 | 
						ID() string    // Generate ID from the key's public key
 | 
				
			||||||
	Remove() error // Remove the unlock key and any associated resources
 | 
						Remove() error // Remove the unlock key and any associated resources
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// DecryptLongTermKey decrypts and returns the long-term private key for this vault
 | 
					 | 
				
			||||||
	DecryptLongTermKey() ([]byte, error)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// DecryptSecret decrypts a secret using this unlock key's long-term key management
 | 
						// DecryptSecret decrypts a secret using this unlock key's long-term key management
 | 
				
			||||||
	DecryptSecret(secret *Secret) ([]byte, error)
 | 
						DecryptSecret(secret *Secret) ([]byte, error)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Loading…
	
		Reference in New Issue
	
	Block a user