man what a clusterfuck
This commit is contained in:
@@ -134,7 +134,6 @@ func newRootCmd() *cobra.Command {
|
||||
cmd.AddCommand(newKeysCmd())
|
||||
cmd.AddCommand(newKeyCmd())
|
||||
cmd.AddCommand(newImportCmd())
|
||||
cmd.AddCommand(newEnrollCmd())
|
||||
cmd.AddCommand(newEncryptCmd())
|
||||
cmd.AddCommand(newDecryptCmd())
|
||||
|
||||
@@ -431,19 +430,6 @@ func newImportCmd() *cobra.Command {
|
||||
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 {
|
||||
cmd := &cobra.Command{
|
||||
Use: "encrypt <secret-name>",
|
||||
@@ -1073,7 +1059,16 @@ func (cli *CLIInstance) KeysAdd(keyType string, cmd *cobra.Command) error {
|
||||
return nil
|
||||
|
||||
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":
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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
|
||||
func (cli *CLIInstance) Encrypt(secretName, inputFile, outputFile string) error {
|
||||
// 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 @@
|
||||
|
||||
Reference in New Issue
Block a user