When building command-line applications in Go, there often comes a time when you need to securely collect sensitive information from users, such as passwords. In this post, we’ll explore how to implement secure password input in a Go CLI application using best practices.

The Challenge

Consider a typical CLI initialization command where you need to collect an admin password:

func NewInitCommand(source *workloadapi.X509Source) *cobra.Command {
    var initCmd = &cobra.Command{
        Use:   "init",
        Short: "Initialize spike configuration",
        Run: func(cmd *cobra.Command, args []string) {
            // How do we securely get the password?
        },
    }
    return initCmd
}

Simply using fmt.Scanln() or similar methods would display the password in plain text as the user types - a significant security risk. We need a better solution.

The Solution: Using term.ReadPassword

Go’s golang.org/x/term package provides a secure way to read passwords from the terminal. Here’s how to implement it:

  1. First, install the required dependency:
go get golang.org/x/term
  1. Import the necessary packages:
import (
    "golang.org/x/term"
    "syscall"
    "fmt"
)
  1. Implement the password collection:
func NewInitCommand(source *workloadapi.X509Source) *cobra.Command {
    var initCmd = &cobra.Command{
        Use:   "init",
        Short: "Initialize spike configuration",
        Run: func(cmd *cobra.Command, args []string) {
            // Check initialization state
            state, err := net.CheckInitState(source)
            if err != nil {
                fmt.Println("Failed to check init state:")
                fmt.Println(err.Error())
                return
            }

            if state == data.AlreadyInitialized {
                fmt.Println("SPIKE is already initialized.")
                fmt.Println("Nothing to do.")
                return
            }

            // Collect password securely
            fmt.Print("Enter admin password: ")
            password, err := term.ReadPassword(int(syscall.Stdin))
            if err != nil {
                fmt.Println("\nFailed to read password:")
                fmt.Println(err.Error())
                return
            }
            fmt.Println() // Add newline after password input

            // Convert to string if needed
            passwordStr := string(password)

            // Use the password...

            fmt.Println("")
            fmt.Println("    SPIKE system initialization completed.")
            fmt.Println("      Generated admin token and saved it to")
            fmt.Println("        ./.spike-admin-token for future use.")
            fmt.Println("")
        },
    }
    return initCmd
}

Why This Approach Works

This implementation offers several security benefits:

  1. Hidden Input: The password isn’t displayed on screen as the user types
  2. Memory Security: Password is initially stored as a byte slice ([]byte), which can be securely wiped from memory
  3. Clean User Experience: The interface is professional and familiar to users
  4. Error Handling: Robust error handling for input failures
  5. Cross-Platform: Works across different operating systems

Best Practices

When implementing password input in your CLI applications:

  1. Clear Memory: If possible, clear the password from memory after use
  2. Validate Input: Add password strength requirements if needed
  3. Confirmation: For new passwords, consider asking users to type twice
  4. Clear Prompts: Use clear prompts to indicate what’s expected
  5. Error Messages: Provide helpful error messages without revealing sensitive information

Example: Adding Password Confirmation

For cases where users are setting a new password, here’s how to add confirmation:

func getPasswordWithConfirmation() (string, error) {
    fmt.Print("Enter new password: ")
    password1, err := term.ReadPassword(int(syscall.Stdin))
    if err != nil {
        return "", err
    }
    fmt.Println()

    fmt.Print("Confirm password: ")
    password2, err := term.ReadPassword(int(syscall.Stdin))
    if err != nil {
        return "", err
    }
    fmt.Println()

    if string(password1) != string(password2) {
        return "", fmt.Errorf("passwords do not match")
    }

    return string(password1), nil
}

Conclusion

Implementing secure password input in Go CLI applications is straightforward with the right tools. The golang.org/x/term package provides a robust solution that works across platforms and follows security best practices. By following these patterns, you can ensure your CLI applications handle sensitive information securely and professionally.

Remember to always consider security implications when handling passwords and other sensitive data, and make sure to follow your organization’s security requirements and best practices.