134 lines
3.7 KiB
Go
134 lines
3.7 KiB
Go
package customer
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"crypto/subtle"
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"errors"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var rawURL = base64.RawURLEncoding
|
|
|
|
func RandomToken(n int) string {
|
|
if n < 16 {
|
|
n = 16
|
|
}
|
|
b := make([]byte, n)
|
|
_, _ = rand.Read(b)
|
|
return rawURL.EncodeToString(b)
|
|
}
|
|
|
|
func NewPasswordHash(password string) (salt string, hash string, err error) {
|
|
if len(strings.TrimSpace(password)) < 12 {
|
|
return "", "", errors.New("password must be at least 12 characters")
|
|
}
|
|
s := make([]byte, 16)
|
|
if _, err := rand.Read(s); err != nil {
|
|
return "", "", err
|
|
}
|
|
h := pbkdf2SHA256([]byte(password), s, 310000, 32)
|
|
return rawURL.EncodeToString(s), rawURL.EncodeToString(h), nil
|
|
}
|
|
func VerifyPassword(password, salt, hash string) bool {
|
|
s, err := rawURL.DecodeString(salt)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
want, err := rawURL.DecodeString(hash)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
got := pbkdf2SHA256([]byte(password), s, 310000, len(want))
|
|
return len(got) == len(want) && subtle.ConstantTimeCompare(got, want) == 1
|
|
}
|
|
func pbkdf2SHA256(password, salt []byte, iterations, keyLen int) []byte {
|
|
hLen := sha256.Size
|
|
blocks := (keyLen + hLen - 1) / hLen
|
|
out := make([]byte, 0, blocks*hLen)
|
|
for block := 1; block <= blocks; block++ {
|
|
mac := hmac.New(sha256.New, password)
|
|
mac.Write(salt)
|
|
var n [4]byte
|
|
binary.BigEndian.PutUint32(n[:], uint32(block))
|
|
mac.Write(n[:])
|
|
u := mac.Sum(nil)
|
|
t := append([]byte(nil), u...)
|
|
for i := 1; i < iterations; i++ {
|
|
mac = hmac.New(sha256.New, password)
|
|
mac.Write(u)
|
|
u = mac.Sum(nil)
|
|
for j := range t {
|
|
t[j] ^= u[j]
|
|
}
|
|
}
|
|
out = append(out, t...)
|
|
}
|
|
return out[:keyLen]
|
|
}
|
|
|
|
// RegistrationChallenge is a stateless, username-bound proof-of-work challenge.
|
|
// It prevents a solved challenge from being reused to create multiple different
|
|
// accounts while avoiding an unbounded server-side challenge store.
|
|
func RegistrationChallenge(secret, username string, ttlSeconds int) string {
|
|
if ttlSeconds <= 0 {
|
|
ttlSeconds = 300
|
|
}
|
|
nonce := RandomToken(18)
|
|
expires := time.Now().UTC().Add(time.Duration(ttlSeconds) * time.Second).Unix()
|
|
body := strings.ToLower(strings.TrimSpace(username)) + "|" + nonce + "|" + strconv.FormatInt(expires, 10)
|
|
mac := hmac.New(sha256.New, []byte(secret))
|
|
mac.Write([]byte(body))
|
|
return nonce + "." + strconv.FormatInt(expires, 10) + "." + rawURL.EncodeToString(mac.Sum(nil))
|
|
}
|
|
|
|
func VerifyRegistrationChallenge(secret, username, challenge string) bool {
|
|
parts := strings.Split(challenge, ".")
|
|
if len(parts) != 3 {
|
|
return false
|
|
}
|
|
expires, err := strconv.ParseInt(parts[1], 10, 64)
|
|
if err != nil || expires < time.Now().UTC().Unix() || expires > time.Now().UTC().Add(10*time.Minute).Unix() {
|
|
return false
|
|
}
|
|
body := strings.ToLower(strings.TrimSpace(username)) + "|" + parts[0] + "|" + parts[1]
|
|
mac := hmac.New(sha256.New, []byte(secret))
|
|
mac.Write([]byte(body))
|
|
want := mac.Sum(nil)
|
|
got, err := rawURL.DecodeString(parts[2])
|
|
return err == nil && len(got) == len(want) && subtle.ConstantTimeCompare(got, want) == 1
|
|
}
|
|
|
|
func VerifyRegistrationProof(username, challenge string, counter uint64, bits int) bool {
|
|
if bits <= 0 {
|
|
return true
|
|
}
|
|
if bits > 28 {
|
|
return false
|
|
}
|
|
msg := "nh-register|" + strings.ToLower(strings.TrimSpace(username)) + "|" + challenge + "|" + strconv.FormatUint(counter, 10)
|
|
h := sha256.Sum256([]byte(msg))
|
|
full := bits / 8
|
|
rem := bits % 8
|
|
for i := 0; i < full; i++ {
|
|
if h[i] != 0 {
|
|
return false
|
|
}
|
|
}
|
|
if rem > 0 {
|
|
mask := byte(0xff << (8 - rem))
|
|
return h[full]&mask == 0
|
|
}
|
|
return true
|
|
}
|
|
|
|
func HashInviteCode(code string) string {
|
|
h := sha256.Sum256([]byte(strings.TrimSpace(code)))
|
|
return base64.RawURLEncoding.EncodeToString(h[:])
|
|
}
|