2017-08-22 01:04:16 +03:00
|
|
|
package protonmail
|
|
|
|
|
|
|
|
import (
|
2020-06-30 18:13:36 +03:00
|
|
|
"bytes"
|
2017-08-22 01:04:16 +03:00
|
|
|
"crypto/rand"
|
|
|
|
"crypto/subtle"
|
|
|
|
"encoding/base64"
|
|
|
|
"errors"
|
2019-04-14 19:07:03 +03:00
|
|
|
"fmt"
|
2017-08-22 01:04:16 +03:00
|
|
|
"io"
|
2020-06-30 18:13:36 +03:00
|
|
|
"log"
|
2017-08-22 01:04:16 +03:00
|
|
|
"math/big"
|
|
|
|
|
2020-06-30 18:13:36 +03:00
|
|
|
"golang.org/x/crypto/openpgp"
|
2017-08-22 01:04:16 +03:00
|
|
|
"golang.org/x/crypto/openpgp/clearsign"
|
2020-06-30 18:13:36 +03:00
|
|
|
openpgperrors "golang.org/x/crypto/openpgp/errors"
|
2017-08-22 01:04:16 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
var randReader io.Reader = rand.Reader
|
|
|
|
|
2020-06-30 18:13:36 +03:00
|
|
|
// Public key for SRP verification
|
|
|
|
// From https://github.com/ProtonMail/proton-bridge/blob/99721b6577fe9079ac7547f11fc77e5090cdd31b/pkg/srp/srp.go#L41-L52
|
|
|
|
const modulusPubkey = `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
|
|
|
|
|
|
|
xjMEXAHLgxYJKwYBBAHaRw8BAQdAFurWXXwjTemqjD7CXjXVyKf0of7n9Ctm
|
|
|
|
L8v9enkzggHNEnByb3RvbkBzcnAubW9kdWx1c8J3BBAWCgApBQJcAcuDBgsJ
|
|
|
|
BwgDAgkQNQWFxOlRjyYEFQgKAgMWAgECGQECGwMCHgEAAPGRAP9sauJsW12U
|
|
|
|
MnTQUZpsbJb53d0Wv55mZIIiJL2XulpWPQD/V6NglBd96lZKBmInSXX/kXat
|
|
|
|
Sv+y0io+LR8i2+jV+AbOOARcAcuDEgorBgEEAZdVAQUBAQdAeJHUz1c9+KfE
|
|
|
|
kSIgcBRE3WuXC4oj5a2/U3oASExGDW4DAQgHwmEEGBYIABMFAlwBy4MJEDUF
|
|
|
|
hcTpUY8mAhsMAAD/XQD8DxNI6E78meodQI+wLsrKLeHn32iLvUqJbVDhfWSU
|
|
|
|
WO4BAMcm1u02t4VKw++ttECPt+HUgPUq5pqQWe5Q2cW4TMsE
|
|
|
|
=Y4Mw
|
|
|
|
-----END PGP PUBLIC KEY BLOCK-----`
|
|
|
|
|
2017-08-22 01:04:16 +03:00
|
|
|
func decodeModulus(msg string) ([]byte, error) {
|
|
|
|
block, _ := clearsign.Decode([]byte(msg))
|
|
|
|
if block == nil {
|
2019-04-14 19:07:03 +03:00
|
|
|
return nil, errors.New("invalid SRP modulus signed PGP block")
|
2017-08-22 01:04:16 +03:00
|
|
|
}
|
|
|
|
|
2020-06-30 18:13:36 +03:00
|
|
|
modulusKeyring, err := openpgp.ReadArmoredKeyRing(bytes.NewReader([]byte(modulusPubkey)))
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("cannot read modulus pubkey: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = openpgp.CheckDetachedSignature(modulusKeyring, bytes.NewReader(block.Bytes), block.ArmoredSignature.Body, nil)
|
|
|
|
if err != nil && err != openpgperrors.ErrUnknownIssuer {
|
2019-04-13 13:58:21 +03:00
|
|
|
//return nil, fmt.Errorf("failed to decode modulus: %v", err)
|
2020-06-30 18:13:36 +03:00
|
|
|
log.Println("warning: failed to check SRP modulus signature:", err)
|
|
|
|
}
|
2017-08-22 01:04:16 +03:00
|
|
|
|
2019-04-14 19:07:03 +03:00
|
|
|
b, err := base64.StdEncoding.DecodeString(string(block.Plaintext))
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("malformed SRP modulus: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return b, nil
|
2017-08-22 01:04:16 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func reverse(b []byte) {
|
|
|
|
for i := 0; i < len(b)/2; i++ {
|
|
|
|
j := len(b) - 1 - i
|
|
|
|
b[i], b[j] = b[j], b[i]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func itoa(i *big.Int, l int) []byte {
|
|
|
|
b := i.Bytes()
|
|
|
|
reverse(b)
|
2017-08-22 11:16:20 +03:00
|
|
|
padding := make([]byte, l/8-len(b))
|
2017-08-22 01:04:16 +03:00
|
|
|
b = append(b, padding...)
|
|
|
|
return b
|
|
|
|
}
|
|
|
|
|
|
|
|
func atoi(b []byte) *big.Int {
|
|
|
|
reverse(b)
|
|
|
|
return big.NewInt(0).SetBytes(b)
|
|
|
|
}
|
|
|
|
|
|
|
|
type proofs struct {
|
2017-08-22 11:16:20 +03:00
|
|
|
clientEphemeral []byte
|
|
|
|
clientProof []byte
|
2017-08-22 01:04:16 +03:00
|
|
|
expectedServerProof []byte
|
|
|
|
}
|
|
|
|
|
|
|
|
// From https://github.com/ProtonMail/WebClient/blob/public/src/app/authentication/services/srp.js#L13
|
|
|
|
func generateProofs(l int, hash func([]byte) []byte, modulusBytes, hashedBytes, serverEphemeralBytes []byte) (*proofs, error) {
|
|
|
|
generator := big.NewInt(2)
|
|
|
|
|
|
|
|
multiplier := atoi(hash(append(itoa(generator, l), modulusBytes...)))
|
|
|
|
modulus := atoi(modulusBytes)
|
|
|
|
hashed := atoi(hashedBytes)
|
|
|
|
serverEphemeral := atoi(serverEphemeralBytes)
|
|
|
|
|
|
|
|
modulusMinusOne := big.NewInt(0).Sub(modulus, big.NewInt(1))
|
|
|
|
if modulus.BitLen() != l {
|
|
|
|
return nil, errors.New("SRP modulus has incorrect size")
|
|
|
|
}
|
|
|
|
|
|
|
|
multiplier = multiplier.Mod(multiplier, modulus)
|
|
|
|
|
|
|
|
if multiplier.Cmp(big.NewInt(1)) <= 0 || multiplier.Cmp(modulusMinusOne) >= 0 {
|
|
|
|
return nil, errors.New("SRP multiplier is out of bounds")
|
|
|
|
}
|
|
|
|
if generator.Cmp(big.NewInt(1)) <= 0 || generator.Cmp(modulusMinusOne) >= 0 {
|
|
|
|
return nil, errors.New("SRP generator is out of bounds")
|
|
|
|
}
|
|
|
|
if serverEphemeral.Cmp(big.NewInt(1)) <= 0 || serverEphemeral.Cmp(modulusMinusOne) >= 0 {
|
|
|
|
return nil, errors.New("SRP server ephemeral is out of bounds")
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: Check primality
|
|
|
|
// TODO: Check safe primality
|
|
|
|
|
|
|
|
var clientSecret, clientEphemeral, scramblingParam *big.Int
|
|
|
|
for {
|
|
|
|
for {
|
|
|
|
var err error
|
|
|
|
clientSecret, err = rand.Int(randReader, modulusMinusOne)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if clientSecret.Cmp(big.NewInt(int64(l)*2)) <= 0 { // Very unlikely
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
clientEphemeral = big.NewInt(0).Exp(generator, clientSecret, modulus)
|
|
|
|
scramblingParam = atoi(hash(append(itoa(clientEphemeral, l), itoa(serverEphemeral, l)...)))
|
|
|
|
if scramblingParam.Cmp(big.NewInt(0)) == 0 { // Very unlikely
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
subtracted := big.NewInt(0).Sub(serverEphemeral, big.NewInt(0).Mod(big.NewInt(0).Mul(big.NewInt(0).Exp(generator, hashed, modulus), multiplier), modulus))
|
|
|
|
if subtracted.Cmp(big.NewInt(0)) < 0 {
|
|
|
|
subtracted.Add(subtracted, modulus)
|
|
|
|
}
|
|
|
|
exponent := big.NewInt(0).Mod(big.NewInt(0).Add(big.NewInt(0).Mul(scramblingParam, hashed), clientSecret), modulusMinusOne)
|
|
|
|
sharedSession := big.NewInt(0).Exp(subtracted, exponent, modulus)
|
|
|
|
|
|
|
|
var clientProof []byte
|
|
|
|
clientProof = append(clientProof, itoa(clientEphemeral, l)...)
|
|
|
|
clientProof = append(clientProof, itoa(serverEphemeral, l)...)
|
|
|
|
clientProof = append(clientProof, itoa(sharedSession, l)...)
|
|
|
|
clientProof = hash(clientProof)
|
|
|
|
|
|
|
|
var serverProof []byte
|
|
|
|
serverProof = append(serverProof, itoa(clientEphemeral, l)...)
|
|
|
|
serverProof = append(serverProof, clientProof...)
|
|
|
|
serverProof = append(serverProof, itoa(sharedSession, l)...)
|
|
|
|
serverProof = hash(serverProof)
|
|
|
|
|
|
|
|
return &proofs{
|
2017-08-22 11:16:20 +03:00
|
|
|
clientEphemeral: itoa(clientEphemeral, l),
|
|
|
|
clientProof: clientProof,
|
2017-08-22 01:04:16 +03:00
|
|
|
expectedServerProof: serverProof,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *proofs) VerifyServerProof(serverProofString string) error {
|
|
|
|
serverProof, err := base64.StdEncoding.DecodeString(serverProofString)
|
|
|
|
if err != nil {
|
2019-04-14 19:07:03 +03:00
|
|
|
return fmt.Errorf("malformed SRP server proof: %v", err)
|
2017-08-22 01:04:16 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if subtle.ConstantTimeCompare(p.expectedServerProof, serverProof) != 1 {
|
2019-04-14 19:07:03 +03:00
|
|
|
return errors.New("invalid SRP server proof")
|
2017-08-22 01:04:16 +03:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// From https://github.com/ProtonMail/WebClient/blob/public/src/app/authentication/services/srp.js#L135
|
|
|
|
func srp(password []byte, info *AuthInfo) (*proofs, error) {
|
|
|
|
modulus, err := decodeModulus(info.modulus)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
serverEphemeral, err := base64.StdEncoding.DecodeString(info.serverEphemeral)
|
|
|
|
if err != nil {
|
2019-04-14 19:07:03 +03:00
|
|
|
return nil, fmt.Errorf("malformed SRP server ephemeral: %v", err)
|
2017-08-22 01:04:16 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
salt, err := base64.StdEncoding.DecodeString(info.salt)
|
|
|
|
if err != nil {
|
2019-04-14 19:07:03 +03:00
|
|
|
return nil, fmt.Errorf("malformed SRP salt: %v", err)
|
2017-08-22 01:04:16 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
hashed, err := hashPassword(info.version, password, salt, modulus)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
proofs, err := generateProofs(2048, expandHash, modulus, hashed, serverEphemeral)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return proofs, nil
|
|
|
|
}
|