forked from Silverfish/proton-bridge
220 lines
7.3 KiB
Go
220 lines
7.3 KiB
Go
// Copyright (c) 2021 Proton Technologies AG
|
|
//
|
|
// This file is part of ProtonMail Bridge.
|
|
//
|
|
// ProtonMail Bridge is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// ProtonMail Bridge is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package srp
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"errors"
|
|
"math/big"
|
|
|
|
"golang.org/x/crypto/openpgp"
|
|
"golang.org/x/crypto/openpgp/clearsign"
|
|
)
|
|
|
|
//nolint[gochecknoglobals]
|
|
var (
|
|
ErrDataAfterModulus = errors.New("pm-srp: extra data after modulus")
|
|
ErrInvalidSignature = errors.New("pm-srp: invalid modulus signature")
|
|
RandReader = rand.Reader
|
|
)
|
|
|
|
// Store random reader in a variable to be able to overwrite it in tests
|
|
|
|
// Amored pubkey for modulus verification.
|
|
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-----`
|
|
|
|
// ReadClearSignedMessage reads the clear text from signed message and verifies
|
|
// signature. There must be no data appended after signed message in input string.
|
|
// The message must be sign by key corresponding to `modulusPubkey`.
|
|
func ReadClearSignedMessage(signedMessage string) (string, error) {
|
|
modulusBlock, rest := clearsign.Decode([]byte(signedMessage))
|
|
if len(rest) != 0 {
|
|
return "", ErrDataAfterModulus
|
|
}
|
|
|
|
modulusKeyring, err := openpgp.ReadArmoredKeyRing(bytes.NewReader([]byte(modulusPubkey)))
|
|
if err != nil {
|
|
return "", errors.New("pm-srp: can not read modulus pubkey")
|
|
}
|
|
|
|
_, err = openpgp.CheckDetachedSignature(modulusKeyring, bytes.NewReader(modulusBlock.Bytes), modulusBlock.ArmoredSignature.Body, nil)
|
|
if err != nil {
|
|
return "", ErrInvalidSignature
|
|
}
|
|
|
|
return string(modulusBlock.Bytes), nil
|
|
}
|
|
|
|
// SrpProofs object.
|
|
type SrpProofs struct { //nolint[golint]
|
|
ClientProof, ClientEphemeral, ExpectedServerProof []byte
|
|
}
|
|
|
|
// SrpAuth stores byte data for the calculation of SRP proofs.
|
|
type SrpAuth struct { //nolint[golint]
|
|
Modulus, ServerEphemeral, HashedPassword []byte
|
|
}
|
|
|
|
// NewSrpAuth creates new SrpAuth from strings input. Salt and server ephemeral are in
|
|
// base64 format. Modulus is base64 with signature attached. The signature is
|
|
// verified against server key. The version controls password hash algorithm.
|
|
func NewSrpAuth(version int, username, password, salt, signedModulus, serverEphemeral string) (auth *SrpAuth, err error) {
|
|
data := &SrpAuth{}
|
|
|
|
// Modulus
|
|
var modulus string
|
|
modulus, err = ReadClearSignedMessage(signedModulus)
|
|
if err != nil {
|
|
return
|
|
}
|
|
data.Modulus, err = base64.StdEncoding.DecodeString(modulus)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Password
|
|
var decodedSalt []byte
|
|
if version >= 3 {
|
|
decodedSalt, err = base64.StdEncoding.DecodeString(salt)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
data.HashedPassword, err = HashPassword(version, password, username, decodedSalt, data.Modulus)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Server ephermeral
|
|
data.ServerEphemeral, err = base64.StdEncoding.DecodeString(serverEphemeral)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
return data, nil
|
|
}
|
|
|
|
// GenerateSrpProofs calculates SPR proofs.
|
|
func (s *SrpAuth) GenerateSrpProofs(length int) (res *SrpProofs, err error) { //nolint[funlen]
|
|
toInt := func(arr []byte) *big.Int {
|
|
var reversed = make([]byte, len(arr))
|
|
for i := 0; i < len(arr); i++ {
|
|
reversed[len(arr)-i-1] = arr[i]
|
|
}
|
|
return big.NewInt(0).SetBytes(reversed)
|
|
}
|
|
|
|
fromInt := func(num *big.Int) []byte {
|
|
var arr = num.Bytes()
|
|
var reversed = make([]byte, length/8)
|
|
for i := 0; i < len(arr); i++ {
|
|
reversed[len(arr)-i-1] = arr[i]
|
|
}
|
|
return reversed
|
|
}
|
|
|
|
generator := big.NewInt(2)
|
|
multiplier := toInt(ExpandHash(append(fromInt(generator), s.Modulus...)))
|
|
|
|
modulus := toInt(s.Modulus)
|
|
serverEphemeral := toInt(s.ServerEphemeral)
|
|
hashedPassword := toInt(s.HashedPassword)
|
|
|
|
modulusMinusOne := big.NewInt(0).Sub(modulus, big.NewInt(1))
|
|
|
|
if modulus.BitLen() != length {
|
|
return nil, errors.New("pm-srp: 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("pm-srp: SRP multiplier is out of bounds")
|
|
}
|
|
|
|
if generator.Cmp(big.NewInt(1)) <= 0 || generator.Cmp(modulusMinusOne) >= 0 {
|
|
return nil, errors.New("pm-srp: SRP generator is out of bounds")
|
|
}
|
|
|
|
if serverEphemeral.Cmp(big.NewInt(1)) <= 0 || serverEphemeral.Cmp(modulusMinusOne) >= 0 {
|
|
return nil, errors.New("pm-srp: SRP server ephemeral is out of bounds")
|
|
}
|
|
|
|
// Check primality
|
|
// Doing exponentiation here is faster than a full call to ProbablyPrime while
|
|
// still perfectly accurate by Pocklington's theorem
|
|
if big.NewInt(0).Exp(big.NewInt(2), modulusMinusOne, modulus).Cmp(big.NewInt(1)) != 0 {
|
|
return nil, errors.New("pm-srp: SRP modulus is not prime")
|
|
}
|
|
|
|
// Check safe primality
|
|
if !big.NewInt(0).Rsh(modulus, 1).ProbablyPrime(10) {
|
|
return nil, errors.New("pm-srp: SRP modulus is not a safe prime")
|
|
}
|
|
|
|
var clientSecret, clientEphemeral, scramblingParam *big.Int
|
|
for {
|
|
for {
|
|
clientSecret, err = rand.Int(RandReader, modulusMinusOne)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if clientSecret.Cmp(big.NewInt(int64(length*2))) > 0 { // Very likely
|
|
break
|
|
}
|
|
}
|
|
|
|
clientEphemeral = big.NewInt(0).Exp(generator, clientSecret, modulus)
|
|
scramblingParam = toInt(ExpandHash(append(fromInt(clientEphemeral), fromInt(serverEphemeral)...)))
|
|
if scramblingParam.Cmp(big.NewInt(0)) != 0 { // Very likely
|
|
break
|
|
}
|
|
}
|
|
|
|
subtracted := big.NewInt(0).Sub(serverEphemeral, big.NewInt(0).Mod(big.NewInt(0).Mul(big.NewInt(0).Exp(generator, hashedPassword, 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, hashedPassword), clientSecret), modulusMinusOne)
|
|
sharedSession := big.NewInt(0).Exp(subtracted, exponent, modulus)
|
|
|
|
clientProof := ExpandHash(bytes.Join([][]byte{fromInt(clientEphemeral), fromInt(serverEphemeral), fromInt(sharedSession)}, []byte{}))
|
|
serverProof := ExpandHash(bytes.Join([][]byte{fromInt(clientEphemeral), clientProof, fromInt(sharedSession)}, []byte{}))
|
|
|
|
return &SrpProofs{ClientEphemeral: fromInt(clientEphemeral), ClientProof: clientProof, ExpectedServerProof: serverProof}, nil
|
|
}
|
|
|
|
// GenerateVerifier verifier for update pwds and create accounts.
|
|
func (s *SrpAuth) GenerateVerifier(length int) ([]byte, error) {
|
|
return nil, errors.New("pm-srp: the client doesn't need SRP GenerateVerifier")
|
|
}
|