auth: complete account authorization flows
(cherry picked from commit 04f4527df32ad5c35720cccc41d27fe51549612f)
This commit is contained in:
parent
af41d18478
commit
6dc42942c8
21 changed files with 1780 additions and 50 deletions
204
internal/app/account/srp.go
Normal file
204
internal/app/account/srp.go
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
package account
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"math/big"
|
||||
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
const (
|
||||
passwordHashSize = 256
|
||||
recoveryCode = "12345"
|
||||
recoveryCodeTTL = 15 * 60
|
||||
)
|
||||
|
||||
var (
|
||||
baseSalt1 = []byte{0xEC, 0xF8, 0x73, 0x76, 0x65, 0xBC, 0x77, 0x5A}
|
||||
baseSalt2 = []byte{0xBE, 0xDE, 0x48, 0x88, 0x8C, 0x0F, 0x42, 0xAC, 0x34, 0xFF, 0xD1, 0xD4, 0x93, 0x5D, 0x8B, 0x21}
|
||||
baseP = mustDecodeHex("c71caeb9c6b1c9048e6c522f70f13f73980d40238e3e21c14934d037563d930f48198a0aa7c14058229493d22530f4dbfa336f6e0ac925139543aed44cce7c3720fd51f69458705ac68cd4fe6b6b13abdc9746512969328454f18faf8c595f642477fe96bb2a941d5bcd1d4ac8cc49880708fa9b378e3c4f3a9060bee67cf9a4a4a695811051907e162753b56b0f6b410dba74d8a84b2a14b3144e0ef1284754fd17ed950d5965b4b9dd46582db1178d169c6bc465b0d6ff9ca3928fef5b9ae4e418fc15e83ebea0f87fa9ff5eed70050ded2849f47bf959d956850ce929851f0d8115f635b105ee2e4e15d04b2454bf6f4fadf034b10403119cd8e3b92fcc5b")
|
||||
baseG = 3
|
||||
)
|
||||
|
||||
func defaultPasswordAlgo() domain.PasswordKDFAlgo {
|
||||
return domain.PasswordKDFAlgo{
|
||||
Salt1: append([]byte(nil), baseSalt1...),
|
||||
Salt2: append([]byte(nil), baseSalt2...),
|
||||
G: baseG,
|
||||
P: append([]byte(nil), baseP...),
|
||||
}
|
||||
}
|
||||
|
||||
func defaultSecureAlgo() domain.SecurePasswordKDFAlgo {
|
||||
return domain.SecurePasswordKDFAlgo{
|
||||
Kind: "pbkdf2_hmac_sha512_iter100000",
|
||||
Salt: []byte{0x7D, 0x04, 0xB3, 0x4B, 0x94, 0x82, 0x8C, 0x3D},
|
||||
}
|
||||
}
|
||||
|
||||
func makeSRPChallenge(verifier []byte) (secret, b []byte, err error) {
|
||||
secret = make([]byte, passwordHashSize)
|
||||
if _, err := rand.Read(secret); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
b, err = calcSRPB(secret, verifier)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return secret, b, nil
|
||||
}
|
||||
|
||||
func calcSRPB(secret, verifier []byte) ([]byte, error) {
|
||||
p := new(big.Int).SetBytes(baseP)
|
||||
g := big.NewInt(int64(baseG))
|
||||
v := new(big.Int).SetBytes(verifier)
|
||||
b := new(big.Int).SetBytes(secret)
|
||||
if v.Sign() <= 0 || v.Cmp(p) >= 0 {
|
||||
return nil, domain.ErrPasswordHashInvalid
|
||||
}
|
||||
k := new(big.Int).SetBytes(hashBytes(padToHash(baseP), padToHash(g.Bytes())))
|
||||
kv := new(big.Int).Mul(k, v)
|
||||
kv.Mod(kv, p)
|
||||
gb := new(big.Int).Exp(g, b, p)
|
||||
out := new(big.Int).Add(kv, gb)
|
||||
out.Mod(out, p)
|
||||
return padToHash(out.Bytes()), nil
|
||||
}
|
||||
|
||||
func checkSRP(settings domain.PasswordSettings, check domain.PasswordCheck) error {
|
||||
if check.Empty {
|
||||
if settings.HasPassword {
|
||||
return domain.ErrPasswordHashInvalid
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !settings.HasPassword {
|
||||
return domain.ErrPasswordHashInvalid
|
||||
}
|
||||
if settings.SRPID == 0 || settings.SRPID != check.SRPID {
|
||||
return domain.ErrSRPIDInvalid
|
||||
}
|
||||
if len(settings.SRPVerifier) == 0 || len(settings.SRPBSecret) == 0 || len(settings.SRPB) == 0 {
|
||||
return domain.ErrSRPPasswordChanged
|
||||
}
|
||||
got, err := calcSRPM1(settings, check.A)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !bytes.Equal(got, check.M1) {
|
||||
return domain.ErrPasswordHashInvalid
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func calcSRPM1(settings domain.PasswordSettings, aBytes []byte) ([]byte, error) {
|
||||
p := new(big.Int).SetBytes(baseP)
|
||||
g := big.NewInt(int64(baseG))
|
||||
a := new(big.Int).SetBytes(aBytes)
|
||||
if !isGoodLarge(a, p) {
|
||||
return nil, domain.ErrPasswordHashInvalid
|
||||
}
|
||||
v := new(big.Int).SetBytes(settings.SRPVerifier)
|
||||
if !isGoodLarge(v, p) {
|
||||
return nil, domain.ErrPasswordHashInvalid
|
||||
}
|
||||
b := new(big.Int).SetBytes(settings.SRPBSecret)
|
||||
bForHash := padToHash(settings.SRPB)
|
||||
aForHash := padToHash(aBytes)
|
||||
u := new(big.Int).SetBytes(hashBytes(aForHash, bForHash))
|
||||
if u.Sign() <= 0 {
|
||||
return nil, domain.ErrPasswordHashInvalid
|
||||
}
|
||||
vu := new(big.Int).Exp(v, u, p)
|
||||
sBase := new(big.Int).Mul(a, vu)
|
||||
sBase.Mod(sBase, p)
|
||||
s := new(big.Int).Exp(sBase, b, p)
|
||||
k := hashBytes(padToHash(s.Bytes()))
|
||||
salt1 := settings.NewAlgo.Salt1
|
||||
if settings.CurrentAlgo != nil {
|
||||
salt1 = settings.CurrentAlgo.Salt1
|
||||
}
|
||||
return hashBytes(
|
||||
xorBytes(hashBytes(padToHash(baseP)), hashBytes(padToHash(g.Bytes()))),
|
||||
hashBytes(salt1),
|
||||
hashBytes(baseSalt2),
|
||||
aForHash,
|
||||
bForHash,
|
||||
k,
|
||||
), nil
|
||||
}
|
||||
|
||||
func validateNewPasswordSettings(in domain.PasswordInputSettings) error {
|
||||
if in.NewAlgo == nil || len(in.NewPasswordHash) == 0 {
|
||||
return domain.ErrNewSettingsInvalid
|
||||
}
|
||||
algo := in.NewAlgo
|
||||
if algo.G != baseG || !bytes.Equal(algo.P, baseP) || !bytes.Equal(algo.Salt2, baseSalt2) {
|
||||
return domain.ErrNewSaltInvalid
|
||||
}
|
||||
if len(algo.Salt1) != len(baseSalt1)+32 || !bytes.Equal(algo.Salt1[:len(baseSalt1)], baseSalt1) {
|
||||
return domain.ErrNewSaltInvalid
|
||||
}
|
||||
v := new(big.Int).SetBytes(in.NewPasswordHash)
|
||||
if !isGoodLarge(v, new(big.Int).SetBytes(baseP)) {
|
||||
return domain.ErrPasswordHashInvalid
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hashBytes(parts ...[]byte) []byte {
|
||||
h := sha256.New()
|
||||
for _, part := range parts {
|
||||
_, _ = h.Write(part)
|
||||
}
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
func passwordDigest(algo domain.PasswordKDFAlgo, password []byte) []byte {
|
||||
hash1 := hashBytes(algo.Salt1, password, algo.Salt1)
|
||||
hash2 := hashBytes(algo.Salt2, hash1, algo.Salt2)
|
||||
hash3 := pbkdf2.Key(hash2, algo.Salt1, 100000, 64, sha512.New)
|
||||
return hashBytes(algo.Salt2, hash3, algo.Salt2)
|
||||
}
|
||||
|
||||
func verifierForPassword(algo domain.PasswordKDFAlgo, password []byte) []byte {
|
||||
p := new(big.Int).SetBytes(algo.P)
|
||||
g := big.NewInt(int64(algo.G))
|
||||
x := new(big.Int).SetBytes(passwordDigest(algo, password))
|
||||
return padToHash(new(big.Int).Exp(g, x, p).Bytes())
|
||||
}
|
||||
|
||||
func padToHash(in []byte) []byte {
|
||||
if len(in) >= passwordHashSize {
|
||||
return append([]byte(nil), in[len(in)-passwordHashSize:]...)
|
||||
}
|
||||
out := make([]byte, passwordHashSize)
|
||||
copy(out[passwordHashSize-len(in):], in)
|
||||
return out
|
||||
}
|
||||
|
||||
func xorBytes(a, b []byte) []byte {
|
||||
out := make([]byte, len(a))
|
||||
for i := range a {
|
||||
out[i] = a[i] ^ b[i]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func isGoodLarge(n, p *big.Int) bool {
|
||||
return n.Sign() > 0 && new(big.Int).Sub(p, n).Sign() > 0
|
||||
}
|
||||
|
||||
func mustDecodeHex(s string) []byte {
|
||||
out, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return out
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue