mirror of https://gogs.blitter.com/RLabs/xs
191 lines
4.1 KiB
Go
191 lines
4.1 KiB
Go
package bcrypt
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"crypto/subtle"
|
|
"encoding/base64"
|
|
"errors"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
var (
|
|
InvalidRounds = errors.New("bcrypt: Invalid rounds parameter")
|
|
InvalidSalt = errors.New("bcrypt: Invalid salt supplied")
|
|
)
|
|
|
|
const (
|
|
MaxRounds = 31
|
|
MinRounds = 4
|
|
DefaultRounds = 12
|
|
SaltLen = 16
|
|
BlowfishRounds = 16
|
|
)
|
|
|
|
var enc = base64.NewEncoding("./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")
|
|
|
|
// Helper function to build the bcrypt hash string
|
|
// payload takes :
|
|
// * []byte -> which it base64 encodes it (trims padding "=") and writes it to the buffer
|
|
// * string -> which it writes straight to the buffer
|
|
func build_bcrypt_str(minor byte, rounds uint, payload ...interface{}) []byte {
|
|
rs := bytes.NewBuffer(make([]byte, 0, 61))
|
|
rs.WriteString("$2")
|
|
if minor >= 'a' {
|
|
rs.WriteByte(minor)
|
|
}
|
|
|
|
rs.WriteByte('$')
|
|
if rounds < 10 {
|
|
rs.WriteByte('0')
|
|
}
|
|
|
|
rs.WriteString(strconv.FormatUint(uint64(rounds), 10))
|
|
rs.WriteByte('$')
|
|
for _, p := range payload {
|
|
if pb, ok := p.([]byte); ok {
|
|
rs.WriteString(strings.TrimRight(enc.EncodeToString(pb), "="))
|
|
} else if ps, ok := p.(string); ok {
|
|
rs.WriteString(ps)
|
|
}
|
|
}
|
|
return rs.Bytes()
|
|
}
|
|
|
|
// Salt generation
|
|
func Salt(rounds ...int) (string, error) {
|
|
rb, err := SaltBytes(rounds...)
|
|
return string(rb), err
|
|
}
|
|
|
|
func SaltBytes(rounds ...int) (salt []byte, err error) {
|
|
r := DefaultRounds
|
|
if len(rounds) > 0 {
|
|
r = rounds[0]
|
|
if r < MinRounds || r > MaxRounds {
|
|
return nil, InvalidRounds
|
|
}
|
|
}
|
|
|
|
rnd := make([]byte, SaltLen)
|
|
read, err := rand.Read(rnd)
|
|
if read != SaltLen || err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return build_bcrypt_str('a', uint(r), rnd), nil
|
|
}
|
|
|
|
func consume(r *bytes.Buffer, b byte) bool {
|
|
got, err := r.ReadByte()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
if got != b {
|
|
r.UnreadByte()
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func Hash(password string, salt ...string) (ps string, err error) {
|
|
var s []byte
|
|
var pb []byte
|
|
|
|
if len(salt) == 0 {
|
|
s, err = SaltBytes()
|
|
if err != nil {
|
|
return
|
|
}
|
|
} else if len(salt) > 0 {
|
|
s = []byte(salt[0])
|
|
}
|
|
|
|
pb, err = HashBytes([]byte(password), s)
|
|
return string(pb), err
|
|
}
|
|
|
|
func HashBytes(password []byte, salt ...[]byte) (hash []byte, err error) {
|
|
var s []byte
|
|
|
|
if len(salt) == 0 {
|
|
s, err = SaltBytes()
|
|
if err != nil {
|
|
return
|
|
}
|
|
} else if len(salt) > 0 {
|
|
s = salt[0]
|
|
}
|
|
|
|
// TODO: use a regex? I hear go has bad regex performance a simple FSM seems faster
|
|
// "^\\$2([a-z]?)\\$([0-3][0-9])\\$([\\./A-Za-z0-9]{22}+)"
|
|
|
|
// Ok, extract the required information
|
|
minor := byte(0)
|
|
sr := bytes.NewBuffer(s)
|
|
|
|
if !consume(sr, '$') || !consume(sr, '2') {
|
|
return nil, InvalidSalt
|
|
}
|
|
|
|
if !consume(sr, '$') {
|
|
minor, _ = sr.ReadByte()
|
|
if minor != 'a' || !consume(sr, '$') {
|
|
return nil, InvalidSalt
|
|
}
|
|
}
|
|
|
|
rounds_bytes := make([]byte, 2)
|
|
read, err := sr.Read(rounds_bytes)
|
|
if err != nil || read != 2 {
|
|
return nil, InvalidSalt
|
|
}
|
|
|
|
if !consume(sr, '$') {
|
|
return nil, InvalidSalt
|
|
}
|
|
|
|
var rounds64 uint64
|
|
rounds64, err = strconv.ParseUint(string(rounds_bytes), 10, 0)
|
|
if err != nil {
|
|
return nil, InvalidSalt
|
|
}
|
|
|
|
rounds := uint(rounds64)
|
|
|
|
// TODO: can't we use base64.NewDecoder(enc, sr) ?
|
|
salt_bytes := make([]byte, 22)
|
|
read, err = sr.Read(salt_bytes)
|
|
if err != nil || read != 22 {
|
|
return nil, InvalidSalt
|
|
}
|
|
|
|
var saltb []byte
|
|
// encoding/base64 expects 4 byte blocks padded, since bcrypt uses only 22 bytes we need to go up
|
|
saltb, err = enc.DecodeString(string(salt_bytes) + "==")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// cipher expects null terminated input (go initializes everything with zero values so this works)
|
|
password_term := make([]byte, len(password)+1)
|
|
copy(password_term, password)
|
|
|
|
hashed := crypt_raw(password_term, saltb[:SaltLen], rounds)
|
|
return build_bcrypt_str(minor, rounds, string(salt_bytes), hashed[:len(bf_crypt_ciphertext)*4-1]), nil
|
|
}
|
|
|
|
func Match(password, hash string) bool {
|
|
return MatchBytes([]byte(password), []byte(hash))
|
|
}
|
|
|
|
func MatchBytes(password []byte, hash []byte) bool {
|
|
h, err := HashBytes(password, hash)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return subtle.ConstantTimeCompare(h, hash) == 1
|
|
}
|