feat: sync iOS compatibility support
This commit is contained in:
parent
1f646ef024
commit
50803a604c
32 changed files with 871 additions and 78 deletions
|
|
@ -1,6 +1,7 @@
|
|||
package mtprotoedge
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
crand "crypto/rand"
|
||||
"encoding/hex"
|
||||
|
|
@ -69,6 +70,17 @@ type serverExchangeCompat struct {
|
|||
commitKey func(context.Context, exchange.ServerExchangeResult) error
|
||||
}
|
||||
|
||||
const pqInnerDataTempTypeID uint32 = 0x3c6a84d4
|
||||
|
||||
// compatPQInnerData is the normalized handshake input accepted at the MTProto
|
||||
// edge. gotd v0.158.0 does not generate p_q_inner_data_temp#3c6a84d4, which is
|
||||
// still emitted by Telegram-iOS for PFS temporary auth keys.
|
||||
type compatPQInnerData struct {
|
||||
Data mt.PQInnerData
|
||||
Temp bool
|
||||
ExpiresIn int
|
||||
}
|
||||
|
||||
func (s serverExchangeCompat) run(ctx context.Context) (exchange.ServerExchangeResult, error) {
|
||||
wrapKeyNotFound := func(err error) error {
|
||||
return exchangeError(codec.CodeAuthKeyNotFound, err)
|
||||
|
|
@ -119,28 +131,36 @@ SendResPQ:
|
|||
|
||||
var innerData mt.PQInnerData
|
||||
{
|
||||
if dhParams.DH.Nonce != req.Nonce {
|
||||
return exchange.ServerExchangeResult{}, gofaster.New("req_DH_params nonce does not match req_pq")
|
||||
}
|
||||
if dhParams.DH.ServerNonce != serverNonce {
|
||||
return exchange.ServerExchangeResult{}, gofaster.New("req_DH_params server_nonce does not match resPQ")
|
||||
}
|
||||
if dhParams.DH.PublicKeyFingerprint != s.key.Fingerprint() {
|
||||
return exchange.ServerExchangeResult{}, gofaster.New("req_DH_params public key fingerprint does not match server key")
|
||||
}
|
||||
|
||||
r, err := crypto.DecodeRSAPad(dhParams.DH.EncryptedData, s.key.RSA)
|
||||
if err != nil {
|
||||
return exchange.ServerExchangeResult{}, wrapKeyNotFound(err)
|
||||
}
|
||||
b.ResetTo(r)
|
||||
|
||||
d, err := mt.DecodePQInnerData(b)
|
||||
d, generated, err := decodeCompatPQInnerData(b)
|
||||
if err != nil {
|
||||
return exchange.ServerExchangeResult{}, err
|
||||
}
|
||||
if err := s.validatePQInnerDataDC(d); err != nil {
|
||||
if generated != nil {
|
||||
if err := s.validatePQInnerDataDC(generated); err != nil {
|
||||
return exchange.ServerExchangeResult{}, err
|
||||
}
|
||||
}
|
||||
if err := validatePQInnerData(d, req, dhParams.DH, serverNonce, pq); err != nil {
|
||||
return exchange.ServerExchangeResult{}, err
|
||||
}
|
||||
|
||||
innerData = mt.PQInnerData{
|
||||
Pq: d.GetPq(),
|
||||
P: d.GetP(),
|
||||
Q: d.GetQ(),
|
||||
Nonce: d.GetNonce(),
|
||||
ServerNonce: d.GetServerNonce(),
|
||||
NewNonce: d.GetNewNonce(),
|
||||
}
|
||||
innerData = d.Data
|
||||
}
|
||||
|
||||
dhPrime, err := s.rng.DhPrime()
|
||||
|
|
@ -188,6 +208,12 @@ SendResPQ:
|
|||
return exchange.ServerExchangeResult{}, err
|
||||
}
|
||||
s.log.Debug("Received client SetClientDHParamsRequest")
|
||||
if clientDhParams.Nonce != req.Nonce {
|
||||
return exchange.ServerExchangeResult{}, gofaster.New("set_client_DH_params nonce does not match req_pq")
|
||||
}
|
||||
if clientDhParams.ServerNonce != serverNonce {
|
||||
return exchange.ServerExchangeResult{}, gofaster.New("set_client_DH_params server_nonce does not match resPQ")
|
||||
}
|
||||
|
||||
decrypted, err := crypto.DecryptExchangeAnswer(clientDhParams.EncryptedData, key, iv)
|
||||
if err != nil {
|
||||
|
|
@ -200,6 +226,12 @@ SendResPQ:
|
|||
if err := clientInnerData.Decode(b); err != nil {
|
||||
return exchange.ServerExchangeResult{}, wrapKeyNotFound(err)
|
||||
}
|
||||
if clientInnerData.Nonce != req.Nonce {
|
||||
return exchange.ServerExchangeResult{}, gofaster.New("client_DH_inner_data nonce does not match req_pq")
|
||||
}
|
||||
if clientInnerData.ServerNonce != serverNonce {
|
||||
return exchange.ServerExchangeResult{}, gofaster.New("client_DH_inner_data server_nonce does not match resPQ")
|
||||
}
|
||||
|
||||
gB := big.NewInt(0).SetBytes(clientInnerData.GB)
|
||||
var authKey crypto.Key
|
||||
|
|
@ -235,6 +267,68 @@ SendResPQ:
|
|||
return serverResult, nil
|
||||
}
|
||||
|
||||
func decodeCompatPQInnerData(b *bin.Buffer) (compatPQInnerData, mt.PQInnerDataClass, error) {
|
||||
id, err := b.PeekID()
|
||||
if err != nil {
|
||||
return compatPQInnerData{}, nil, err
|
||||
}
|
||||
if id == pqInnerDataTempTypeID {
|
||||
if err := b.ConsumeID(pqInnerDataTempTypeID); err != nil {
|
||||
return compatPQInnerData{}, nil, err
|
||||
}
|
||||
var data mt.PQInnerData
|
||||
if err := data.DecodeBare(b); err != nil {
|
||||
return compatPQInnerData{}, nil, fmt.Errorf("decode p_q_inner_data_temp: %w", err)
|
||||
}
|
||||
expiresIn, err := b.Int()
|
||||
if err != nil {
|
||||
return compatPQInnerData{}, nil, fmt.Errorf("decode p_q_inner_data_temp expires_in: %w", err)
|
||||
}
|
||||
return compatPQInnerData{Data: data, Temp: true, ExpiresIn: expiresIn}, nil, nil
|
||||
}
|
||||
|
||||
generated, err := mt.DecodePQInnerData(b)
|
||||
if err != nil {
|
||||
return compatPQInnerData{}, nil, err
|
||||
}
|
||||
result := compatPQInnerData{Data: mt.PQInnerData{
|
||||
Pq: generated.GetPq(),
|
||||
P: generated.GetP(),
|
||||
Q: generated.GetQ(),
|
||||
Nonce: generated.GetNonce(),
|
||||
ServerNonce: generated.GetServerNonce(),
|
||||
NewNonce: generated.GetNewNonce(),
|
||||
}}
|
||||
if temp, ok := generated.(*mt.PQInnerDataTempDC); ok {
|
||||
result.Temp = true
|
||||
result.ExpiresIn = temp.ExpiresIn
|
||||
}
|
||||
return result, generated, nil
|
||||
}
|
||||
|
||||
func validatePQInnerData(d compatPQInnerData, req compatReqPQ, dh mt.ReqDHParamsRequest, serverNonce bin.Int128, pq *big.Int) error {
|
||||
if d.Data.Nonce != req.Nonce {
|
||||
return gofaster.New("p_q_inner_data nonce does not match req_pq")
|
||||
}
|
||||
if d.Data.ServerNonce != serverNonce {
|
||||
return gofaster.New("p_q_inner_data server_nonce does not match resPQ")
|
||||
}
|
||||
if !bytes.Equal(d.Data.Pq, pq.Bytes()) {
|
||||
return gofaster.New("p_q_inner_data pq does not match resPQ")
|
||||
}
|
||||
if !bytes.Equal(d.Data.P, dh.P) || !bytes.Equal(d.Data.Q, dh.Q) {
|
||||
return gofaster.New("p_q_inner_data factors do not match req_DH_params")
|
||||
}
|
||||
product := new(big.Int).Mul(new(big.Int).SetBytes(d.Data.P), new(big.Int).SetBytes(d.Data.Q))
|
||||
if product.Cmp(pq) != 0 {
|
||||
return gofaster.New("p_q_inner_data factors do not multiply to pq")
|
||||
}
|
||||
if d.Temp && d.ExpiresIn <= 0 {
|
||||
return gofaster.New("p_q_inner_data temporary key expires_in must be positive")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s serverExchangeCompat) validatePQInnerDataDC(d mt.PQInnerDataClass) error {
|
||||
switch innerDataDC := d.(type) {
|
||||
case *mt.PQInnerDataDC:
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
package mtprotoedge
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"math/big"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
|
@ -366,6 +368,99 @@ func TestKeyExchangeRejectsWrongNegativeTempDC(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestDecodeCompatPQInnerDataTemp(t *testing.T) {
|
||||
want := mt.PQInnerData{
|
||||
Pq: []byte{0x0f},
|
||||
P: []byte{0x03},
|
||||
Q: []byte{0x05},
|
||||
Nonce: bin.Int128{1, 2, 3},
|
||||
ServerNonce: bin.Int128{4, 5, 6},
|
||||
NewNonce: bin.Int256{7, 8, 9},
|
||||
}
|
||||
b := new(bin.Buffer)
|
||||
b.PutID(pqInnerDataTempTypeID)
|
||||
if err := want.EncodeBare(b); err != nil {
|
||||
t.Fatalf("encode bare: %v", err)
|
||||
}
|
||||
b.PutInt(86400)
|
||||
|
||||
got, generated, err := decodeCompatPQInnerData(b)
|
||||
if err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if generated != nil {
|
||||
t.Fatalf("generated class = %T, want nil for iOS temp compatibility type", generated)
|
||||
}
|
||||
if !got.Temp || got.ExpiresIn != 86400 {
|
||||
t.Fatalf("temp metadata = (%v, %d), want (true, 86400)", got.Temp, got.ExpiresIn)
|
||||
}
|
||||
if !bytes.Equal(got.Data.Pq, want.Pq) || !bytes.Equal(got.Data.P, want.P) || !bytes.Equal(got.Data.Q, want.Q) ||
|
||||
got.Data.Nonce != want.Nonce || got.Data.ServerNonce != want.ServerNonce || got.Data.NewNonce != want.NewNonce {
|
||||
t.Fatalf("decoded data = %+v, want %+v", got.Data, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeCompatPQInnerDataTempRejectsTruncatedData(t *testing.T) {
|
||||
b := new(bin.Buffer)
|
||||
b.PutID(pqInnerDataTempTypeID)
|
||||
b.PutBytes([]byte{0x0f})
|
||||
if _, _, err := decodeCompatPQInnerData(b); err == nil {
|
||||
t.Fatal("truncated p_q_inner_data_temp decoded successfully")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePQInnerDataInvariants(t *testing.T) {
|
||||
nonce := bin.Int128{1}
|
||||
serverNonce := bin.Int128{2}
|
||||
pq := big.NewInt(15)
|
||||
valid := compatPQInnerData{
|
||||
Data: mt.PQInnerData{
|
||||
Pq: pq.Bytes(),
|
||||
P: []byte{3},
|
||||
Q: []byte{5},
|
||||
Nonce: nonce,
|
||||
ServerNonce: serverNonce,
|
||||
},
|
||||
Temp: true,
|
||||
ExpiresIn: 86400,
|
||||
}
|
||||
req := compatReqPQ{Nonce: nonce}
|
||||
dh := mt.ReqDHParamsRequest{P: []byte{3}, Q: []byte{5}}
|
||||
if err := validatePQInnerData(valid, req, dh, serverNonce, pq); err != nil {
|
||||
t.Fatalf("valid inner data: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
mutate func(*compatPQInnerData)
|
||||
}{
|
||||
{name: "nonce", mutate: func(d *compatPQInnerData) { d.Data.Nonce = bin.Int128{9} }},
|
||||
{name: "server nonce", mutate: func(d *compatPQInnerData) { d.Data.ServerNonce = bin.Int128{9} }},
|
||||
{name: "pq", mutate: func(d *compatPQInnerData) { d.Data.Pq = []byte{21} }},
|
||||
{name: "outer factors", mutate: func(d *compatPQInnerData) { d.Data.P = []byte{5} }},
|
||||
{name: "factor product", mutate: func(d *compatPQInnerData) { d.Data.P = []byte{2}; dh.P = []byte{2} }},
|
||||
{name: "expiry", mutate: func(d *compatPQInnerData) { d.ExpiresIn = 0 }},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
candidate := valid
|
||||
candidate.Data.Pq = bytes.Clone(valid.Data.Pq)
|
||||
candidate.Data.P = bytes.Clone(valid.Data.P)
|
||||
candidate.Data.Q = bytes.Clone(valid.Data.Q)
|
||||
localDH := dh
|
||||
if tt.name == "factor product" {
|
||||
candidate.Data.P = []byte{2}
|
||||
localDH.P = []byte{2}
|
||||
} else {
|
||||
tt.mutate(&candidate)
|
||||
}
|
||||
if err := validatePQInnerData(candidate, req, localDH, serverNonce, pq); err == nil {
|
||||
t.Fatal("invalid inner data validated successfully")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyExchangeIgnoresUnencryptedMsgsAck(t *testing.T) {
|
||||
const dc = 2
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue