diff --git a/internal/encryption/passphrase/prompt.go b/internal/encryption/passphrase/prompt.go index 9692bc8..340d5ad 100644 --- a/internal/encryption/passphrase/prompt.go +++ b/internal/encryption/passphrase/prompt.go @@ -2,6 +2,7 @@ package passphrase import ( "fmt" + "strings" "github.com/manifoldco/promptui" @@ -9,23 +10,41 @@ import ( passphrasevalidation "github.com/substantialcattle5/sietch/internal/passphrase" ) -// promptForPassphrase prompts the user for a passphrase +// promptForPassphrase prompts the user for a passphrase with enhanced feedback func PromptForPassphrase(confirm bool) (string, error) { promptLabel := "Enter passphrase" if confirm { promptLabel = "Create new passphrase" } + // Enhanced validation function with real-time feedback + validate := func(input string) error { + if input == "" { + return nil // Allow empty during typing + } + + result := passphrasevalidation.ValidateHybrid(input) + + // Show real-time feedback + showPassphraseFeedback(input, result) + + // Only enforce validation on final submission + if !result.Valid { + return fmt.Errorf("%s", passphrasevalidation.GetHybridErrorMessage(result)) + } + + // Show warnings but don't prevent submission + if len(result.Warnings) > 0 { + fmt.Printf("\033[33m⚠️ Warning: %s\033[0m\n", result.Warnings[0]) + } + + return nil + } + passphrasePrompt := promptui.Prompt{ - Label: promptLabel, - Mask: '*', - Validate: func(input string) error { - result := passphrasevalidation.ValidateHybrid(input) - if !result.Valid || len(result.Warnings) > 0 { - return fmt.Errorf("%s", passphrasevalidation.GetHybridErrorMessage(result)) - } - return nil - }, + Label: promptLabel, + Mask: '*', + Validate: validate, } passphrase, err := passphrasePrompt.Run() @@ -33,6 +52,21 @@ func PromptForPassphrase(confirm bool) (string, error) { return "", fmt.Errorf("passphrase prompt failed: %w", err) } + // Clear the feedback lines AND the duplicate prompt line + linesToClear := feedbackLineCount + if feedbackLineCount > 0 { + linesToClear++ // Also clear the duplicate prompt line printed by promptui + } + for i := 0; i < linesToClear; i++ { + fmt.Print("\033[F\033[K") // Move up and clear each line + } + + // Show success message + result := passphrasevalidation.ValidateHybrid(passphrase) + if result.Valid { + fmt.Printf("\033[32m✅ Passphrase meets all requirements (Strength: %s)\033[0m\n", result.Strength) + } + if confirm { confirmPrompt := promptui.Prompt{ Label: "Confirm passphrase", @@ -49,11 +83,185 @@ func PromptForPassphrase(confirm bool) (string, error) { if err != nil { return "", fmt.Errorf("passphrase confirmation failed: %w", err) } + + fmt.Print("\033[32m✅ Passphrase confirmed successfully\033[0m\n") } return passphrase, nil } +// Track feedback state to know how many lines to clear +var feedbackLineCount = 0 + +// showPassphraseFeedback displays real-time feedback about passphrase requirements +func showPassphraseFeedback(passphrase string, result passphrasevalidation.HybridValidationResult) { + if passphrase == "" { + return + } + + // Clear previous feedback lines if they exist + if feedbackLineCount > 0 { + for i := 0; i < feedbackLineCount; i++ { + fmt.Print("\033[1A\033[2K") // Move up one line and clear it + } + feedbackLineCount = 0 + } + + // Check individual requirements + requirements := []struct { + label string + met bool + progress string + }{ + {"At least 12 characters", len(passphrase) >= 12, fmt.Sprintf("(%d/12)", len(passphrase))}, + {"Uppercase letter", hasUppercase(passphrase), ""}, + {"Lowercase letter", hasLowercase(passphrase), ""}, + {"Digit", hasDigit(passphrase), ""}, + {"Special character", hasSpecialChar(passphrase), ""}, + } + + fmt.Print("\nRequirements:\n") + feedbackLineCount += 2 + + for _, req := range requirements { + symbol := "✗" + color := "\033[31m" // Red + if req.met { + symbol = "✓" + color = "\033[32m" // Green + } + + label := req.label + if req.progress != "" { + label += " " + req.progress + } + + fmt.Printf(" %s%s %s\033[0m\n", color, symbol, label) + feedbackLineCount++ + } + + // Show strength meter + score := calculateStrengthScore(passphrase, result) + strengthLabel := getStrengthLabel(score) + + filledBars := score + emptyBars := 10 - score + + var strengthColor string + switch { + case score <= 3: + strengthColor = "\033[31m" // Red + case score <= 6: + strengthColor = "\033[33m" // Yellow + case score <= 8: + strengthColor = "\033[36m" // Cyan + default: + strengthColor = "\033[32m" // Green + } + + fmt.Print("\n") + fmt.Printf("Strength: %s%s\033[0m %s\033[37m%s\033[0m (%d/10)\n", + strengthColor, + strengthLabel, + strings.Repeat("█", filledBars), + strings.Repeat("░", emptyBars), + score) + feedbackLineCount += 2 + + // Show warnings + if len(result.Warnings) > 0 { + for _, warning := range result.Warnings { + fmt.Printf("\033[33m⚠️ %s\033[0m\n", warning) + feedbackLineCount++ + } + } +} + +// Helper functions for character validation +func hasUppercase(s string) bool { + for _, r := range s { + if r >= 'A' && r <= 'Z' { + return true + } + } + return false +} + +func hasLowercase(s string) bool { + for _, r := range s { + if r >= 'a' && r <= 'z' { + return true + } + } + return false +} + +func hasDigit(s string) bool { + for _, r := range s { + if r >= '0' && r <= '9' { + return true + } + } + return false +} + +func hasSpecialChar(s string) bool { + specialChars := "!@#$%^&*()_+-=[]{}|;:,.<>?" + for _, r := range s { + for _, special := range specialChars { + if r == special { + return true + } + } + } + return false +} + +func calculateStrengthScore(passphrase string, result passphrasevalidation.HybridValidationResult) int { + if len(passphrase) == 0 { + return 0 + } + + // Base score from zxcvbn (0-4) converted to 0-10 scale + score := result.Score * 2 + + // Bonus points for meeting basic requirements + if len(passphrase) >= 12 { + score += 1 + } + if len(passphrase) >= 16 { + score += 1 + } + + // Penalty for common passwords + if result.IsCommon { + score -= 2 + } + + // Ensure score is within bounds + if score < 0 { + score = 0 + } + if score > 10 { + score = 10 + } + + return score +} + +func getStrengthLabel(score int) string { + switch { + case score <= 3: + return "Weak" + case score <= 6: + return "Fair" + case score <= 8: + return "Good" + default: + return "Strong" + } +} + // promptPassphraseProtection asks if the vault should be protected with a passphrase func PromptPassphraseProtection(configuration *config.VaultConfig) error { passphrasePrompt := promptui.Prompt{ diff --git a/internal/encryption/passphrase/prompt_test.go b/internal/encryption/passphrase/prompt_test.go new file mode 100644 index 0000000..cb584c6 --- /dev/null +++ b/internal/encryption/passphrase/prompt_test.go @@ -0,0 +1,123 @@ +package passphrase + +import ( + "testing" + + passphrasevalidation "github.com/substantialcattle5/sietch/internal/passphrase" +) + +// Test helper functions for character validation +func TestCharacterValidationHelpers(t *testing.T) { + tests := []struct { + name string + input string + upper bool + lower bool + digit bool + special bool + }{ + {"empty", "", false, false, false, false}, + {"upper only", "HELLO", true, false, false, false}, + {"lower only", "hello", false, true, false, false}, + {"digit only", "12345", false, false, true, false}, + {"special only", "!@#$%", false, false, false, true}, + {"mixed", "Hello123!", true, true, true, true}, + {"no special", "Hello123", true, true, true, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := hasUppercase(tt.input); got != tt.upper { + t.Errorf("hasUppercase() = %v, want %v", got, tt.upper) + } + if got := hasLowercase(tt.input); got != tt.lower { + t.Errorf("hasLowercase() = %v, want %v", got, tt.lower) + } + if got := hasDigit(tt.input); got != tt.digit { + t.Errorf("hasDigit() = %v, want %v", got, tt.digit) + } + if got := hasSpecialChar(tt.input); got != tt.special { + t.Errorf("hasSpecialChar() = %v, want %v", got, tt.special) + } + }) + } +} + +// Test strength calculation +func TestCalculateStrengthScore(t *testing.T) { + tests := []struct { + name string + passphrase string + result passphrasevalidation.HybridValidationResult + want int + }{ + {"empty", "", passphrasevalidation.HybridValidationResult{Score: 0}, 0}, + {"short weak", "pass", passphrasevalidation.HybridValidationResult{Score: 2, IsCommon: false}, 4}, + {"long strong", "VeryLongPassword123!", passphrasevalidation.HybridValidationResult{Score: 4, IsCommon: false}, 10}, + {"common penalty", "password", passphrasevalidation.HybridValidationResult{Score: 1, IsCommon: true}, 0}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := calculateStrengthScore(tt.passphrase, tt.result); got != tt.want { + t.Errorf("calculateStrengthScore() = %v, want %v", got, tt.want) + } + }) + } +} + +// Test strength labels +func TestGetStrengthLabel(t *testing.T) { + tests := []struct { + score int + want string + }{ + {0, "Weak"}, + {3, "Weak"}, + {4, "Fair"}, + {6, "Fair"}, + {7, "Good"}, + {8, "Good"}, + {9, "Strong"}, + {10, "Strong"}, + } + + for _, tt := range tests { + t.Run("", func(t *testing.T) { + if got := getStrengthLabel(tt.score); got != tt.want { + t.Errorf("getStrengthLabel(%d) = %v, want %v", tt.score, got, tt.want) + } + }) + } +} + +// Test feedback line counting (simple state test) +func TestFeedbackLineCount(t *testing.T) { + // Reset counter + feedbackLineCount = 0 + + // Simulate some feedback display + result := passphrasevalidation.HybridValidationResult{Valid: false, Score: 1} + showPassphraseFeedback("test", result) + + // Should have counted some lines + if feedbackLineCount == 0 { + t.Error("Expected feedbackLineCount to be greater than 0 after showing feedback") + } + + // Reset for other tests + feedbackLineCount = 0 +} + +// Test empty passphrase feedback (should return early) +func TestShowPassphraseFeedbackEmpty(t *testing.T) { + initialCount := feedbackLineCount + result := passphrasevalidation.HybridValidationResult{} + + showPassphraseFeedback("", result) + + // Should not change the line count for empty passphrase + if feedbackLineCount != initialCount { + t.Errorf("Expected feedbackLineCount to remain %d, got %d", initialCount, feedbackLineCount) + } +} diff --git a/internal/ui/passphrase.go b/internal/ui/passphrase.go index a12ddf5..54adffa 100644 --- a/internal/ui/passphrase.go +++ b/internal/ui/passphrase.go @@ -218,48 +218,22 @@ func GetPassphraseForInitialization(cmd *cobra.Command, requireConfirmation bool interactiveMode, _ := cmd.Flags().GetBool("interactive") if interactiveMode { - // Use promptui for better terminal UI - passphrasePrompt := promptui.Prompt{ - Label: "Enter encryption passphrase", - Mask: '*', - Validate: func(input string) error { - result := passphrasevalidation.ValidateHybrid(input) - if !result.Valid || len(result.Warnings) > 0 { - return fmt.Errorf("%s", passphrasevalidation.GetHybridErrorMessage(result)) - } - return nil - }, - } - - enteredPassphrase, err := passphrasePrompt.Run() - if err != nil { - return "", fmt.Errorf("failed to get passphrase: %w", err) - } - - // Add confirmation prompt if required - if requireConfirmation { - confirmPrompt := promptui.Prompt{ - Label: "Confirm passphrase", - Mask: '*', - Validate: func(input string) error { - if input != enteredPassphrase { - return fmt.Errorf("passphrases do not match") - } - return nil - }, - } - - _, err = confirmPrompt.Run() - if err != nil { - return "", fmt.Errorf("passphrase confirmation failed: %w", err) - } - } - - // Removed dangerous passphrase exposure via printf - return enteredPassphrase, nil + // Use enhanced passphrase prompt with real-time feedback + fmt.Println() + fmt.Println("🔐 Enhanced Passphrase Entry") + fmt.Println("-" + strings.Repeat("=", 28)) + return getEnhancedPassphrase(requireConfirmation) } else { // Use simple terminal input for non-interactive mode - fmt.Print("Enter encryption passphrase (min 8 characters): ") + // Show all requirements upfront + fmt.Println("\nPassphrase Requirements:") + fmt.Println(" • At least 12 characters") + fmt.Println(" • Uppercase letter") + fmt.Println(" • Lowercase letter") + fmt.Println(" • Digit") + fmt.Println(" • Special character (!@#$%^&*()_+-=[]{}|;:,.<>?)") + fmt.Println() + fmt.Print("Enter encryption passphrase: ") bytePassphrase, err := term.ReadPassword(int(syscall.Stdin)) if err != nil { return "", fmt.Errorf("error reading passphrase: %w", err) @@ -270,8 +244,13 @@ func GetPassphraseForInitialization(cmd *cobra.Command, requireConfirmation bool // Validate passphrase using hybrid validation (strict rules + zxcvbn intelligence) result := passphrasevalidation.ValidateHybrid(enteredPassphrase) - if !result.Valid || len(result.Warnings) > 0 { - return "", fmt.Errorf("%s", passphrasevalidation.GetHybridErrorMessage(result)) + if !result.Valid { + return "", fmt.Errorf("passphrase does not meet the requirements listed above") + } + + // Show warnings but don't fail + if len(result.Warnings) > 0 { + fmt.Printf("\033[33m⚠️ Warning: %s\033[0m\n", result.Warnings[0]) } // Add confirmation if required @@ -293,3 +272,276 @@ func GetPassphraseForInitialization(cmd *cobra.Command, requireConfirmation bool return enteredPassphrase, nil } } + +// getEnhancedPassphrase provides an enhanced passphrase prompt with real-time feedback +func getEnhancedPassphrase(requireConfirmation bool) (string, error) { + // Show initial instructions + fmt.Println("💡 Passphrase requirements:") + fmt.Println(" • At least 12 characters") + fmt.Println(" • Uppercase letter (A-Z)") + fmt.Println(" • Lowercase letter (a-z)") + fmt.Println(" • Digit (0-9)") + fmt.Println(" • Special character (!@#$%^&*()_+-=[]{}|;:,.<>?)") + fmt.Println() + + // Use custom input with true in-place real-time feedback + passphrase, err := getPassphraseWithInPlaceFeedback("Enter encryption passphrase") + if err != nil { + return "", fmt.Errorf("passphrase prompt failed: %w", err) + } + + // Show success message + result := passphrasevalidation.ValidateHybrid(passphrase) + if result.Valid { + fmt.Printf("✅ Passphrase meets all requirements (Strength: %s)\n", result.Strength) + } + + // Handle confirmation if required + if requireConfirmation { + confirmPrompt := promptui.Prompt{ + Label: "Confirm passphrase", + Mask: '*', + Validate: func(input string) error { + if input != passphrase { + return fmt.Errorf("passphrases do not match") + } + return nil + }, + } + + _, err = confirmPrompt.Run() + if err != nil { + return "", fmt.Errorf("passphrase confirmation failed: %w", err) + } + + fmt.Println("✅ Passphrase confirmed successfully") + } + + return passphrase, nil +} + +// Helper functions for character validation +func hasUppercaseChar(s string) bool { + for _, r := range s { + if r >= 'A' && r <= 'Z' { + return true + } + } + return false +} + +func hasLowercaseChar(s string) bool { + for _, r := range s { + if r >= 'a' && r <= 'z' { + return true + } + } + return false +} + +func hasDigitChar(s string) bool { + for _, r := range s { + if r >= '0' && r <= '9' { + return true + } + } + return false +} + +func hasSpecialCharacter(s string) bool { + specialChars := "!@#$%^&*()_+-=[]{}|;:,.<>?" + for _, r := range s { + for _, special := range specialChars { + if r == special { + return true + } + } + } + return false +} + +func calculatePassphraseStrength(passphrase string, result passphrasevalidation.HybridValidationResult) int { + if len(passphrase) == 0 { + return 0 + } + + // Base score from zxcvbn (0-4) converted to 0-10 scale + score := result.Score * 2 + + // Bonus points for meeting basic requirements + if len(passphrase) >= 12 { + score += 1 + } + if len(passphrase) >= 16 { + score += 1 + } + + // Penalty for common passwords + if result.IsCommon { + score -= 2 + } + + // Ensure score is within bounds + if score < 0 { + score = 0 + } + if score > 10 { + score = 10 + } + + return score +} + +func getPassphraseStrengthLabel(score int) string { + switch { + case score <= 3: + return "Weak" + case score <= 6: + return "Fair" + case score <= 8: + return "Good" + default: + return "Strong" + } +} + +// getPassphraseWithInPlaceFeedback implements true in-place real-time feedback +func getPassphraseWithInPlaceFeedback(label string) (string, error) { + fmt.Printf("%s: ", label) + + // Initialize feedback area - show it once and then only update it + fmt.Print("\n\nStatus: ✗12+ ✗Upper ✗Lower ✗Digit ✗Special | Strength: Weak ░░░░░ (0/10)\n") + fmt.Printf("\033[1A\033[%dC", len(label)+2) // Move cursor back to input position + + var passphrase []rune + + // Set terminal to raw mode for character-by-character input + oldState, err := term.MakeRaw(int(syscall.Stdin)) + if err != nil { + return "", fmt.Errorf("failed to set raw mode: %w", err) + } + defer term.Restore(int(syscall.Stdin), oldState) + + for { + // Read single character + var buf [3]byte // UTF-8 can be up to 3 bytes + n, err := os.Stdin.Read(buf[:]) + if err != nil { + return "", err + } + + if n == 1 { + char := buf[0] + + switch char { + case 13: // Enter key + currentPass := string(passphrase) + result := passphrasevalidation.ValidateHybrid(currentPass) + if result.Valid { + // Move to next line and show success + fmt.Print("\n\n") + fmt.Printf("✅ Passphrase accepted (Strength: %s)\n", result.Strength) + return currentPass, nil + } else { + // Beep and stay in place (password invalid) + fmt.Print("\a") // Bell sound + } + + case 3: // Ctrl+C + fmt.Print("\n") + return "", fmt.Errorf("^C") + + case 127, 8: // Backspace/Delete + if len(passphrase) > 0 { + passphrase = passphrase[:len(passphrase)-1] + fmt.Print("\b \b") // Erase character visually + updateStatusLine(string(passphrase)) + } + + default: + if char >= 32 && char <= 126 { // Printable ASCII + passphrase = append(passphrase, rune(char)) + fmt.Print("*") + updateStatusLine(string(passphrase)) + } + } + } + } +} + +// updateStatusLine updates just the status line in place +func updateStatusLine(passphrase string) { + if len(passphrase) == 0 { + // Reset to initial state + fmt.Print("\033[s") // Save cursor position + fmt.Print("\033[1B") // Move down one line to status line + fmt.Print("\033[2K") // Clear entire line + fmt.Print("Status: ✗12+ ✗Upper ✗Lower ✗Digit ✗Special | Strength: Weak ░░░░░ (0/10)") + fmt.Print("\033[u") // Restore cursor position + return + } + + // Save current cursor position + fmt.Print("\033[s") + + // Move to status line (one line down) + fmt.Print("\033[1B") + fmt.Print("\033[2K") // Clear the line + + // Check requirements + hasLength := len(passphrase) >= 12 + hasUpper := hasUppercaseChar(passphrase) + hasLower := hasLowercaseChar(passphrase) + hasDigit := hasDigitChar(passphrase) + hasSpecial := hasSpecialCharacter(passphrase) + + // Build status with colors + statusParts := []string{ + fmt.Sprintf("%s12+", getSymbol(hasLength)), + fmt.Sprintf("%sUpper", getSymbol(hasUpper)), + fmt.Sprintf("%sLower", getSymbol(hasLower)), + fmt.Sprintf("%sDigit", getSymbol(hasDigit)), + fmt.Sprintf("%sSpecial", getSymbol(hasSpecial)), + } + + if hasLength { + statusParts[0] = fmt.Sprintf("%s12+(%d)", getSymbol(hasLength), len(passphrase)) + } + + // Calculate strength + result := passphrasevalidation.ValidateHybrid(passphrase) + score := calculatePassphraseStrength(passphrase, result) + strengthLabel := getPassphraseStrengthLabel(score) + + // Create compact strength meter (5 bars) + filledBars := (score + 1) / 2 // 0-10 -> 0-5 + if filledBars > 5 { + filledBars = 5 + } + emptyBars := 5 - filledBars + + meter := strings.Repeat("█", filledBars) + strings.Repeat("░", emptyBars) + + // Write the complete status line + fmt.Printf("Status: %s | Strength: %s %s (%d/10)", + strings.Join(statusParts, " "), + strengthLabel, + meter, + score) + + // Add warning if common password + if result.IsCommon { + fmt.Print(" | ⚠️ Common") + } + + // Restore cursor position + fmt.Print("\033[u") +} + +// getSymbol returns colored checkmark or X +func getSymbol(met bool) string { + if met { + return "\033[32m✓\033[0m" // Green checkmark + } + return "\033[31m✗\033[0m" // Red X +} diff --git a/internal/ui/passphrase_test.go b/internal/ui/passphrase_test.go index 0e942b4..724b789 100644 --- a/internal/ui/passphrase_test.go +++ b/internal/ui/passphrase_test.go @@ -8,6 +8,8 @@ import ( "path/filepath" "strings" "testing" + + passphrasevalidation "github.com/substantialcattle5/sietch/internal/passphrase" ) func TestReadPassphraseFromFile(t *testing.T) { @@ -121,3 +123,90 @@ func TestReadPassphraseFromStdin(t *testing.T) { // This test demonstrates the concept but may need to be run manually t.Skip("Skipping stdin test - requires manual testing with actual stdin redirection") } + +// Test helper functions for character validation +func TestCharacterValidationHelpers(t *testing.T) { + tests := []struct { + name string + input string + upper bool + lower bool + digit bool + special bool + }{ + {"empty", "", false, false, false, false}, + {"upper only", "HELLO", true, false, false, false}, + {"lower only", "hello", false, true, false, false}, + {"digit only", "12345", false, false, true, false}, + {"special only", "!@#$%", false, false, false, true}, + {"mixed", "Hello123!", true, true, true, true}, + {"no special", "Hello123", true, true, true, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := hasUppercaseChar(tt.input); got != tt.upper { + t.Errorf("hasUppercaseChar() = %v, want %v", got, tt.upper) + } + if got := hasLowercaseChar(tt.input); got != tt.lower { + t.Errorf("hasLowercaseChar() = %v, want %v", got, tt.lower) + } + if got := hasDigitChar(tt.input); got != tt.digit { + t.Errorf("hasDigitChar() = %v, want %v", got, tt.digit) + } + if got := hasSpecialCharacter(tt.input); got != tt.special { + t.Errorf("hasSpecialCharacter() = %v, want %v", got, tt.special) + } + }) + } +} + +// Test strength calculation +func TestCalculatePassphraseStrength(t *testing.T) { + result := passphrasevalidation.HybridValidationResult{Score: 2, IsCommon: false} + + tests := []struct { + name string + passphrase string + result passphrasevalidation.HybridValidationResult + want int + }{ + {"empty", "", result, 0}, + {"short weak", "pass", result, 4}, // score 2*2 = 4 + {"long strong", "VeryLongPassword123!", passphrasevalidation.HybridValidationResult{Score: 4, IsCommon: false}, 10}, // 4*2 + 1 + 1 = 10 + {"common penalty", "password", passphrasevalidation.HybridValidationResult{Score: 1, IsCommon: true}, 0}, // 1*2 - 2 = 0 + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := calculatePassphraseStrength(tt.passphrase, tt.result); got != tt.want { + t.Errorf("calculatePassphraseStrength() = %v, want %v", got, tt.want) + } + }) + } +} + +// Test strength labels +func TestGetPassphraseStrengthLabel(t *testing.T) { + tests := []struct { + score int + want string + }{ + {0, "Weak"}, + {3, "Weak"}, + {4, "Fair"}, + {6, "Fair"}, + {7, "Good"}, + {8, "Good"}, + {9, "Strong"}, + {10, "Strong"}, + } + + for _, tt := range tests { + t.Run("", func(t *testing.T) { + if got := getPassphraseStrengthLabel(tt.score); got != tt.want { + t.Errorf("getPassphraseStrengthLabel(%d) = %v, want %v", tt.score, got, tt.want) + } + }) + } +}