Please purchase the course to watch this video.

Full Course
Accepting passwords securely in command-line applications requires careful consideration to protect user data from onlookers and system logs. While web interfaces typically hide password input by default, CLI tools must implement additional measures to prevent passwords from appearing in shell history or on-screen. This can be addressed by using standard input rather than CLI arguments or flags, but to avoid displaying typed characters—even as asterisks—the Go extension package golang.org/x/term
offers the ReadPassword
function, which reads input without echoing it to the terminal. For password verification, the bcrypt hashing algorithm is recommended, as it salts hashes to thwart attacks like rainbow tables, but unlike basic hash functions, bcrypt hashes cannot be directly compared and require specialized comparison functions such as CompareHashAndPassword
. Though bcrypt remains suitable for legacy systems, stronger algorithms like Argon2i or Scrypt are preferred in production. As a practical application, building a CLI tool to check if passwords have been compromised—by securely hashing inputs and cross-referencing with the "Have I Been Pwned" API—highlights the importance of combining secure input handling with robust password hashing techniques.
When it comes to accepting passwords as user inputs, one key UX decision has been to ensure that this input defaults to being hidden. For example, on the web, when you type in a password field, the characters are replaced with asterisks or dots instead of showing the actual password.
This UX pattern of hiding or obscuring sensitive inputs is incredibly useful in the real world:
- Screen sharing: Passwords remain hidden even during presentations
- Public spaces: Prevents shoulder-surfing and visual eavesdropping
- Security recordings: Protects sensitive data in screen captures
However, when it comes to CLI applications and inputting sensitive data in the terminal, obscuring sensitive input requires more work from us as developers.
The Security Problem with Simple Approaches
Let's examine why naive approaches to password input are problematic.
Approach 1: Command Line Arguments (❌ Insecure)
# DON'T DO THIS
go run main.go password123
Problems:
- Visible on screen: Anyone looking can see the password
- Shell history: Password gets stored in command history
- Process lists: Password visible in
ps
command output
# Check shell history - password is exposed!
tail ~/.bash_history
# Output shows: go run main.go password123
Approach 2: Environment Variables (⚠️ Better, but not ideal)
# Slightly better
PASSWORD="mypassword" go run main.go
Problems:
- Still visible during entry
- May appear in shell history
- Visible in process environment
Benefits:
- Not passed as arguments
- Can be set securely in files
- Better for automation
The Correct Approach: Secure Terminal Input
For interactive password entry, we need to use standard input without echo - meaning the characters don't appear on screen as you type them.
Understanding the Goal
When you set a password in Unix systems:
passwd
# Prompts: Enter new password:
# (typing is hidden - no characters appear)
This is what we want to achieve in our applications.
Building Secure Password Input
Let's create a password verification application that securely accepts user input.
Project Setup
mkdir secure-password
cd secure-password
go mod init secure-password
Understanding bcrypt
Our application will verify passwords against bcrypt hashes. bcrypt is a key derivation function (KDF) designed specifically for password hashing.
Why bcrypt over SHA256?
// ❌ Don't do this for passwords
hash := sha256.Sum256([]byte("password"))
// ✅ Use bcrypt for passwords
hash, _ := bcrypt.GenerateFromPassword([]byte("password"), 12)
bcrypt advantages:
- Computational cost: Deliberately slow to prevent brute force
- Built-in salting: Automatic random salt generation
- Adaptive: Can increase cost over time as hardware improves
Password Hashing Recommendations (OWASP)
- Argon2id (preferred)
- scrypt (if Argon2id unavailable)
- bcrypt (for legacy systems, still secure)
For this lesson, we use bcrypt due to excellent Go standard library support.
Step 1: Basic Structure
package main
import (
"fmt"
"log"
"golang.org/x/crypto/bcrypt"
"golang.org/x/term"
)
func main() {
// This is a bcrypt hash of a secret password
// Generated with cost 12
hashedPassword := "$2a$12$example.hash.here"
// Get password securely
password, err := getSecurePassword()
if err != nil {
log.Fatal("Error reading password:", err)
}
// Verify password
if verifyPassword(password, hashedPassword) {
fmt.Println("✅ Password matches!")
} else {
fmt.Println("❌ Invalid password")
}
}
Step 2: Secure Password Input
First, let's install the required package:
go get golang.org/x/term
Now implement secure password reading:
import (
"fmt"
"os"
"syscall"
"golang.org/x/term"
)
func getSecurePassword() ([]byte, error) {
fmt.Print("Enter password: ")
// Read password without echo
password, err := term.ReadPassword(int(os.Stdin.Fd()))
if err != nil {
return nil, err
}
fmt.Println() // Add newline after password input
return password, nil
}
Step 3: Password Verification with bcrypt
go get golang.org/x/crypto/bcrypt
import "golang.org/x/crypto/bcrypt"
func verifyPassword(password []byte, hashedPassword string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), password)
return err == nil
}
func generateHash(password string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(password), 12)
if err != nil {
return "", err
}
return string(hash), nil
}
Understanding bcrypt Salting
Why can't we just generate a hash and compare strings?
// This WON'T work with bcrypt!
hash1, _ := bcrypt.GenerateFromPassword([]byte("password"), 12)
hash2, _ := bcrypt.GenerateFromPassword([]byte("password"), 12)
fmt.Println(string(hash1))
fmt.Println(string(hash2))
// Output: Two DIFFERENT hashes for the same password!
Why different hashes?
- bcrypt includes a random salt in each hash
- Salt prevents rainbow table attacks
- Same password + different salt = different hash
Rainbow Table Attack Prevention:
Without salt:
"password" → SHA256 → always same hash
Attacker can pre-compute hashes for common passwords
With salt:
"password" + random_salt → bcrypt → different hash each time
Attacker must compute hash for each password/salt combination
Complete Implementation
Here's a complete secure password verification application:
package main
import (
"fmt"
"log"
"os"
"golang.org/x/crypto/bcrypt"
"golang.org/x/term"
)
const (
// bcrypt cost - should take ~250ms on modern hardware
bcryptCost = 12
// Example hashed password (bcrypt hash of "secretpassword")
hashedPassword = "$2a$12$LQQb8wPLX8sKDY6bTLWKOO8O3M.NKzZczPT8.UE8XYQk9YPzHQQZ6"
)
func main() {
log.SetFlags(0) // Remove timestamp from logs
password, err := getSecurePassword()
if err != nil {
log.Fatal("Error reading password:", err)
}
if verifyPassword(password, hashedPassword) {
fmt.Println("✅ Access granted!")
} else {
fmt.Println("❌ Access denied - invalid password")
}
}
func getSecurePassword() ([]byte, error) {
fmt.Print("Enter password: ")
// Read password without echoing to terminal
password, err := term.ReadPassword(int(os.Stdin.Fd()))
if err != nil {
return nil, fmt.Errorf("reading password: %w", err)
}
fmt.Println() // Print newline after hidden input
return password, nil
}
func verifyPassword(password []byte, hashedPassword string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), password)
return err == nil
}
// Helper function to generate bcrypt hash (for testing)
func generatePasswordHash(password string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost)
if err != nil {
return "", fmt.Errorf("generating hash: %w", err)
}
return string(hash), nil
}
Testing the Application
go run main.go
# Output:
# Enter password: [typing hidden]
# ✅ Access granted!
Security features demonstrated:
- ✅ No password visible on screen during entry
- ✅ No password stored in shell history
- ✅ Secure bcrypt comparison
- ✅ Proper error handling
Advanced Features
1. Multiple Password Attempts
func main() {
const maxAttempts = 3
for attempt := 1; attempt <= maxAttempts; attempt++ {
password, err := getSecurePassword()
if err != nil {
log.Fatal("Error reading password:", err)
}
if verifyPassword(password, hashedPassword) {
fmt.Println("✅ Access granted!")
return
}
remaining := maxAttempts - attempt
if remaining > 0 {
fmt.Printf("❌ Invalid password. %d attempts remaining.\n", remaining)
} else {
fmt.Println("❌ Access denied - too many failed attempts")
os.Exit(1)
}
}
}
2. Password Strength Validation
import (
"regexp"
"unicode"
)
func validatePasswordStrength(password string) []string {
var issues []string
if len(password) < 8 {
issues = append(issues, "Password must be at least 8 characters")
}
hasUpper := false
hasLower := false
hasDigit := false
hasSpecial := false
for _, char := range password {
switch {
case unicode.IsUpper(char):
hasUpper = true
case unicode.IsLower(char):
hasLower = true
case unicode.IsDigit(char):
hasDigit = true
case unicode.IsPunct(char) || unicode.IsSymbol(char):
hasSpecial = true
}
}
if !hasUpper {
issues = append(issues, "Password must contain uppercase letters")
}
if !hasLower {
issues = append(issues, "Password must contain lowercase letters")
}
if !hasDigit {
issues = append(issues, "Password must contain numbers")
}
if !hasSpecial {
issues = append(issues, "Password must contain special characters")
}
return issues
}
3. Password Creation Mode
func createNewPassword() error {
fmt.Print("Enter new password: ")
password1, err := term.ReadPassword(int(os.Stdin.Fd()))
if err != nil {
return err
}
fmt.Println()
// Validate password strength
if issues := validatePasswordStrength(string(password1)); len(issues) > 0 {
fmt.Println("Password strength issues:")
for _, issue := range issues {
fmt.Printf(" • %s\n", issue)
}
return fmt.Errorf("password does not meet requirements")
}
fmt.Print("Confirm password: ")
password2, err := term.ReadPassword(int(os.Stdin.Fd()))
if err != nil {
return err
}
fmt.Println()
if string(password1) != string(password2) {
return fmt.Errorf("passwords do not match")
}
hash, err := bcrypt.GenerateFromPassword(password1, bcryptCost)
if err != nil {
return err
}
fmt.Printf("Password hash: %s\n", string(hash))
return nil
}
4. Handling Different Input Sources
import (
"bufio"
"io"
"os"
)
func getPassword() ([]byte, error) {
// Check if stdin is from terminal or pipe
if term.IsTerminal(int(os.Stdin.Fd())) {
// Interactive terminal - use secure input
return getSecurePassword()
} else {
// Piped input - read from stdin
return getPasswordFromPipe()
}
}
func getPasswordFromPipe() ([]byte, error) {
reader := bufio.NewReader(os.Stdin)
password, err := reader.ReadBytes('\n')
if err != nil && err != io.EOF {
return nil, err
}
// Remove trailing newline
if len(password) > 0 && password[len(password)-1] == '\n' {
password = password[:len(password)-1]
}
return password, nil
}
This allows usage like:
# Interactive mode
go run main.go
# Piped from password manager
pass show myservice | go run main.go
# From file
echo "mypassword" | go run main.go
Security Best Practices
1. Memory Security
import "crypto/subtle"
func secureCompare(a, b []byte) bool {
return subtle.ConstantTimeCompare(a, b) == 1
}
// Clear sensitive data from memory
func clearSensitiveData(data []byte) {
for i := range data {
data[i] = 0
}
}
func main() {
password, err := getSecurePassword()
if err != nil {
log.Fatal(err)
}
defer clearSensitiveData(password) // Clear password from memory
// ... use password ...
}
2. Timing Attack Prevention
bcrypt's CompareHashAndPassword
is designed to be timing-safe, but for additional security:
func verifyPasswordSecure(password []byte, hashedPassword string) bool {
// bcrypt.CompareHashAndPassword already provides timing safety
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), password)
return err == nil
}
3. Rate Limiting
import (
"sync"
"time"
)
type rateLimiter struct {
mu sync.Mutex
attempts map[string][]time.Time
maxTries int
timeWindow time.Duration
}
func newRateLimiter(maxTries int, window time.Duration) *rateLimiter {
return &rateLimiter{
attempts: make(map[string][]time.Time),
maxTries: maxTries,
timeWindow: window,
}
}
func (rl *rateLimiter) allowAttempt(identifier string) bool {
rl.mu.Lock()
defer rl.mu.Unlock()
now := time.Now()
cutoff := now.Add(-rl.timeWindow)
// Clean old attempts
attempts := rl.attempts[identifier]
var validAttempts []time.Time
for _, attempt := range attempts {
if attempt.After(cutoff) {
validAttempts = append(validAttempts, attempt)
}
}
if len(validAttempts) >= rl.maxTries {
return false
}
// Record this attempt
validAttempts = append(validAttempts, now)
rl.attempts[identifier] = validAttempts
return true
}
Common Pitfalls and Solutions
1. ❌ Logging Passwords
// DON'T DO THIS
log.Printf("User entered password: %s", password)
fmt.Printf("Debug: password = %s\n", password)
2. ❌ Storing Passwords in Variables Too Long
// DON'T DO THIS
password := getPassword()
// ... lots of code ...
// password still in memory
// ✅ DO THIS
password := getPassword()
defer clearSensitiveData(password)
// Use password immediately
3. ❌ Using Wrong Hash Functions
// DON'T DO THIS for passwords
hash := sha256.Sum256([]byte(password))
// ✅ DO THIS
hash, _ := bcrypt.GenerateFromPassword([]byte(password), 12)
Testing Secure Input
Manual Testing
# Test normal input
go run main.go
# Test with piped input
echo "testpassword" | go run main.go
# Test with password manager
pass show myservice | go run main.go
Unit Testing
func TestPasswordVerification(t *testing.T) {
password := "testpassword123"
// Generate hash
hash, err := bcrypt.GenerateFromPassword([]byte(password), 12)
if err != nil {
t.Fatal(err)
}
// Test correct password
if !verifyPassword([]byte(password), string(hash)) {
t.Error("Password verification failed for correct password")
}
// Test incorrect password
if verifyPassword([]byte("wrongpassword"), string(hash)) {
t.Error("Password verification succeeded for incorrect password")
}
}
Summary
We've learned how to implement secure password input in CLI applications:
✅ Secure Input: Using golang.org/x/term
for hidden password entry
✅ Proper Hashing: bcrypt for password hashing with salt
✅ Security Features: Rate limiting, attempt counting, memory clearing
✅ Flexible Input: Supporting both interactive and piped input
✅ Best Practices: Avoiding common security pitfalls
Key Security Principles:
- Never display passwords on screen
- Use proper password hashing (bcrypt/Argon2)
- Clear sensitive data from memory
- Implement rate limiting
- Validate input sources
🏆 Project Assignment
Create a comprehensive password checking CLI that integrates with the Have I Been Pwned API:
Requirements
Build a CLI tool that:
- Securely accepts password input using the techniques learned
- Checks against Have I Been Pwned API to see if password has been breached
- Uses k-anonymity model for privacy (only send first 5 SHA1 hash characters)
Implementation Steps
-
Secure Password Input
gopassword := getSecurePassword() // Using term.ReadPassword
-
Generate SHA1 Hash
gohash := sha1.Sum(password) hexHash := hex.EncodeToString(hash[:])
-
API Integration
goprefix := hexHash[:5] // First 5 characters url := fmt.Sprintf("https://api.pwnedpasswords.com/range/%s", prefix) // Make HTTP request...
-
Check Results
gosuffix := hexHash[5:] // Remaining 35 characters // Check if suffix appears in API response
Expected Output
go run main.go
Enter password: [hidden input]
✅ Password appears to be safe!
# OR
go run main.go
Enter password: [hidden input]
❌ Password has been pwned! Found 23,597 times in data breaches.
Security Notes
- ⚠️ Never send plain text password to the API
- ✅ Use SHA1 hashing as required by the API
- ✅ Only send first 5 hash characters (k-anonymity)
- ✅ Use HTTPS for all API requests
This project combines everything learned: secure input, hashing, HTTP requests, and privacy-preserving techniques!
What's Next?
In the next lesson, we'll explore environment variables as another secure method for providing sensitive configuration data to CLI applications.
Secure Password Input Implementation
Implement the basic secure password input functionality using Go's golang.org/x/term
package.
Requirements:
- Use
term.ReadPassword()
to hide password input from the terminal - Add proper error handling for input failures
- Ensure the cursor moves to a new line after password entry
- Clear sensitive data from memory after use
Implementation Notes:
- Import
golang.org/x/term
package - Handle both
os.Stdin.Fd()
conversion and potential errors - Use
defer
statements to clear password data from memory - Test with both interactive terminal input and piped input
Example Usage:
go run main.go
Enter password: [typing should be hidden]
✅ Password accepted!
SHA1 Hash Generation for API Integration
Implement SHA1 hashing functionality to prepare passwords for the Have I Been Pwned API integration.
Requirements:
- Generate SHA1 hash of the input password
- Convert hash to uppercase hexadecimal string (as required by the API)
- Split hash into prefix (first 5 characters) and suffix (remaining 35 characters)
- Implement proper error handling for hash generation
Implementation Notes:
- Use
crypto/sha1
package for hashing - Use
encoding/hex
for hexadecimal conversion - Ensure uppercase output with
strings.ToUpper()
- The API requires SHA1 specifically, not SHA256 or other algorithms
Example:
password := "password123"
hash := generateSHA1Hash(password)
prefix := hash[:5] // "F25A2"
suffix := hash[5:] // "FC72690B9..."
Have I Been Pwned API Integration
Integrate with the Have I Been Pwned API using the k-anonymity model to check if a password has been compromised in data breaches.
Requirements:
- Make HTTP GET request to
https://api.pwnedpasswords.com/range/{hash_prefix}
- Send only the first 5 characters of the SHA1 hash (k-anonymity for privacy)
- Parse the API response to find matching hash suffixes
- Return the breach count if password is found, or 0 if safe
Implementation Notes:
- Use
net/http
package for API requests - Set appropriate User-Agent header for the requests
- Handle HTTP errors and network timeouts gracefully
- Parse response format:
{hash_suffix}:{count}\r\n
- Use
strings.Contains()
orstrings.Split()
for response parsing
API Response Format:
0018A45C4D1DEF81644B54AB7F969B88D65:1
00D4F6E8FA6EECAD2A3AA415EEC418D38EC:2
011053FD0102E94D6AE2F8B83D76FAF94F6:1