up
This commit is contained in:
518
passwordgenerator/generator.go
Normal file
518
passwordgenerator/generator.go
Normal file
@@ -0,0 +1,518 @@
|
||||
package passwordgenerator
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Length int `json:"length"`
|
||||
IncludeUpper bool `json:"includeUpper"`
|
||||
IncludeLower bool `json:"includeLower"`
|
||||
NumberCount int `json:"numberCount"`
|
||||
SpecialChars string `json:"specialChars"`
|
||||
NoConsecutive bool `json:"noConsecutive"`
|
||||
UsePassphrase bool `json:"usePassphrase"`
|
||||
WordCount int `json:"wordCount"`
|
||||
NumberPosition string `json:"numberPosition"` // "start", "end", "each"
|
||||
PassphraseUseNumbers bool `json:"passphraseUseNumbers"`
|
||||
PassphraseUseSpecial bool `json:"passphraseUseSpecial"`
|
||||
}
|
||||
|
||||
// Fallback word list in case online fetch fails
|
||||
var fallbackWordList = []string{
|
||||
"apple", "brave", "chair", "dance", "eagle", "flame", "grape", "happy", "island", "jungle",
|
||||
"kite", "lemon", "magic", "night", "ocean", "piano", "quiet", "river", "storm", "tiger",
|
||||
"under", "voice", "water", "young", "zebra", "beach", "cloud", "dream", "fresh", "glass",
|
||||
"heart", "light", "money", "paper", "quick", "royal", "smile", "table", "violet", "world",
|
||||
"bright", "castle", "gentle", "honest", "knight", "marble", "orange", "purple", "silver", "yellow",
|
||||
"ancient", "crystal", "distant", "emerald", "fantasy", "harmony", "journey", "mystery", "outdoor", "perfect",
|
||||
"rainbow", "serenity", "thunder", "universe", "victory", "whisper", "amazing", "balance", "courage", "dignity",
|
||||
"elegant", "freedom", "golden", "holiday", "inspire", "justice", "kindness", "library", "mountain", "natural",
|
||||
"peaceful", "quality", "respect", "sunshine", "triumph", "unique", "wonderful", "adventure", "beautiful", "creative",
|
||||
"delicate", "exciting", "friendly", "generous", "hilarious", "incredible", "joyful", "lovely", "magnificent", "optimistic",
|
||||
}
|
||||
|
||||
// Global word list cache
|
||||
var (
|
||||
wordList []string
|
||||
wordListMux sync.RWMutex
|
||||
lastFetch time.Time
|
||||
fetchTimeout = 10 * time.Second
|
||||
cacheExpiry = 24 * time.Hour // Cache for 24 hours
|
||||
wordListFile = "wordlist.txt" // Local file path
|
||||
)
|
||||
|
||||
// InitWordList initializes the word list cache. Call this at app startup.
|
||||
func InitWordList() {
|
||||
go func() {
|
||||
// Fetch word list in background to warm up cache
|
||||
getWordList()
|
||||
}()
|
||||
}
|
||||
|
||||
// GetWordListInfo returns information about the current word list
|
||||
func GetWordListInfo() (count int, source string, lastUpdate time.Time) {
|
||||
wordListMux.RLock()
|
||||
defer wordListMux.RUnlock()
|
||||
|
||||
count = len(wordList)
|
||||
if count == 0 {
|
||||
count = len(fallbackWordList)
|
||||
source = "fallback"
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we have a local file
|
||||
if _, err := os.Stat(wordListFile); err == nil {
|
||||
fileTime := getFileModTime(wordListFile)
|
||||
if time.Since(fileTime) > cacheExpiry {
|
||||
source = "local file (expired)"
|
||||
} else if len(wordList) > len(fallbackWordList) {
|
||||
source = "local file (from MIT)"
|
||||
} else {
|
||||
source = "local file"
|
||||
}
|
||||
return count, source, fileTime
|
||||
}
|
||||
|
||||
if time.Since(lastFetch) > cacheExpiry {
|
||||
source = "cached (expired)"
|
||||
} else if len(wordList) > len(fallbackWordList) {
|
||||
source = "MIT online"
|
||||
} else {
|
||||
source = "fallback"
|
||||
}
|
||||
|
||||
return count, source, lastFetch
|
||||
}
|
||||
|
||||
// getWordList returns the current word list, fetching from online source if needed
|
||||
func getWordList() []string {
|
||||
wordListMux.RLock()
|
||||
|
||||
// Check if we have a cached word list that's still valid
|
||||
if len(wordList) > 0 && time.Since(lastFetch) < cacheExpiry {
|
||||
defer wordListMux.RUnlock()
|
||||
return wordList
|
||||
}
|
||||
wordListMux.RUnlock()
|
||||
|
||||
// Need to fetch or refresh
|
||||
wordListMux.Lock()
|
||||
defer wordListMux.Unlock()
|
||||
|
||||
// Double-check in case another goroutine already fetched
|
||||
if len(wordList) > 0 && time.Since(lastFetch) < cacheExpiry {
|
||||
return wordList
|
||||
}
|
||||
|
||||
// First try to load from local file
|
||||
if localWords, err := loadWordListFromFile(); err == nil && len(localWords) >= 100 {
|
||||
wordList = localWords
|
||||
lastFetch = getFileModTime(wordListFile)
|
||||
return wordList
|
||||
}
|
||||
|
||||
// If local file doesn't exist or is invalid, try to fetch from MIT
|
||||
newWordList, err := fetchWordListFromMIT()
|
||||
if err != nil || len(newWordList) < 100 { // Sanity check
|
||||
// Use fallback if fetch failed or list is too small
|
||||
if len(wordList) == 0 {
|
||||
wordList = make([]string, len(fallbackWordList))
|
||||
copy(wordList, fallbackWordList)
|
||||
}
|
||||
return wordList
|
||||
}
|
||||
|
||||
// Save downloaded word list to local file
|
||||
saveWordListToFile(newWordList)
|
||||
|
||||
wordList = newWordList
|
||||
lastFetch = time.Now()
|
||||
return wordList
|
||||
}
|
||||
|
||||
// loadWordListFromFile loads the word list from local file
|
||||
func loadWordListFromFile() ([]string, error) {
|
||||
file, err := os.Open(wordListFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var words []string
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
word := strings.TrimSpace(scanner.Text())
|
||||
if len(word) > 0 && len(word) <= 15 {
|
||||
words = append(words, word)
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return words, nil
|
||||
}
|
||||
|
||||
// saveWordListToFile saves the word list to local file
|
||||
func saveWordListToFile(words []string) error {
|
||||
file, err := os.Create(wordListFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
for _, word := range words {
|
||||
if _, err := fmt.Fprintln(file, word); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getFileModTime returns the modification time of a file
|
||||
func getFileModTime(filename string) time.Time {
|
||||
if info, err := os.Stat(filename); err == nil {
|
||||
return info.ModTime()
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
// fetchWordListFromMIT fetches the word list from MIT's server
|
||||
// fetchWordListFromMIT fetches the word list from MIT's server
|
||||
func fetchWordListFromMIT() ([]string, error) {
|
||||
client := &http.Client{
|
||||
Timeout: fetchTimeout,
|
||||
}
|
||||
|
||||
resp, err := client.Get("https://www.mit.edu/~ecprice/wordlist.10000")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch word list: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var words []string
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
for scanner.Scan() {
|
||||
word := strings.TrimSpace(scanner.Text())
|
||||
if len(word) > 0 && len(word) <= 15 { // Filter out empty lines and very long words
|
||||
words = append(words, word)
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("error reading word list: %w", err)
|
||||
}
|
||||
|
||||
if len(words) < 100 {
|
||||
return nil, fmt.Errorf("word list too small: got %d words", len(words))
|
||||
}
|
||||
|
||||
return words, nil
|
||||
}
|
||||
|
||||
func DefaultConfig() Config {
|
||||
return Config{
|
||||
Length: 12,
|
||||
IncludeUpper: true,
|
||||
IncludeLower: true,
|
||||
NumberCount: 1,
|
||||
SpecialChars: "!@#$%^&*-_=+",
|
||||
NoConsecutive: false,
|
||||
UsePassphrase: true, // Default to passphrase
|
||||
WordCount: 3,
|
||||
NumberPosition: "end",
|
||||
PassphraseUseNumbers: true,
|
||||
PassphraseUseSpecial: true,
|
||||
}
|
||||
}
|
||||
|
||||
func GeneratePassword(config Config) (string, error) {
|
||||
if config.UsePassphrase {
|
||||
return generatePassphrase(config)
|
||||
}
|
||||
return generateRandomPassword(config)
|
||||
}
|
||||
|
||||
func generateRandomPassword(config Config) (string, error) {
|
||||
if config.Length < 1 {
|
||||
return "", fmt.Errorf("password length must be at least 1")
|
||||
}
|
||||
|
||||
var charset string
|
||||
var required []string
|
||||
|
||||
// Build character set
|
||||
if config.IncludeLower {
|
||||
charset += "abcdefghijklmnopqrstuvwxyz"
|
||||
required = append(required, "abcdefghijklmnopqrstuvwxyz")
|
||||
}
|
||||
if config.IncludeUpper {
|
||||
charset += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
required = append(required, "ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
}
|
||||
if config.NumberCount > 0 {
|
||||
charset += "0123456789"
|
||||
required = append(required, "0123456789")
|
||||
}
|
||||
if len(config.SpecialChars) > 0 {
|
||||
charset += config.SpecialChars
|
||||
required = append(required, config.SpecialChars)
|
||||
}
|
||||
|
||||
if len(charset) == 0 {
|
||||
return "", fmt.Errorf("no character types selected")
|
||||
}
|
||||
|
||||
password := make([]byte, config.Length)
|
||||
|
||||
// Ensure at least one character from each required set
|
||||
usedPositions := make(map[int]bool)
|
||||
|
||||
// First, place required numbers
|
||||
numbersPlaced := 0
|
||||
for numbersPlaced < config.NumberCount && numbersPlaced < config.Length {
|
||||
pos, err := rand.Int(rand.Reader, big.NewInt(int64(config.Length)))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
posInt := int(pos.Int64())
|
||||
|
||||
if !usedPositions[posInt] {
|
||||
char, err := rand.Int(rand.Reader, big.NewInt(10))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
password[posInt] = byte('0' + char.Int64())
|
||||
usedPositions[posInt] = true
|
||||
numbersPlaced++
|
||||
}
|
||||
}
|
||||
|
||||
// Then place at least one from each other required character set
|
||||
for _, reqSet := range required {
|
||||
if reqSet == "0123456789" {
|
||||
continue // Already handled numbers
|
||||
}
|
||||
|
||||
placed := false
|
||||
attempts := 0
|
||||
for !placed && attempts < config.Length*2 {
|
||||
pos, err := rand.Int(rand.Reader, big.NewInt(int64(config.Length)))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
posInt := int(pos.Int64())
|
||||
|
||||
if !usedPositions[posInt] {
|
||||
char, err := rand.Int(rand.Reader, big.NewInt(int64(len(reqSet))))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
password[posInt] = reqSet[char.Int64()]
|
||||
usedPositions[posInt] = true
|
||||
placed = true
|
||||
}
|
||||
attempts++
|
||||
}
|
||||
}
|
||||
|
||||
// Fill remaining positions
|
||||
for i := 0; i < config.Length; i++ {
|
||||
if !usedPositions[i] {
|
||||
char, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
password[i] = charset[char.Int64()]
|
||||
}
|
||||
}
|
||||
|
||||
// Handle no consecutive characters requirement
|
||||
if config.NoConsecutive {
|
||||
return ensureNoConsecutive(string(password), charset)
|
||||
}
|
||||
|
||||
return string(password), nil
|
||||
}
|
||||
|
||||
func generatePassphrase(config Config) (string, error) {
|
||||
if config.WordCount < 1 {
|
||||
return "", fmt.Errorf("word count must be at least 1")
|
||||
}
|
||||
|
||||
// Get current word list (online or fallback)
|
||||
currentWordList := getWordList()
|
||||
|
||||
words := make([]string, config.WordCount)
|
||||
for i := 0; i < config.WordCount; i++ {
|
||||
wordIndex, err := rand.Int(rand.Reader, big.NewInt(int64(len(currentWordList))))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
word := currentWordList[wordIndex.Int64()]
|
||||
|
||||
// Capitalize first letter if upper case is enabled
|
||||
if config.IncludeUpper {
|
||||
word = strings.Title(word)
|
||||
}
|
||||
words[i] = word
|
||||
}
|
||||
|
||||
// Generate numbers if enabled and needed
|
||||
var numbers []string
|
||||
if config.PassphraseUseNumbers && config.NumberCount > 0 {
|
||||
for i := 0; i < config.NumberCount; i++ {
|
||||
num, err := rand.Int(rand.Reader, big.NewInt(10))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
numbers = append(numbers, fmt.Sprintf("%d", num.Int64()))
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get random separator
|
||||
getSeparator := func() string {
|
||||
if config.PassphraseUseSpecial && len(config.SpecialChars) > 0 {
|
||||
sepIndex, err := rand.Int(rand.Reader, big.NewInt(int64(len(config.SpecialChars))))
|
||||
if err != nil {
|
||||
return "-" // fallback
|
||||
}
|
||||
return string(config.SpecialChars[sepIndex.Int64()])
|
||||
}
|
||||
return "-"
|
||||
}
|
||||
|
||||
// Default separator for non-random cases
|
||||
defaultSeparator := "-"
|
||||
|
||||
// Combine based on number position
|
||||
var result string
|
||||
if len(numbers) == 0 {
|
||||
// No numbers, join words with random separators
|
||||
if config.PassphraseUseSpecial && len(config.SpecialChars) > 0 {
|
||||
var parts []string
|
||||
for i, word := range words {
|
||||
parts = append(parts, word)
|
||||
if i < len(words)-1 { // Don't add separator after last word
|
||||
parts = append(parts, getSeparator())
|
||||
}
|
||||
}
|
||||
result = strings.Join(parts, "")
|
||||
} else {
|
||||
result = strings.Join(words, defaultSeparator)
|
||||
}
|
||||
} else {
|
||||
switch config.NumberPosition {
|
||||
case "start":
|
||||
numberStr := strings.Join(numbers, "")
|
||||
if config.PassphraseUseSpecial && len(config.SpecialChars) > 0 {
|
||||
var parts []string
|
||||
parts = append(parts, numberStr)
|
||||
for i, word := range words {
|
||||
parts = append(parts, getSeparator(), word)
|
||||
if i < len(words)-1 {
|
||||
parts = append(parts, getSeparator())
|
||||
}
|
||||
}
|
||||
result = strings.Join(parts, "")
|
||||
} else {
|
||||
result = numberStr + defaultSeparator + strings.Join(words, defaultSeparator)
|
||||
}
|
||||
case "end":
|
||||
numberStr := strings.Join(numbers, "")
|
||||
if config.PassphraseUseSpecial && len(config.SpecialChars) > 0 {
|
||||
var parts []string
|
||||
for i, word := range words {
|
||||
parts = append(parts, word)
|
||||
if i < len(words)-1 {
|
||||
parts = append(parts, getSeparator())
|
||||
}
|
||||
}
|
||||
parts = append(parts, getSeparator(), numberStr)
|
||||
result = strings.Join(parts, "")
|
||||
} else {
|
||||
result = strings.Join(words, defaultSeparator) + defaultSeparator + numberStr
|
||||
}
|
||||
case "each":
|
||||
var parts []string
|
||||
for i, word := range words {
|
||||
parts = append(parts, word)
|
||||
|
||||
// Add the specified number of digits after each word
|
||||
for j := 0; j < config.NumberCount; j++ {
|
||||
num, err := rand.Int(rand.Reader, big.NewInt(10))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("%d", num.Int64()))
|
||||
}
|
||||
|
||||
// Add separator after each word+numbers (except last)
|
||||
if i < len(words)-1 {
|
||||
parts = append(parts, getSeparator())
|
||||
}
|
||||
}
|
||||
result = strings.Join(parts, "")
|
||||
default:
|
||||
numberStr := strings.Join(numbers, "")
|
||||
if config.PassphraseUseSpecial && len(config.SpecialChars) > 0 {
|
||||
var parts []string
|
||||
for i, word := range words {
|
||||
parts = append(parts, word)
|
||||
if i < len(words)-1 {
|
||||
parts = append(parts, getSeparator())
|
||||
}
|
||||
}
|
||||
parts = append(parts, getSeparator(), numberStr)
|
||||
result = strings.Join(parts, "")
|
||||
} else {
|
||||
result = strings.Join(words, defaultSeparator) + defaultSeparator + numberStr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func ensureNoConsecutive(password, charset string) (string, error) {
|
||||
passwordRunes := []rune(password)
|
||||
maxAttempts := 1000
|
||||
|
||||
for attempt := 0; attempt < maxAttempts; attempt++ {
|
||||
hasConsecutive := false
|
||||
|
||||
for i := 0; i < len(passwordRunes)-1; i++ {
|
||||
if passwordRunes[i] == passwordRunes[i+1] {
|
||||
// Replace the second character
|
||||
newChar, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
passwordRunes[i+1] = rune(charset[newChar.Int64()])
|
||||
hasConsecutive = true
|
||||
}
|
||||
}
|
||||
|
||||
if !hasConsecutive {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return string(passwordRunes), nil
|
||||
}
|
||||
Reference in New Issue
Block a user