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.
279 lines
8.9 KiB
279 lines
8.9 KiB
3 years ago
|
package exchange
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"crypto/rand"
|
||
|
"math/big"
|
||
|
|
||
|
"github.com/go-faster/errors"
|
||
|
"go.uber.org/zap"
|
||
|
|
||
|
"github.com/gotd/td/bin"
|
||
|
"github.com/gotd/td/internal/crypto"
|
||
|
"github.com/gotd/td/internal/mt"
|
||
|
"github.com/gotd/td/internal/proto"
|
||
|
)
|
||
|
|
||
|
// Run runs client-side flow.
|
||
|
func (c ClientExchange) Run(ctx context.Context) (ClientExchangeResult, error) {
|
||
|
// 1. DH exchange initiation.
|
||
|
nonce, err := crypto.RandInt128(c.rand)
|
||
|
if err != nil {
|
||
|
return ClientExchangeResult{}, errors.Wrap(err, "client nonce generation")
|
||
|
}
|
||
|
b := new(bin.Buffer)
|
||
|
|
||
|
c.log.Debug("Sending ReqPqMultiRequest")
|
||
|
if err := c.writeUnencrypted(ctx, b, &mt.ReqPqMultiRequest{Nonce: nonce}); err != nil {
|
||
|
return ClientExchangeResult{}, errors.Wrap(err, "write ReqPqMultiRequest")
|
||
|
}
|
||
|
|
||
|
// 2. Server sends response of the form
|
||
|
// resPQ#05162463 nonce:int128 server_nonce:int128 pq:string server_public_key_fingerprints:Vector long = ResPQ;
|
||
|
var res mt.ResPQ
|
||
|
if err := c.readUnencrypted(ctx, b, &res); err != nil {
|
||
|
return ClientExchangeResult{}, errors.Wrap(err, "read ResPQ response")
|
||
|
}
|
||
|
c.log.Debug("Received server ResPQ")
|
||
|
if res.Nonce != nonce {
|
||
|
return ClientExchangeResult{}, errors.New("ResPQ nonce mismatch")
|
||
|
}
|
||
|
serverNonce := res.ServerNonce
|
||
|
|
||
|
// Selecting first public key that match fingerprint.
|
||
|
var selectedPubKey PublicKey
|
||
|
Loop:
|
||
|
for _, key := range c.keys {
|
||
|
f := key.Fingerprint()
|
||
|
|
||
|
for _, fingerprint := range res.ServerPublicKeyFingerprints {
|
||
|
if fingerprint == f {
|
||
|
selectedPubKey = key
|
||
|
break Loop
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if selectedPubKey.Zero() {
|
||
|
return ClientExchangeResult{}, ErrKeyFingerprintNotFound
|
||
|
}
|
||
|
|
||
|
// The pq is a representation of a natural number (in binary big endian format).
|
||
|
// SetBytes is also big endian.
|
||
|
pq := big.NewInt(0).SetBytes(res.Pq)
|
||
|
// Normally pq is less than or equal to 2^63-1.
|
||
|
pqMax := big.NewInt(0).Exp(big.NewInt(2), big.NewInt(63), nil)
|
||
|
if pq.Cmp(pqMax) > 0 {
|
||
|
return ClientExchangeResult{}, errors.New("server provided bad pq")
|
||
|
}
|
||
|
|
||
|
start := c.clock.Now()
|
||
|
// 3. Client decomposes pq into prime factors such that p < q.
|
||
|
// Performing proof of work.
|
||
|
p, q, err := crypto.DecomposePQ(pq, c.rand)
|
||
|
if err != nil {
|
||
|
return ClientExchangeResult{}, errors.Wrap(err, "decompose pq")
|
||
|
}
|
||
|
c.log.Debug("PQ decomposing complete", zap.Duration("took", c.clock.Now().Sub(start)))
|
||
|
// Make a copy of p and q values to reduce allocations.
|
||
|
pBytes := p.Bytes()
|
||
|
qBytes := q.Bytes()
|
||
|
|
||
|
// 4. Client sends query to server.
|
||
|
// req_DH_params#d712e4be nonce:int128 server_nonce:int128 p:string q:string
|
||
|
// public_key_fingerprint:long encrypted_data:string = Server_DH_Params
|
||
|
newNonce, err := crypto.RandInt256(c.rand)
|
||
|
if err != nil {
|
||
|
return ClientExchangeResult{}, errors.Wrap(err, "generate new nonce")
|
||
|
}
|
||
|
|
||
|
var encryptedData []byte
|
||
|
pqInnerData := &mt.PQInnerDataDC{
|
||
|
Pq: res.Pq,
|
||
|
Nonce: nonce,
|
||
|
NewNonce: newNonce,
|
||
|
ServerNonce: serverNonce,
|
||
|
P: pBytes,
|
||
|
Q: qBytes,
|
||
|
DC: c.dc,
|
||
|
}
|
||
|
b.Reset()
|
||
|
if err := pqInnerData.Encode(b); err != nil {
|
||
|
return ClientExchangeResult{}, err
|
||
|
}
|
||
|
|
||
|
// `encrypted_data := RSA_PAD(data, server_public_key);`
|
||
|
data, err := crypto.RSAPad(b.Buf, selectedPubKey.RSA, c.rand)
|
||
|
if err != nil {
|
||
|
return ClientExchangeResult{}, errors.Wrap(err, "encrypted_data generation")
|
||
|
}
|
||
|
|
||
|
encryptedData = data
|
||
|
|
||
|
reqDHParams := &mt.ReqDHParamsRequest{
|
||
|
Nonce: nonce,
|
||
|
ServerNonce: serverNonce,
|
||
|
P: pBytes,
|
||
|
Q: qBytes,
|
||
|
PublicKeyFingerprint: selectedPubKey.Fingerprint(),
|
||
|
EncryptedData: encryptedData,
|
||
|
}
|
||
|
c.log.Debug("Sending ReqDHParamsRequest")
|
||
|
if err := c.writeUnencrypted(ctx, b, reqDHParams); err != nil {
|
||
|
return ClientExchangeResult{}, errors.Wrap(err, "write ReqDHParamsRequest")
|
||
|
}
|
||
|
|
||
|
// 5. Server responds with Server_DH_Params.
|
||
|
if err := c.conn.Recv(ctx, b); err != nil {
|
||
|
return ClientExchangeResult{}, errors.Wrap(err, "read ServerDHParams message")
|
||
|
}
|
||
|
c.log.Debug("Received server ServerDHParams")
|
||
|
|
||
|
var plaintextMsg proto.UnencryptedMessage
|
||
|
if err := plaintextMsg.Decode(b); err != nil {
|
||
|
return ClientExchangeResult{}, errors.Wrap(err, "decode ServerDHParams message")
|
||
|
}
|
||
|
|
||
|
b.ResetTo(plaintextMsg.MessageData)
|
||
|
dhParams, err := mt.DecodeServerDHParams(b)
|
||
|
if err != nil {
|
||
|
return ClientExchangeResult{}, err
|
||
|
}
|
||
|
switch p := dhParams.(type) {
|
||
|
case *mt.ServerDHParamsOk:
|
||
|
// Success.
|
||
|
if p.Nonce != nonce {
|
||
|
return ClientExchangeResult{}, errors.New("ServerDHParamsOk nonce mismatch")
|
||
|
}
|
||
|
if p.ServerNonce != serverNonce {
|
||
|
return ClientExchangeResult{}, errors.New("ServerDHParamsOk server nonce mismatch")
|
||
|
}
|
||
|
|
||
|
key, iv := crypto.TempAESKeys(newNonce.BigInt(), serverNonce.BigInt())
|
||
|
// Decrypting inner data.
|
||
|
data, err := crypto.DecryptExchangeAnswer(p.EncryptedAnswer, key, iv)
|
||
|
if err != nil {
|
||
|
return ClientExchangeResult{}, errors.Wrap(err, "exchange answer decrypt")
|
||
|
}
|
||
|
b.ResetTo(data)
|
||
|
|
||
|
innerData := mt.ServerDHInnerData{}
|
||
|
if err := innerData.Decode(b); err != nil {
|
||
|
return ClientExchangeResult{}, err
|
||
|
}
|
||
|
if innerData.Nonce != nonce {
|
||
|
return ClientExchangeResult{}, errors.New("ServerDHInnerData nonce mismatch")
|
||
|
}
|
||
|
if innerData.ServerNonce != serverNonce {
|
||
|
return ClientExchangeResult{}, errors.New("ServerDHInnerData server nonce mismatch")
|
||
|
}
|
||
|
|
||
|
dhPrime := big.NewInt(0).SetBytes(innerData.DhPrime)
|
||
|
g := big.NewInt(int64(innerData.G))
|
||
|
if err := crypto.CheckDH(innerData.G, dhPrime); err != nil {
|
||
|
return ClientExchangeResult{}, errors.Wrap(err, "check DH params")
|
||
|
}
|
||
|
gA := big.NewInt(0).SetBytes(innerData.GA)
|
||
|
|
||
|
// 6. Random number b is computed:
|
||
|
randMax := big.NewInt(0).SetBit(big.NewInt(0), crypto.RSAKeyBits, 1)
|
||
|
bParam, err := rand.Int(c.rand, randMax)
|
||
|
if err != nil {
|
||
|
return ClientExchangeResult{}, errors.Wrap(err, "number b generation")
|
||
|
}
|
||
|
// g_b = g^b mod dh_prime
|
||
|
gB := big.NewInt(0).Exp(g, bParam, dhPrime)
|
||
|
|
||
|
// Checking key exchange parameters.
|
||
|
if err := crypto.CheckDHParams(dhPrime, g, gA, gB); err != nil {
|
||
|
return ClientExchangeResult{}, errors.Wrap(err, "key exchange failed: invalid params")
|
||
|
}
|
||
|
|
||
|
clientInnerData := mt.ClientDHInnerData{
|
||
|
ServerNonce: innerData.ServerNonce,
|
||
|
Nonce: innerData.Nonce,
|
||
|
GB: gB.Bytes(),
|
||
|
// first attempt
|
||
|
RetryID: 0,
|
||
|
}
|
||
|
b.Reset()
|
||
|
if err := clientInnerData.Encode(b); err != nil {
|
||
|
return ClientExchangeResult{}, err
|
||
|
}
|
||
|
clientEncrypted, err := crypto.EncryptExchangeAnswer(c.rand, b.Buf, key, iv)
|
||
|
if err != nil {
|
||
|
return ClientExchangeResult{}, errors.Wrap(err, "exchange answer encrypt")
|
||
|
}
|
||
|
|
||
|
setParamsReq := &mt.SetClientDHParamsRequest{
|
||
|
Nonce: nonce,
|
||
|
ServerNonce: reqDHParams.ServerNonce,
|
||
|
EncryptedData: clientEncrypted,
|
||
|
}
|
||
|
c.log.Debug("Sending SetClientDHParamsRequest")
|
||
|
if err := c.writeUnencrypted(ctx, b, setParamsReq); err != nil {
|
||
|
return ClientExchangeResult{}, errors.Wrap(err, "write SetClientDHParamsRequest")
|
||
|
}
|
||
|
|
||
|
// 7. Computing auth_key using formula (g_a)^b mod dh_prime
|
||
|
authKey := big.NewInt(0).Exp(gA, bParam, dhPrime)
|
||
|
|
||
|
b.Reset()
|
||
|
if err := c.conn.Recv(ctx, b); err != nil {
|
||
|
return ClientExchangeResult{}, errors.Wrap(err, "read DhGen message")
|
||
|
}
|
||
|
c.log.Debug("Received server DhGen")
|
||
|
|
||
|
if err := plaintextMsg.Decode(b); err != nil {
|
||
|
return ClientExchangeResult{}, errors.Wrap(err, "decode DhGen message")
|
||
|
}
|
||
|
b.ResetTo(plaintextMsg.MessageData)
|
||
|
dhSetRes, err := mt.DecodeSetClientDHParamsAnswer(b)
|
||
|
if err != nil {
|
||
|
return ClientExchangeResult{}, errors.Wrap(err, "decode DhGen answer")
|
||
|
}
|
||
|
switch v := dhSetRes.(type) {
|
||
|
case *mt.DhGenOk: // dh_gen_ok#3bcbf734
|
||
|
if v.Nonce != nonce {
|
||
|
return ClientExchangeResult{}, errors.New("DhGenOk nonce mismatch")
|
||
|
}
|
||
|
if v.ServerNonce != serverNonce {
|
||
|
return ClientExchangeResult{}, errors.New("DhGenOk server nonce mismatch")
|
||
|
}
|
||
|
|
||
|
var key crypto.Key
|
||
|
authKey.FillBytes(key[:])
|
||
|
authKeyID := key.ID()
|
||
|
|
||
|
// Checking received hash.
|
||
|
nonceHash1 := crypto.NonceHash1(newNonce, key)
|
||
|
serverSalt := crypto.ServerSalt(newNonce, v.ServerNonce)
|
||
|
|
||
|
if nonceHash1 != v.NewNonceHash1 {
|
||
|
return ClientExchangeResult{}, errors.New("key exchange verification failed: hash mismatch")
|
||
|
}
|
||
|
|
||
|
// Generating new session id and salt.
|
||
|
sessionID, err := crypto.NewSessionID(c.rand)
|
||
|
if err != nil {
|
||
|
return ClientExchangeResult{}, err
|
||
|
}
|
||
|
|
||
|
return ClientExchangeResult{
|
||
|
AuthKey: crypto.AuthKey{Value: key, ID: authKeyID},
|
||
|
SessionID: sessionID,
|
||
|
ServerSalt: serverSalt,
|
||
|
}, nil
|
||
|
case *mt.DhGenRetry: // dh_gen_retry#46dc1fb9
|
||
|
return ClientExchangeResult{}, errors.Errorf("retry required: %x", v.NewNonceHash2)
|
||
|
case *mt.DhGenFail: // dh_gen_fail#a69dae02
|
||
|
return ClientExchangeResult{}, errors.Errorf("dh_hen_fail: %x", v.NewNonceHash3)
|
||
|
default:
|
||
|
return ClientExchangeResult{}, errors.Errorf("unexpected SetClientDHParamsRequest result %T", v)
|
||
|
}
|
||
|
case *mt.ServerDHParamsFail:
|
||
|
return ClientExchangeResult{}, errors.New("server respond with server_DH_params_fail")
|
||
|
default:
|
||
|
return ClientExchangeResult{}, errors.Errorf("unexpected ReqDHParamsRequest result %T", p)
|
||
|
}
|
||
|
}
|