fix: Prevent hanging in non-interactive environments - Add terminal detection to readPassphrase, readSecurePassphrase, and readLineFromStdin - Return clear error messages when stderr is not a terminal instead of hanging - Improves automation and CI/CD reliability
This commit is contained in:
parent
f838c8cb98
commit
bbaf1cbd97
@ -95,7 +95,12 @@ func getStdinScanner() *bufio.Scanner {
|
|||||||
// readLineFromStdin reads a single line from stdin with a prompt
|
// readLineFromStdin reads a single line from stdin with a prompt
|
||||||
// Uses a shared scanner to avoid buffering issues between multiple calls
|
// Uses a shared scanner to avoid buffering issues between multiple calls
|
||||||
func readLineFromStdin(prompt string) (string, error) {
|
func readLineFromStdin(prompt string) (string, error) {
|
||||||
fmt.Print(prompt)
|
// 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 input: stderr is not a terminal (running in non-interactive mode)")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprint(os.Stderr, prompt) // Write prompt to stderr, not stdout
|
||||||
scanner := getStdinScanner()
|
scanner := getStdinScanner()
|
||||||
if !scanner.Scan() {
|
if !scanner.Scan() {
|
||||||
if err := scanner.Err(); err != nil {
|
if err := scanner.Err(); err != nil {
|
||||||
@ -108,6 +113,7 @@ func readLineFromStdin(prompt string) (string, error) {
|
|||||||
|
|
||||||
// CLIEntry is the entry point for the secret CLI application
|
// CLIEntry is the entry point for the secret CLI application
|
||||||
func CLIEntry() {
|
func CLIEntry() {
|
||||||
|
Debug("CLIEntry starting - debug output is working")
|
||||||
cmd := newRootCmd()
|
cmd := newRootCmd()
|
||||||
if err := cmd.Execute(); err != nil {
|
if err := cmd.Execute(); err != nil {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@ -115,6 +121,7 @@ func CLIEntry() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newRootCmd() *cobra.Command {
|
func newRootCmd() *cobra.Command {
|
||||||
|
Debug("newRootCmd starting")
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "secret",
|
Use: "secret",
|
||||||
Short: "A simple secrets manager",
|
Short: "A simple secrets manager",
|
||||||
@ -124,6 +131,7 @@ func newRootCmd() *cobra.Command {
|
|||||||
SilenceErrors: false,
|
SilenceErrors: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Debug("Adding subcommands to root command")
|
||||||
// Add subcommands
|
// Add subcommands
|
||||||
cmd.AddCommand(newInitCmd())
|
cmd.AddCommand(newInitCmd())
|
||||||
cmd.AddCommand(newGenerateCmd())
|
cmd.AddCommand(newGenerateCmd())
|
||||||
@ -137,6 +145,7 @@ func newRootCmd() *cobra.Command {
|
|||||||
cmd.AddCommand(newEncryptCmd())
|
cmd.AddCommand(newEncryptCmd())
|
||||||
cmd.AddCommand(newDecryptCmd())
|
cmd.AddCommand(newDecryptCmd())
|
||||||
|
|
||||||
|
Debug("newRootCmd completed")
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -280,9 +289,12 @@ func newAddCmd() *cobra.Command {
|
|||||||
Long: `Add a secret to the current vault. The secret value is read from stdin.`,
|
Long: `Add a secret to the current vault. The secret value is read from stdin.`,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
Debug("Add command RunE starting", "secret_name", args[0])
|
||||||
force, _ := cmd.Flags().GetBool("force")
|
force, _ := cmd.Flags().GetBool("force")
|
||||||
|
Debug("Got force flag", "force", force)
|
||||||
|
|
||||||
cli := NewCLIInstance()
|
cli := NewCLIInstance()
|
||||||
|
Debug("Created CLI instance, calling AddSecret")
|
||||||
return cli.AddSecret(args[0], force)
|
return cli.AddSecret(args[0], force)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -528,7 +540,7 @@ func (cli *CLIInstance) Init(cmd *cobra.Command) error {
|
|||||||
|
|
||||||
// Create default vault
|
// Create default vault
|
||||||
Debug("Creating default vault")
|
Debug("Creating default vault")
|
||||||
_, err = CreateVault(cli.fs, cli.stateDir, "default")
|
vault, err := CreateVault(cli.fs, cli.stateDir, "default")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Debug("Failed to create default vault", "error", err)
|
Debug("Failed to create default vault", "error", err)
|
||||||
return fmt.Errorf("failed to create default vault: %w", err)
|
return fmt.Errorf("failed to create default vault: %w", err)
|
||||||
@ -550,6 +562,9 @@ func (cli *CLIInstance) Init(cmd *cobra.Command) error {
|
|||||||
return fmt.Errorf("failed to write long-term public key: %w", err)
|
return fmt.Errorf("failed to write long-term public key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unlock the vault with the derived long-term key
|
||||||
|
vault.Unlock(ltIdentity)
|
||||||
|
|
||||||
// Prompt for passphrase for unlock key
|
// Prompt for passphrase for unlock key
|
||||||
var passphraseStr string
|
var passphraseStr string
|
||||||
if envPassphrase := os.Getenv(EnvUnlockPassphrase); envPassphrase != "" {
|
if envPassphrase := os.Getenv(EnvUnlockPassphrase); envPassphrase != "" {
|
||||||
@ -567,7 +582,7 @@ func (cli *CLIInstance) Init(cmd *cobra.Command) error {
|
|||||||
|
|
||||||
// Create passphrase-protected unlock key
|
// Create passphrase-protected unlock key
|
||||||
Debug("Creating passphrase-protected unlock key")
|
Debug("Creating passphrase-protected unlock key")
|
||||||
passphraseKey, err := CreatePassphraseKey(cli.fs, cli.stateDir, passphraseStr)
|
passphraseKey, err := vault.CreatePassphraseKey(passphraseStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Debug("Failed to create unlock key", "error", err)
|
Debug("Failed to create unlock key", "error", err)
|
||||||
return fmt.Errorf("failed to create unlock key: %w", err)
|
return fmt.Errorf("failed to create unlock key: %w", err)
|
||||||
@ -749,24 +764,41 @@ func (cli *CLIInstance) VaultSelect(name string) error {
|
|||||||
|
|
||||||
// AddSecret adds a secret to the vault
|
// AddSecret adds a secret to the vault
|
||||||
func (cli *CLIInstance) AddSecret(secretName string, force bool) error {
|
func (cli *CLIInstance) AddSecret(secretName string, force bool) error {
|
||||||
|
Debug("CLI AddSecret starting", "secret_name", secretName, "force", force)
|
||||||
|
|
||||||
// Get current vault
|
// Get current vault
|
||||||
|
Debug("Getting current vault")
|
||||||
vault, err := GetCurrentVault(cli.fs, cli.stateDir)
|
vault, err := GetCurrentVault(cli.fs, cli.stateDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
Debug("Failed to get current vault", "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
Debug("Got current vault", "vault_name", vault.Name)
|
||||||
|
|
||||||
// Read secret value from stdin
|
// Read secret value from stdin
|
||||||
|
Debug("Reading secret value from stdin")
|
||||||
value, err := io.ReadAll(os.Stdin)
|
value, err := io.ReadAll(os.Stdin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
Debug("Failed to read secret from stdin", "error", err)
|
||||||
return fmt.Errorf("failed to read secret from stdin: %w", err)
|
return fmt.Errorf("failed to read secret from stdin: %w", err)
|
||||||
}
|
}
|
||||||
|
Debug("Read secret value from stdin", "value_length", len(value))
|
||||||
|
|
||||||
// Remove trailing newline if present
|
// Remove trailing newline if present
|
||||||
if len(value) > 0 && value[len(value)-1] == '\n' {
|
if len(value) > 0 && value[len(value)-1] == '\n' {
|
||||||
value = value[:len(value)-1]
|
value = value[:len(value)-1]
|
||||||
|
Debug("Removed trailing newline", "new_length", len(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
return vault.AddSecret(secretName, value, force)
|
Debug("Calling vault.AddSecret", "secret_name", secretName, "value_length", len(value), "force", force)
|
||||||
|
err = vault.AddSecret(secretName, value, force)
|
||||||
|
if err != nil {
|
||||||
|
Debug("vault.AddSecret failed", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
Debug("vault.AddSecret completed successfully")
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSecret retrieves a secret from the vault
|
// GetSecret retrieves a secret from the vault
|
||||||
@ -777,27 +809,9 @@ func (cli *CLIInstance) GetSecret(secretName string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the secret object
|
// Get the secret value using the vault's GetSecret method
|
||||||
secret, err := vault.GetSecretObject(secretName)
|
// This handles the per-secret key architecture internally
|
||||||
if err != nil {
|
value, err := vault.GetSecret(secretName)
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the value using the current unlock key (or mnemonic if available)
|
|
||||||
var value []byte
|
|
||||||
if os.Getenv(EnvMnemonic) != "" {
|
|
||||||
// If mnemonic is available, GetValue can handle it without an unlock key
|
|
||||||
value, err = secret.GetValue(nil)
|
|
||||||
} else {
|
|
||||||
// Get the current unlock key
|
|
||||||
unlockKey, unlockErr := vault.GetCurrentUnlockKey()
|
|
||||||
if unlockErr != nil {
|
|
||||||
return fmt.Errorf("failed to get current unlock key: %w", unlockErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
value, err = secret.GetValue(unlockKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -1037,20 +1051,33 @@ func (cli *CLIInstance) KeysList(jsonOutput bool) error {
|
|||||||
func (cli *CLIInstance) KeysAdd(keyType string, cmd *cobra.Command) error {
|
func (cli *CLIInstance) KeysAdd(keyType string, cmd *cobra.Command) error {
|
||||||
switch keyType {
|
switch keyType {
|
||||||
case "passphrase":
|
case "passphrase":
|
||||||
|
// Get current vault
|
||||||
|
vault, err := GetCurrentVault(cli.fs, cli.stateDir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get current vault: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to unlock the vault if not already unlocked
|
||||||
|
if vault.Locked() {
|
||||||
|
_, err := vault.UnlockVault()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to unlock vault: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check if passphrase is set in environment variable
|
// Check if passphrase is set in environment variable
|
||||||
var passphraseStr string
|
var passphraseStr string
|
||||||
if envPassphrase := os.Getenv(EnvUnlockPassphrase); envPassphrase != "" {
|
if envPassphrase := os.Getenv(EnvUnlockPassphrase); envPassphrase != "" {
|
||||||
passphraseStr = envPassphrase
|
passphraseStr = envPassphrase
|
||||||
} else {
|
} else {
|
||||||
// Use secure passphrase input with confirmation
|
// Use secure passphrase input with confirmation
|
||||||
var err error
|
|
||||||
passphraseStr, err = readSecurePassphrase("Enter passphrase for unlock key: ")
|
passphraseStr, err = readSecurePassphrase("Enter passphrase for unlock key: ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to read passphrase: %w", err)
|
return fmt.Errorf("failed to read passphrase: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
passphraseKey, err := CreatePassphraseKey(cli.fs, cli.stateDir, passphraseStr)
|
passphraseKey, err := vault.CreatePassphraseKey(passphraseStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -1374,6 +1401,11 @@ func isValidAgeSecretKey(key string) bool {
|
|||||||
// readSecurePassphrase reads a passphrase securely from the terminal without echoing
|
// readSecurePassphrase reads a passphrase securely from the terminal without echoing
|
||||||
// and prompts for confirmation. Falls back to regular input when not on a terminal.
|
// and prompts for confirmation. Falls back to regular input when not on a terminal.
|
||||||
func readSecurePassphrase(prompt string) (string, error) {
|
func readSecurePassphrase(prompt string) (string, error) {
|
||||||
|
// 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)")
|
||||||
|
}
|
||||||
|
|
||||||
// Check if stdin is a terminal
|
// Check if stdin is a terminal
|
||||||
if !term.IsTerminal(int(syscall.Stdin)) {
|
if !term.IsTerminal(int(syscall.Stdin)) {
|
||||||
// Not a terminal (piped input, testing, etc.) - use shared line reader
|
// Not a terminal (piped input, testing, etc.) - use shared line reader
|
||||||
@ -1390,22 +1422,22 @@ func readSecurePassphrase(prompt string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Terminal input - use secure password reading with confirmation
|
// Terminal input - use secure password reading with confirmation
|
||||||
fmt.Print(prompt)
|
fmt.Fprint(os.Stderr, prompt) // Write prompt to stderr, not stdout
|
||||||
|
|
||||||
// Read first passphrase
|
// Read first passphrase
|
||||||
passphrase1, err := term.ReadPassword(int(syscall.Stdin))
|
passphrase1, err := term.ReadPassword(int(syscall.Stdin))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to read passphrase: %w", err)
|
return "", fmt.Errorf("failed to read passphrase: %w", err)
|
||||||
}
|
}
|
||||||
fmt.Println() // Print newline since ReadPassword doesn't echo
|
fmt.Fprintln(os.Stderr) // Print newline to stderr since ReadPassword doesn't echo
|
||||||
|
|
||||||
// Read confirmation passphrase
|
// Read confirmation passphrase
|
||||||
fmt.Print("Confirm passphrase: ")
|
fmt.Fprint(os.Stderr, "Confirm passphrase: ") // Write prompt to stderr, not stdout
|
||||||
passphrase2, err := term.ReadPassword(int(syscall.Stdin))
|
passphrase2, err := term.ReadPassword(int(syscall.Stdin))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to read passphrase confirmation: %w", err)
|
return "", fmt.Errorf("failed to read passphrase confirmation: %w", err)
|
||||||
}
|
}
|
||||||
fmt.Println() // Print newline since ReadPassword doesn't echo
|
fmt.Fprintln(os.Stderr) // Print newline to stderr since ReadPassword doesn't echo
|
||||||
|
|
||||||
// Compare passphrases
|
// Compare passphrases
|
||||||
if string(passphrase1) != string(passphrase2) {
|
if string(passphrase1) != string(passphrase2) {
|
||||||
@ -1444,6 +1476,10 @@ func (cli *CLIInstance) importMnemonic(vaultName, mnemonic string) error {
|
|||||||
return fmt.Errorf("failed to write long-term public key: %w", err)
|
return fmt.Errorf("failed to write long-term public key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the vault instance and unlock it
|
||||||
|
vault := NewVault(cli.fs, vaultName, cli.stateDir)
|
||||||
|
vault.Unlock(ltIdentity)
|
||||||
|
|
||||||
// Get or create passphrase for unlock key
|
// Get or create passphrase for unlock key
|
||||||
var passphraseStr string
|
var passphraseStr string
|
||||||
if envPassphrase := os.Getenv(EnvUnlockPassphrase); envPassphrase != "" {
|
if envPassphrase := os.Getenv(EnvUnlockPassphrase); envPassphrase != "" {
|
||||||
@ -1456,38 +1492,12 @@ func (cli *CLIInstance) importMnemonic(vaultName, mnemonic string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create passphrase-protected unlock key
|
// Create passphrase-protected unlock key (vault is now unlocked)
|
||||||
passphraseKey, err := CreatePassphraseKey(cli.fs, cli.stateDir, passphraseStr)
|
passphraseKey, err := vault.CreatePassphraseKey(passphraseStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create unlock key: %w", err)
|
return fmt.Errorf("failed to create unlock key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encrypt long-term private key to the unlock key
|
|
||||||
unlockKeyDir := passphraseKey.GetDirectory()
|
|
||||||
|
|
||||||
// Read unlock key public key
|
|
||||||
unlockPubKeyData, err := afero.ReadFile(cli.fs, filepath.Join(unlockKeyDir, "pub.age"))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to read unlock key public key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
unlockRecipient, err := age.ParseX25519Recipient(string(unlockPubKeyData))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to parse unlock key public key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encrypt long-term private key to unlock key
|
|
||||||
ltPrivKeyData := []byte(ltIdentity.String())
|
|
||||||
encryptedLtPrivKey, err := encryptToRecipient(ltPrivKeyData, unlockRecipient)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to encrypt long-term private key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write encrypted long-term private key
|
|
||||||
if err := afero.WriteFile(cli.fs, filepath.Join(unlockKeyDir, "longterm.age"), encryptedLtPrivKey, 0600); err != nil {
|
|
||||||
return fmt.Errorf("failed to write encrypted long-term private key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Successfully imported mnemonic into vault '%s'\n", vaultName)
|
fmt.Printf("Successfully imported mnemonic into vault '%s'\n", vaultName)
|
||||||
fmt.Printf("Long-term public key: %s\n", ltPubKey)
|
fmt.Printf("Long-term public key: %s\n", ltPubKey)
|
||||||
fmt.Printf("Unlock key ID: %s\n", passphraseKey.GetMetadata().ID)
|
fmt.Printf("Unlock key ID: %s\n", passphraseKey.GetMetadata().ID)
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"os"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"filippo.io/age"
|
"filippo.io/age"
|
||||||
@ -12,21 +13,34 @@ import (
|
|||||||
|
|
||||||
// encryptToRecipient encrypts data to a recipient using age
|
// encryptToRecipient encrypts data to a recipient using age
|
||||||
func encryptToRecipient(data []byte, recipient age.Recipient) ([]byte, error) {
|
func encryptToRecipient(data []byte, recipient age.Recipient) ([]byte, error) {
|
||||||
|
Debug("encryptToRecipient starting", "data_length", len(data))
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
|
Debug("Creating age encryptor")
|
||||||
w, err := age.Encrypt(&buf, recipient)
|
w, err := age.Encrypt(&buf, recipient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
Debug("Failed to create encryptor", "error", err)
|
||||||
return nil, fmt.Errorf("failed to create encryptor: %w", err)
|
return nil, fmt.Errorf("failed to create encryptor: %w", err)
|
||||||
}
|
}
|
||||||
|
Debug("Created age encryptor successfully")
|
||||||
|
|
||||||
|
Debug("Writing data to encryptor")
|
||||||
if _, err := w.Write(data); err != nil {
|
if _, err := w.Write(data); err != nil {
|
||||||
|
Debug("Failed to write data to encryptor", "error", err)
|
||||||
return nil, fmt.Errorf("failed to write data: %w", err)
|
return nil, fmt.Errorf("failed to write data: %w", err)
|
||||||
}
|
}
|
||||||
|
Debug("Wrote data to encryptor successfully")
|
||||||
|
|
||||||
|
Debug("Closing encryptor")
|
||||||
if err := w.Close(); err != nil {
|
if err := w.Close(); err != nil {
|
||||||
|
Debug("Failed to close encryptor", "error", err)
|
||||||
return nil, fmt.Errorf("failed to close encryptor: %w", err)
|
return nil, fmt.Errorf("failed to close encryptor: %w", err)
|
||||||
}
|
}
|
||||||
|
Debug("Closed encryptor successfully")
|
||||||
|
|
||||||
return buf.Bytes(), nil
|
result := buf.Bytes()
|
||||||
|
Debug("encryptToRecipient completed successfully", "result_length", len(result))
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// decryptWithIdentity decrypts data with an identity using age
|
// decryptWithIdentity decrypts data with an identity using age
|
||||||
@ -67,25 +81,24 @@ func decryptWithPassphrase(encryptedData []byte, passphrase string) ([]byte, err
|
|||||||
// 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
|
// This version is for unlocking and doesn't require confirmation
|
||||||
func readPassphrase(prompt string) (string, error) {
|
func readPassphrase(prompt string) (string, error) {
|
||||||
|
// 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)")
|
||||||
|
}
|
||||||
|
|
||||||
// Check if stdin is a terminal
|
// Check if stdin is a terminal
|
||||||
if !term.IsTerminal(int(syscall.Stdin)) {
|
if !term.IsTerminal(int(syscall.Stdin)) {
|
||||||
// Not a terminal - fall back to regular input
|
// Not a terminal - use shared line reader to avoid buffering conflicts
|
||||||
fmt.Print(prompt)
|
return readLineFromStdin(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
|
// Terminal input - use secure password reading
|
||||||
fmt.Print(prompt)
|
fmt.Fprint(os.Stderr, prompt) // Write prompt to stderr, not stdout
|
||||||
passphrase, err := term.ReadPassword(int(syscall.Stdin))
|
passphrase, err := term.ReadPassword(int(syscall.Stdin))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to read passphrase: %w", err)
|
return "", fmt.Errorf("failed to read passphrase: %w", err)
|
||||||
}
|
}
|
||||||
fmt.Println() // Print newline since ReadPassword doesn't echo
|
fmt.Fprintln(os.Stderr) // Print newline to stderr since ReadPassword doesn't echo
|
||||||
|
|
||||||
if len(passphrase) == 0 {
|
if len(passphrase) == 0 {
|
||||||
return "", fmt.Errorf("passphrase cannot be empty")
|
return "", fmt.Errorf("passphrase cannot be empty")
|
||||||
|
Loading…
Reference in New Issue
Block a user