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.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. package migrations
  2. import (
  3. "bytes"
  4. "crypto/hmac"
  5. "crypto/md5"
  6. "crypto/sha1"
  7. "crypto/sha256"
  8. "crypto/sha512"
  9. "crypto/subtle"
  10. "encoding/base64"
  11. "encoding/hex"
  12. "errors"
  13. "hash"
  14. "strconv"
  15. "github.com/GehirnInc/crypt/md5_crypt"
  16. "golang.org/x/crypto/pbkdf2"
  17. )
  18. var (
  19. ErrHashInvalid = errors.New("password hash invalid for algorithm")
  20. ErrHashCheckFailed = errors.New("passphrase did not match stored hash")
  21. hmacServerKeyText = []byte("Server Key")
  22. athemePBKDF2V2Prefix = []byte("$z")
  23. )
  24. type PassphraseCheck func(hash, passphrase []byte) (err error)
  25. func CheckAthemePassphrase(hash, passphrase []byte) (err error) {
  26. if len(hash) < 60 {
  27. return checkAthemePosixCrypt(hash, passphrase)
  28. } else if bytes.HasPrefix(hash, athemePBKDF2V2Prefix) {
  29. return checkAthemePBKDF2V2(hash, passphrase)
  30. } else {
  31. return checkAthemePBKDF2(hash, passphrase)
  32. }
  33. }
  34. func checkAthemePosixCrypt(hash, passphrase []byte) (err error) {
  35. // crypto/posix: the platform's crypt(3) function
  36. // MD5 on linux, DES on MacOS: forget MacOS
  37. md5crypt := md5_crypt.New()
  38. return md5crypt.Verify(string(hash), []byte(passphrase))
  39. }
  40. type pbkdf2v2Algo struct {
  41. Hash func() hash.Hash
  42. OutputSize int
  43. SCRAM bool
  44. SaltB64 bool
  45. }
  46. func athemePBKDF2V2ParseAlgo(algo string) (result pbkdf2v2Algo, err error) {
  47. // https://github.com/atheme/atheme/blob/a11e85efc67d86fc4738e3e2a4f220bfa69153f0/include/atheme/pbkdf2.h#L34-L52
  48. algoInt, err := strconv.Atoi(algo)
  49. if err != nil {
  50. return result, ErrHashInvalid
  51. }
  52. hashCode := algoInt % 10
  53. algoCode := algoInt - hashCode
  54. switch algoCode {
  55. case 0:
  56. // e.g., #define PBKDF2_PRF_HMAC_MD5 3U
  57. // no SCRAM, no SHA256
  58. case 20:
  59. // e.g., #define PBKDF2_PRF_HMAC_MD5_S64 23U
  60. // no SCRAM, base64
  61. result.SaltB64 = true
  62. case 40:
  63. // e.g., #define PBKDF2_PRF_SCRAM_MD5 43U
  64. // SCRAM, no base64
  65. result.SCRAM = true
  66. case 60:
  67. // e.g., #define PBKDF2_PRF_SCRAM_MD5_S64 63U
  68. result.SaltB64 = true
  69. result.SCRAM = true
  70. default:
  71. return result, ErrHashInvalid
  72. }
  73. switch hashCode {
  74. case 3:
  75. result.Hash, result.OutputSize = md5.New, (128 / 8)
  76. case 4:
  77. result.Hash, result.OutputSize = sha1.New, (160 / 8)
  78. case 5:
  79. result.Hash, result.OutputSize = sha256.New, (256 / 8)
  80. case 6:
  81. result.Hash, result.OutputSize = sha512.New, (512 / 8)
  82. default:
  83. return result, ErrHashInvalid
  84. }
  85. return result, nil
  86. }
  87. func checkAthemePBKDF2V2(hash, passphrase []byte) (err error) {
  88. // crypto/pbkdf2v2, the default as of september 2020:
  89. // "the format for pbkdf2v2 is $z$alg$iter$salt$digest
  90. // where the z is literal,
  91. // the alg is one from https://github.com/atheme/atheme/blob/master/include/atheme/pbkdf2.h#L34-L52
  92. // iter is the iteration count.
  93. // if the alg ends in _S64 then the salt is base64-encoded, otherwise taken literally
  94. // (an ASCII salt, inherited from the pbkdf2 module).
  95. // if alg is a SCRAM one, then digest is actually serverkey$storedkey (see RFC 5802).
  96. // digest, serverkey and storedkey are base64-encoded."
  97. parts := bytes.Split(hash, []byte{'$'})
  98. if len(parts) < 6 {
  99. return ErrHashInvalid
  100. }
  101. algo, err := athemePBKDF2V2ParseAlgo(string(parts[2]))
  102. if err != nil {
  103. return err
  104. }
  105. iter, err := strconv.Atoi(string(parts[3]))
  106. if err != nil {
  107. return ErrHashInvalid
  108. }
  109. salt := parts[4]
  110. if algo.SaltB64 {
  111. salt, err = base64.StdEncoding.DecodeString(string(salt))
  112. if err != nil {
  113. return err
  114. }
  115. }
  116. // if SCRAM, parts[5] is ServerKey; otherwise it's the actual PBKDF2 output
  117. // either way, it's what we'll test against
  118. expected, err := base64.StdEncoding.DecodeString(string(parts[5]))
  119. if err != nil {
  120. return err
  121. }
  122. var key []byte
  123. if algo.SCRAM {
  124. if len(parts) != 7 {
  125. return ErrHashInvalid
  126. }
  127. stretch := pbkdf2.Key(passphrase, salt, iter, algo.OutputSize, algo.Hash)
  128. mac := hmac.New(algo.Hash, stretch)
  129. mac.Write(hmacServerKeyText)
  130. key = mac.Sum(nil)
  131. } else {
  132. if len(parts) != 6 {
  133. return ErrHashInvalid
  134. }
  135. key = pbkdf2.Key(passphrase, salt, iter, len(expected), algo.Hash)
  136. }
  137. if subtle.ConstantTimeCompare(key, expected) == 1 {
  138. return nil
  139. } else {
  140. return ErrHashCheckFailed
  141. }
  142. }
  143. func checkAthemePBKDF2(hash, passphrase []byte) (err error) {
  144. // crypto/pbkdf2:
  145. // "SHA2-512, 128000 iterations, 16-ASCII-character salt, hexadecimal encoding of digest,
  146. // digest appended directly to salt, for a single string consisting of only 144 characters"
  147. if len(hash) != 144 {
  148. return ErrHashInvalid
  149. }
  150. salt := hash[:16]
  151. digest := make([]byte, 64)
  152. cnt, err := hex.Decode(digest, hash[16:])
  153. if err != nil || cnt != 64 {
  154. return ErrHashCheckFailed
  155. }
  156. key := pbkdf2.Key(passphrase, salt, 128000, 64, sha512.New)
  157. if subtle.ConstantTimeCompare(key, digest) == 1 {
  158. return nil
  159. } else {
  160. return ErrHashCheckFailed
  161. }
  162. }