Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion cmd/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,30 @@ Use different salts (like site names) to create unique passwords for different s

Example:
dg generate mypassword --salt facebook
dg generate mypassword --salt twitter`,
dg generate mypassword --salt twitter -l 16
dg generate mypassword`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
baseWord := args[0]

if salt == "" {
fmt.Println("Warning: No salt provided. Using base word only.")
fmt.Println("Consider using --salt for site-specific passwords.")
fmt.Println()
}

// enforce minimum length
if length < 8 {
length = 8
fmt.Println("Note: Minimum password length is 8 characters for your security.")
}

// enforce maximum length
if length > 128 {
length = 128
fmt.Println("Note: Maximum password length is 128 characters.")
}

password := generator.Generate(baseWord, salt, length)
fmt.Println(password)
},
Expand Down
20 changes: 12 additions & 8 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,18 @@ import (

// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "detergen",
Short: "A brief description of your application",
Long: `A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:

Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
Use: "dg",
Short: "Deterministic password generator",
Long: `detergen (dg) is a deterministic password generator that creates secure,
reproducible passwords using Argon2 hashing.

The same base word and salt will always produce the same password, allowing
you to recreate passwords without storing them. Use different salts (like
site names) to generate unique passwords for different services.

Example:
dg generate myword -s facebook
dg generate myword -s twitter -l 16`,
// Uncomment the following line if your bare application
// has an action associated with it:
// Run: func(cmd *cobra.Command, args []string) { },
Expand Down
86 changes: 70 additions & 16 deletions internal/generator/generator.go
Original file line number Diff line number Diff line change
@@ -1,54 +1,108 @@
package generator

import (
"encoding/hex"
"encoding/binary"

"golang.org/x/crypto/argon2"
)

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

// use Argon2id, parameters: time=3, memory=256mb, threads=4
hash := argon2.IDKey([]byte(input), []byte("detergen-v1"), 3, 256*1024, 4, 32)
hashString := hex.EncodeToString(hash)
// use Argon2id for hashing
// parameters: time=3, memory=256MB, threads=4
// These are OWASP recommended parameters for password hashing
hash := argon2.IDKey([]byte(input), []byte("detergen-v1"), 3, 256*1024, 4, 64)

// character sets
uppercase := "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
lowercase := "abcdefghijklmnopqrstuvwxyz"
numbers := "0123456789"
special := "!@#$%^&*"

if length < 4 {
length = 4
// ensure min length
if length < 8 {
length = 8
}

password := make([]byte, length)
// ensure max length
if length > 128 {
length = 128
}

/* we need at least one of each type so we need to guarantee that
* first then fill the remaining positions with mixed characters
*/
password := make([]byte, length)

// guaranteed characters, one of each type
password[0] = uppercase[int(hashString[0])%len(uppercase)]
password[1] = lowercase[int(hashString[1])%len(lowercase)]
password[2] = numbers[int(hashString[2])%len(numbers)]
password[3] = special[int(hashString[3])%len(special)]
password[0] = selectChar(hash, 0, uppercase)
password[1] = selectChar(hash, 1, lowercase)
password[2] = selectChar(hash, 2, numbers)
password[3] = selectChar(hash, 3, special)

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

// we shuffle the password with the hash for deterministic shuffling
// shuffle the password using the hash for deterministic shuffling
for i := len(password) - 1; i > 0; i-- {
// use different parts of the hash for shuffling
j := int(hashString[(8+i)%len(hashString)]) % (i + 1)
// use hash bytes for shuffling with unbiased selection
j := int(selectIndex(hash, length+i, i+1))
password[i], password[j] = password[j], password[i]
}

return string(password)
}

// use rejection sampling to avoid modulo bias
func selectChar(hash []byte, offset int, charset string) byte {
charsetLen := len(charset)

// 4 bytes from hash to create a uint32
hashOffset := (offset * 4) % len(hash)
if hashOffset+4 > len(hash) {
hashOffset = len(hash) - 4
}

value := binary.BigEndian.Uint32(hash[hashOffset : hashOffset+4])

// calculate the largest multiple of charsetLen that fits in uint32
maxValid := (0xFFFFFFFF / uint32(charsetLen)) * uint32(charsetLen)

// if value is in the biased range, fold it back (deterministic rejection)
if value >= maxValid {
value = value % maxValid
}

return charset[value%uint32(charsetLen)]
}

// returns an unbiased index in range [0, max]
func selectIndex(hash []byte, offset int, max int) uint32 {
if max <= 1 {
return 0
}

hashOffset := (offset * 4) % len(hash)
if hashOffset+4 > len(hash) {
hashOffset = len(hash) - 4
}

value := binary.BigEndian.Uint32(hash[hashOffset : hashOffset+4])

maxValid := (0xFFFFFFFF / uint32(max)) * uint32(max)

if value >= maxValid {
value = value % maxValid
}

return value % uint32(max)
}