|
1 | 1 | package generator |
2 | 2 |
|
3 | 3 | import ( |
4 | | - "encoding/hex" |
| 4 | + "encoding/binary" |
5 | 5 |
|
6 | 6 | "golang.org/x/crypto/argon2" |
7 | 7 | ) |
8 | 8 |
|
9 | | -// create a deterministic 8-character password with optional salt |
| 9 | +// create a deterministic password of specified length |
10 | 10 | func Generate(baseWord string, salt string, length int) string { |
| 11 | + // combine base word with option salt |
11 | 12 | input := baseWord |
12 | 13 | if salt != "" { |
13 | 14 | input = baseWord + salt |
14 | 15 | } |
15 | 16 |
|
16 | | - // use Argon2id, parameters: time=3, memory=256mb, threads=4 |
17 | | - hash := argon2.IDKey([]byte(input), []byte("detergen-v1"), 3, 256*1024, 4, 32) |
18 | | - hashString := hex.EncodeToString(hash) |
| 17 | + // use Argon2id for hashing |
| 18 | + // parameters: time=3, memory=256MB, threads=4 |
| 19 | + // These are OWASP recommended parameters for password hashing |
| 20 | + hash := argon2.IDKey([]byte(input), []byte("detergen-v1"), 3, 256*1024, 4, 64) |
19 | 21 |
|
| 22 | + // character sets |
20 | 23 | uppercase := "ABCDEFGHIJKLMNOPQRSTUVWXYZ" |
21 | 24 | lowercase := "abcdefghijklmnopqrstuvwxyz" |
22 | 25 | numbers := "0123456789" |
23 | 26 | special := "!@#$%^&*" |
24 | 27 |
|
25 | | - if length < 4 { |
26 | | - length = 4 |
| 28 | + // ensure min length |
| 29 | + if length < 8 { |
| 30 | + length = 8 |
27 | 31 | } |
28 | 32 |
|
29 | | - password := make([]byte, length) |
| 33 | + // ensure max length |
| 34 | + if length > 128 { |
| 35 | + length = 128 |
| 36 | + } |
30 | 37 |
|
31 | 38 | /* we need at least one of each type so we need to guarantee that |
32 | 39 | * first then fill the remaining positions with mixed characters |
33 | 40 | */ |
| 41 | + password := make([]byte, length) |
| 42 | + |
34 | 43 | // guaranteed characters, one of each type |
35 | | - password[0] = uppercase[int(hashString[0])%len(uppercase)] |
36 | | - password[1] = lowercase[int(hashString[1])%len(lowercase)] |
37 | | - password[2] = numbers[int(hashString[2])%len(numbers)] |
38 | | - password[3] = special[int(hashString[3])%len(special)] |
| 44 | + password[0] = selectChar(hash, 0, uppercase) |
| 45 | + password[1] = selectChar(hash, 1, lowercase) |
| 46 | + password[2] = selectChar(hash, 2, numbers) |
| 47 | + password[3] = selectChar(hash, 3, special) |
39 | 48 |
|
40 | 49 | // fill remaining positions with mixed characters |
41 | 50 | allChars := uppercase + lowercase + numbers + special |
42 | 51 | for i := 4; i < length; i++ { |
43 | | - password[i] = allChars[int(hashString[i%len(hashString)])%len(allChars)] |
| 52 | + password[i] = selectChar(hash, i, allChars) |
44 | 53 | } |
45 | 54 |
|
46 | | - // we shuffle the password with the hash for deterministic shuffling |
| 55 | + // shuffle the password using the hash for deterministic shuffling |
47 | 56 | for i := len(password) - 1; i > 0; i-- { |
48 | | - // use different parts of the hash for shuffling |
49 | | - j := int(hashString[(8+i)%len(hashString)]) % (i + 1) |
| 57 | + // use hash bytes for shuffling with unbiased selection |
| 58 | + j := int(selectIndex(hash, length+i, i+1)) |
50 | 59 | password[i], password[j] = password[j], password[i] |
51 | 60 | } |
52 | 61 |
|
53 | 62 | return string(password) |
54 | 63 | } |
| 64 | + |
| 65 | +// use rejection sampling to avoid modulo bias |
| 66 | +func selectChar(hash []byte, offset int, charset string) byte { |
| 67 | + charsetLen := len(charset) |
| 68 | + |
| 69 | + // 4 bytes from hash to create a uint32 |
| 70 | + hashOffset := (offset * 4) % len(hash) |
| 71 | + if hashOffset+4 > len(hash) { |
| 72 | + hashOffset = len(hash) - 4 |
| 73 | + } |
| 74 | + |
| 75 | + value := binary.BigEndian.Uint32(hash[hashOffset : hashOffset+4]) |
| 76 | + |
| 77 | + // calculate the largest multiple of charsetLen that fits in uint32 |
| 78 | + maxValid := (0xFFFFFFFF / uint32(charsetLen)) * uint32(charsetLen) |
| 79 | + |
| 80 | + // if value is in the biased range, fold it back (deterministic rejection) |
| 81 | + if value >= maxValid { |
| 82 | + value = value % maxValid |
| 83 | + } |
| 84 | + |
| 85 | + return charset[value%uint32(charsetLen)] |
| 86 | +} |
| 87 | + |
| 88 | +// returns an unbiased index in range [0, max] |
| 89 | +func selectIndex(hash []byte, offset int, max int) uint32 { |
| 90 | + if max <= 1 { |
| 91 | + return 0 |
| 92 | + } |
| 93 | + |
| 94 | + hashOffset := (offset * 4) % len(hash) |
| 95 | + if hashOffset+4 > len(hash) { |
| 96 | + hashOffset = len(hash) - 4 |
| 97 | + } |
| 98 | + |
| 99 | + value := binary.BigEndian.Uint32(hash[hashOffset : hashOffset+4]) |
| 100 | + |
| 101 | + maxValid := (0xFFFFFFFF / uint32(max)) * uint32(max) |
| 102 | + |
| 103 | + if value >= maxValid { |
| 104 | + value = value % maxValid |
| 105 | + } |
| 106 | + |
| 107 | + return value % uint32(max) |
| 108 | +} |
0 commit comments