本站源代码
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

208 lines
5.9KB

  1. // @author Couchbase <info@couchbase.com>
  2. // @copyright 2018 Couchbase, Inc.
  3. //
  4. // Licensed under the Apache License, Version 2.0 (the "License");
  5. // you may not use this file except in compliance with the License.
  6. // You may obtain a copy of the License at
  7. //
  8. // http://www.apache.org/licenses/LICENSE-2.0
  9. //
  10. // Unless required by applicable law or agreed to in writing, software
  11. // distributed under the License is distributed on an "AS IS" BASIS,
  12. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. // See the License for the specific language governing permissions and
  14. // limitations under the License.
  15. // Package scramsha provides implementation of client side SCRAM-SHA
  16. // according to https://tools.ietf.org/html/rfc5802
  17. package scramsha
  18. import (
  19. "crypto/hmac"
  20. "crypto/rand"
  21. "crypto/sha1"
  22. "crypto/sha256"
  23. "crypto/sha512"
  24. "encoding/base64"
  25. "fmt"
  26. "github.com/pkg/errors"
  27. "golang.org/x/crypto/pbkdf2"
  28. "hash"
  29. "strconv"
  30. "strings"
  31. )
  32. func hmacHash(message []byte, secret []byte, hashFunc func() hash.Hash) []byte {
  33. h := hmac.New(hashFunc, secret)
  34. h.Write(message)
  35. return h.Sum(nil)
  36. }
  37. func shaHash(message []byte, hashFunc func() hash.Hash) []byte {
  38. h := hashFunc()
  39. h.Write(message)
  40. return h.Sum(nil)
  41. }
  42. func generateClientNonce(size int) (string, error) {
  43. randomBytes := make([]byte, size)
  44. _, err := rand.Read(randomBytes)
  45. if err != nil {
  46. return "", errors.Wrap(err, "Unable to generate nonce")
  47. }
  48. return base64.StdEncoding.EncodeToString(randomBytes), nil
  49. }
  50. // ScramSha provides context for SCRAM-SHA handling
  51. type ScramSha struct {
  52. hashSize int
  53. hashFunc func() hash.Hash
  54. clientNonce string
  55. serverNonce string
  56. salt []byte
  57. i int
  58. saltedPassword []byte
  59. authMessage string
  60. }
  61. var knownMethods = []string{"SCRAM-SHA512", "SCRAM-SHA256", "SCRAM-SHA1"}
  62. // BestMethod returns SCRAM-SHA method we consider the best out of suggested
  63. // by server
  64. func BestMethod(methods string) (string, error) {
  65. for _, m := range knownMethods {
  66. if strings.Index(methods, m) != -1 {
  67. return m, nil
  68. }
  69. }
  70. return "", errors.Errorf(
  71. "None of the server suggested methods [%s] are supported",
  72. methods)
  73. }
  74. // NewScramSha creates context for SCRAM-SHA handling
  75. func NewScramSha(method string) (*ScramSha, error) {
  76. s := &ScramSha{}
  77. if method == knownMethods[0] {
  78. s.hashFunc = sha512.New
  79. s.hashSize = 64
  80. } else if method == knownMethods[1] {
  81. s.hashFunc = sha256.New
  82. s.hashSize = 32
  83. } else if method == knownMethods[2] {
  84. s.hashFunc = sha1.New
  85. s.hashSize = 20
  86. } else {
  87. return nil, errors.Errorf("Unsupported method %s", method)
  88. }
  89. return s, nil
  90. }
  91. // GetStartRequest builds start SCRAM-SHA request to be sent to server
  92. func (s *ScramSha) GetStartRequest(user string) (string, error) {
  93. var err error
  94. s.clientNonce, err = generateClientNonce(24)
  95. if err != nil {
  96. return "", errors.Wrapf(err, "Unable to generate SCRAM-SHA "+
  97. "start request for user %s", user)
  98. }
  99. message := fmt.Sprintf("n,,n=%s,r=%s", user, s.clientNonce)
  100. s.authMessage = message[3:]
  101. return message, nil
  102. }
  103. // HandleStartResponse handles server response on start SCRAM-SHA request
  104. func (s *ScramSha) HandleStartResponse(response string) error {
  105. parts := strings.Split(response, ",")
  106. if len(parts) != 3 {
  107. return errors.Errorf("expected 3 fields in first SCRAM-SHA-1 "+
  108. "server message %s", response)
  109. }
  110. if !strings.HasPrefix(parts[0], "r=") || len(parts[0]) < 3 {
  111. return errors.Errorf("Server sent an invalid nonce %s",
  112. parts[0])
  113. }
  114. if !strings.HasPrefix(parts[1], "s=") || len(parts[1]) < 3 {
  115. return errors.Errorf("Server sent an invalid salt %s", parts[1])
  116. }
  117. if !strings.HasPrefix(parts[2], "i=") || len(parts[2]) < 3 {
  118. return errors.Errorf("Server sent an invalid iteration count %s",
  119. parts[2])
  120. }
  121. s.serverNonce = parts[0][2:]
  122. encodedSalt := parts[1][2:]
  123. var err error
  124. s.i, err = strconv.Atoi(parts[2][2:])
  125. if err != nil {
  126. return errors.Errorf("Iteration count %s must be integer.",
  127. parts[2][2:])
  128. }
  129. if s.i < 1 {
  130. return errors.New("Iteration count should be positive")
  131. }
  132. if !strings.HasPrefix(s.serverNonce, s.clientNonce) {
  133. return errors.Errorf("Server nonce %s doesn't contain client"+
  134. " nonce %s", s.serverNonce, s.clientNonce)
  135. }
  136. s.salt, err = base64.StdEncoding.DecodeString(encodedSalt)
  137. if err != nil {
  138. return errors.Wrapf(err, "Unable to decode salt %s",
  139. encodedSalt)
  140. }
  141. s.authMessage = s.authMessage + "," + response
  142. return nil
  143. }
  144. // GetFinalRequest builds final SCRAM-SHA request to be sent to server
  145. func (s *ScramSha) GetFinalRequest(pass string) string {
  146. clientFinalMessageBare := "c=biws,r=" + s.serverNonce
  147. s.authMessage = s.authMessage + "," + clientFinalMessageBare
  148. s.saltedPassword = pbkdf2.Key([]byte(pass), s.salt, s.i,
  149. s.hashSize, s.hashFunc)
  150. clientKey := hmacHash([]byte("Client Key"), s.saltedPassword, s.hashFunc)
  151. storedKey := shaHash(clientKey, s.hashFunc)
  152. clientSignature := hmacHash([]byte(s.authMessage), storedKey, s.hashFunc)
  153. clientProof := make([]byte, len(clientSignature))
  154. for i := 0; i < len(clientSignature); i++ {
  155. clientProof[i] = clientKey[i] ^ clientSignature[i]
  156. }
  157. return clientFinalMessageBare + ",p=" +
  158. base64.StdEncoding.EncodeToString(clientProof)
  159. }
  160. // HandleFinalResponse handles server's response on final SCRAM-SHA request
  161. func (s *ScramSha) HandleFinalResponse(response string) error {
  162. if strings.Contains(response, ",") ||
  163. !strings.HasPrefix(response, "v=") {
  164. return errors.Errorf("Server sent an invalid final message %s",
  165. response)
  166. }
  167. decodedMessage, err := base64.StdEncoding.DecodeString(response[2:])
  168. if err != nil {
  169. return errors.Wrapf(err, "Unable to decode server message %s",
  170. response[2:])
  171. }
  172. serverKey := hmacHash([]byte("Server Key"), s.saltedPassword,
  173. s.hashFunc)
  174. serverSignature := hmacHash([]byte(s.authMessage), serverKey,
  175. s.hashFunc)
  176. if string(decodedMessage) != string(serverSignature) {
  177. return errors.Errorf("Server proof %s doesn't match "+
  178. "the expected: %s",
  179. string(decodedMessage), string(serverSignature))
  180. }
  181. return nil
  182. }
上海开阖软件有限公司 沪ICP备12045867号-1