Skip to content

Commit 29a880c

Browse files
authored
Merge pull request #4 from danielkosgei/develop
Develop
2 parents cdec482 + 6b06532 commit 29a880c

3 files changed

Lines changed: 97 additions & 25 deletions

File tree

cmd/generate.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,30 @@ Use different salts (like site names) to create unique passwords for different s
3636
3737
Example:
3838
dg generate mypassword --salt facebook
39-
dg generate mypassword --salt twitter`,
39+
dg generate mypassword --salt twitter -l 16
40+
dg generate mypassword`,
4041
Args: cobra.ExactArgs(1),
4142
Run: func(cmd *cobra.Command, args []string) {
4243
baseWord := args[0]
4344

45+
if salt == "" {
46+
fmt.Println("Warning: No salt provided. Using base word only.")
47+
fmt.Println("Consider using --salt for site-specific passwords.")
48+
fmt.Println()
49+
}
50+
4451
// enforce minimum length
4552
if length < 8 {
4653
length = 8
4754
fmt.Println("Note: Minimum password length is 8 characters for your security.")
4855
}
56+
57+
// enforce maximum length
58+
if length > 128 {
59+
length = 128
60+
fmt.Println("Note: Maximum password length is 128 characters.")
61+
}
62+
4963
password := generator.Generate(baseWord, salt, length)
5064
fmt.Println(password)
5165
},

cmd/root.go

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,18 @@ import (
2323

2424
// rootCmd represents the base command when called without any subcommands
2525
var rootCmd = &cobra.Command{
26-
Use: "detergen",
27-
Short: "A brief description of your application",
28-
Long: `A longer description that spans multiple lines and likely contains
29-
examples and usage of using your application. For example:
30-
31-
Cobra is a CLI library for Go that empowers applications.
32-
This application is a tool to generate the needed files
33-
to quickly create a Cobra application.`,
26+
Use: "dg",
27+
Short: "Deterministic password generator",
28+
Long: `detergen (dg) is a deterministic password generator that creates secure,
29+
reproducible passwords using Argon2 hashing.
30+
31+
The same base word and salt will always produce the same password, allowing
32+
you to recreate passwords without storing them. Use different salts (like
33+
site names) to generate unique passwords for different services.
34+
35+
Example:
36+
dg generate myword -s facebook
37+
dg generate myword -s twitter -l 16`,
3438
// Uncomment the following line if your bare application
3539
// has an action associated with it:
3640
// Run: func(cmd *cobra.Command, args []string) { },

internal/generator/generator.go

Lines changed: 70 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,108 @@
11
package generator
22

33
import (
4-
"encoding/hex"
4+
"encoding/binary"
55

66
"golang.org/x/crypto/argon2"
77
)
88

9-
// create a deterministic 8-character password with optional salt
9+
// create a deterministic password of specified length
1010
func Generate(baseWord string, salt string, length int) string {
11+
// combine base word with option salt
1112
input := baseWord
1213
if salt != "" {
1314
input = baseWord + salt
1415
}
1516

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)
1921

22+
// character sets
2023
uppercase := "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
2124
lowercase := "abcdefghijklmnopqrstuvwxyz"
2225
numbers := "0123456789"
2326
special := "!@#$%^&*"
2427

25-
if length < 4 {
26-
length = 4
28+
// ensure min length
29+
if length < 8 {
30+
length = 8
2731
}
2832

29-
password := make([]byte, length)
33+
// ensure max length
34+
if length > 128 {
35+
length = 128
36+
}
3037

3138
/* we need at least one of each type so we need to guarantee that
3239
* first then fill the remaining positions with mixed characters
3340
*/
41+
password := make([]byte, length)
42+
3443
// 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)
3948

4049
// fill remaining positions with mixed characters
4150
allChars := uppercase + lowercase + numbers + special
4251
for i := 4; i < length; i++ {
43-
password[i] = allChars[int(hashString[i%len(hashString)])%len(allChars)]
52+
password[i] = selectChar(hash, i, allChars)
4453
}
4554

46-
// we shuffle the password with the hash for deterministic shuffling
55+
// shuffle the password using the hash for deterministic shuffling
4756
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))
5059
password[i], password[j] = password[j], password[i]
5160
}
5261

5362
return string(password)
5463
}
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

Comments
 (0)