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 }