diff --git a/internal/cli/init.go b/internal/cli/init.go index 291a1cb..a84fb6d 100644 --- a/internal/cli/init.go +++ b/internal/cli/init.go @@ -6,7 +6,6 @@ import ( "os" "path/filepath" "strings" - "syscall" "filippo.io/age" "git.eeqj.de/sneak/secret/internal/secret" @@ -15,7 +14,6 @@ import ( "github.com/spf13/afero" "github.com/spf13/cobra" "github.com/tyler-smith/go-bip39" - "golang.org/x/term" ) func newInitCmd() *cobra.Command { @@ -173,45 +171,24 @@ func (cli *CLIInstance) Init(cmd *cobra.Command) error { } // readSecurePassphrase reads a passphrase securely from the terminal without echoing -// and prompts for confirmation. Falls back to regular input when not on a terminal. +// This version adds confirmation (read twice) for creating new unlock keys func readSecurePassphrase(prompt string) (string, error) { - // Check if stdin is a terminal - if !term.IsTerminal(int(syscall.Stdin)) { - // Not a terminal - never read passphrases from piped input for security reasons - return "", fmt.Errorf("cannot read passphrase from non-terminal stdin (piped input or script). Please set the SB_UNLOCK_PASSPHRASE environment variable or run interactively") - } - - // Check if stderr is a terminal - if not, we can't prompt interactively - if !term.IsTerminal(int(syscall.Stderr)) { - return "", fmt.Errorf("cannot prompt for passphrase: stderr is not a terminal (running in non-interactive mode). Please set the SB_UNLOCK_PASSPHRASE environment variable") - } - - // Terminal input - use secure password reading with confirmation - fmt.Fprint(os.Stderr, prompt) // Write prompt to stderr, not stdout - - // Read first passphrase - passphrase1, err := term.ReadPassword(int(syscall.Stdin)) + // Get the first passphrase + passphrase1, err := secret.ReadPassphrase(prompt) if err != nil { - return "", fmt.Errorf("failed to read passphrase: %w", err) + return "", err } - fmt.Fprintln(os.Stderr) // Print newline to stderr since ReadPassword doesn't echo // Read confirmation passphrase - fmt.Fprint(os.Stderr, "Confirm passphrase: ") // Write prompt to stderr, not stdout - passphrase2, err := term.ReadPassword(int(syscall.Stdin)) + passphrase2, err := secret.ReadPassphrase("Confirm passphrase: ") if err != nil { return "", fmt.Errorf("failed to read passphrase confirmation: %w", err) } - fmt.Fprintln(os.Stderr) // Print newline to stderr since ReadPassword doesn't echo // Compare passphrases - if string(passphrase1) != string(passphrase2) { + if passphrase1 != passphrase2 { return "", fmt.Errorf("passphrases do not match") } - if len(passphrase1) == 0 { - return "", fmt.Errorf("passphrase cannot be empty") - } - - return string(passphrase1), nil + return passphrase1, nil } diff --git a/internal/cli/keys.go b/internal/cli/keys.go index d33baec..59c5207 100644 --- a/internal/cli/keys.go +++ b/internal/cli/keys.go @@ -14,6 +14,10 @@ import ( "github.com/spf13/cobra" ) +// Import from init.go + +// ... existing imports ... + func newKeysCmd() *cobra.Command { cmd := &cobra.Command{ Use: "keys", diff --git a/internal/secret/crypto.go b/internal/secret/crypto.go index 8705da5..c3c010d 100644 --- a/internal/secret/crypto.go +++ b/internal/secret/crypto.go @@ -13,7 +13,7 @@ import ( // EncryptToRecipient encrypts data to a recipient using age func EncryptToRecipient(data []byte, recipient age.Recipient) ([]byte, error) { - Debug("encryptToRecipient starting", "data_length", len(data)) + Debug("EncryptToRecipient starting", "data_length", len(data)) var buf bytes.Buffer Debug("Creating age encryptor") @@ -39,22 +39,12 @@ func EncryptToRecipient(data []byte, recipient age.Recipient) ([]byte, error) { Debug("Closed encryptor successfully") result := buf.Bytes() - Debug("encryptToRecipient completed successfully", "result_length", len(result)) + Debug("EncryptToRecipient completed successfully", "result_length", len(result)) return result, nil } -// encryptToRecipient encrypts data to a recipient using age (internal version) -func encryptToRecipient(data []byte, recipient age.Recipient) ([]byte, error) { - return EncryptToRecipient(data, recipient) -} - -// DecryptWithIdentity decrypts data with an identity using age (public version) +// DecryptWithIdentity decrypts data with an identity using age func DecryptWithIdentity(data []byte, identity age.Identity) ([]byte, error) { - return decryptWithIdentity(data, identity) -} - -// 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) @@ -68,34 +58,29 @@ func decryptWithIdentity(data []byte, identity age.Identity) ([]byte, error) { return result, nil } -// EncryptWithPassphrase encrypts data using a passphrase with age's scrypt-based encryption (public version) +// EncryptWithPassphrase encrypts data using a passphrase with age's scrypt-based encryption func EncryptWithPassphrase(data []byte, passphrase string) ([]byte, error) { - return encryptWithPassphrase(data, passphrase) -} - -// 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) + return EncryptToRecipient(data, recipient) } -// decryptWithPassphrase decrypts data using a passphrase with age's scrypt-based decryption -func decryptWithPassphrase(encryptedData []byte, passphrase string) ([]byte, error) { +// 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) + return DecryptWithIdentity(encryptedData, identity) } -// readPassphrase reads a passphrase securely from the terminal without echoing +// 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) { +func ReadPassphrase(prompt string) (string, error) { // Check if stdin is a terminal if !term.IsTerminal(int(syscall.Stdin)) { // Not a terminal - never read passphrases from piped input for security reasons diff --git a/internal/secret/keychainunlock.go b/internal/secret/keychainunlock.go index 5b8833e..c607160 100644 --- a/internal/secret/keychainunlock.go +++ b/internal/secret/keychainunlock.go @@ -91,7 +91,7 @@ func (k *KeychainUnlockKey) GetIdentity() (*age.X25519Identity, error) { // 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) + 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) @@ -265,7 +265,7 @@ func CreateKeychainUnlockKey(fs afero.Fs, stateDir string) (*KeychainUnlockKey, // Step 4: Encrypt age private key with the generated passphrase and store on disk agePrivateKeyBytes := []byte(ageIdentity.String()) - encryptedAgePrivKey, err := encryptWithPassphrase(agePrivateKeyBytes, agePrivKeyPassphrase) + encryptedAgePrivKey, err := EncryptWithPassphrase(agePrivateKeyBytes, agePrivKeyPassphrase) if err != nil { return nil, fmt.Errorf("failed to encrypt age private key with passphrase: %w", err) } @@ -328,14 +328,14 @@ func CreateKeychainUnlockKey(fs afero.Fs, stateDir string) (*KeychainUnlockKey, } // Decrypt long-term private key using current unlock key - ltPrivKeyData, err = decryptWithIdentity(encryptedLtPrivKey, currentUnlockIdentity) + 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()) + 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) } diff --git a/internal/secret/passphraseunlock.go b/internal/secret/passphraseunlock.go index a0a3350..0ad3397 100644 --- a/internal/secret/passphraseunlock.go +++ b/internal/secret/passphraseunlock.go @@ -12,9 +12,10 @@ import ( // PassphraseUnlockKey represents a passphrase-protected unlock key type PassphraseUnlockKey struct { - Directory string - Metadata UnlockKeyMetadata - fs afero.Fs + Directory string + Metadata UnlockKeyMetadata + fs afero.Fs + Passphrase string } // GetIdentity implements UnlockKey interface for passphrase-based unlock keys @@ -24,6 +25,28 @@ func (p *PassphraseUnlockKey) GetIdentity() (*age.X25519Identity, error) { slog.String("key_type", p.GetType()), ) + // First check if we already have the passphrase + passphraseStr := p.Passphrase + if passphraseStr == "" { + Debug("No passphrase in memory, checking environment") + // Check environment variable for passphrase + passphraseStr = os.Getenv(EnvUnlockPassphrase) + if passphraseStr == "" { + Debug("No passphrase in environment, prompting user") + // Prompt for passphrase + var err error + passphraseStr, err = ReadPassphrase("Enter unlock passphrase: ") + if err != nil { + Debug("Failed to read passphrase", "error", err, "key_id", p.GetID()) + return nil, fmt.Errorf("failed to read passphrase: %w", err) + } + } else { + Debug("Using passphrase from environment", "key_id", p.GetID()) + } + } else { + Debug("Using in-memory passphrase", "key_id", p.GetID()) + } + // Read encrypted private key of unlock key unlockKeyPrivPath := filepath.Join(p.Directory, "priv.age") Debug("Reading encrypted passphrase unlock key", "path", unlockKeyPrivPath) @@ -39,25 +62,10 @@ func (p *PassphraseUnlockKey) GetIdentity() (*age.X25519Identity, error) { 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) + 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) diff --git a/internal/secret/pgpunlock.go b/internal/secret/pgpunlock.go index 7fbd19c..9856e84 100644 --- a/internal/secret/pgpunlock.go +++ b/internal/secret/pgpunlock.go @@ -250,15 +250,15 @@ func CreatePGPUnlockKey(fs afero.Fs, stateDir string, gpgKeyID string) (*PGPUnlo 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) + // Step 6: 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()) + // Step 7: 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) } @@ -269,7 +269,7 @@ func CreatePGPUnlockKey(fs afero.Fs, stateDir string, gpgKeyID string) (*PGPUnlo 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 + // Step 8: Encrypt age private key to the GPG key ID agePrivateKeyBytes := []byte(ageIdentity.String()) encryptedAgePrivKey, err := gpgEncrypt(agePrivateKeyBytes, gpgKeyID) if err != nil { @@ -281,7 +281,7 @@ func CreatePGPUnlockKey(fs afero.Fs, stateDir string, gpgKeyID string) (*PGPUnlo return nil, fmt.Errorf("failed to write encrypted age private key: %w", err) } - // Step 6: Create and write enhanced metadata + // Step 9: Create and write enhanced metadata // Generate the key ID directly using the GPG key ID keyID := fmt.Sprintf("%s-pgp", gpgKeyID) diff --git a/internal/secret/secret.go b/internal/secret/secret.go index 888637f..8860153 100644 --- a/internal/secret/secret.go +++ b/internal/secret/secret.go @@ -149,9 +149,9 @@ func (s *Secret) GetValue(unlockKey UnlockKey) ([]byte, error) { return nil, fmt.Errorf("failed to read encrypted long-term private key: %w", err) } - // Decrypt the vault's long-term private key using the unlock key - Debug("Decrypting vault's long-term private key with unlock key", "secret_name", s.Name) - ltPrivKeyData, err := decryptWithIdentity(encryptedLtPrivKey, unlockIdentity) + // Decrypt the encrypted long-term private key using the unlock key + Debug("Decrypting long-term private key using unlock key", "secret_name", s.Name) + ltPrivKeyData, err := DecryptWithIdentity(encryptedLtPrivKey, unlockIdentity) if err != nil { Debug("Failed to decrypt long-term private key", "error", err, "secret_name", s.Name) return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err) @@ -198,11 +198,11 @@ func (s *Secret) decryptWithLongTermKey(ltIdentity *age.X25519Identity) ([]byte, ) // Step 2: Decrypt the secret's private key using the vault's long-term private key - Debug("Decrypting secret's private key with vault's long-term key", "secret_name", s.Name) - secretPrivKeyData, err := decryptWithIdentity(encryptedSecretPrivKey, ltIdentity) + Debug("Decrypting secret private key using long-term key", "secret_name", s.Name) + secretPrivKeyData, err := DecryptWithIdentity(encryptedSecretPrivKey, ltIdentity) if err != nil { - Debug("Failed to decrypt secret's private key", "error", err, "secret_name", s.Name) - return nil, fmt.Errorf("failed to decrypt secret's private key: %w", err) + Debug("Failed to decrypt secret private key", "error", err, "secret_name", s.Name) + return nil, fmt.Errorf("failed to decrypt secret private key: %w", err) } // Parse the secret's private key @@ -234,8 +234,8 @@ func (s *Secret) decryptWithLongTermKey(ltIdentity *age.X25519Identity) ([]byte, ) // Step 4: Decrypt the secret's value using the secret's private key - Debug("Decrypting secret value with secret's private key", "secret_name", s.Name) - decryptedValue, err := decryptWithIdentity(encryptedValue, secretIdentity) + Debug("Decrypting value using secret key", "secret_name", s.Name) + decryptedValue, err := DecryptWithIdentity(encryptedValue, secretIdentity) if err != nil { Debug("Failed to decrypt secret value", "error", err, "secret_name", s.Name) return nil, fmt.Errorf("failed to decrypt secret value: %w", err)