2020-10-02 22:48:37 +02:00
|
|
|
package migrations
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"crypto/hmac"
|
|
|
|
"crypto/md5"
|
|
|
|
"crypto/sha1"
|
|
|
|
"crypto/sha256"
|
|
|
|
"crypto/sha512"
|
|
|
|
"crypto/subtle"
|
|
|
|
"encoding/base64"
|
2020-10-12 21:06:17 +02:00
|
|
|
"encoding/binary"
|
2020-10-02 22:48:37 +02:00
|
|
|
"encoding/hex"
|
|
|
|
"errors"
|
|
|
|
"hash"
|
|
|
|
"strconv"
|
|
|
|
|
|
|
|
"github.com/GehirnInc/crypt/md5_crypt"
|
2020-10-12 21:06:17 +02:00
|
|
|
"golang.org/x/crypto/bcrypt"
|
2020-10-02 22:48:37 +02:00
|
|
|
"golang.org/x/crypto/pbkdf2"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
ErrHashInvalid = errors.New("password hash invalid for algorithm")
|
|
|
|
ErrHashCheckFailed = errors.New("passphrase did not match stored hash")
|
|
|
|
|
|
|
|
hmacServerKeyText = []byte("Server Key")
|
|
|
|
athemePBKDF2V2Prefix = []byte("$z")
|
2020-10-12 21:06:17 +02:00
|
|
|
athemeRawSHA1Prefix = []byte("$rawsha1$")
|
2020-10-02 22:48:37 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
type PassphraseCheck func(hash, passphrase []byte) (err error)
|
|
|
|
|
|
|
|
func CheckAthemePassphrase(hash, passphrase []byte) (err error) {
|
2020-10-12 21:06:17 +02:00
|
|
|
if bytes.HasPrefix(hash, athemeRawSHA1Prefix) {
|
|
|
|
return checkAthemeRawSha1(hash, passphrase)
|
2020-10-02 22:48:37 +02:00
|
|
|
} else if bytes.HasPrefix(hash, athemePBKDF2V2Prefix) {
|
|
|
|
return checkAthemePBKDF2V2(hash, passphrase)
|
2020-10-12 21:06:17 +02:00
|
|
|
} else if len(hash) < 60 {
|
|
|
|
return checkAthemePosixCrypt(hash, passphrase)
|
2020-10-02 22:48:37 +02:00
|
|
|
} else {
|
|
|
|
return checkAthemePBKDF2(hash, passphrase)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func checkAthemePosixCrypt(hash, passphrase []byte) (err error) {
|
|
|
|
// crypto/posix: the platform's crypt(3) function
|
|
|
|
// MD5 on linux, DES on MacOS: forget MacOS
|
|
|
|
md5crypt := md5_crypt.New()
|
|
|
|
return md5crypt.Verify(string(hash), []byte(passphrase))
|
|
|
|
}
|
|
|
|
|
|
|
|
type pbkdf2v2Algo struct {
|
|
|
|
Hash func() hash.Hash
|
|
|
|
OutputSize int
|
|
|
|
SCRAM bool
|
|
|
|
SaltB64 bool
|
|
|
|
}
|
|
|
|
|
|
|
|
func athemePBKDF2V2ParseAlgo(algo string) (result pbkdf2v2Algo, err error) {
|
|
|
|
// https://github.com/atheme/atheme/blob/a11e85efc67d86fc4738e3e2a4f220bfa69153f0/include/atheme/pbkdf2.h#L34-L52
|
|
|
|
algoInt, err := strconv.Atoi(algo)
|
|
|
|
if err != nil {
|
|
|
|
return result, ErrHashInvalid
|
|
|
|
}
|
|
|
|
hashCode := algoInt % 10
|
|
|
|
algoCode := algoInt - hashCode
|
|
|
|
|
|
|
|
switch algoCode {
|
|
|
|
case 0:
|
|
|
|
// e.g., #define PBKDF2_PRF_HMAC_MD5 3U
|
|
|
|
// no SCRAM, no SHA256
|
|
|
|
case 20:
|
|
|
|
// e.g., #define PBKDF2_PRF_HMAC_MD5_S64 23U
|
|
|
|
// no SCRAM, base64
|
|
|
|
result.SaltB64 = true
|
|
|
|
case 40:
|
|
|
|
// e.g., #define PBKDF2_PRF_SCRAM_MD5 43U
|
|
|
|
// SCRAM, no base64
|
|
|
|
result.SCRAM = true
|
|
|
|
case 60:
|
|
|
|
// e.g., #define PBKDF2_PRF_SCRAM_MD5_S64 63U
|
|
|
|
result.SaltB64 = true
|
|
|
|
result.SCRAM = true
|
|
|
|
default:
|
|
|
|
return result, ErrHashInvalid
|
|
|
|
}
|
|
|
|
|
|
|
|
switch hashCode {
|
|
|
|
case 3:
|
|
|
|
result.Hash, result.OutputSize = md5.New, (128 / 8)
|
|
|
|
case 4:
|
|
|
|
result.Hash, result.OutputSize = sha1.New, (160 / 8)
|
|
|
|
case 5:
|
|
|
|
result.Hash, result.OutputSize = sha256.New, (256 / 8)
|
|
|
|
case 6:
|
|
|
|
result.Hash, result.OutputSize = sha512.New, (512 / 8)
|
|
|
|
default:
|
|
|
|
return result, ErrHashInvalid
|
|
|
|
}
|
|
|
|
|
|
|
|
return result, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func checkAthemePBKDF2V2(hash, passphrase []byte) (err error) {
|
|
|
|
// crypto/pbkdf2v2, the default as of september 2020:
|
|
|
|
// "the format for pbkdf2v2 is $z$alg$iter$salt$digest
|
|
|
|
// where the z is literal,
|
|
|
|
// the alg is one from https://github.com/atheme/atheme/blob/master/include/atheme/pbkdf2.h#L34-L52
|
|
|
|
// iter is the iteration count.
|
|
|
|
// if the alg ends in _S64 then the salt is base64-encoded, otherwise taken literally
|
|
|
|
// (an ASCII salt, inherited from the pbkdf2 module).
|
|
|
|
// if alg is a SCRAM one, then digest is actually serverkey$storedkey (see RFC 5802).
|
|
|
|
// digest, serverkey and storedkey are base64-encoded."
|
|
|
|
parts := bytes.Split(hash, []byte{'$'})
|
|
|
|
if len(parts) < 6 {
|
|
|
|
return ErrHashInvalid
|
|
|
|
}
|
|
|
|
algo, err := athemePBKDF2V2ParseAlgo(string(parts[2]))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
iter, err := strconv.Atoi(string(parts[3]))
|
|
|
|
if err != nil {
|
|
|
|
return ErrHashInvalid
|
|
|
|
}
|
|
|
|
|
|
|
|
salt := parts[4]
|
|
|
|
if algo.SaltB64 {
|
|
|
|
salt, err = base64.StdEncoding.DecodeString(string(salt))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// if SCRAM, parts[5] is ServerKey; otherwise it's the actual PBKDF2 output
|
|
|
|
// either way, it's what we'll test against
|
|
|
|
expected, err := base64.StdEncoding.DecodeString(string(parts[5]))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
var key []byte
|
|
|
|
if algo.SCRAM {
|
|
|
|
if len(parts) != 7 {
|
|
|
|
return ErrHashInvalid
|
|
|
|
}
|
|
|
|
stretch := pbkdf2.Key(passphrase, salt, iter, algo.OutputSize, algo.Hash)
|
|
|
|
mac := hmac.New(algo.Hash, stretch)
|
|
|
|
mac.Write(hmacServerKeyText)
|
|
|
|
key = mac.Sum(nil)
|
|
|
|
} else {
|
|
|
|
if len(parts) != 6 {
|
|
|
|
return ErrHashInvalid
|
|
|
|
}
|
|
|
|
key = pbkdf2.Key(passphrase, salt, iter, len(expected), algo.Hash)
|
|
|
|
}
|
|
|
|
|
|
|
|
if subtle.ConstantTimeCompare(key, expected) == 1 {
|
|
|
|
return nil
|
|
|
|
} else {
|
|
|
|
return ErrHashCheckFailed
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func checkAthemePBKDF2(hash, passphrase []byte) (err error) {
|
|
|
|
// crypto/pbkdf2:
|
|
|
|
// "SHA2-512, 128000 iterations, 16-ASCII-character salt, hexadecimal encoding of digest,
|
|
|
|
// digest appended directly to salt, for a single string consisting of only 144 characters"
|
|
|
|
if len(hash) != 144 {
|
|
|
|
return ErrHashInvalid
|
|
|
|
}
|
|
|
|
|
|
|
|
salt := hash[:16]
|
|
|
|
digest := make([]byte, 64)
|
|
|
|
cnt, err := hex.Decode(digest, hash[16:])
|
|
|
|
if err != nil || cnt != 64 {
|
|
|
|
return ErrHashCheckFailed
|
|
|
|
}
|
|
|
|
|
|
|
|
key := pbkdf2.Key(passphrase, salt, 128000, 64, sha512.New)
|
|
|
|
if subtle.ConstantTimeCompare(key, digest) == 1 {
|
|
|
|
return nil
|
|
|
|
} else {
|
|
|
|
return ErrHashCheckFailed
|
|
|
|
}
|
|
|
|
}
|
2020-10-12 21:06:17 +02:00
|
|
|
|
|
|
|
func checkAthemeRawSha1(hash, passphrase []byte) (err error) {
|
|
|
|
return checkRawHash(hash[len(athemeRawSHA1Prefix):], passphrase, sha1.New())
|
|
|
|
}
|
|
|
|
|
|
|
|
func checkRawHash(expected, passphrase []byte, h hash.Hash) (err error) {
|
|
|
|
var rawExpected []byte
|
|
|
|
size := h.Size()
|
|
|
|
if len(expected) == 2*size {
|
|
|
|
rawExpected = make([]byte, h.Size())
|
|
|
|
_, err = hex.Decode(rawExpected, expected)
|
|
|
|
if err != nil {
|
|
|
|
return ErrHashInvalid
|
|
|
|
}
|
|
|
|
} else if len(expected) == size {
|
|
|
|
rawExpected = expected
|
|
|
|
} else {
|
|
|
|
return ErrHashInvalid
|
|
|
|
}
|
|
|
|
|
|
|
|
h.Write(passphrase)
|
|
|
|
hashedPassphrase := h.Sum(nil)
|
|
|
|
if subtle.ConstantTimeCompare(rawExpected, hashedPassphrase) == 1 {
|
|
|
|
return nil
|
|
|
|
} else {
|
|
|
|
return ErrHashCheckFailed
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func checkAnopeEncSha256(hashBytes, ivBytes, passphrase []byte) (err error) {
|
|
|
|
if len(ivBytes) != 32 {
|
|
|
|
return ErrHashInvalid
|
|
|
|
}
|
|
|
|
// https://github.com/anope/anope/blob/2cf507ed662620d0b97c8484fbfbfa09265e86e1/modules/encryption/enc_sha256.cpp#L67
|
|
|
|
var iv [8]uint32
|
|
|
|
for i := 0; i < 8; i++ {
|
|
|
|
iv[i] = binary.BigEndian.Uint32(ivBytes[i*4 : (i+1)*4])
|
|
|
|
}
|
|
|
|
result := anopeSum256(passphrase, iv)
|
|
|
|
if subtle.ConstantTimeCompare(result[:], hashBytes) == 1 {
|
|
|
|
return nil
|
|
|
|
} else {
|
|
|
|
return ErrHashCheckFailed
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func CheckAnopePassphrase(hash, passphrase []byte) (err error) {
|
|
|
|
pieces := bytes.Split(hash, []byte{':'})
|
|
|
|
if len(pieces) < 2 {
|
|
|
|
return ErrHashInvalid
|
|
|
|
}
|
|
|
|
switch string(pieces[0]) {
|
|
|
|
case "plain":
|
|
|
|
// base64, standard encoding
|
|
|
|
expectedPassphrase, err := base64.StdEncoding.DecodeString(string(pieces[1]))
|
|
|
|
if err != nil {
|
|
|
|
return ErrHashInvalid
|
|
|
|
}
|
|
|
|
if subtle.ConstantTimeCompare(passphrase, expectedPassphrase) == 1 {
|
|
|
|
return nil
|
|
|
|
} else {
|
|
|
|
return ErrHashCheckFailed
|
|
|
|
}
|
|
|
|
case "md5":
|
|
|
|
// raw MD5
|
|
|
|
return checkRawHash(pieces[1], passphrase, md5.New())
|
|
|
|
case "sha1":
|
|
|
|
// raw SHA-1
|
|
|
|
return checkRawHash(pieces[1], passphrase, sha1.New())
|
|
|
|
case "bcrypt":
|
|
|
|
if bcrypt.CompareHashAndPassword(pieces[1], passphrase) == nil {
|
|
|
|
return nil
|
|
|
|
} else {
|
|
|
|
return ErrHashCheckFailed
|
|
|
|
}
|
|
|
|
case "sha256":
|
|
|
|
// SHA-256 with an overridden IV
|
|
|
|
if len(pieces) != 3 {
|
|
|
|
return ErrHashInvalid
|
|
|
|
}
|
|
|
|
hashBytes, err := hex.DecodeString(string(pieces[1]))
|
|
|
|
if err != nil {
|
|
|
|
return ErrHashInvalid
|
|
|
|
}
|
|
|
|
ivBytes, err := hex.DecodeString(string(pieces[2]))
|
|
|
|
if err != nil {
|
|
|
|
return ErrHashInvalid
|
|
|
|
}
|
|
|
|
return checkAnopeEncSha256(hashBytes, ivBytes, passphrase)
|
|
|
|
default:
|
|
|
|
return ErrHashInvalid
|
|
|
|
}
|
|
|
|
}
|