Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions algorithm/bcrypt/decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,13 @@ func decoderParts(encodedDigest string) (variant Variant, parts []string, err er
return variant, nil, fmt.Errorf("%w: identifier '%s' is not an encoded %s digest", algorithm.ErrEncodedHashInvalidIdentifier, parts[1], AlgName)
}

return variant, parts[2:], nil
return variant, parts[1:], nil
}

func decode(variant Variant, parts []string) (digest algorithm.Digest, err error) {
identifier := parts[0]
parts = parts[1:]

countParts := len(parts)

var (
Expand All @@ -122,6 +125,8 @@ func decode(variant Variant, parts []string) (digest algorithm.Digest, err error
return nil, algorithm.ErrEncodedHashInvalidFormat
}

decoded.version = identifier

if decoded.iterations, err = strconv.Atoi(parts[0]); err != nil {
return nil, fmt.Errorf("%w: iterations could not be parsed: %v", algorithm.ErrEncodedHashInvalidOptionValue, err)
}
Expand Down Expand Up @@ -173,8 +178,10 @@ func decode(variant Variant, parts []string) (digest algorithm.Digest, err error

for _, param := range params {
switch param.Key {
case oV, oT:
case oV:
break
case oT:
decoded.version = param.Value
case oR:
decoded.iterations, err = param.Int()
default:
Expand Down
24 changes: 23 additions & 1 deletion algorithm/bcrypt/digest.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import (
type Digest struct {
variant Variant

version string

iterations int

salt, key []byte
Expand Down Expand Up @@ -41,6 +43,10 @@ func (d *Digest) MatchBytesAdvanced(passwordBytes []byte) (match bool, err error
return false, fmt.Errorf(algorithm.ErrFmtDigestMatch, AlgName, fmt.Errorf("%w: key has 0 bytes", algorithm.ErrPasswordInvalid))
}

if d.variant == VariantStandard && d.version == AlgIdentifierVerX && !isASCII(passwordBytes) {
return false, fmt.Errorf(algorithm.ErrFmtDigestMatch, AlgName, fmt.Errorf("%w: the %s version can't be verified for passwords containing non-ASCII bytes", algorithm.ErrPasswordInvalid, AlgIdentifierVerX))
}

input := d.variant.EncodeInput(passwordBytes, d.salt)

var key []byte
Expand All @@ -54,7 +60,13 @@ func (d *Digest) MatchBytesAdvanced(passwordBytes []byte) (match bool, err error

// Encode returns the encoded form of this bcrypt.Digest.
func (d *Digest) Encode() string {
return d.variant.Encode(d.iterations, AlgIdentifier, bcrypt.Base64Encode(d.salt), d.key)
version := d.version

if version == "" {
version = AlgIdentifier
}

return d.variant.Encode(d.iterations, version, bcrypt.Base64Encode(d.salt), d.key)
}

// String returns the storable format of the bcrypt.Digest encoded hash.
Expand Down Expand Up @@ -86,3 +98,13 @@ func (d *Digest) defaults() {
d.iterations = IterationsDefault
}
}

func isASCII(b []byte) bool {
for _, c := range b {
if c >= 0x80 {
return false
}
}

return true
}
103 changes: 103 additions & 0 deletions algorithm/bcrypt/regression_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package bcrypt

import (
"strings"
"testing"

xbcrypt "github.com/go-crypt/x/bcrypt"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/go-crypt/crypt/algorithm"
)

func TestDecodeRejectsUnusableCost(t *testing.T) {
Expand Down Expand Up @@ -115,3 +118,103 @@ const (
validSHA256Salt = "3XCpXfcQBjcbXFHTLcbFju"
validSHA256Key = "AXNZ1B7NPTf7XyCqUKcvIUOB5eKKZ4C"
)

func TestDecodePreservesVersionIdentifier(t *testing.T) {
testCases := []string{
"$2a$05$CCCCCCCCCCCCCCCCCCCCC.E5YPO9kmyuRGyh0XouQYb4YMJKvyOeW",
"$2b$05$CCCCCCCCCCCCCCCCCCCCC.E5YPO9kmyuRGyh0XouQYb4YMJKvyOeW",
"$2x$05$CCCCCCCCCCCCCCCCCCCCC.E5YPO9kmyuRGyh0XouQYb4YMJKvyOeW",
"$2y$05$CCCCCCCCCCCCCCCCCCCCC.E5YPO9kmyuRGyh0XouQYb4YMJKvyOeW",
"$bcrypt-sha256$v=2,t=2a,r=10$oYmTNJVOBi3hdhUYy4JqOe$jCuMDm.Pw9hhoF/FDC6sOi48yBAoWvC",
"$bcrypt-sha256$v=2,t=2b,r=10$oYmTNJVOBi3hdhUYy4JqOe$jCuMDm.Pw9hhoF/FDC6sOi48yBAoWvC",
"$bcrypt-sha256$v=2,t=2x,r=10$oYmTNJVOBi3hdhUYy4JqOe$jCuMDm.Pw9hhoF/FDC6sOi48yBAoWvC",
"$bcrypt-sha256$v=2,t=2y,r=10$oYmTNJVOBi3hdhUYy4JqOe$jCuMDm.Pw9hhoF/FDC6sOi48yBAoWvC",
}

for _, encoded := range testCases {
t.Run(encoded, func(t *testing.T) {
digest, err := Decode(encoded)
require.NoError(t, err)

assert.Equal(t, encoded, digest.Encode())
})
}
}

func TestHashedDigestsUseVersion2b(t *testing.T) {
for _, variant := range []Variant{VariantStandard, VariantSHA256} {
t.Run(variant.String(), func(t *testing.T) {
hasher, err := New(WithVariant(variant), WithIterations(IterationsMin))
require.NoError(t, err)

digest, err := hasher.Hash("password")
require.NoError(t, err)

switch variant {
case VariantSHA256:
assert.Contains(t, digest.Encode(), "t=2b,")
default:
assert.Regexp(t, `^\$2b\$`, digest.Encode())
}
})
}
}

func TestVersion2xMatchesASCIIPasswords(t *testing.T) {
// The 2x version only differs from the other versions for passwords containing bytes with the high bit set.
digest, err := Decode("$2x$05$CCCCCCCCCCCCCCCCCCCCC.E5YPO9kmyuRGyh0XouQYb4YMJKvyOeW")
require.NoError(t, err)

match, err := digest.MatchAdvanced("U*U")
assert.NoError(t, err)
assert.True(t, match)

match, err = digest.MatchAdvanced("U*V")
assert.NoError(t, err)
assert.False(t, match)
}

func TestVersion2xRejectsNonASCIIPasswords(t *testing.T) {
// Reference vector from crypt_blowfish, which produced 2x digests using its sign extension bug.
digest, err := Decode("$2x$05$/OK.fbVrR/bpIqNJ5ianF.CE5elHaaO4EbggVDjb8P19RukzXSM3e")
require.NoError(t, err)

match, err := digest.MatchAdvanced("\xa3")
assert.False(t, match)
assert.ErrorIs(t, err, algorithm.ErrPasswordInvalid)
assert.EqualError(t, err, "bcrypt match error: password is invalid: the 2x version can't be verified for passwords containing non-ASCII bytes")

assert.False(t, digest.Match("\xa3"))
}

func TestVersion2yMatchesNonASCIIPasswords(t *testing.T) {
// Reference vector from crypt_blowfish.
digest, err := Decode("$2y$05$/OK.fbVrR/bpIqNJ5ianF.Sa7shbm4.OzKpvFnX1pQLmQW96oUlCq")
require.NoError(t, err)

match, err := digest.MatchAdvanced("\xa3")
assert.NoError(t, err)
assert.True(t, match)
}

func TestSHA256VariantVersion2xMatches(t *testing.T) {
hasher, err := NewSHA256(WithIterations(IterationsMin))
require.NoError(t, err)

hashed, err := hasher.Hash("\xa3")
require.NoError(t, err)

encoded := strings.Replace(hashed.Encode(), ",t=2b,", ",t=2x,", 1)
require.Contains(t, encoded, ",t=2x,")

digest, err := Decode(encoded)
require.NoError(t, err)

match, err := digest.MatchAdvanced("\xa3")
assert.NoError(t, err)
assert.True(t, match)

match, err = digest.MatchAdvanced("\xa4")
assert.NoError(t, err)
assert.False(t, match)
}
Loading