1
0
Fork 0
hats/pkg/passgen/generator.go

146 lines
3.2 KiB
Go
Raw Permalink Normal View History

2024-05-13 20:42:52 +00:00
package passgen
import (
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"fmt"
"math/big"
"regexp"
"strconv"
"strings"
"unicode/utf16"
"ekyu.moe/base91"
"github.com/aead/skein"
)
func (p *Passgen) Generate() string {
switch p.Version {
case 1:
return p.V1()
default:
return p.V2()
}
}
func (p *Passgen) V2() string {
charset := p.createCharset()
var out string
for i := 0; true; i++ {
if len(out) >= p.Length { // password is long enough
if p.containsNecessaryCharacters(out[0:p.Length]) { // password contains all necessary character classes
break
} else { // password is missing character classes, keep second half and keep generating if necessary
out = out[int(len(out)/2):]
}
}
passphrasePart := strings.Repeat(p.Passphrase, i+1)
hash := sha256.Sum256([]byte(passphrasePart + p.Salt))
// convert hash array of bytes into bigint
num := new(big.Int)
num.SetString(hex.EncodeToString(hash[:]), 16)
// encode bigint in custom character set
encoded := encodeToCustomCharset(num, charset)
out = fmt.Sprintf("%s%s", out, encoded)
}
return out[0:p.Length]
}
func (p *Passgen) createCharset() string {
charset := ""
if !p.NoUppers {
charset += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
}
charset += "abcdefghijklmnopqrstuvwxyz"
if !p.NoNumbers {
charset += "0123456789"
}
if !p.NoSpecials {
charset += p.CustomSpecials
}
return charset
}
func (p *Passgen) containsNecessaryCharacters(password string) bool {
if !p.NoNumbers {
if matches, _ := regexp.MatchString("[0-9]", password); !matches {
return false
}
}
if !p.NoUppers {
if matches, _ := regexp.MatchString("[A-Z]", password); !matches {
return false
}
}
if !p.NoSpecials {
customSpecials := p.CustomSpecials
escapeChars := []string{"-", "[", "]", "^"}
for _, e := range escapeChars {
customSpecials = strings.ReplaceAll(customSpecials, e, "\\"+e)
}
if matches, _ := regexp.MatchString(fmt.Sprintf("[%s]", customSpecials), password); !matches {
return false
}
}
if matches, _ := regexp.MatchString("[a-z]", password); !matches {
return false
}
return true
}
func encodeToCustomCharset(v *big.Int, charset string) string {
charsetLength := big.NewInt(int64(len(charset)))
var ret string
for {
if v.Cmp(big.NewInt(0)) == 0 {
break
}
remainder := new(big.Int).Mod(v, charsetLength)
v = new(big.Int).Div(v, charsetLength)
ret = fmt.Sprintf("%c%s", charset[remainder.Uint64()], ret)
}
return ret
}
func (p *Passgen) V1() string {
encoded := utf16.Encode([]rune(p.Passphrase + p.Salt))
var hashOut [64]byte
skein.Sum512(&hashOut, convertUTF16ToLittleEndianBytes(encoded), nil)
hashHex := hex.EncodeToString(hashOut[:])
start := hexToInt(string(hashHex[0])) + hexToInt(string(hashHex[1])) + hexToInt(string(hashHex[2]))
end := start + p.Length
encoded91 := base91.EncodeToString([]byte(hashHex))
for {
if len(encoded91) > end {
break
}
encoded91 = encoded91 + encoded91
}
return encoded91[start:end]
}
func convertUTF16ToLittleEndianBytes(u []uint16) []byte {
b := make([]byte, 2*len(u))
for index, value := range u {
binary.LittleEndian.PutUint16(b[index*2:], value)
}
return b
}
func hexToInt(hexString string) int {
i, _ := strconv.ParseInt(hexString, 16, 0)
return int(i)
}