From c49a14f7244c60f19b8da977f90806fdb73567d6 Mon Sep 17 00:00:00 2001 From: Peter Palmreuther Date: Sun, 9 May 2021 23:37:36 +0200 Subject: [PATCH 1/7] Implement SHA-256 and SHA-512 hashed passwords Fix #72 --- basic.go | 255 ++++++++++++++++++++++++++++++++++++++++++++++++++ basic_test.go | 37 ++++++++ 2 files changed, 292 insertions(+) diff --git a/basic.go b/basic.go index f328a32..6667834 100644 --- a/basic.go +++ b/basic.go @@ -3,11 +3,14 @@ package auth import ( "bytes" "context" + "crypto" "crypto/sha1" "crypto/subtle" "encoding/base64" "errors" + "fmt" "net/http" + "strconv" "strings" "golang.org/x/crypto/bcrypt" @@ -15,6 +18,15 @@ import ( type compareFunc func(hashedPassword, password []byte) error +const ( + shaEncoding = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + cryptPassDelim = "$" + cryptPassRounds = "rounds=" + shaRoundsDefault = uint(5_000) + shaRoundsMin = uint(1_000) + shaRoundsMax = uint(999_999_999) +) + var ( errMismatchedHashAndPassword = errors.New("mismatched hash and password") @@ -24,6 +36,8 @@ var ( }{ {"", compareMD5HashAndPassword}, // default compareFunc {"{SHA}", compareShaHashAndPassword}, + {"$5$", compareShaCryptHashAndPassword}, + {"$6$", compareShaCryptHashAndPassword}, // Bcrypt is complicated. According to crypt(3) from // crypt_blowfish version 1.3 (fetched from // http://www.openwall.com/crypt/crypt_blowfish-1.3.tar.gz), there @@ -45,6 +59,24 @@ var ( {"$2x$", bcrypt.CompareHashAndPassword}, {"$2y$", bcrypt.CompareHashAndPassword}, } + + shaHashAlgo = map[string]crypto.Hash{ + "5": crypto.SHA256, + "6": crypto.SHA512, + } + shaHashDigestBytes = map[crypto.Hash][]uint8{ + crypto.SHA256: { + 0, 10, 20, 21, 1, 11, 12, 22, 2, 3, 13, 23, 24, 4, 14, 15, 25, 5, + 6, 16, 26, 27, 7, 17, 18, 28, 8, 9, 19, 29, 31, 30, + }, + crypto.SHA512: { + 0, 21, 42, 22, 43, 1, 44, 2, 23, 3, 24, 45, 25, 46, 4, 47, 5, 26, + 6, 27, 48, 28, 49, 7, 50, 8, 29, 9, 30, 51, 31, 52, 10, 53, 11, 32, + 12, 33, 54, 34, 55, 13, 56, 14, 35, 15, 36, 57, 37, 58, 16, 59, 17, 38, + 18, 39, 60, 40, 61, 19, 62, 20, 41, 63, + }, + } + cryptPassStructureError = errors.New("hashed password structure mismatch") ) // BasicAuth is an authenticator implementation for 'Basic' HTTP @@ -103,6 +135,229 @@ func compareShaHashAndPassword(hashedPassword, password []byte) error { return nil } +func compareShaCryptHashAndPassword(hashedPassword, password []byte) error { + hash, rounds, defaultRounds, salt, _, err := dissectShaCryptHash(hashedPassword) + if err != nil { + return errMismatchedHashAndPassword + } + + result, err := shaCryptPassword(hash, password, salt, rounds, defaultRounds) + if err != nil || subtle.ConstantTimeCompare(hashedPassword, result) != 1 { + return errMismatchedHashAndPassword + } + + return nil +} + +// dissectShaCryptHash splits SHA-256/512 password hash into it's parts. +// optional 'rounds=N$' is signaled +func dissectShaCryptHash(hashedPassword []byte) (crypto.Hash, uint, bool, []byte, []byte, error) { + rounds := shaRoundsDefault + defaultRounds := true + parts := bytes.Split(hashedPassword, []byte(cryptPassDelim)) + offset := 0 + + if len(parts) < 4 { + return 0, 0, false, nil, nil, cryptPassStructureError + } + + if len(parts) > 4 { + if len(parts) != 5 || !bytes.HasPrefix(parts[2], []byte(cryptPassRounds)) { + return 0, 0, false, nil, nil, cryptPassStructureError + } + + offset += 1 + defaultRounds = false + i, e := strconv.ParseUint(string(bytes.TrimPrefix(parts[2], []byte(cryptPassRounds))), 10, 32) + + if e != nil { + return 0, 0, false, nil, nil, cryptPassStructureError + } + + // 'i' is uint64 but parsed to fit into 32 bit and 'rounds' as uint is at least 32 bit + rounds = uint(i) + if rounds < shaRoundsMin { + rounds = shaRoundsMin + } + if rounds > shaRoundsMax { + rounds = shaRoundsMax + } + } + + if hash, ok := shaHashAlgo[string(parts[1])]; !ok { + return 0, 0, false, nil, nil, cryptPassStructureError + } else { + salt := parts[2+offset] + digest := parts[3+offset] + + return hash, rounds, defaultRounds, salt, digest, nil + } +} + +// Implements SHA-crypt, as openssl does, following instructions in +// https://www.akkadia.org/drepper/SHA-crypt.txt +// It's 21 complex digest creating steps, so expect nothing easy to read +func shaCryptPassword(hash crypto.Hash, password, salt []byte, rounds uint, defaultRounds bool) ([]byte, error) { + // #1 - #3 + A := hash.New() + A.Write(password) + A.Write(salt) + + // #4 - #8 + B := hash.New() + B.Write(password) + B.Write(salt) + B.Write(password) + BDigest := B.Sum(nil) + + // #9 + i := len(password) + for ; i > hash.Size(); i -= hash.Size() { + A.Write(BDigest) + } + // #10 + A.Write(BDigest[:i]) + + // #11 + for i = len(password); i > 0; i >>= 1 { + // last bit is set to 1 + if i&1 != 0 { + A.Write(BDigest) + } else { + A.Write(password) + } + } + + // #12 + ADigest := A.Sum(nil) + + // #13 - #15 + DP := hash.New() + for i = 0; i < len(password); i++ { + DP.Write(password) + } + DPDigest := DP.Sum(nil) + + // #16 + i = len(password) + P := make([]byte, 0, i) + for ; i > hash.Size(); i -= hash.Size() { + P = append(P, DPDigest...) + } + P = append(P, DPDigest[:i]...) + + // #17 - #19 + DS := hash.New() + times := 16 + uint8(ADigest[0]) + for ; times > 0; times-- { + DS.Write(salt) + } + DSDigest := DS.Sum(nil) + + // #20 + i = len(salt) + S := make([]byte, 0, i) + for ; i > hash.Size(); i -= hash.Size() { + S = append(S, DSDigest...) + } + S = append(S, DSDigest[:i]...) + + // #21 + var finalDigest = ADigest + for rCount := uint(0); rCount < rounds; rCount++ { + R := hash.New() + var seq []byte + if rCount%2 != 0 { + seq = P + } else { + seq = finalDigest + } + R.Write(seq) + if rCount%3 != 0 { + R.Write(S) + } + if rCount%7 != 0 { + R.Write(P) + } + if rCount%2 != 0 { + R.Write(finalDigest) + } else { + R.Write(P) + } + RDigest := R.Sum(nil) + finalDigest = RDigest + } + + if mapping, ok := shaHashDigestBytes[hash]; !ok { + return nil, errors.New("unable to map SHA digest") + } else { + result := make([]byte, len(mapping)) + for i = 0; i < len(mapping); i++ { + result[i] = finalDigest[mapping[i]] + } + + hString := func(h crypto.Hash) string { + for k, v := range shaHashAlgo { + if v == h { + return k + } + } + return "0" + } + rString := func(d bool) string { + if !d { + return fmt.Sprintf("rounds=%d%s", rounds, cryptPassDelim) + } + return "" + } + + // #22 + return []byte( + fmt.Sprintf( + cryptPassDelim+"%s"+cryptPassDelim+"%s%s"+cryptPassDelim+"%s", + hString(hash), rString(defaultRounds), string(salt[:16]), string(shaBase64Encode(result)), + )), nil + } +} + +// shaBase64Encode is used to encode SHA-256 or SHA-512 digests into +// base 64 bytes, following SHA-crypt encoding rules. +// While default Base64 operates LTR SHA-crypt works RTL. +func shaBase64Encode(src []byte) (dst []byte) { + dst = make([]byte, ((len(src)*8)+5)/6) + + si, di := 0, 0 + n := (len(src) / 3) * 3 + for si < n { + val := uint(src[si])<<16 | uint(src[si+1])<<8 | uint(src[si+2]) + dst[di] = shaEncoding[val&0x3f] + dst[di+1] = shaEncoding[(val>>6)&0x3f] + dst[di+2] = shaEncoding[(val>>12)&0x3f] + dst[di+3] = shaEncoding[(val>>18)&0x3f] + + si += 3 + di += 4 + } + + remain := len(src) - si + val := uint(0) + switch remain { + case 0: + return + case 1: + val = uint(src[si]) + case 2: + val = uint(src[si])<<8 | uint(src[si+1]) + } + dst[di] = shaEncoding[val&0x3f] + dst[di+1] = shaEncoding[(val>>6)&0x3f] + if remain == 2 { + dst[di+2] = shaEncoding[(val>>12)&0x3f] + } + + return +} + func compareMD5HashAndPassword(hashedPassword, password []byte) error { parts := bytes.SplitN(hashedPassword, []byte("$"), 4) if len(parts) != 4 { diff --git a/basic_test.go b/basic_test.go index e45095b..5deb8ed 100644 --- a/basic_test.go +++ b/basic_test.go @@ -15,6 +15,8 @@ var basicSecrets = map[string]string{ "testsha": "{SHA}qvTGHdzF6KLavt4PO0gs2a6pQ00=", "testmd5": "$apr1$0.KbAJur$4G9MiqUjDLCuihkMfmg6e1", "testmd5broken": "$apr10.KbAJur$4G9MiqUjDLCuihkMfmg6e1", + "testsha256": "$5$Eg2QLTpmL3TegBv7$h8PsM/fa1xxOXmhUWWIQvV8.BVl9o3vax2S0C4C7Km3", + "testsha512": "$6$uqVy33l0y9YMJV15$UeR3rqmGvrgmc6cn6ZMKUrUqH9YBdrCbjTQK3K2gvprRWay45S6TC3fGQX4Ml4RY8cqkQ2f9CFqFmV02pyGhx.", } type credentials struct { @@ -125,6 +127,10 @@ func TestBasicAuthWrap(t *testing.T) { {"", "", http.StatusUnauthorized}, {"testsha", "invalid", http.StatusUnauthorized}, {"testsha", "hello", http.StatusOK}, + {"testsha256", "invalid", http.StatusUnauthorized}, + {"testsha256", "hello", http.StatusOK}, + {"testsha512", "invalid", http.StatusUnauthorized}, + {"testsha512", "hello", http.StatusOK}, } { r, err := http.NewRequest("GET", ts.URL, nil) if err != nil { @@ -151,6 +157,10 @@ func TestCheckSecret(t *testing.T) { {"openssl-md5", "$1$mvmz31IB$U9KpHBLegga2doA0e3s3N0"}, {"htpasswd-sha", "{SHA}vFznddje0Ht4+pmO0FaxwrUKN/M="}, {"htpasswd-bcrypt", "$2y$10$Q6GeMFPd0dAxhQULPDdAn.DFy6NDmLaU0A7e2XoJz7PFYAEADFKbC"}, + {"openssl-sha256", "$5$qgB401R/ggz11Q5U$QAsQZuMF.xfkj7A0QrEvWpYgcStxtU8V3Wj5DSLOSI0"}, + {"openssl-sha512", "$6$lseRR5fEdsK0sOkR$QTkArA5Z/arPmd78I7qmi8Wj/4bc8CbNw0FH59SYVXCfesr.AqOJINkGx/aaZ6gKYDbmYeFPSSMjMFW9HrMwR."}, + {"0123456789012345678901234567890123456789", "$5$Kpm4hE9Eu9MU.vG8$G8z0lSskQaziCzlCbHSEDmoYr3kLhd7ineD3p0RiWD8"}, + {"01234567890123456789012345678901234567890123456789012345678901234567890123456789", "$6$U1JDv/VIVgQ103od$4oHmr5qqJIJExEZfLzz0z3VfNznjcxTIL7c1RACsBWJnk/FPAc/oHwFGLZ0OJQaN.obx/2NdwYlGuiGu4KJ5K/"}, // common bcrypt test vectors {"", "$2a$06$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s."}, {"", "$2a$08$HqWuK6/Ng6sg9gQzbLrgb.Tl.ZHfXLhvt/SgVyWhQqgqcZ7ZuUtye"}, @@ -193,3 +203,30 @@ func TestCheckSecret(t *testing.T) { }) } } + +func TestDissectSha(t *testing.T) { + t.Parallel() + type testData struct { + password string + secret string + result bool + } + data := []testData{ + {"openssl-sha256", "$5$qgB401R/ggz11Q5U$QAsQZuMF.xfkj7A0QrEvWpYgcStxtU8V3Wj5DSLOSI0", true}, + {"openssl-sha256", "$5$rounds=5000$qgB401R/ggz11Q5U$QAsQZuMF.xfkj7A0QrEvWpYgcStxtU8V3Wj5DSLOSI0", true}, + {"openssl-sha256", "$5$rounds=5000$foobar$qgB401R/ggz11Q5U$QAsQZuMF.xfkj7A0QrEvWpYgcStxtU8V3Wj5DSLOSI0", false}, + {"openssl-sha256", "$5$QAsQZuMF.xfkj7A0QrEvWpYgcStxtU8V3Wj5DSLOSI0", false}, + {"openssl-sha512", "$6$lseRR5fEdsK0sOkR$QTkArA5Z/arPmd78I7qmi8Wj/4bc8CbNw0FH59SYVXCfesr.AqOJINkGx/aaZ6gKYDbmYeFPSSMjMFW9HrMwR.", true}, + {"openssl-sha512", "$6$rounds=5000$lseRR5fEdsK0sOkR$QTkArA5Z/arPmd78I7qmi8Wj/4bc8CbNw0FH59SYVXCfesr.AqOJINkGx/aaZ6gKYDbmYeFPSSMjMFW9HrMwR.", true}, + {"openssl-sha512", "$6$rounds=5000$foobar$lseRR5fEdsK0sOkR$QTkArA5Z/arPmd78I7qmi8Wj/4bc8CbNw0FH59SYVXCfesr.AqOJINkGx/aaZ6gKYDbmYeFPSSMjMFW9HrMwR.", false}, + {"openssl-sha512", "$6$QTkArA5Z/arPmd78I7qmi8Wj/4bc8CbNw0FH59SYVXCfesr.AqOJINkGx/aaZ6gKYDbmYeFPSSMjMFW9HrMwR.", false}, + } + for i, tc := range data { + t.Run(fmt.Sprintf("Vector%d", i), func(t *testing.T) { + t.Parallel() + if CheckSecret(tc.password, tc.secret) != tc.result { + t.Errorf("CheckSecret returned %t, want %t", !tc.result, tc.result) + } + }) + } +} From 9902bda4114278395bb15068fd5579996d4b34c1 Mon Sep 17 00:00:00 2001 From: Peter Palmreuther Date: Sun, 9 May 2021 23:41:27 +0200 Subject: [PATCH 2/7] Fix integer literal travis checks using go version 1.10.x and 1.11.x revealed integer literal obviously only usable at newer go versions. --- basic.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/basic.go b/basic.go index 6667834..76263c6 100644 --- a/basic.go +++ b/basic.go @@ -22,9 +22,9 @@ const ( shaEncoding = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" cryptPassDelim = "$" cryptPassRounds = "rounds=" - shaRoundsDefault = uint(5_000) - shaRoundsMin = uint(1_000) - shaRoundsMax = uint(999_999_999) + shaRoundsDefault = uint(5000) + shaRoundsMin = uint(1000) + shaRoundsMax = uint(999999999) ) var ( From 51a74bb97905201284d72fab19c482dc59c1e54c Mon Sep 17 00:00:00 2001 From: Peter Palmreuther Date: Mon, 10 May 2021 12:49:26 +0200 Subject: [PATCH 3/7] Move SHA-crypt implementation SHA-crypt implementation moved to a separate file. Copying already existing MD5Crypt implementation base64 encoding could be realized much shorter and more elegant. --- basic.go | 243 +------------------------------------------------- basic_test.go | 31 ++++--- shacrypt.go | 226 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 246 insertions(+), 254 deletions(-) create mode 100644 shacrypt.go diff --git a/basic.go b/basic.go index 76263c6..9c1d26f 100644 --- a/basic.go +++ b/basic.go @@ -3,14 +3,11 @@ package auth import ( "bytes" "context" - "crypto" "crypto/sha1" "crypto/subtle" "encoding/base64" "errors" - "fmt" "net/http" - "strconv" "strings" "golang.org/x/crypto/bcrypt" @@ -18,15 +15,6 @@ import ( type compareFunc func(hashedPassword, password []byte) error -const ( - shaEncoding = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" - cryptPassDelim = "$" - cryptPassRounds = "rounds=" - shaRoundsDefault = uint(5000) - shaRoundsMin = uint(1000) - shaRoundsMax = uint(999999999) -) - var ( errMismatchedHashAndPassword = errors.New("mismatched hash and password") @@ -59,24 +47,6 @@ var ( {"$2x$", bcrypt.CompareHashAndPassword}, {"$2y$", bcrypt.CompareHashAndPassword}, } - - shaHashAlgo = map[string]crypto.Hash{ - "5": crypto.SHA256, - "6": crypto.SHA512, - } - shaHashDigestBytes = map[crypto.Hash][]uint8{ - crypto.SHA256: { - 0, 10, 20, 21, 1, 11, 12, 22, 2, 3, 13, 23, 24, 4, 14, 15, 25, 5, - 6, 16, 26, 27, 7, 17, 18, 28, 8, 9, 19, 29, 31, 30, - }, - crypto.SHA512: { - 0, 21, 42, 22, 43, 1, 44, 2, 23, 3, 24, 45, 25, 46, 4, 47, 5, 26, - 6, 27, 48, 28, 49, 7, 50, 8, 29, 9, 30, 51, 31, 52, 10, 53, 11, 32, - 12, 33, 54, 34, 55, 13, 56, 14, 35, 15, 36, 57, 37, 58, 16, 59, 17, 38, - 18, 39, 60, 40, 61, 19, 62, 20, 41, 63, - }, - } - cryptPassStructureError = errors.New("hashed password structure mismatch") ) // BasicAuth is an authenticator implementation for 'Basic' HTTP @@ -136,12 +106,12 @@ func compareShaHashAndPassword(hashedPassword, password []byte) error { } func compareShaCryptHashAndPassword(hashedPassword, password []byte) error { - hash, rounds, defaultRounds, salt, _, err := dissectShaCryptHash(hashedPassword) + hash, err := DissectShaCryptHash(hashedPassword) if err != nil { return errMismatchedHashAndPassword } - result, err := shaCryptPassword(hash, password, salt, rounds, defaultRounds) + result, err := SHACrypt(hash.Hash, password, hash.Salt, hash.Magic, hash.Rounds, hash.DefaultRounds) if err != nil || subtle.ConstantTimeCompare(hashedPassword, result) != 1 { return errMismatchedHashAndPassword } @@ -149,215 +119,6 @@ func compareShaCryptHashAndPassword(hashedPassword, password []byte) error { return nil } -// dissectShaCryptHash splits SHA-256/512 password hash into it's parts. -// optional 'rounds=N$' is signaled -func dissectShaCryptHash(hashedPassword []byte) (crypto.Hash, uint, bool, []byte, []byte, error) { - rounds := shaRoundsDefault - defaultRounds := true - parts := bytes.Split(hashedPassword, []byte(cryptPassDelim)) - offset := 0 - - if len(parts) < 4 { - return 0, 0, false, nil, nil, cryptPassStructureError - } - - if len(parts) > 4 { - if len(parts) != 5 || !bytes.HasPrefix(parts[2], []byte(cryptPassRounds)) { - return 0, 0, false, nil, nil, cryptPassStructureError - } - - offset += 1 - defaultRounds = false - i, e := strconv.ParseUint(string(bytes.TrimPrefix(parts[2], []byte(cryptPassRounds))), 10, 32) - - if e != nil { - return 0, 0, false, nil, nil, cryptPassStructureError - } - - // 'i' is uint64 but parsed to fit into 32 bit and 'rounds' as uint is at least 32 bit - rounds = uint(i) - if rounds < shaRoundsMin { - rounds = shaRoundsMin - } - if rounds > shaRoundsMax { - rounds = shaRoundsMax - } - } - - if hash, ok := shaHashAlgo[string(parts[1])]; !ok { - return 0, 0, false, nil, nil, cryptPassStructureError - } else { - salt := parts[2+offset] - digest := parts[3+offset] - - return hash, rounds, defaultRounds, salt, digest, nil - } -} - -// Implements SHA-crypt, as openssl does, following instructions in -// https://www.akkadia.org/drepper/SHA-crypt.txt -// It's 21 complex digest creating steps, so expect nothing easy to read -func shaCryptPassword(hash crypto.Hash, password, salt []byte, rounds uint, defaultRounds bool) ([]byte, error) { - // #1 - #3 - A := hash.New() - A.Write(password) - A.Write(salt) - - // #4 - #8 - B := hash.New() - B.Write(password) - B.Write(salt) - B.Write(password) - BDigest := B.Sum(nil) - - // #9 - i := len(password) - for ; i > hash.Size(); i -= hash.Size() { - A.Write(BDigest) - } - // #10 - A.Write(BDigest[:i]) - - // #11 - for i = len(password); i > 0; i >>= 1 { - // last bit is set to 1 - if i&1 != 0 { - A.Write(BDigest) - } else { - A.Write(password) - } - } - - // #12 - ADigest := A.Sum(nil) - - // #13 - #15 - DP := hash.New() - for i = 0; i < len(password); i++ { - DP.Write(password) - } - DPDigest := DP.Sum(nil) - - // #16 - i = len(password) - P := make([]byte, 0, i) - for ; i > hash.Size(); i -= hash.Size() { - P = append(P, DPDigest...) - } - P = append(P, DPDigest[:i]...) - - // #17 - #19 - DS := hash.New() - times := 16 + uint8(ADigest[0]) - for ; times > 0; times-- { - DS.Write(salt) - } - DSDigest := DS.Sum(nil) - - // #20 - i = len(salt) - S := make([]byte, 0, i) - for ; i > hash.Size(); i -= hash.Size() { - S = append(S, DSDigest...) - } - S = append(S, DSDigest[:i]...) - - // #21 - var finalDigest = ADigest - for rCount := uint(0); rCount < rounds; rCount++ { - R := hash.New() - var seq []byte - if rCount%2 != 0 { - seq = P - } else { - seq = finalDigest - } - R.Write(seq) - if rCount%3 != 0 { - R.Write(S) - } - if rCount%7 != 0 { - R.Write(P) - } - if rCount%2 != 0 { - R.Write(finalDigest) - } else { - R.Write(P) - } - RDigest := R.Sum(nil) - finalDigest = RDigest - } - - if mapping, ok := shaHashDigestBytes[hash]; !ok { - return nil, errors.New("unable to map SHA digest") - } else { - result := make([]byte, len(mapping)) - for i = 0; i < len(mapping); i++ { - result[i] = finalDigest[mapping[i]] - } - - hString := func(h crypto.Hash) string { - for k, v := range shaHashAlgo { - if v == h { - return k - } - } - return "0" - } - rString := func(d bool) string { - if !d { - return fmt.Sprintf("rounds=%d%s", rounds, cryptPassDelim) - } - return "" - } - - // #22 - return []byte( - fmt.Sprintf( - cryptPassDelim+"%s"+cryptPassDelim+"%s%s"+cryptPassDelim+"%s", - hString(hash), rString(defaultRounds), string(salt[:16]), string(shaBase64Encode(result)), - )), nil - } -} - -// shaBase64Encode is used to encode SHA-256 or SHA-512 digests into -// base 64 bytes, following SHA-crypt encoding rules. -// While default Base64 operates LTR SHA-crypt works RTL. -func shaBase64Encode(src []byte) (dst []byte) { - dst = make([]byte, ((len(src)*8)+5)/6) - - si, di := 0, 0 - n := (len(src) / 3) * 3 - for si < n { - val := uint(src[si])<<16 | uint(src[si+1])<<8 | uint(src[si+2]) - dst[di] = shaEncoding[val&0x3f] - dst[di+1] = shaEncoding[(val>>6)&0x3f] - dst[di+2] = shaEncoding[(val>>12)&0x3f] - dst[di+3] = shaEncoding[(val>>18)&0x3f] - - si += 3 - di += 4 - } - - remain := len(src) - si - val := uint(0) - switch remain { - case 0: - return - case 1: - val = uint(src[si]) - case 2: - val = uint(src[si])<<8 | uint(src[si+1]) - } - dst[di] = shaEncoding[val&0x3f] - dst[di+1] = shaEncoding[(val>>6)&0x3f] - if remain == 2 { - dst[di+2] = shaEncoding[(val>>12)&0x3f] - } - - return -} - func compareMD5HashAndPassword(hashedPassword, password []byte) error { parts := bytes.SplitN(hashedPassword, []byte("$"), 4) if len(parts) != 4 { diff --git a/basic_test.go b/basic_test.go index 5deb8ed..5e23760 100644 --- a/basic_test.go +++ b/basic_test.go @@ -207,25 +207,30 @@ func TestCheckSecret(t *testing.T) { func TestDissectSha(t *testing.T) { t.Parallel() type testData struct { - password string - secret string - result bool + secret string + result bool } data := []testData{ - {"openssl-sha256", "$5$qgB401R/ggz11Q5U$QAsQZuMF.xfkj7A0QrEvWpYgcStxtU8V3Wj5DSLOSI0", true}, - {"openssl-sha256", "$5$rounds=5000$qgB401R/ggz11Q5U$QAsQZuMF.xfkj7A0QrEvWpYgcStxtU8V3Wj5DSLOSI0", true}, - {"openssl-sha256", "$5$rounds=5000$foobar$qgB401R/ggz11Q5U$QAsQZuMF.xfkj7A0QrEvWpYgcStxtU8V3Wj5DSLOSI0", false}, - {"openssl-sha256", "$5$QAsQZuMF.xfkj7A0QrEvWpYgcStxtU8V3Wj5DSLOSI0", false}, - {"openssl-sha512", "$6$lseRR5fEdsK0sOkR$QTkArA5Z/arPmd78I7qmi8Wj/4bc8CbNw0FH59SYVXCfesr.AqOJINkGx/aaZ6gKYDbmYeFPSSMjMFW9HrMwR.", true}, - {"openssl-sha512", "$6$rounds=5000$lseRR5fEdsK0sOkR$QTkArA5Z/arPmd78I7qmi8Wj/4bc8CbNw0FH59SYVXCfesr.AqOJINkGx/aaZ6gKYDbmYeFPSSMjMFW9HrMwR.", true}, - {"openssl-sha512", "$6$rounds=5000$foobar$lseRR5fEdsK0sOkR$QTkArA5Z/arPmd78I7qmi8Wj/4bc8CbNw0FH59SYVXCfesr.AqOJINkGx/aaZ6gKYDbmYeFPSSMjMFW9HrMwR.", false}, - {"openssl-sha512", "$6$QTkArA5Z/arPmd78I7qmi8Wj/4bc8CbNw0FH59SYVXCfesr.AqOJINkGx/aaZ6gKYDbmYeFPSSMjMFW9HrMwR.", false}, + {"$5$qgB401R/ggz11Q5U$QAsQZuMF.xfkj7A0QrEvWpYgcStxtU8V3Wj5DSLOSI0", true}, + {"$5$rounds=5000$qgB401R/ggz11Q5U$QAsQZuMF.xfkj7A0QrEvWpYgcStxtU8V3Wj5DSLOSI0", true}, + {"$5$rounds=5000$foobar$qgB401R/ggz11Q5U$QAsQZuMF.xfkj7A0QrEvWpYgcStxtU8V3Wj5DSLOSI0", false}, + {"$5$QAsQZuMF.xfkj7A0QrEvWpYgcStxtU8V3Wj5DSLOSI0", false}, + {"$6$lseRR5fEdsK0sOkR$QTkArA5Z/arPmd78I7qmi8Wj/4bc8CbNw0FH59SYVXCfesr.AqOJINkGx/aaZ6gKYDbmYeFPSSMjMFW9HrMwR.", true}, + {"$6$rounds=5000$lseRR5fEdsK0sOkR$QTkArA5Z/arPmd78I7qmi8Wj/4bc8CbNw0FH59SYVXCfesr.AqOJINkGx/aaZ6gKYDbmYeFPSSMjMFW9HrMwR.", true}, + {"$6$rounds=5000$foobar$lseRR5fEdsK0sOkR$QTkArA5Z/arPmd78I7qmi8Wj/4bc8CbNw0FH59SYVXCfesr.AqOJINkGx/aaZ6gKYDbmYeFPSSMjMFW9HrMwR.", false}, + {"$6$QTkArA5Z/arPmd78I7qmi8Wj/4bc8CbNw0FH59SYVXCfesr.AqOJINkGx/aaZ6gKYDbmYeFPSSMjMFW9HrMwR.", false}, } for i, tc := range data { t.Run(fmt.Sprintf("Vector%d", i), func(t *testing.T) { t.Parallel() - if CheckSecret(tc.password, tc.secret) != tc.result { - t.Errorf("CheckSecret returned %t, want %t", !tc.result, tc.result) + _, err := DissectShaCryptHash([]byte(tc.secret)) + if !tc.result && err == nil { + t.Error("DissectShaCrypthHash returned no error, want one") + return + } + if tc.result && err != nil { + t.Errorf("DissectShaCrypthHash returned error: %v, want none", err) + return } }) } diff --git a/shacrypt.go b/shacrypt.go new file mode 100644 index 0000000..4226c32 --- /dev/null +++ b/shacrypt.go @@ -0,0 +1,226 @@ +package auth + +import ( + "bytes" + "crypto" + "errors" + "strconv" +) + +type SHAHash struct { + Hash crypto.Hash + Magic []byte + Rounds uint + DefaultRounds bool + Salt []byte + Digest []byte +} + +type SHACryptAlgo struct { + algo crypto.Hash + swaps []uint +} + +const ( + shaEncoding = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + cryptPassDelim = "$" + cryptPassRounds = "rounds=" + shaRoundsDefault = uint(5000) + shaRoundsMin = uint(1000) + shaRoundsMax = uint(999999999) +) + +var ( + shaCryptAlgo = map[string]SHACryptAlgo{ + "$5$": {crypto.SHA256, []uint{ + 20, 10, 0, 11, 1, 21, 2, 22, 12, 23, 13, 3, 14, 4, 24, 5, 25, 15, + 26, 16, 6, 17, 7, 27, 8, 28, 18, 29, 19, 9, 30, 31, + }}, + "$6$": {crypto.SHA512, []uint{ + 42, 21, 0, 1, 43, 22, 23, 2, 44, 45, 24, 3, 4, 46, 25, 26, 5, 47, + 48, 27, 6, 7, 49, 28, 29, 8, 50, 51, 30, 9, 10, 52, 31, 32, 11, 53, + 54, 33, 12, 13, 55, 34, 35, 14, 56, 57, 36, 15, 16, 58, 37, 38, 17, 59, + 60, 39, 18, 19, 61, 40, 41, 20, 62, 63, + }}, + } + + cryptPassStructureError = errors.New("hashed password structure mismatch") + missingByteSwapMapperError = errors.New("unable to map SHA digest") +) + +// SHACrypt implements SHA-crypt, as openssl does, following instructions in +// https://www.akkadia.org/drepper/SHA-crypt.txt +// It's 21 complex digest creating steps, so expect nothing easy to read. +func SHACrypt(hash crypto.Hash, password, salt, magic []byte, rounds uint, defaultRounds bool) ([]byte, error) { + // #1 - #3 + A := hash.New() + A.Write(password) + A.Write(salt) + + // #4 - #8 + B := hash.New() + B.Write(password) + B.Write(salt) + B.Write(password) + BDigest := B.Sum(nil) + + // #9 + i := len(password) + for ; i > hash.Size(); i -= hash.Size() { + A.Write(BDigest) + } + // #10 + A.Write(BDigest[:i]) + + // #11 + for i = len(password); i > 0; i >>= 1 { + // last bit is set to 1 + if i&1 != 0 { + A.Write(BDigest) + } else { + A.Write(password) + } + } + + // #12 + ADigest := A.Sum(nil) + + // #13 - #15 + DP := hash.New() + for i = 0; i < len(password); i++ { + DP.Write(password) + } + DPDigest := DP.Sum(nil) + + // #16 + i = len(password) + P := make([]byte, 0, i) + for ; i > hash.Size(); i -= hash.Size() { + P = append(P, DPDigest...) + } + P = append(P, DPDigest[:i]...) + + // #17 - #19 + DS := hash.New() + times := 16 + uint8(ADigest[0]) + for ; times > 0; times-- { + DS.Write(salt) + } + DSDigest := DS.Sum(nil) + + // #20 + i = len(salt) + S := make([]byte, 0, i) + for ; i > hash.Size(); i -= hash.Size() { + S = append(S, DSDigest...) + } + S = append(S, DSDigest[:i]...) + + // #21 + var finalDigest = ADigest + for rCount := uint(0); rCount < rounds; rCount++ { + R := hash.New() + var seq []byte + if rCount%2 != 0 { + seq = P + } else { + seq = finalDigest + } + R.Write(seq) + if rCount%3 != 0 { + R.Write(S) + } + if rCount%7 != 0 { + R.Write(P) + } + if rCount%2 != 0 { + R.Write(finalDigest) + } else { + R.Write(P) + } + RDigest := R.Sum(nil) + finalDigest = RDigest + } + + var ok bool + var algo SHACryptAlgo + if algo, ok = shaCryptAlgo[string(magic)]; !ok { + return nil, missingByteSwapMapperError + } + + mapping := algo.swaps + + // base64 encode following sha-crypt rules [#22 e)] + encoded := make([]byte, 0, ((len(finalDigest)*8)+5)/6) + v := uint(0) + bits := uint(0) + for _, idx := range mapping { + v |= uint(finalDigest[idx]) << bits + for bits = bits + 8; bits > 6; bits -= 6 { + encoded = append(encoded, shaEncoding[v&0x3f]) + v >>= 6 + } + } + encoded = append(encoded, shaEncoding[v&0x3f]) + + // #22 a) + result := magic + // #22 b) + if !defaultRounds { + result = append(append(result, strconv.AppendUint([]byte("rounds="), uint64(rounds), 10)...), cryptPassDelim...) + } + // #22 c) + d) + result = append(append(result, salt[:16]...), cryptPassDelim...) + // #22 e) result + result = append(result, encoded...) + + return result, nil +} + +// DissectShaCryptHash splits SHA-256/512 password hash into it's parts. +// optional 'rounds=N$' is signaled +func DissectShaCryptHash(hashedPassword []byte) (*SHAHash, error) { + rounds := shaRoundsDefault + defaultRounds := true + parts := bytes.Split(hashedPassword, []byte(cryptPassDelim)) + offset := 0 + + if len(parts) < 4 { + return nil, cryptPassStructureError + } + + if len(parts) > 4 { + if len(parts) != 5 || !bytes.HasPrefix(parts[2], []byte(cryptPassRounds)) { + return nil, cryptPassStructureError + } + + offset += 1 + defaultRounds = false + i, e := strconv.ParseUint(string(bytes.TrimPrefix(parts[2], []byte(cryptPassRounds))), 10, 32) + + if e != nil { + return nil, cryptPassStructureError + } + + // 'i' is uint64 but parsed to fit into 32 bit and 'rounds' as uint is at least 32 bit + rounds = uint(i) + if rounds < shaRoundsMin { + rounds = shaRoundsMin + } + if rounds > shaRoundsMax { + rounds = shaRoundsMax + } + } + + magic := append(append(append([]byte{}, cryptPassDelim...), parts[1]...), cryptPassDelim...) + + if hash, ok := shaCryptAlgo[string(magic)]; !ok { + return nil, cryptPassStructureError + } else { + salt := parts[2+offset] + digest := parts[3+offset] + + result := SHAHash{hash.algo, magic, rounds, defaultRounds, salt, digest} + return &result, nil + } +} From 0a24f63faa9eb5cb061588d4004443bbe1bd2b34 Mon Sep 17 00:00:00 2001 From: Peter Palmreuther Date: Tue, 11 May 2021 00:48:38 +0200 Subject: [PATCH 4/7] Move SHA-crypt test to individual test file --- basic_test.go | 31 ------------------------------- shacrypt_test.go | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 31 deletions(-) create mode 100644 shacrypt_test.go diff --git a/basic_test.go b/basic_test.go index 5e23760..6baee5a 100644 --- a/basic_test.go +++ b/basic_test.go @@ -204,34 +204,3 @@ func TestCheckSecret(t *testing.T) { } } -func TestDissectSha(t *testing.T) { - t.Parallel() - type testData struct { - secret string - result bool - } - data := []testData{ - {"$5$qgB401R/ggz11Q5U$QAsQZuMF.xfkj7A0QrEvWpYgcStxtU8V3Wj5DSLOSI0", true}, - {"$5$rounds=5000$qgB401R/ggz11Q5U$QAsQZuMF.xfkj7A0QrEvWpYgcStxtU8V3Wj5DSLOSI0", true}, - {"$5$rounds=5000$foobar$qgB401R/ggz11Q5U$QAsQZuMF.xfkj7A0QrEvWpYgcStxtU8V3Wj5DSLOSI0", false}, - {"$5$QAsQZuMF.xfkj7A0QrEvWpYgcStxtU8V3Wj5DSLOSI0", false}, - {"$6$lseRR5fEdsK0sOkR$QTkArA5Z/arPmd78I7qmi8Wj/4bc8CbNw0FH59SYVXCfesr.AqOJINkGx/aaZ6gKYDbmYeFPSSMjMFW9HrMwR.", true}, - {"$6$rounds=5000$lseRR5fEdsK0sOkR$QTkArA5Z/arPmd78I7qmi8Wj/4bc8CbNw0FH59SYVXCfesr.AqOJINkGx/aaZ6gKYDbmYeFPSSMjMFW9HrMwR.", true}, - {"$6$rounds=5000$foobar$lseRR5fEdsK0sOkR$QTkArA5Z/arPmd78I7qmi8Wj/4bc8CbNw0FH59SYVXCfesr.AqOJINkGx/aaZ6gKYDbmYeFPSSMjMFW9HrMwR.", false}, - {"$6$QTkArA5Z/arPmd78I7qmi8Wj/4bc8CbNw0FH59SYVXCfesr.AqOJINkGx/aaZ6gKYDbmYeFPSSMjMFW9HrMwR.", false}, - } - for i, tc := range data { - t.Run(fmt.Sprintf("Vector%d", i), func(t *testing.T) { - t.Parallel() - _, err := DissectShaCryptHash([]byte(tc.secret)) - if !tc.result && err == nil { - t.Error("DissectShaCrypthHash returned no error, want one") - return - } - if tc.result && err != nil { - t.Errorf("DissectShaCrypthHash returned error: %v, want none", err) - return - } - }) - } -} diff --git a/shacrypt_test.go b/shacrypt_test.go new file mode 100644 index 0000000..522c3af --- /dev/null +++ b/shacrypt_test.go @@ -0,0 +1,38 @@ +package auth + +import ( + "fmt" + "testing" +) + +func TestDissectSha(t *testing.T) { + t.Parallel() + type testData struct { + secret string + result bool + } + data := []testData{ + {"$5$qgB401R/ggz11Q5U$QAsQZuMF.xfkj7A0QrEvWpYgcStxtU8V3Wj5DSLOSI0", true}, + {"$5$rounds=5000$qgB401R/ggz11Q5U$QAsQZuMF.xfkj7A0QrEvWpYgcStxtU8V3Wj5DSLOSI0", true}, + {"$5$rounds=5000$foobar$qgB401R/ggz11Q5U$QAsQZuMF.xfkj7A0QrEvWpYgcStxtU8V3Wj5DSLOSI0", false}, + {"$5$QAsQZuMF.xfkj7A0QrEvWpYgcStxtU8V3Wj5DSLOSI0", false}, + {"$6$lseRR5fEdsK0sOkR$QTkArA5Z/arPmd78I7qmi8Wj/4bc8CbNw0FH59SYVXCfesr.AqOJINkGx/aaZ6gKYDbmYeFPSSMjMFW9HrMwR.", true}, + {"$6$rounds=5000$lseRR5fEdsK0sOkR$QTkArA5Z/arPmd78I7qmi8Wj/4bc8CbNw0FH59SYVXCfesr.AqOJINkGx/aaZ6gKYDbmYeFPSSMjMFW9HrMwR.", true}, + {"$6$rounds=5000$foobar$lseRR5fEdsK0sOkR$QTkArA5Z/arPmd78I7qmi8Wj/4bc8CbNw0FH59SYVXCfesr.AqOJINkGx/aaZ6gKYDbmYeFPSSMjMFW9HrMwR.", false}, + {"$6$QTkArA5Z/arPmd78I7qmi8Wj/4bc8CbNw0FH59SYVXCfesr.AqOJINkGx/aaZ6gKYDbmYeFPSSMjMFW9HrMwR.", false}, + } + for i, tc := range data { + t.Run(fmt.Sprintf("Vector%d", i), func(t *testing.T) { + t.Parallel() + _, err := DissectShaCryptHash([]byte(tc.secret)) + if !tc.result && err == nil { + t.Error("DissectShaCrypthHash returned no error, want one") + return + } + if tc.result && err != nil { + t.Errorf("DissectShaCrypthHash returned error: %v, want none", err) + return + } + }) + } +} From 1d895e3a2ad0b0562d97fa9c901f1d9f2f51b480 Mon Sep 17 00:00:00 2001 From: Peter Palmreuther Date: Tue, 11 May 2021 09:00:06 +0200 Subject: [PATCH 5/7] Add some sanity check and cleanup code Extract digest length calculation and add sanity check. Wrap hash algorithm information in helper functions to centralize access and simplify code. --- shacrypt.go | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/shacrypt.go b/shacrypt.go index 4226c32..283c6e4 100644 --- a/shacrypt.go +++ b/shacrypt.go @@ -142,16 +142,13 @@ func SHACrypt(hash crypto.Hash, password, salt, magic []byte, rounds uint, defau finalDigest = RDigest } - var ok bool - var algo SHACryptAlgo - if algo, ok = shaCryptAlgo[string(magic)]; !ok { + mapping, err := getSwapBytes(string(magic)) + if err != nil { return nil, missingByteSwapMapperError } - mapping := algo.swaps - // base64 encode following sha-crypt rules [#22 e)] - encoded := make([]byte, 0, ((len(finalDigest)*8)+5)/6) + encoded := make([]byte, 0, encodedLength(hash)) v := uint(0) bits := uint(0) for _, idx := range mapping { @@ -214,13 +211,35 @@ func DissectShaCryptHash(hashedPassword []byte) (*SHAHash, error) { magic := append(append(append([]byte{}, cryptPassDelim...), parts[1]...), cryptPassDelim...) - if hash, ok := shaCryptAlgo[string(magic)]; !ok { + if hash, err := getHash(string(magic)); err != nil { return nil, cryptPassStructureError } else { salt := parts[2+offset] digest := parts[3+offset] - result := SHAHash{hash.algo, magic, rounds, defaultRounds, salt, digest} + if len(digest) != encodedLength(hash) { + return nil, cryptPassStructureError + } + + result := SHAHash{hash, magic, rounds, defaultRounds, salt, digest} return &result, nil } } + +func encodedLength(h crypto.Hash) int { + return ((h.Size() * 8) + 5) / 6 +} + +func getHash(magic string) (crypto.Hash, error) { + if a, ok := shaCryptAlgo[magic]; ok { + return a.algo, nil + } + return 0, errors.New("unable to gather hash algorithm") +} + +func getSwapBytes(magic string) ([]uint, error) { + if a, ok := shaCryptAlgo[magic]; ok { + return a.swaps, nil + } + return nil, errors.New("unable to gather hash specific bytes swapping") +} From 8dec01613236d4a2ba2318a2143b5ce975e9dcf1 Mon Sep 17 00:00:00 2001 From: Peter Palmreuther Date: Tue, 11 May 2021 09:16:01 +0200 Subject: [PATCH 6/7] Reduce memory footprint SHA digest byte mapping as []uint consumed 8 times the memory it really needs. It's now []uint8, sufficient to store indexes in digest []byte. --- shacrypt.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/shacrypt.go b/shacrypt.go index 283c6e4..3dade18 100644 --- a/shacrypt.go +++ b/shacrypt.go @@ -18,7 +18,7 @@ type SHAHash struct { type SHACryptAlgo struct { algo crypto.Hash - swaps []uint + swaps []uint8 } const ( @@ -31,12 +31,12 @@ const ( ) var ( - shaCryptAlgo = map[string]SHACryptAlgo{ - "$5$": {crypto.SHA256, []uint{ + shaCryptAlgo = map[string]*SHACryptAlgo{ + "$5$": {crypto.SHA256, []uint8{ 20, 10, 0, 11, 1, 21, 2, 22, 12, 23, 13, 3, 14, 4, 24, 5, 25, 15, 26, 16, 6, 17, 7, 27, 8, 28, 18, 29, 19, 9, 30, 31, }}, - "$6$": {crypto.SHA512, []uint{ + "$6$": {crypto.SHA512, []uint8{ 42, 21, 0, 1, 43, 22, 23, 2, 44, 45, 24, 3, 4, 46, 25, 26, 5, 47, 48, 27, 6, 7, 49, 28, 29, 8, 50, 51, 30, 9, 10, 52, 31, 32, 11, 53, 54, 33, 12, 13, 55, 34, 35, 14, 56, 57, 36, 15, 16, 58, 37, 38, 17, 59, @@ -237,7 +237,7 @@ func getHash(magic string) (crypto.Hash, error) { return 0, errors.New("unable to gather hash algorithm") } -func getSwapBytes(magic string) ([]uint, error) { +func getSwapBytes(magic string) ([]uint8, error) { if a, ok := shaCryptAlgo[magic]; ok { return a.swaps, nil } From 5c8462262e858f11f4fe6efbb4a9668f0707d96c Mon Sep 17 00:00:00 2001 From: Peter Palmreuther Date: Tue, 11 May 2021 12:53:26 +0200 Subject: [PATCH 7/7] Reduce number of memory allocations By carefully pre-allocating and reusing already allocated but no more needed structures and slices one can significantly reduce number of allocation calls. Within SHA-crypt "X rounds hashing" impact can be significant. --- shacrypt.go | 65 ++++++++++++++++++++++++++++------------------------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/shacrypt.go b/shacrypt.go index 3dade18..e1953c3 100644 --- a/shacrypt.go +++ b/shacrypt.go @@ -57,26 +57,27 @@ func SHACrypt(hash crypto.Hash, password, salt, magic []byte, rounds uint, defau A.Write(password) A.Write(salt) + intermediate := make([]byte, 0, hash.Size()) // #4 - #8 B := hash.New() B.Write(password) B.Write(salt) B.Write(password) - BDigest := B.Sum(nil) + intermediate = B.Sum(intermediate[:0]) // #9 i := len(password) for ; i > hash.Size(); i -= hash.Size() { - A.Write(BDigest) + A.Write(intermediate) } // #10 - A.Write(BDigest[:i]) + A.Write(intermediate[:i]) // #11 for i = len(password); i > 0; i >>= 1 { // last bit is set to 1 if i&1 != 0 { - A.Write(BDigest) + A.Write(intermediate) } else { A.Write(password) } @@ -86,40 +87,43 @@ func SHACrypt(hash crypto.Hash, password, salt, magic []byte, rounds uint, defau ADigest := A.Sum(nil) // #13 - #15 - DP := hash.New() + B.Reset() + DP := B for i = 0; i < len(password); i++ { DP.Write(password) } - DPDigest := DP.Sum(nil) + intermediate = DP.Sum(intermediate[:0]) // #16 i = len(password) P := make([]byte, 0, i) for ; i > hash.Size(); i -= hash.Size() { - P = append(P, DPDigest...) + P = append(P, intermediate...) } - P = append(P, DPDigest[:i]...) + P = append(P, intermediate[:i]...) // #17 - #19 - DS := hash.New() + B.Reset() + DS := B times := 16 + uint8(ADigest[0]) for ; times > 0; times-- { DS.Write(salt) } - DSDigest := DS.Sum(nil) + intermediate = DS.Sum(intermediate[:0]) // #20 i = len(salt) S := make([]byte, 0, i) for ; i > hash.Size(); i -= hash.Size() { - S = append(S, DSDigest...) + S = append(S, intermediate...) } - S = append(S, DSDigest[:i]...) + S = append(S, intermediate[:i]...) // #21 - var finalDigest = ADigest + finalDigest := append(intermediate[:0], ADigest...) for rCount := uint(0); rCount < rounds; rCount++ { - R := hash.New() + B.Reset() + R := B var seq []byte if rCount%2 != 0 { seq = P @@ -138,8 +142,7 @@ func SHACrypt(hash crypto.Hash, password, salt, magic []byte, rounds uint, defau } else { R.Write(P) } - RDigest := R.Sum(nil) - finalDigest = RDigest + finalDigest = R.Sum(finalDigest[:0]) } mapping, err := getSwapBytes(string(magic)) @@ -147,29 +150,28 @@ func SHACrypt(hash crypto.Hash, password, salt, magic []byte, rounds uint, defau return nil, missingByteSwapMapperError } + digestLength := encodedLength(hash) + result := make([]byte, 0, 37+digestLength) + // #22 a) + result = append(result, magic...) + // #22 b) + if !defaultRounds { + result = append(strconv.AppendUint(append(result, []byte("rounds")...), uint64(rounds), 10), cryptPassDelim...) + } + // #22 c) + d) + result = append(append(result, salt[:16]...), cryptPassDelim...) + // #22 e) result // base64 encode following sha-crypt rules [#22 e)] - encoded := make([]byte, 0, encodedLength(hash)) v := uint(0) bits := uint(0) for _, idx := range mapping { v |= uint(finalDigest[idx]) << bits for bits = bits + 8; bits > 6; bits -= 6 { - encoded = append(encoded, shaEncoding[v&0x3f]) + result = append(result, shaEncoding[v&0x3f]) v >>= 6 } } - encoded = append(encoded, shaEncoding[v&0x3f]) - - // #22 a) - result := magic - // #22 b) - if !defaultRounds { - result = append(append(result, strconv.AppendUint([]byte("rounds="), uint64(rounds), 10)...), cryptPassDelim...) - } - // #22 c) + d) - result = append(append(result, salt[:16]...), cryptPassDelim...) - // #22 e) result - result = append(result, encoded...) + result = append(result, shaEncoding[v&0x3f]) return result, nil } @@ -209,7 +211,8 @@ func DissectShaCryptHash(hashedPassword []byte) (*SHAHash, error) { } } - magic := append(append(append([]byte{}, cryptPassDelim...), parts[1]...), cryptPassDelim...) + magic := make([]byte, 0, 2*len(cryptPassDelim) + len(parts[1])) + magic = append(append(append(magic, cryptPassDelim...), parts[1]...), cryptPassDelim...) if hash, err := getHash(string(magic)); err != nil { return nil, cryptPassStructureError