]> Cypherpunks repositories - gostls13.git/commitdiff
crypto/bcrypt: new package
authorJeff Hodges <jeff@somethingsimilar.com>
Mon, 19 Sep 2011 14:29:02 +0000 (10:29 -0400)
committerAdam Langley <agl@golang.org>
Mon, 19 Sep 2011 14:29:02 +0000 (10:29 -0400)
A port of Provos and Mazières's adapative hashing algorithm. See http://www.usenix.org/events/usenix99/provos/provos_html/node1.html

R=bradfitz, agl, rsc, dchest
CC=golang-dev
https://golang.org/cl/4964078

src/pkg/Makefile
src/pkg/crypto/bcrypt/Makefile [new file with mode: 0644]
src/pkg/crypto/bcrypt/base64.go [new file with mode: 0644]
src/pkg/crypto/bcrypt/bcrypt.go [new file with mode: 0644]
src/pkg/crypto/bcrypt/bcrypt_test.go [new file with mode: 0644]

index c7e65c029e11f1f8628b4152975076d72da6beb7..85c5031e517d85b579b886b35bb3e4c69615a075 100644 (file)
@@ -33,6 +33,7 @@ DIRS=\
        crypto\
        crypto/aes\
        crypto/blowfish\
+       crypto/bcrypt\
        crypto/cast5\
        crypto/cipher\
        crypto/des\
