man what a clusterfuck
This commit is contained in:
parent
1b8ea9695b
commit
ee49ace397
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
secret
|
|
122
README.md
122
README.md
@ -22,7 +22,7 @@ Build from source:
|
|||||||
```bash
|
```bash
|
||||||
git clone <repository>
|
git clone <repository>
|
||||||
cd secret
|
cd secret
|
||||||
make build
|
go build -o secret ./cmd/secret
|
||||||
```
|
```
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
@ -106,15 +106,15 @@ Lists all unlock keys in the current vault with their metadata.
|
|||||||
Creates a new unlock key of the specified type:
|
Creates a new unlock key of the specified type:
|
||||||
|
|
||||||
**Types:**
|
**Types:**
|
||||||
- `passphrase`: Password-protected unlock key
|
- `passphrase`: Traditional passphrase-protected unlock key
|
||||||
- `keychain`: macOS Keychain unlock key (Touch ID/Face ID)
|
- `keychain`: macOS Keychain-protected unlock key (macOS only)
|
||||||
- `pgp`: GPG/PGP key unlock key
|
- `pgp`: Uses an existing GPG key for encryption/decryption
|
||||||
|
|
||||||
**Options:**
|
**Options:**
|
||||||
- `--keyid <id>`: GPG key ID (required for PGP type)
|
- `--keyid <id>`: GPG key ID (required for PGP type)
|
||||||
|
|
||||||
#### `secret keys rm <key-id>`
|
#### `secret keys rm <key-id>`
|
||||||
Removes an unlock key from the current vault.
|
Removes an unlock key.
|
||||||
|
|
||||||
#### `secret key select <key-id>`
|
#### `secret key select <key-id>`
|
||||||
Selects an unlock key as the current default for operations.
|
Selects an unlock key as the current default for operations.
|
||||||
@ -127,9 +127,6 @@ Imports a secret from a file and stores it in the current vault under the given
|
|||||||
#### `secret vault import [vault-name]`
|
#### `secret vault import [vault-name]`
|
||||||
Imports a mnemonic phrase into the specified vault (defaults to "default").
|
Imports a mnemonic phrase into the specified vault (defaults to "default").
|
||||||
|
|
||||||
#### `secret enroll`
|
|
||||||
Enrolls a macOS Keychain unlock key for biometric authentication.
|
|
||||||
|
|
||||||
### Encryption Operations
|
### Encryption Operations
|
||||||
|
|
||||||
#### `secret encrypt <secret-name> [--input=file] [--output=file]`
|
#### `secret encrypt <secret-name> [--input=file] [--output=file]`
|
||||||
@ -143,43 +140,23 @@ Decrypts data using an Age key stored as a secret.
|
|||||||
### Directory Structure
|
### Directory Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
$BASE/ # ~/.config/berlin.sneak.pkg.secret (Linux) or ~/Library/Application Support/berlin.sneak.pkg.secret (macOS)
|
~/.local/share/secret/
|
||||||
├── configuration.json # Global configuration
|
├── vaults.d/
|
||||||
├── currentvault -> vaults.d/default # Symlink to current vault
|
│ ├── default/
|
||||||
└── vaults.d/
|
│ │ ├── unlock-keys.d/
|
||||||
├── default/ # Default vault
|
│ │ │ ├── passphrase/ # Passphrase unlock key
|
||||||
│ ├── vault-metadata.json # Vault metadata
|
│ │ │ ├── keychain/ # Keychain unlock key (macOS)
|
||||||
│ ├── pub.age # Long-term public key
|
│ │ │ └── pgp/ # PGP unlock key
|
||||||
│ ├── current-unlock-key -> unlock.d/passphrase # Current unlock key symlink
|
│ │ ├── secrets.d/
|
||||||
│ ├── unlock.d/ # Unlock keys directory
|
│ │ │ ├── api%key/ # Secret: api/key
|
||||||
│ │ ├── passphrase/ # Passphrase unlock key
|
│ │ │ └── database%password/ # Secret: database/password
|
||||||
│ │ │ ├── unlock-metadata.json # Unlock key metadata
|
│ │ └── current-unlock-key -> ../unlock-keys.d/passphrase
|
||||||
│ │ │ ├── pub.age # Unlock key public key
|
│ └── work/
|
||||||
│ │ │ ├── priv.age # Unlock key private key (encrypted)
|
│ ├── unlock-keys.d/
|
||||||
│ │ │ └── longterm.age # Long-term private key (encrypted to this unlock key)
|
│ ├── secrets.d/
|
||||||
│ │ ├── keychain/ # Keychain unlock key
|
│ └── current-unlock-key
|
||||||
│ │ │ ├── unlock-metadata.json
|
├── currentvault -> vaults.d/default
|
||||||
│ │ │ ├── pub.age
|
└── configuration.json
|
||||||
│ │ │ ├── priv.age
|
|
||||||
│ │ │ └── longterm.age
|
|
||||||
│ │ └── pgp/ # PGP unlock key
|
|
||||||
│ │ ├── unlock-metadata.json
|
|
||||||
│ │ ├── pub.age
|
|
||||||
│ │ ├── priv.asc # PGP-encrypted private key
|
|
||||||
│ │ └── longterm.age
|
|
||||||
│ └── secrets.d/ # Secrets directory
|
|
||||||
│ ├── my-service%password/ # Secret directory (slashes encoded as %)
|
|
||||||
│ │ ├── value.age # Encrypted secret value
|
|
||||||
│ │ ├── pub.age # Secret-specific public key
|
|
||||||
│ │ ├── priv.age # Secret-specific private key (encrypted to long-term key)
|
|
||||||
│ │ └── secret-metadata.json # Secret metadata
|
|
||||||
│ └── api%keys%production/
|
|
||||||
│ ├── value.age
|
|
||||||
│ ├── pub.age
|
|
||||||
│ ├── priv.age
|
|
||||||
│ └── secret-metadata.json
|
|
||||||
└── work/ # Additional vault
|
|
||||||
└── ... (same structure as default)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Key Management and Encryption Flow
|
### Key Management and Encryption Flow
|
||||||
@ -192,19 +169,22 @@ $BASE/ # ~/.config/berlin.sneak.pkg.secret
|
|||||||
#### Unlock Keys
|
#### Unlock Keys
|
||||||
Unlock keys provide different authentication methods to access the long-term keys:
|
Unlock keys provide different authentication methods to access the long-term keys:
|
||||||
|
|
||||||
1. **Passphrase Unlock Keys**:
|
1. **Passphrase Keys**:
|
||||||
- Private key encrypted using a user-provided passphrase
|
- Encrypted with user-provided passphrase
|
||||||
- Stored as encrypted Age identity in `priv.age`
|
- Stored as encrypted Age keys
|
||||||
|
- Cross-platform compatible
|
||||||
|
|
||||||
2. **macOS Keychain Keys**:
|
2. **Keychain Keys** (macOS only):
|
||||||
- Private key stored in the macOS Keychain
|
- Uses macOS Keychain for secure storage
|
||||||
- Requires biometric authentication (Touch ID/Face ID)
|
- Provides seamless authentication on macOS systems
|
||||||
- Provides hardware-backed security
|
- Age private key encrypted with random passphrase stored in Keychain
|
||||||
|
|
||||||
3. **PGP Unlock Keys**:
|
3. **PGP Keys**:
|
||||||
- Private key encrypted using an existing GPG key
|
- Uses existing GPG key infrastructure
|
||||||
- Compatible with hardware tokens (YubiKey, etc.)
|
- Leverages existing key management workflows
|
||||||
- Stored as PGP-encrypted data in `priv.asc`
|
- Strong authentication through GPG
|
||||||
|
|
||||||
|
Each vault maintains its own set of unlock keys and one long-term key. The long-term key is encrypted to each unlock key, allowing any authorized unlock key to access vault secrets.
|
||||||
|
|
||||||
#### Secret-specific Keys
|
#### Secret-specific Keys
|
||||||
- Each secret has its own encryption key pair
|
- Each secret has its own encryption key pair
|
||||||
@ -234,9 +214,9 @@ Unlock keys provide different authentication methods to access the long-term key
|
|||||||
- 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
|
||||||
|
|
||||||
### Hardware Integration
|
### Platform Integration
|
||||||
- macOS Keychain support for biometric authentication
|
- macOS Keychain integration for seamless authentication
|
||||||
- Hardware token support via PGP/GPG integration
|
- GPG integration for existing key management workflows
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
@ -266,7 +246,7 @@ secret vault create personal
|
|||||||
# Work with work vault
|
# Work with work vault
|
||||||
secret vault select work
|
secret vault select work
|
||||||
echo "work-db-pass" | secret add database/password
|
echo "work-db-pass" | secret add database/password
|
||||||
secret keys add keychain # Add Touch ID authentication
|
secret keys add passphrase # Add passphrase authentication
|
||||||
|
|
||||||
# Switch to personal vault
|
# Switch to personal vault
|
||||||
secret vault select personal
|
secret vault select personal
|
||||||
@ -280,7 +260,7 @@ secret vault list
|
|||||||
```bash
|
```bash
|
||||||
# Add multiple unlock methods
|
# Add multiple unlock methods
|
||||||
secret keys add passphrase # Password-based
|
secret keys add passphrase # Password-based
|
||||||
secret keys add keychain # Touch ID (macOS only)
|
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
|
||||||
@ -325,11 +305,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 hardware-backed authentication where available
|
- Supports platform-specific 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 hardware authentication (Keychain, hardware tokens) when available
|
2. Enable platform-specific authentication (Keychain) 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
|
||||||
@ -337,15 +317,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
|
||||||
- Hardware features limited to supported platforms
|
- Platform-specific features limited to supported platforms
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
### Building
|
### Building
|
||||||
```bash
|
```bash
|
||||||
make build # Build binary
|
go build -o secret ./cmd/secret # Build binary
|
||||||
make test # Run tests
|
go test ./... # Run tests
|
||||||
make lint # Run linter
|
go vet ./... # Run static analysis
|
||||||
```
|
```
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
@ -355,3 +335,11 @@ The project includes comprehensive tests:
|
|||||||
go test ./... # Unit tests
|
go test ./... # Unit tests
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Multiple Authentication Methods**: Supports passphrase-based, keychain-based (macOS), and PGP-based unlock keys
|
||||||
|
- **Vault Isolation**: Complete separation between different vaults
|
||||||
|
- **Per-Secret Encryption**: Each secret has its own encryption key
|
||||||
|
- **BIP39 Mnemonic Support**: Keyless operation using mnemonic phrases
|
||||||
|
- **Cross-Platform**: Works on macOS, Linux, and other Unix-like systems
|
||||||
|
|
||||||
|
5
cmd/secret/main.go
Normal file
5
cmd/secret/main.go
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
CLIEntry()
|
||||||
|
}
|
@ -134,7 +134,6 @@ func newRootCmd() *cobra.Command {
|
|||||||
cmd.AddCommand(newKeysCmd())
|
cmd.AddCommand(newKeysCmd())
|
||||||
cmd.AddCommand(newKeyCmd())
|
cmd.AddCommand(newKeyCmd())
|
||||||
cmd.AddCommand(newImportCmd())
|
cmd.AddCommand(newImportCmd())
|
||||||
cmd.AddCommand(newEnrollCmd())
|
|
||||||
cmd.AddCommand(newEncryptCmd())
|
cmd.AddCommand(newEncryptCmd())
|
||||||
cmd.AddCommand(newDecryptCmd())
|
cmd.AddCommand(newDecryptCmd())
|
||||||
|
|
||||||
@ -431,19 +430,6 @@ func newImportCmd() *cobra.Command {
|
|||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func newEnrollCmd() *cobra.Command {
|
|
||||||
return &cobra.Command{
|
|
||||||
Use: "enroll",
|
|
||||||
Short: "Enroll a macOS Keychain unlock key",
|
|
||||||
Long: `Enroll a macOS Keychain unlock key that uses Touch ID/Face ID for biometric authentication.`,
|
|
||||||
Args: cobra.NoArgs,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
cli := NewCLIInstance()
|
|
||||||
return cli.Enroll()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newEncryptCmd() *cobra.Command {
|
func newEncryptCmd() *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "encrypt <secret-name>",
|
Use: "encrypt <secret-name>",
|
||||||
@ -1073,7 +1059,16 @@ func (cli *CLIInstance) KeysAdd(keyType string, cmd *cobra.Command) error {
|
|||||||
return nil
|
return nil
|
||||||
|
|
||||||
case "keychain":
|
case "keychain":
|
||||||
return fmt.Errorf("macOS Keychain unlock keys should be created using 'secret enroll' command")
|
keychainKey, err := CreateKeychainUnlockKey(cli.fs, cli.stateDir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create macOS Keychain unlock key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Printf("Created macOS Keychain unlock key: %s\n", keychainKey.GetMetadata().ID)
|
||||||
|
if keyName, err := keychainKey.GetKeychainItemName(); err == nil {
|
||||||
|
cmd.Printf("Keychain Item Name: %s\n", keyName)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
|
||||||
case "pgp":
|
case "pgp":
|
||||||
// Get GPG key ID from flag or environment variable
|
// Get GPG key ID from flag or environment variable
|
||||||
@ -1150,25 +1145,6 @@ func (cli *CLIInstance) Import(vaultName string) error {
|
|||||||
return cli.importMnemonic(vaultName, mnemonicStr)
|
return cli.importMnemonic(vaultName, mnemonicStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enroll enrolls a hardware security module
|
|
||||||
func (cli *CLIInstance) Enroll() error {
|
|
||||||
keychainKey, err := CreateKeychainUnlockKey(cli.fs, cli.stateDir)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to enroll macOS Keychain unlock key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("macOS Keychain unlock key enrolled successfully!\n")
|
|
||||||
fmt.Printf("Key ID: %s\n", keychainKey.GetMetadata().ID)
|
|
||||||
fmt.Printf("Directory: %s\n", keychainKey.GetDirectory())
|
|
||||||
|
|
||||||
// Load the key name to show the keychain key name
|
|
||||||
if keyName, err := keychainKey.GetKeyName(); err == nil {
|
|
||||||
fmt.Printf("Keychain Key Name: %s\n", keyName)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encrypt encrypts data using an age secret key stored in a secret
|
// Encrypt encrypts data using an age secret key stored in a secret
|
||||||
func (cli *CLIInstance) Encrypt(secretName, inputFile, outputFile string) error {
|
func (cli *CLIInstance) Encrypt(secretName, inputFile, outputFile string) error {
|
||||||
// Get current vault
|
// Get current vault
|
||||||
|
112
internal/secret/crypto.go
Normal file
112
internal/secret/crypto.go
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
package secret
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"filippo.io/age"
|
||||||
|
"golang.org/x/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
// encryptToRecipient encrypts data to a recipient using age
|
||||||
|
func encryptToRecipient(data []byte, recipient age.Recipient) ([]byte, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
w, err := age.Encrypt(&buf, recipient)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create encryptor: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := w.Write(data); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to write data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := w.Close(); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to close encryptor: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// decryptWithIdentity decrypts data with an identity using age
|
||||||
|
func decryptWithIdentity(data []byte, identity age.Identity) ([]byte, error) {
|
||||||
|
r, err := age.Decrypt(bytes.NewReader(data), identity)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create decryptor: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := io.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read decrypted data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// encryptWithPassphrase encrypts data using a passphrase with age's scrypt-based encryption
|
||||||
|
func encryptWithPassphrase(data []byte, passphrase string) ([]byte, error) {
|
||||||
|
recipient, err := age.NewScryptRecipient(passphrase)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create scrypt recipient: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return encryptToRecipient(data, recipient)
|
||||||
|
}
|
||||||
|
|
||||||
|
// decryptWithPassphrase decrypts data using a passphrase with age's scrypt-based decryption
|
||||||
|
func decryptWithPassphrase(encryptedData []byte, passphrase string) ([]byte, error) {
|
||||||
|
identity, err := age.NewScryptIdentity(passphrase)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create scrypt identity: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return decryptWithIdentity(encryptedData, identity)
|
||||||
|
}
|
||||||
|
|
||||||
|
// readPassphrase reads a passphrase securely from the terminal without echoing
|
||||||
|
// This version is for unlocking and doesn't require confirmation
|
||||||
|
func readPassphrase(prompt string) (string, error) {
|
||||||
|
// Check if stdin is a terminal
|
||||||
|
if !term.IsTerminal(int(syscall.Stdin)) {
|
||||||
|
// Not a terminal - fall back to regular input
|
||||||
|
fmt.Print(prompt)
|
||||||
|
var passphrase string
|
||||||
|
_, err := fmt.Scanln(&passphrase)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to read passphrase: %w", err)
|
||||||
|
}
|
||||||
|
return passphrase, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Terminal input - use secure password reading
|
||||||
|
fmt.Print(prompt)
|
||||||
|
passphrase, err := term.ReadPassword(int(syscall.Stdin))
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to read passphrase: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Println() // Print newline since ReadPassword doesn't echo
|
||||||
|
|
||||||
|
if len(passphrase) == 0 {
|
||||||
|
return "", fmt.Errorf("passphrase cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
231
internal/secret/passphraseunlock.go
Normal file
231
internal/secret/passphraseunlock.go
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
package secret
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"filippo.io/age"
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PassphraseUnlockKey represents a passphrase-protected unlock key
|
||||||
|
type PassphraseUnlockKey struct {
|
||||||
|
Directory string
|
||||||
|
Metadata UnlockKeyMetadata
|
||||||
|
fs afero.Fs
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIdentity implements UnlockKey interface for passphrase-based unlock keys
|
||||||
|
func (p *PassphraseUnlockKey) GetIdentity() (*age.X25519Identity, error) {
|
||||||
|
DebugWith("Getting passphrase unlock key identity",
|
||||||
|
slog.String("key_id", p.GetID()),
|
||||||
|
slog.String("key_type", p.GetType()),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Read encrypted private key of unlock key
|
||||||
|
unlockKeyPrivPath := filepath.Join(p.Directory, "priv.age")
|
||||||
|
Debug("Reading encrypted passphrase unlock key", "path", unlockKeyPrivPath)
|
||||||
|
|
||||||
|
encryptedPrivKeyData, err := afero.ReadFile(p.fs, unlockKeyPrivPath)
|
||||||
|
if err != nil {
|
||||||
|
Debug("Failed to read passphrase unlock key private key", "error", err, "path", unlockKeyPrivPath)
|
||||||
|
return nil, fmt.Errorf("failed to read unlock key private key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
DebugWith("Read encrypted passphrase unlock key",
|
||||||
|
slog.String("key_id", p.GetID()),
|
||||||
|
slog.Int("encrypted_length", len(encryptedPrivKeyData)),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get passphrase for decrypting the unlock key
|
||||||
|
var passphraseStr string
|
||||||
|
if envPassphrase := os.Getenv(EnvUnlockPassphrase); envPassphrase != "" {
|
||||||
|
Debug("Using passphrase from environment variable", "key_id", p.GetID())
|
||||||
|
passphraseStr = envPassphrase
|
||||||
|
} else {
|
||||||
|
Debug("Prompting for passphrase", "key_id", p.GetID())
|
||||||
|
// Prompt for passphrase
|
||||||
|
passphraseStr, err = readPassphrase("Enter passphrase to unlock vault: ")
|
||||||
|
if err != nil {
|
||||||
|
Debug("Failed to read passphrase", "error", err, "key_id", p.GetID())
|
||||||
|
return nil, fmt.Errorf("failed to read passphrase: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug("Decrypting unlock key private key with passphrase", "key_id", p.GetID())
|
||||||
|
|
||||||
|
// Decrypt the unlock key private key with passphrase
|
||||||
|
privKeyData, err := decryptWithPassphrase(encryptedPrivKeyData, passphraseStr)
|
||||||
|
if err != nil {
|
||||||
|
Debug("Failed to decrypt unlock key private key", "error", err, "key_id", p.GetID())
|
||||||
|
return nil, fmt.Errorf("failed to decrypt unlock key private key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
DebugWith("Successfully decrypted unlock key private key",
|
||||||
|
slog.String("key_id", p.GetID()),
|
||||||
|
slog.Int("decrypted_length", len(privKeyData)),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Parse the decrypted private key
|
||||||
|
Debug("Parsing decrypted unlock key identity", "key_id", p.GetID())
|
||||||
|
identity, err := age.ParseX25519Identity(string(privKeyData))
|
||||||
|
if err != nil {
|
||||||
|
Debug("Failed to parse unlock key private key", "error", err, "key_id", p.GetID())
|
||||||
|
return nil, fmt.Errorf("failed to parse unlock key private key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
DebugWith("Successfully parsed passphrase unlock key identity",
|
||||||
|
slog.String("key_id", p.GetID()),
|
||||||
|
slog.String("public_key", identity.Recipient().String()),
|
||||||
|
)
|
||||||
|
|
||||||
|
return identity, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetType implements UnlockKey interface
|
||||||
|
func (p *PassphraseUnlockKey) GetType() string {
|
||||||
|
return "passphrase"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMetadata implements UnlockKey interface
|
||||||
|
func (p *PassphraseUnlockKey) GetMetadata() UnlockKeyMetadata {
|
||||||
|
return p.Metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDirectory implements UnlockKey interface
|
||||||
|
func (p *PassphraseUnlockKey) GetDirectory() string {
|
||||||
|
return p.Directory
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetID implements UnlockKey interface
|
||||||
|
func (p *PassphraseUnlockKey) GetID() string {
|
||||||
|
return p.Metadata.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID implements UnlockKey interface - generates ID from creation timestamp
|
||||||
|
func (p *PassphraseUnlockKey) ID() string {
|
||||||
|
// Generate ID using creation timestamp: YYYY-MM-DD.HH.mm-passphrase
|
||||||
|
createdAt := p.Metadata.CreatedAt
|
||||||
|
return fmt.Sprintf("%s-passphrase", createdAt.Format("2006-01-02.15.04"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove implements UnlockKey interface - removes the passphrase unlock key
|
||||||
|
func (p *PassphraseUnlockKey) Remove() error {
|
||||||
|
// For passphrase 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 passphrase unlock key directory: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPassphraseUnlockKey creates a new PassphraseUnlockKey instance
|
||||||
|
func NewPassphraseUnlockKey(fs afero.Fs, directory string, metadata UnlockKeyMetadata) *PassphraseUnlockKey {
|
||||||
|
return &PassphraseUnlockKey{
|
||||||
|
Directory: directory,
|
||||||
|
Metadata: metadata,
|
||||||
|
fs: fs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatePassphraseKey creates a new passphrase-protected unlock key
|
||||||
|
func CreatePassphraseKey(fs afero.Fs, stateDir string, passphrase string) (*PassphraseUnlockKey, error) {
|
||||||
|
// Get current vault
|
||||||
|
currentVault, err := GetCurrentVault(fs, stateDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get current vault: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentVault.CreatePassphraseKey(passphrase)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecryptLongTermKey decrypts and returns the long-term private key for this vault
|
||||||
|
func (p *PassphraseUnlockKey) DecryptLongTermKey() ([]byte, error) {
|
||||||
|
DebugWith("Decrypting long-term key with passphrase unlock key",
|
||||||
|
slog.String("key_id", p.GetID()),
|
||||||
|
slog.String("key_type", p.GetType()),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get our unlock key identity
|
||||||
|
unlockIdentity, err := p.GetIdentity()
|
||||||
|
if err != nil {
|
||||||
|
Debug("Failed to get passphrase unlock identity for long-term decryption", "error", err, "key_id", p.GetID())
|
||||||
|
return nil, fmt.Errorf("failed to get unlock identity: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read encrypted long-term private key
|
||||||
|
encryptedLtPrivKeyPath := filepath.Join(p.Directory, "longterm.age")
|
||||||
|
Debug("Reading encrypted long-term private key", "path", encryptedLtPrivKeyPath)
|
||||||
|
|
||||||
|
encryptedLtPrivKey, err := afero.ReadFile(p.fs, encryptedLtPrivKeyPath)
|
||||||
|
if err != nil {
|
||||||
|
Debug("Failed to read encrypted long-term private key", "error", err, "path", encryptedLtPrivKeyPath)
|
||||||
|
return nil, fmt.Errorf("failed to read encrypted long-term private key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
DebugWith("Read encrypted long-term private key",
|
||||||
|
slog.String("key_id", p.GetID()),
|
||||||
|
slog.Int("encrypted_length", len(encryptedLtPrivKey)),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Decrypt long-term private key using our unlock key
|
||||||
|
Debug("Decrypting long-term private key with passphrase unlock key", "key_id", p.GetID())
|
||||||
|
ltPrivKeyData, err := decryptWithIdentity(encryptedLtPrivKey, unlockIdentity)
|
||||||
|
if err != nil {
|
||||||
|
Debug("Failed to decrypt long-term private key", "error", err, "key_id", p.GetID())
|
||||||
|
return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
DebugWith("Successfully decrypted long-term private key",
|
||||||
|
slog.String("key_id", p.GetID()),
|
||||||
|
slog.Int("decrypted_length", len(ltPrivKeyData)),
|
||||||
|
)
|
||||||
|
|
||||||
|
return ltPrivKeyData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
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)),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Decrypt long-term private key using our unlock key
|
||||||
|
ltPrivKeyData, err := p.DecryptLongTermKey()
|
||||||
|
if err != nil {
|
||||||
|
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 {
|
||||||
|
Debug("Failed to decrypt secret with long-term key", "error", err, "secret_name", secret.Name, "key_id", p.GetID())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
DebugWith("Successfully decrypted secret with passphrase unlock key",
|
||||||
|
slog.String("secret_name", secret.Name),
|
||||||
|
slog.String("key_id", p.GetID()),
|
||||||
|
slog.Int("decrypted_length", len(decryptedData)),
|
||||||
|
)
|
||||||
|
|
||||||
|
return decryptedData, nil
|
||||||
|
}
|
1
internal/secret/pgpunlock.go
Normal file
1
internal/secret/pgpunlock.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
22
internal/secret/unlock.go
Normal file
22
internal/secret/unlock.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package secret
|
||||||
|
|
||||||
|
import (
|
||||||
|
"filippo.io/age"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UnlockKey interface defines the methods all unlock key types must implement
|
||||||
|
type UnlockKey interface {
|
||||||
|
GetIdentity() (*age.X25519Identity, error)
|
||||||
|
GetType() string
|
||||||
|
GetMetadata() UnlockKeyMetadata
|
||||||
|
GetDirectory() string
|
||||||
|
GetID() string
|
||||||
|
ID() string // Generate ID from the key's public key
|
||||||
|
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(secret *Secret) ([]byte, error)
|
||||||
|
}
|
1
internal/secret/vault.go
Normal file
1
internal/secret/vault.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
Loading…
Reference in New Issue
Block a user