diff --git a/src/pkg/crypto/bcrypt/Makefile b/src/pkg/crypto/bcrypt/Makefile
new file mode 100644 (file)
index 0000000..3c83d9c
--- /dev/null
@@ -0,0 +1,12 @@
+# Copyright 2011 The Go Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+include ../../../Make.inc
+
+TARG=crypto/bcrypt
+GOFILES=\
+       base64.go \
+       bcrypt.go
+
+include ../../../Make.pkg
diff --git a/src/pkg/crypto/bcrypt/base64.go b/src/pkg/crypto/bcrypt/base64.go
new file mode 100644 (file)
index 0000000..ed6cea7
--- /dev/null
@@ -0,0 +1,38 @@
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package bcrypt
+
+import (
+       "encoding/base64"
+       "os"
+)
+
+const alphabet = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
+
+var bcEncoding = base64.NewEncoding(alphabet)
+
+func base64Encode(src []byte) []byte {
+       n := bcEncoding.EncodedLen(len(src))
+       dst := make([]byte, n)
+       bcEncoding.Encode(dst, src)
+       for dst[n-1] == '=' {
+               n--
+       }
+       return dst[:n]
+}
+
+func base64Decode(src []byte) ([]byte, os.Error) {
+       numOfEquals := 4 - (len(src) % 4)
+       for i := 0; i < numOfEquals; i++ {
+               src = append(src, '=')
+       }
+
+       dst := make([]byte, bcEncoding.DecodedLen(len(src)))
+       n, err := bcEncoding.Decode(dst, src)
+       if err != nil {
+               return nil, err
+       }
+       return dst[:n], nil
+}
diff --git a/src/pkg/crypto/bcrypt/bcrypt.go b/src/pkg/crypto/bcrypt/bcrypt.go
new file mode 100644 (file)
index 0000000..1e8ccfa
--- /dev/null
@@ -0,0 +1,282 @@
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package bcrypt implements Provos and Mazières's bcrypt adapative hashing
+// algorithm. See http://www.usenix.org/event/usenix99/provos/provos.pdf
+package bcrypt
+
+// The code is a port of Provos and Mazières's C implementation. 
+import (
+       "crypto/blowfish"
+       "crypto/rand"
+       "crypto/subtle"
+       "fmt"
+       "io"
+       "os"
+       "strconv"
+)
+
+const (
+       MinCost     int = 4  // the minimum allowable cost as passed in to GenerateFromPassword
+       MaxCost     int = 31 // the maximum allowable cost as passed in to GenerateFromPassword
+       DefaultCost int = 10 // the cost that will actually be set if a cost below MinCost is passed into GenerateFromPassword
+)
+
+// The error returned from CompareHashAndPassword when a password and hash do
+// not match.
+var MismatchedHashAndPasswordError = os.NewError("crypto/bcrypt: hashedPassword is not the hash of the given password")
+
+// The error returned from CompareHashAndPassword when a hash is too short to
+// be a bcrypt hash.
+var HashTooShortError = os.NewError("crypto/bcrypt: hashedSecret too short to be a bcrypted password")
+
+// The error returned from CompareHashAndPassword when a hash was created with
+// a bcrypt algorithm newer than this implementation.
+type HashVersionTooNewError byte
+
+func (hv HashVersionTooNewError) String() string {
+       return fmt.Sprintf("crypto/bcrypt: bcrypt algorithm version '%c' requested is newer than current version '%c'", byte(hv), majorVersion)
+}
+
+// The error returned from CompareHashAndPassword when a hash starts with something other than '$'
+type InvalidHashPrefixError byte
+
+func (ih InvalidHashPrefixError) String() string {
+       return fmt.Sprintf("crypto/bcrypt: bcrypt hashes must start with '$', but hashedSecret started with '%c'", byte(ih))
+}
+
+type InvalidCostError int
+
+func (ic InvalidCostError) String() string {
+       return fmt.Sprintf("crypto/bcrypt: cost %d is outside allowed range (%d,%d)", int(ic), int(MinCost), int(MaxCost))
+}
+
+const (
+       majorVersion       = '2'
+       minorVersion       = 'a'
+       maxSaltSize        = 16
+       maxCryptedHashSize = 23
+       encodedSaltSize    = 22
+       encodedHashSize    = 31
+       minHashSize        = 59
+)
+
+// magicCipherData is an IV for the 64 Blowfish encryption calls in
+// bcrypt(). It's the string "OrpheanBeholderScryDoubt" in big-endian bytes.
+var magicCipherData = []byte{
+       0x4f, 0x72, 0x70, 0x68,
+       0x65, 0x61, 0x6e, 0x42,
+       0x65, 0x68, 0x6f, 0x6c,
+       0x64, 0x65, 0x72, 0x53,
+       0x63, 0x72, 0x79, 0x44,
+       0x6f, 0x75, 0x62, 0x74,
+}
+
+type hashed struct {
+       hash  []byte
+       salt  []byte
+       cost  uint32 // allowed range is MinCost to MaxCost
+       major byte
+       minor byte
+}
+
+// GenerateFromPassword returns the bcrypt hash of the password at the given
+// cost. If the cost given is less than MinCost, the cost will be set to
+// MinCost, instead. Use CompareHashAndPassword, as defined in this package,
+// to compare the returned hashed password with its cleartext version.
+func GenerateFromPassword(password []byte, cost int) ([]byte, os.Error) {
+       p, err := newFromPassword(password, cost)
+       if err != nil {
+               return nil, err
+       }
+       return p.Hash(), nil
+}
+
+// CompareHashAndPassword compares a bcrypt hashed password with its possible
+// plaintext equivalent. Note: Using bytes.Equal for this job is
+// insecure. Returns nil on success, or an error on failure.
+func CompareHashAndPassword(hashedPassword, password []byte) os.Error {
+       p, err := newFromHash(hashedPassword)
+       if err != nil {
+               return err
+       }
+
+       otherHash, err := bcrypt(password, p.cost, p.salt)
+       if err != nil {
+               return err
+       }
+
+       otherP := &hashed{otherHash, p.salt, p.cost, p.major, p.minor}
+       if subtle.ConstantTimeCompare(p.Hash(), otherP.Hash()) == 1 {
+               return nil
+       }
+
+       return MismatchedHashAndPasswordError
+}
+
+func newFromPassword(password []byte, cost int) (*hashed, os.Error) {
+       if cost < MinCost {
+               cost = DefaultCost
+       }
+       p := new(hashed)
+       p.major = majorVersion
+       p.minor = minorVersion
+
+       err := checkCost(cost)
+       if err != nil {
+               return nil, err
+       }
+       p.cost = uint32(cost)
+
+       unencodedSalt := make([]byte, maxSaltSize)
+       _, err = io.ReadFull(rand.Reader, unencodedSalt)
+       if err != nil {
+               return nil, err
+       }
+
+       p.salt = base64Encode(unencodedSalt)
+       hash, err := bcrypt(password, p.cost, p.salt)
+       if err != nil {
+               return nil, err
+       }
+       p.hash = hash
+       return p, err
+}
+
+func newFromHash(hashedSecret []byte) (*hashed, os.Error) {
+       if len(hashedSecret) < minHashSize {
+               return nil, HashTooShortError
+       }
+       p := new(hashed)
+       n, err := p.decodeVersion(hashedSecret)
+       if err != nil {
+               return nil, err
+       }
+       hashedSecret = hashedSecret[n:]
+       n, err = p.decodeCost(hashedSecret)
+       if err != nil {
+               return nil, err
+       }
+       hashedSecret = hashedSecret[n:]
+
+       // The "+2" is here because we'll have to append at most 2 '=' to the salt
+       // when base64 decoding it in expensiveBlowfishSetup().
+       p.salt = make([]byte, encodedSaltSize, encodedSaltSize+2)
+       copy(p.salt, hashedSecret[:encodedSaltSize])
+
+       hashedSecret = hashedSecret[encodedSaltSize:]
+       p.hash = make([]byte, len(hashedSecret))
+       copy(p.hash, hashedSecret)
+
+       return p, nil
+}
+
+func bcrypt(password []byte, cost uint32, salt []byte) ([]byte, os.Error) {
+       cipherData := make([]byte, len(magicCipherData))
+       copy(cipherData, magicCipherData)
+
+       c, err := expensiveBlowfishSetup(password, cost, salt)
+       if err != nil {
+               return nil, err
+       }
+
+       for i := 0; i < 24; i += 8 {
+               for j := 0; j < 64; j++ {
+                       c.Encrypt(cipherData[i:i+8], cipherData[i:i+8])
+               }
+       }
+
+       // Bug compatibility with C bcrypt implementations. We only encode 23 of
+       // the 24 bytes encrypted.
+       hsh := base64Encode(cipherData[:maxCryptedHashSize])
+       return hsh, nil
+}
+
+func expensiveBlowfishSetup(key []byte, cost uint32, salt []byte) (*blowfish.Cipher, os.Error) {
+
+       csalt, err := base64Decode(salt)
+       if err != nil {
+               return nil, err
+       }
+
+       // Bug compatibility with C bcrypt implementations. They use the trailing
+       // NULL in the key string during expansion.
+       ckey := append(key, 0)
+
+       c, err := blowfish.NewSaltedCipher(ckey, csalt)
+       if err != nil {
+               return nil, err
+       }
+
+       rounds := 1 << cost
+       for i := 0; i < rounds; i++ {
+               blowfish.ExpandKey(ckey, c)
+               blowfish.ExpandKey(csalt, c)
+       }
+
+       return c, nil
+}
+
+func (p *hashed) Hash() []byte {
+       arr := make([]byte, 60)
+       arr[0] = '$'
+       arr[1] = p.major
+       n := 2
+       if p.minor != 0 {
+               arr[2] = p.minor
+               n = 3
+       }
+       arr[n] = '$'
+       n += 1
+       copy(arr[n:], []byte(fmt.Sprintf("%02d", p.cost)))
+       n += 2
+       arr[n] = '$'
+       n += 1
+       copy(arr[n:], p.salt)
+       n += encodedSaltSize
+       copy(arr[n:], p.hash)
+       n += encodedHashSize
+       return arr[:n]
+}
+
+func (p *hashed) decodeVersion(sbytes []byte) (int, os.Error) {
+       if sbytes[0] != '$' {
+               return -1, InvalidHashPrefixError(sbytes[0])
+       }
+       if sbytes[1] > majorVersion {
+               return -1, HashVersionTooNewError(sbytes[1])
+       }
+       p.major = sbytes[1]
+       n := 3
+       if sbytes[2] != '$' {
+               p.minor = sbytes[2]
+               n++
+       }
+       return n, nil
+}
+
+// sbytes should begin where decodeVersion left off.
+func (p *hashed) decodeCost(sbytes []byte) (int, os.Error) {
+       cost, err := strconv.Atoi(string(sbytes[0:2]))
+       if err != nil {
+               return -1, err
+       }
+       err = checkCost(cost)
+       if err != nil {
+               return -1, err
+       }
+       p.cost = uint32(cost)
+       return 3, nil
+}
+
+func (p *hashed) String() string {
+       return fmt.Sprintf("&{hash: %#v, salt: %#v, cost: %d, major: %c, minor: %c}", string(p.hash), p.salt, p.cost, p.major, p.minor)
+}
+
+func checkCost(cost int) os.Error {
+       if cost < MinCost || cost > MaxCost {
+               return InvalidCostError(cost)
+       }
+       return nil
+}
diff --git a/src/pkg/crypto/bcrypt/bcrypt_test.go b/src/pkg/crypto/bcrypt/bcrypt_test.go
new file mode 100644 (file)
index 0000000..89eca0a
--- /dev/null
@@ -0,0 +1,195 @@
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package bcrypt
+
+import (
+       "bytes"
+       "os"
+       "testing"
+)
+
+func TestBcryptingIsEasy(t *testing.T) {
+       pass := []byte("mypassword")
+       hp, err := GenerateFromPassword(pass, 0)
+       if err != nil {
+               t.Fatalf("GenerateFromPassword error: %s", err)
+       }
+
+       if CompareHashAndPassword(hp, pass) != nil {
+               t.Errorf("%v should hash %s correctly", hp, pass)
+       }
+
+       notPass := "notthepass"
+       err = CompareHashAndPassword(hp, []byte(notPass))
+       if err != MismatchedHashAndPasswordError {
+               t.Errorf("%v and %s should be mismatched", hp, notPass)
+       }
+}
+
+func TestBcryptingIsCorrect(t *testing.T) {
+       pass := []byte("allmine")
+       salt := []byte("XajjQvNhvvRt5GSeFk1xFe")
+       expectedHash := []byte("$2a$10$XajjQvNhvvRt5GSeFk1xFeyqRrsxkhBkUiQeg0dt.wU1qD4aFDcga")
+
+       hash, err := bcrypt(pass, 10, salt)
+       if err != nil {
+               t.Fatalf("bcrypt blew up: %v", err)
+       }
+       if !bytes.HasSuffix(expectedHash, hash) {
+               t.Errorf("%v should be the suffix of %v", hash, expectedHash)
+       }
+
+       h, err := newFromHash(expectedHash)
+       if err != nil {
+               t.Errorf("Unable to parse %s: %v", string(expectedHash), err)
+       }
+
+       // This is not the safe way to compare these hashes. We do this only for
+       // testing clarity. Use bcrypt.CompareHashAndPassword()
+       if err == nil && !bytes.Equal(expectedHash, h.Hash()) {
+               t.Errorf("Parsed hash %v should equal %v", h.Hash(), expectedHash)
+       }
+}
+
+func TestTooLongPasswordsWork(t *testing.T) {
+       salt := []byte("XajjQvNhvvRt5GSeFk1xFe")
+       // One byte over the usual 56 byte limit that blowfish has
+       tooLongPass := []byte("012345678901234567890123456789012345678901234567890123456")
+       tooLongExpected := []byte("$2a$10$XajjQvNhvvRt5GSeFk1xFe5l47dONXg781AmZtd869sO8zfsHuw7C")
+       hash, err := bcrypt(tooLongPass, 10, salt)
+       if err != nil {
+               t.Fatalf("bcrypt blew up on long password: %v", err)
+       }
+       if !bytes.HasSuffix(tooLongExpected, hash) {
+               t.Errorf("%v should be the suffix of %v", hash, tooLongExpected)
+       }
+}
+
+type InvalidHashTest struct {
+       err  os.Error
+       hash []byte
+}
+
+var invalidTests = []InvalidHashTest{
+       {HashTooShortError, []byte("$2a$10$fooo")},
+       {HashTooShortError, []byte("$2a")},
+       {HashVersionTooNewError('3'), []byte("$3a$10$sssssssssssssssssssssshhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh")},
+       {InvalidHashPrefixError('%'), []byte("%2a$10$sssssssssssssssssssssshhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh")},
+       {InvalidCostError(32), []byte("$2a$32$sssssssssssssssssssssshhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh")},
+}
+
+func TestInvalidHashErrors(t *testing.T) {
+       check := func(name string, expected, err os.Error) {
+               if err == nil {
+                       t.Errorf("%s: Should have returned an error", name)
+               }
+               if err != nil && err != expected {
+                       t.Errorf("%s gave err %v but should have given %v", name, err.String(), expected.String())
+               }
+       }
+       for _, iht := range invalidTests {
+               _, err := newFromHash(iht.hash)
+               check("newFromHash", iht.err, err)
+               err = CompareHashAndPassword(iht.hash, []byte("anything"))
+               check("CompareHashAndPassword", iht.err, err)
+       }
+}
+
+func TestUnpaddedBase64Encoding(t *testing.T) {
+       original := []byte{101, 201, 101, 75, 19, 227, 199, 20, 239, 236, 133, 32, 30, 109, 243, 30}
+       encodedOriginal := []byte("XajjQvNhvvRt5GSeFk1xFe")
+
+       encoded := base64Encode(original)
+
+       if !bytes.Equal(encodedOriginal, encoded) {
+               t.Errorf("Encoded %v should have equaled %v", encoded, encodedOriginal)
+       }
+
+       decoded, err := base64Decode(encodedOriginal)
+       if err != nil {
+               t.Fatalf("base64Decode blew up: %s", err)
+       }
+
+       if !bytes.Equal(decoded, original) {
+               t.Errorf("Decoded %v should have equaled %v", decoded, original)
+       }
+}
+
+func TestCost(t *testing.T) {
+       if testing.Short() {
+               return
+       }
+
+       pass := []byte("mypassword")
+
+       for c := 0; c < MinCost; c++ {
+               p, _ := newFromPassword(pass, c)
+               if p.cost != uint32(DefaultCost) {
+                       t.Errorf("newFromPassword should default costs below %d to %d, but was %d", MinCost, DefaultCost, p.cost)
+               }
+       }
+
+       p, _ := newFromPassword(pass, 14)
+       if p.cost != 14 {
+               t.Errorf("newFromPassword should default cost to 14, but was %d", p.cost)
+       }
+
+       hp, _ := newFromHash(p.Hash())
+       if p.cost != hp.cost {
+               t.Errorf("newFromHash should maintain the cost at %d, but was %d", p.cost, hp.cost)
+       }
+
+       _, err := newFromPassword(pass, 32)
+       if err == nil {
+               t.Fatalf("newFromPassword: should return a cost error")
+       }
+       if err != InvalidCostError(32) {
+               t.Errorf("newFromPassword: should return cost error, got %#v", err)
+       }
+}
+
+func TestCostReturnsWithLeadingZeroes(t *testing.T) {
+       hp, _ := newFromPassword([]byte("abcdefgh"), 7)
+       cost := hp.Hash()[4:7]
+       expected := []byte("07$")
+
+       if !bytes.Equal(expected, cost) {
+               t.Errorf("single digit costs in hash should have leading zeros: was %v instead of %v", cost, expected)
+       }
+}
+
+func TestMinorNotRequired(t *testing.T) {
+       noMinorHash := []byte("$2$10$XajjQvNhvvRt5GSeFk1xFeyqRrsxkhBkUiQeg0dt.wU1qD4aFDcga")
+       h, err := newFromHash(noMinorHash)
+       if err != nil {
+               t.Fatalf("No minor hash blew up: %s", err)
+       }
+       if h.minor != 0 {
+               t.Errorf("Should leave minor version at 0, but was %d", h.minor)
+       }
+
+       if !bytes.Equal(noMinorHash, h.Hash()) {
+               t.Errorf("Should generate hash %v, but created %v", noMinorHash, h.Hash())
+       }
+}
+
+func BenchmarkEqual(b *testing.B) {
+       b.StopTimer()
+       passwd := []byte("somepasswordyoulike")
+       hash, _ := GenerateFromPassword(passwd, 10)
+       b.StartTimer()
+       for i := 0; i < b.N; i++ {
+               CompareHashAndPassword(hash, passwd)
+       }
+}
+
+func BenchmarkGeneration(b *testing.B) {
+       b.StopTimer()
+       passwd := []byte("mylongpassword1234")
+       b.StartTimer()
+       for i := 0; i < b.N; i++ {
+               GenerateFromPassword(passwd, 10)
+       }
+}