move gotd fork into repo. (#111)

- update to latest telegram layer
- remove some references to fields in tg.Entities that don't exist in
the schema
- originally added here:
https://github.com/beeper/td/commit/820929062a2ba0104397bc01235ab58a9cff780e
  - referenced here
-
https://github.com/mautrix/telegramgo/commit/124f0967ed195b5a380c9bd02e170ada9710dde3
-
https://github.com/mautrix/telegramgo/commit/4205047aab2e0639217148b5d125bfaab668bd8e
This commit is contained in:
Adam Van Ymeren
2025-06-27 20:03:37 -07:00
committed by GitHub
parent 0952df0244
commit 7a04f298d2
19264 changed files with 1539697 additions and 84 deletions
+54
View File
@@ -0,0 +1,54 @@
package crypto
import (
"math/big"
"github.com/go-faster/errors"
)
// CheckDH performs DH parameters check described in Telegram docs.
//
// Client is expected to check whether p is a safe 2048-bit prime (meaning that both p and (p-1)/2 are prime,
// and that 2^2047 < p < 2^2048), and that g generates a cyclic subgroup of prime order (p-1)/2, i.e.
// is a quadratic residue mod p. Since g is always equal to 2, 3, 4, 5, 6 or 7, this is easily done using quadratic
// reciprocity law, yielding a simple condition on p mod 4g — namely, p mod 8 = 7 for g = 2; p mod 3 = 2 for g = 3;
// no extra condition for g = 4; p mod 5 = 1 or 4 for g = 5; p mod 24 = 19 or 23 for g = 6; and p mod 7 = 3,
// 5 or 6 for g = 7.
//
// See https://core.telegram.org/mtproto/auth_key#presenting-proof-of-work-server-authentication.
//
// See https://core.telegram.org/api/srp#checking-the-password-with-srp.
//
// See https://core.telegram.org/api/end-to-end#sending-a-request.
func CheckDH(g int, p *big.Int) error {
// The client is expected to check whether p is a safe 2048-bit prime
// (meaning that both p and (p-1)/2 are prime, and that 2^2047 < p < 2^2048).
// FIXME(tdakkota): we check that 2^2047 <= p < 2^2048
// but docs says to check 2^2047 < p < 2^2048.
//
// TDLib check 2^2047 <= too:
// https://github.com/tdlib/td/blob/d161323858a782bc500d188b9ae916982526c262/td/mtproto/DhHandshake.cpp#L23
if p.BitLen() != RSAKeyBits {
return errors.New("p should be 2^2047 < p < 2^2048")
}
if err := CheckGP(g, p); err != nil {
return err
}
return checkPrime(p)
}
func checkPrime(p *big.Int) error {
if !Prime(p) {
return errors.New("p is not prime number")
}
sub := big.NewInt(0).Sub(p, big.NewInt(1))
pr := sub.Quo(sub, big.NewInt(2))
if !Prime(pr) {
return errors.New("(p-1)/2 is not prime number")
}
return nil
}
+42
View File
@@ -0,0 +1,42 @@
package crypto
import (
"math/big"
"testing"
"github.com/stretchr/testify/require"
)
func BenchmarkCheckDH(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = CheckDH(checkGPg, checkGPdhPrime)
}
}
func TestCheckDH(t *testing.T) {
t.Run("OK", func(t *testing.T) {
a := require.New(t)
a.NoError(CheckDH(checkGPg, checkGPdhPrime))
})
t.Run("WrongG", func(t *testing.T) {
require.Error(t, CheckDH(1337, checkGPdhPrime))
})
t.Run("TooSmallBits", func(t *testing.T) {
require.Error(t, CheckDH(3, big.NewInt(4)))
})
}
func Test_checkPrime(t *testing.T) {
t.Run("OK", func(t *testing.T) {
require.NoError(t, checkPrime(big.NewInt(5)))
})
t.Run("PNotPrime", func(t *testing.T) {
require.Error(t, checkPrime(big.NewInt(4)))
})
t.Run("HalfPMinusOneNotPrime", func(t *testing.T) {
require.Error(t, checkPrime(big.NewInt(13)))
})
}
+61
View File
@@ -0,0 +1,61 @@
package crypto
import (
"math/big"
"github.com/go-faster/errors"
)
// CheckGP checks whether g generates a cyclic subgroup of prime order (p-1)/2, i.e. is a quadratic residue mod p.
// Also check that g is 2, 3, 4, 5, 6 or 7.
//
// This function is needed by some Telegram algorithms(Key generation, SRP 2FA).
//
// See https://core.telegram.org/mtproto/auth_key.
//
// See https://core.telegram.org/api/srp.
func CheckGP(g int, p *big.Int) error {
// Since g is always equal to 2, 3, 4, 5, 6 or 7,
// this is easily done using quadratic reciprocity law, yielding a simple condition on p mod 4g -- namely,
var result bool
switch g {
case 2:
// p mod 8 = 7 for g = 2;
result = checkSubgroup(p, 8, 7)
case 3:
// p mod 3 = 2 for g = 3;
result = checkSubgroup(p, 3, 2)
case 4:
// no extra condition for g = 4
result = true
case 5:
// p mod 5 = 1 or 4 for g = 5;
result = checkSubgroup(p, 5, 1, 4)
case 6:
// p mod 24 = 19 or 23 for g = 6;
result = checkSubgroup(p, 24, 19, 23)
case 7:
// and p mod 7 = 3, 5 or 6 for g = 7.
result = checkSubgroup(p, 7, 3, 5, 6)
default:
return errors.Errorf("unexpected g = %d: g should be equal to 2, 3, 4, 5, 6 or 7", g)
}
if !result {
return errors.New("g should be a quadratic residue mod p")
}
return nil
}
func checkSubgroup(p *big.Int, divider int64, expected ...int64) bool {
rem := new(big.Int).Rem(p, big.NewInt(divider)).Int64()
for _, e := range expected {
if rem == e {
return true
}
}
return false
}
+55
View File
@@ -0,0 +1,55 @@
package crypto
import (
"encoding/hex"
"math/big"
"testing"
"github.com/stretchr/testify/require"
)
// From Telegram docs example.
//
// See https://core.telegram.org/mtproto/auth_key#presenting-proof-of-work-server-authentication.
var (
checkGPdhPrime = func() *big.Int {
data, err := hex.DecodeString("C71CAEB9C6B1C9048E6C522F70F13F73980D40238E3E21C14934D037563D930F" +
"48198A0AA7C14058229493D22530F4DBFA336F6E0AC925139543AED44CCE7C37" +
"20FD51F69458705AC68CD4FE6B6B13ABDC9746512969328454F18FAF8C595F64" +
"2477FE96BB2A941D5BCD1D4AC8CC49880708FA9B378E3C4F3A9060BEE67CF9A4" +
"A4A695811051907E162753B56B0F6B410DBA74D8A84B2A14B3144E0EF1284754" +
"FD17ED950D5965B4B9DD46582DB1178D169C6BC465B0D6FF9CA3928FEF5B9AE4" +
"E418FC15E83EBEA0F87FA9FF5EED70050DED2849F47BF959D956850CE929851F" +
"0D8115F635B105EE2E4E15D04B2454BF6F4FADF034B10403119CD8E3B92FCC5B")
if err != nil {
panic(err)
}
return big.NewInt(0).SetBytes(data)
}()
checkGPg = 3
)
func BenchmarkCheckGP(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = CheckGP(checkGPg, checkGPdhPrime)
}
}
func TestCheckGP(t *testing.T) {
t.Run("OK", func(t *testing.T) {
a := require.New(t)
a.NoError(CheckGP(checkGPg, checkGPdhPrime))
})
t.Run("WrongG", func(t *testing.T) {
require.Error(t, CheckGP(1337, checkGPdhPrime))
})
t.Run("WrongDivider", func(t *testing.T) {
// CheckGP should check that p mod 3 = 2 for g = 3;
// We pass p = 4, so p mod 3 = 1.
require.Error(t, CheckGP(3, big.NewInt(4)))
})
}
+24
View File
@@ -0,0 +1,24 @@
package crypto
import "io"
// Cipher is message encryption utility struct.
type Cipher struct {
rand io.Reader
encryptSide Side
}
// Rand returns random generator.
func (c Cipher) Rand() io.Reader {
return c.rand
}
// NewClientCipher creates new client-side Cipher.
func NewClientCipher(rand io.Reader) Cipher {
return Cipher{rand: rand, encryptSide: Client}
}
// NewServerCipher creates new server-side Cipher.
func NewServerCipher(rand io.Reader) Cipher {
return Cipher{rand: rand, encryptSide: Server}
}
+91
View File
@@ -0,0 +1,91 @@
package crypto
import (
"crypto/aes"
"github.com/go-faster/errors"
"github.com/gotd/ige"
"go.mau.fi/mautrix-telegram/pkg/gotd/bin"
)
// DecryptFromBuffer decodes EncryptedMessage and decrypts it.
func (c Cipher) DecryptFromBuffer(k AuthKey, buf *bin.Buffer) (*EncryptedMessageData, error) {
msg := &EncryptedMessage{}
// Because we assume that buffer is valid during decrypting, we able to
// use DecodeWithoutCopy and do not allocate inner buffer for EncryptedMessage.
if err := msg.DecodeWithoutCopy(buf); err != nil {
return nil, err
}
return c.Decrypt(k, msg)
}
func (c Cipher) DecryptRaw(k AuthKey, encrypted *EncryptedMessage) ([]byte, error) {
plaintext, err := c.decryptMessage(k, encrypted)
if err != nil {
return nil, err
}
side := c.encryptSide.DecryptSide()
// Checking SHA256 hash value of msg_key
msgKey := MessageKey(k.Value, plaintext, side)
if msgKey != encrypted.MsgKey {
return nil, errors.New("msg_key is invalid")
}
return plaintext, nil
}
// Decrypt decrypts data from encrypted message using AES-IGE.
func (c Cipher) Decrypt(k AuthKey, encrypted *EncryptedMessage) (*EncryptedMessageData, error) {
plaintext, err := c.DecryptRaw(k, encrypted)
if err != nil {
return nil, err
}
msg := &EncryptedMessageData{}
// Notice: do not re-use plaintext, because we use DecodeWithoutCopy, it references
// original buffer.
if err := msg.DecodeWithoutCopy(&bin.Buffer{Buf: plaintext}); err != nil {
return nil, err
}
{
// Checking that padding of decrypted message is not too big.
const maxPadding = 1024
n := int(msg.MessageDataLen)
paddingLen := len(msg.MessageDataWithPadding) - n
switch {
case n < 0:
return nil, errors.Errorf("message length is invalid: %d less than zero", n)
case n%4 != 0:
return nil, errors.Errorf("message length is invalid: %d is not divisible by 4", n)
case paddingLen > maxPadding:
return nil, errors.Errorf("padding %d of message is too big", paddingLen)
}
}
return msg, nil
}
// decryptMessage decrypts data from encrypted message using AES-IGE.
func (c Cipher) decryptMessage(k AuthKey, encrypted *EncryptedMessage) ([]byte, error) {
if k.ID != encrypted.AuthKeyID {
return nil, errors.New("unknown auth key id")
}
if len(encrypted.EncryptedData)%16 != 0 {
return nil, errors.New("invalid encrypted data padding")
}
key, iv := Keys(k.Value, encrypted.MsgKey, c.encryptSide.DecryptSide())
cipher, err := aes.NewCipher(key[:])
if err != nil {
return nil, err
}
plaintext := make([]byte, len(encrypted.EncryptedData))
ige.DecryptBlocks(cipher, iv[:], plaintext, encrypted.EncryptedData)
return plaintext, nil
}
+87
View File
@@ -0,0 +1,87 @@
package crypto
import (
"bytes"
"io"
"testing"
"github.com/stretchr/testify/require"
"go.mau.fi/mautrix-telegram/pkg/gotd/bin"
"go.mau.fi/mautrix-telegram/pkg/gotd/testutil"
)
func TestDecrypt(t *testing.T) {
// Test vector from grammers.
c := NewClientCipher(testutil.ZeroRand{})
var msg EncryptedMessage
b := &bin.Buffer{Buf: []byte{
122, 113, 131, 194, 193, 14, 79, 77, 249, 69, 250, 154, 154, 189, 53, 231, 195, 132,
11, 97, 240, 69, 48, 79, 57, 103, 76, 25, 192, 226, 9, 120, 79, 80, 246, 34, 106, 7,
53, 41, 214, 117, 201, 44, 191, 11, 250, 140, 153, 167, 155, 63, 57, 199, 42, 93, 154,
2, 109, 67, 26, 183, 64, 124, 160, 78, 204, 85, 24, 125, 108, 69, 241, 120, 113, 82,
78, 221, 144, 206, 160, 46, 215, 40, 225, 77, 124, 177, 138, 234, 42, 99, 97, 88, 240,
148, 89, 169, 67, 119, 16, 216, 148, 199, 159, 54, 140, 78, 129, 100, 183, 100, 126,
169, 134, 18, 174, 254, 148, 44, 93, 146, 18, 26, 203, 141, 176, 45, 204, 206, 182,
109, 15, 135, 32, 172, 18, 160, 109, 176, 88, 43, 253, 149, 91, 227, 79, 54, 81, 24,
227, 186, 184, 205, 8, 12, 230, 180, 91, 40, 234, 197, 109, 205, 42, 41, 55, 78,
}}
if err := msg.Decode(b); err != nil {
t.Fatal(err)
}
plaintext, err := c.decryptMessage(testAuthKey, &msg)
if err != nil {
t.Fatal(err)
}
expectedPlaintext := []byte{
252, 130, 106, 2, 36, 139, 40, 253, 96, 242, 196, 130, 36, 67, 173, 104, 1, 240, 193,
194, 145, 139, 48, 94, 2, 0, 0, 0, 88, 0, 0, 0, 220, 248, 241, 115, 2, 0, 0, 0, 1, 168,
193, 194, 145, 139, 48, 94, 1, 0, 0, 0, 28, 0, 0, 0, 8, 9, 194, 158, 196, 253, 51, 173,
145, 139, 48, 94, 24, 168, 142, 166, 7, 238, 88, 22, 252, 130, 106, 2, 36, 139, 40,
253, 1, 204, 193, 194, 145, 139, 48, 94, 2, 0, 0, 0, 20, 0, 0, 0, 197, 115, 119, 52,
196, 253, 51, 173, 145, 139, 48, 94, 100, 8, 48, 0, 0, 0, 0, 0, 252, 230, 103, 4, 163,
205, 142, 233, 208, 174, 111, 171, 103, 44, 96, 192, 74, 63, 31, 212, 73, 14, 81, 246,
}
if !bytes.Equal(expectedPlaintext, plaintext) {
t.Error("mismatch")
}
}
func TestCipher_Decrypt(t *testing.T) {
var key AuthKey
if _, err := io.ReadFull(testutil.Rand([]byte{10}), key.Value[:]); err != nil {
t.Fatal(err)
}
c := NewClientCipher(testutil.ZeroRand{})
s := NewServerCipher(testutil.ZeroRand{})
tests := []struct {
name string
data []byte
dataLen int
expectErr bool
}{
{"NegativeLength", []byte{1, 2, 3, 4}, -1, true},
{"NoPadBy4", []byte{1, 2, 3}, 3, true},
{"Good", bytes.Repeat([]byte{1, 2, 3, 4}, 4), 16, false},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
a := require.New(t)
b := bin.Buffer{}
data := EncryptedMessageData{
MessageDataLen: int32(test.dataLen),
MessageDataWithPadding: test.data,
}
a.NoError(s.Encrypt(key, data, &b))
_, err := c.DecryptFromBuffer(key, &b)
if test.expectErr {
a.Error(err)
return
}
a.NoError(err)
})
}
}
+55
View File
@@ -0,0 +1,55 @@
package crypto
import (
"crypto/aes"
"io"
"github.com/gotd/ige"
"go.mau.fi/mautrix-telegram/pkg/gotd/bin"
)
func countPadding(l int) int { return 16 + (16 - (l % 16)) }
// encryptMessage encrypts plaintext using AES-IGE.
func (c Cipher) encryptMessage(k AuthKey, plaintext *bin.Buffer) (EncryptedMessage, error) {
offset := len(plaintext.Buf)
plaintext.Buf = append(plaintext.Buf, make([]byte, countPadding(offset))...)
if _, err := io.ReadFull(c.rand, plaintext.Buf[offset:]); err != nil {
return EncryptedMessage{}, err
}
messageKey := MessageKey(k.Value, plaintext.Buf, c.encryptSide)
key, iv := Keys(k.Value, messageKey, c.encryptSide)
aesBlock, err := aes.NewCipher(key[:])
if err != nil {
return EncryptedMessage{}, err
}
msg := EncryptedMessage{
AuthKeyID: k.ID,
MsgKey: messageKey,
EncryptedData: make([]byte, len(plaintext.Buf)),
}
ige.EncryptBlocks(aesBlock, iv[:], msg.EncryptedData, plaintext.Buf)
return msg, nil
}
// Encrypt encrypts EncryptedMessageData using AES-IGE to given buffer.
func (c Cipher) Encrypt(key AuthKey, data EncryptedMessageData, b *bin.Buffer) error {
b.Reset()
if err := data.EncodeWithoutCopy(b); err != nil {
return err
}
msg, err := c.encryptMessage(key, b)
if err != nil {
return err
}
b.Reset()
if err := msg.Encode(b); err != nil {
return err
}
return nil
}
+41
View File
@@ -0,0 +1,41 @@
package crypto
import (
"testing"
"github.com/stretchr/testify/require"
"go.mau.fi/mautrix-telegram/pkg/gotd/bin"
"go.mau.fi/mautrix-telegram/pkg/gotd/testutil"
)
func TestEncrypt(t *testing.T) {
c := NewClientCipher(testutil.ZeroRand{})
var authKey Key
for i := 0; i < 256; i++ {
authKey[i] = byte(i)
}
// Testing vector from grammers.
msg, err := c.encryptMessage(
authKey.WithID(),
&bin.Buffer{Buf: []byte("Hello, world! This data should remain secure!")},
)
if err != nil {
t.Fatal(err)
}
b := &bin.Buffer{}
if err := msg.Encode(b); err != nil {
t.Fatal(err)
}
expected := []byte{
50, 209, 88, 110, 164, 87, 223, 200, 168, 23, 41, 212, 109, 181, 64, 25, 162, 191, 215,
247, 68, 249, 185, 108, 79, 113, 108, 253, 196, 71, 125, 178, 162, 193, 95, 109, 219,
133, 35, 95, 185, 85, 47, 29, 132, 7, 198, 170, 234, 0, 204, 132, 76, 90, 27, 246, 172,
68, 183, 155, 94, 220, 42, 35, 134, 139, 61, 96, 115, 165, 144, 153, 44, 15, 41, 117,
36, 61, 86, 62, 161, 128, 210, 24, 238, 117, 124, 154,
}
require.Equal(t, expected, b.Buf)
}
+60
View File
@@ -0,0 +1,60 @@
package crypto
import (
"crypto/rand"
"math/big"
"testing"
"github.com/stretchr/testify/require"
"go.mau.fi/mautrix-telegram/pkg/gotd/bin"
)
var testAuthKey = Key{
93, 46, 125, 101, 244, 158, 194, 139, 208, 41, 168, 135, 97, 234, 39, 184, 164, 199,
159, 18, 34, 101, 37, 68, 62, 125, 124, 89, 110, 243, 48, 53, 48, 219, 33, 7, 232, 154,
169, 151, 199, 160, 22, 74, 182, 148, 24, 122, 222, 255, 21, 107, 214, 239, 113, 24,
161, 150, 35, 71, 117, 60, 14, 126, 137, 160, 53, 75, 142, 195, 100, 249, 153, 126,
113, 188, 105, 35, 251, 134, 232, 228, 52, 145, 224, 16, 96, 106, 108, 232, 69, 226,
250, 1, 148, 9, 119, 239, 10, 163, 42, 223, 90, 151, 219, 246, 212, 40, 236, 4, 52,
215, 23, 162, 211, 173, 25, 98, 44, 192, 88, 135, 100, 33, 19, 199, 150, 95, 251, 134,
42, 62, 60, 203, 10, 185, 90, 221, 218, 87, 248, 146, 69, 219, 215, 107, 73, 35, 72,
248, 233, 75, 213, 167, 192, 224, 184, 72, 8, 82, 60, 253, 30, 168, 11, 50, 254, 154,
209, 152, 188, 46, 16, 63, 206, 183, 213, 36, 146, 236, 192, 39, 58, 40, 103, 75, 201,
35, 238, 229, 146, 101, 171, 23, 160, 2, 223, 31, 74, 162, 197, 155, 129, 154, 94, 94,
29, 16, 94, 193, 23, 51, 111, 92, 118, 198, 177, 135, 3, 125, 75, 66, 112, 206, 233,
204, 33, 7, 29, 151, 233, 188, 162, 32, 198, 215, 176, 27, 153, 140, 242, 229, 205,
185, 165, 14, 205, 161, 133, 42, 54, 230, 53, 105, 12, 142,
}.WithID()
func checkSame(t *testing.T, a, b Cipher) {
asserts := require.New(t)
sessionID, err := rand.Int(rand.Reader, big.NewInt(2345512351))
asserts.NoError(err)
msg := []byte("data")
data := EncryptedMessageData{
SessionID: sessionID.Int64(),
MessageDataLen: int32(len(msg)),
MessageDataWithPadding: msg,
}
var buf bin.Buffer
err = a.Encrypt(testAuthKey, data, &buf)
asserts.NoError(err)
decrypt, err := b.DecryptFromBuffer(testAuthKey, &buf)
asserts.NoError(err)
asserts.Equal(data.SessionID, decrypt.SessionID)
asserts.Equal(data.Data(), decrypt.Data())
}
func TestCipher(t *testing.T) {
client := NewClientCipher(rand.Reader)
server := NewServerCipher(rand.Reader)
checkSame(t, client, server)
checkSame(t, server, client)
}
+55
View File
@@ -0,0 +1,55 @@
package crypto
import (
"bytes"
"crypto/sha1" // #nosec
"io"
)
// GuessDataWithHash guesses data from data_with_hash.
func GuessDataWithHash(dataWithHash []byte) []byte {
// data_with_hash := SHA1(data) + data + (0-15 random bytes);
// such that length be divisible by 16;
if len(dataWithHash) <= sha1.Size {
// Data length too small.
return nil
}
v := dataWithHash[:sha1.Size]
for i := 0; i < 16; i++ {
if len(dataWithHash)-i < sha1.Size {
// End of slice reached.
return nil
}
data := dataWithHash[sha1.Size : len(dataWithHash)-i]
h := sha1.Sum(data) // #nosec
if bytes.Equal(h[:], v) {
// Found.
return data
}
}
return nil
}
func paddedLen16(l int) int {
n := 16 * (l / 16)
if n < l {
n += 16
}
return n
}
// DataWithHash prepends data with SHA1(data) and 0..15 random bytes so result
// length is divisible by 16.
//
// Use GuessDataWithHash(result) to obtain data.
func DataWithHash(data []byte, randomSource io.Reader) ([]byte, error) {
dataWithHash := make([]byte, paddedLen16(len(data)+sha1.Size))
h := sha1.Sum(data) // #nosec
copy(dataWithHash, h[:])
copy(dataWithHash[sha1.Size:], data)
if _, err := io.ReadFull(randomSource, dataWithHash[sha1.Size+len(data):]); err != nil {
return nil, err
}
return dataWithHash, nil
}
+31
View File
@@ -0,0 +1,31 @@
package crypto
import (
"bytes"
mathrand "math/rand"
"testing"
)
func TestGuessData(t *testing.T) {
for _, s := range []string{
"a",
"foo",
"bar",
"wake up neo",
"24145",
} {
rnd := mathrand.New(mathrand.NewSource(239))
data := []byte(s)
dataWithHash, err := DataWithHash(data, rnd)
if err != nil {
t.Fatal(err)
}
guessed := GuessDataWithHash(dataWithHash)
if guessed == nil {
t.Fatal("got nil")
}
if !bytes.Equal(guessed, data) {
t.Fatal("invalid guess")
}
}
}
+47
View File
@@ -0,0 +1,47 @@
package crypto
import (
"math/big"
"github.com/go-faster/errors"
)
// CheckDHParams checks that g_a, g_b and g params meet key exchange conditions.
//
// https://core.telegram.org/mtproto/auth_key#dh-key-exchange-complete
func CheckDHParams(dhPrime, g, gA, gB *big.Int) error {
one := big.NewInt(1)
dhPrimeMinusOne := big.NewInt(0).Sub(dhPrime, one)
if !InRange(g, one, dhPrimeMinusOne) {
return errors.New("kex: bad g, g must be 1 < g < dh_prime - 1")
}
if !InRange(gA, one, dhPrimeMinusOne) {
return errors.New("kex: bad g_a, g_a must be 1 < g_a < dh_prime - 1")
}
if !InRange(gB, one, dhPrimeMinusOne) {
return errors.New("kex: bad g_b, g_b must be 1 < g_b < dh_prime - 1")
}
// IMPORTANT: Apart from the conditions on the Diffie-Hellman prime
// dh_prime and generator g, both sides are to check that g, g_a and
// g_b are greater than 1 and less than dh_prime - 1. We recommend
// checking that g_a and g_b are between 2^{2048-64} and
// dh_prime - 2^{2048-64} as well.
// 2^{2048-64}
safetyRangeMin := big.NewInt(0).Exp(big.NewInt(2), big.NewInt(RSAKeyBits-64), nil)
safetyRangeMax := big.NewInt(0).Sub(dhPrime, safetyRangeMin)
if !InRange(gA, safetyRangeMin, safetyRangeMax) {
return errors.New("kex: bad g_a, g_a must be 2^{2048-64} < g_a < dh_prime - 2^{2048-64}")
}
if !InRange(gB, safetyRangeMin, safetyRangeMax) {
return errors.New("kex: bad g_b, g_b must be 2^{2048-64} < g_b < dh_prime - 2^{2048-64}")
}
return nil
}
// InRange checks whether x is in (min, max) range, i.e. min < x < max.
func InRange(x, min, max *big.Int) bool {
return x.Cmp(min) > 0 && x.Cmp(max) < 0
}
+141
View File
@@ -0,0 +1,141 @@
package crypto
import (
"crypto/rand"
"io"
"math/big"
mathrand "math/rand"
"testing"
"github.com/stretchr/testify/require"
)
// genGAB is part of Diffie-Hellman key exchange.
//
// See https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange
//
// Values:
//
// gA is g_a
// dhPrime is dh_prime
// gB is g_b
// gAB is gab.
func genGAB(dhPrime, g, gA *big.Int, randSource io.Reader) (b, gB, gAB *big.Int, err error) {
randMax := big.NewInt(0).SetBit(big.NewInt(0), RSAKeyBits, 1)
// 6. Random number b is computed:
if b, err = rand.Int(randSource, randMax); err != nil {
return nil, nil, nil, err
}
gB, gAB = gab(dhPrime, b, g, gA)
return b, gB, gAB, nil
}
func gab(dhPrime, b, g, gA *big.Int) (gB, gAB *big.Int) {
gB = big.NewInt(0).Exp(g, b, dhPrime)
gAB = big.NewInt(0).Exp(gA, b, dhPrime)
return gB, gAB
}
func TestGAB(t *testing.T) {
// https://core.telegram.org/mtproto/samples-auth_key#server-dh-inner-data-decomposition-using-the-following-formula
g := big.NewInt(2)
gA, ok := big.NewInt(0).SetString("262AABA621CC4DF587DC94CF8252258C"+
"0B9337DFB47545A49CDD5C9B8EAE7236"+
"C6CADC40B24E88590F1CC2CC762EBF1C"+
"F11DCC0B393CAAD6CEE4EE5848001C73"+
"ACBB1D127E4CB93072AA3D1C8151B6FB"+
"6AA6124B7CD782EAF981BDCFCE9D7A00"+
"E423BD9D194E8AF78EF6501F415522E4"+
"4522281C79D906DDB79C72E9C63D83FB"+
"2A940FF779DFB5F2FD786FB4AD71C9F0"+
"8CF48758E534E9815F634F1E3A80A5E1"+
"C2AF210C5AB762755AD4B2126DFA61A7"+
"7FA9DA967D65DFD0AFB5CDF26C4D4E1A"+
"88B180F4E0D0B45BA1484F95CB2712B5"+
"0BF3F5968D9D55C99C0FB9FB67BFF56D"+
"7D4481B634514FBA3488C4CDA2FC0659"+
"990E8E868B28632875A9AA703BCDCE8F", 16)
require.True(t, ok)
dhPrimeStr := "C71CAEB9C6B1C9048E6C522F70F13F73" +
"980D40238E3E21C14934D037563D930F" +
"48198A0AA7C14058229493D22530F4DB" +
"FA336F6E0AC925139543AED44CCE7C37" +
"20FD51F69458705AC68CD4FE6B6B13AB" +
"DC9746512969328454F18FAF8C595F64" +
"2477FE96BB2A941D5BCD1D4AC8CC4988" +
"0708FA9B378E3C4F3A9060BEE67CF9A4" +
"A4A695811051907E162753B56B0F6B41" +
"0DBA74D8A84B2A14B3144E0EF1284754" +
"FD17ED950D5965B4B9DD46582DB1178D" +
"169C6BC465B0D6FF9CA3928FEF5B9AE4" +
"E418FC15E83EBEA0F87FA9FF5EED7005" +
"0DED2849F47BF959D956850CE929851F" +
"0D8115F635B105EE2E4E15D04B2454BF" +
"6F4FADF034B10403119CD8E3B92FCC5B"
dhPrime, ok := big.NewInt(0).SetString(dhPrimeStr, 16)
require.True(t, ok)
t.Run("Static", func(t *testing.T) {
b, ok := big.NewInt(0).SetString("6F620AFA575C9233EB4C014110A7BCAF49464F798A18A0981FEA1E05E8DA"+
"67D9681E0FD6DF0EDF0272AE3492451A84502F2EFC0DA18741A5FB80BD82296919A70FAA6D07CBBBCA2037EA7D3E327B61D"+
"585ED3373EE0553A91CBD29B01FA9A89D479CA53D57BDE3A76FBD922A923A0A38B922C1D0701F53FF52D7EA9217080163A64901"+
"E766EB6A0F20BC391B64B9D1DD2CD13A7D0C946A3A7DF8CEC9E2236446F646C42CFE2B60A2A8D776E56C8D7519B08B88ED0970E"+
"10D12A8C9E355D765F2B7BBB7B4CA9360083435523CB0D57D2B106FD14F94B4EEE79D8AC131CA56AD389C84FE279716F8124A54"+
"3337FB9EA3D988EC5FA63D90A4BA3970E7A39E5C0DE5", 16)
require.True(t, ok)
gB, gAB := gab(dhPrime, b, g, gA)
if err := CheckDHParams(dhPrime, g, gA, gB); err != nil {
t.Fatal(err)
}
if b == nil || gAB == nil {
t.Fatal("nil")
}
gBVector, ok := big.NewInt(0).SetString("73700E7BFC7AEEC828EB8E0DCC04D09A"+
"0DD56A1B4B35F72F0B55FCE7DB7EBB72"+
"D7C33C5D4AA59E1C74D09B01AE536B31"+
"8CFED436AFDB15FE9EB4C70D7F0CB14E"+
"46DBBDE9053A64304361EB358A9BB32E"+
"9D5C2843FE87248B89C3F066A7D5876D"+
"61657ACC52B0D81CD683B2A0FA93E8AD"+
"AB20377877F3BC3369BBF57B10F5B589"+
"E65A9C27490F30A0C70FFCFD3453F5B3"+
"79C1B9727A573CFFDCA8D23C721B135B"+
"92E529B1CDD2F7ABD4F34DAC4BE1EEAF"+
"60993DDE8ED45890E4F47C26F2C0B2E0"+
"37BB502739C8824F2A99E2B1E7E41658"+
"3417CC79A8807A4BDAC6A5E9805D4F61"+
"86C37D66F6988C9F9C752896F3D34D25"+
"529263FAF2670A09B2A59CE35264511F", 16)
require.True(t, ok)
if gBVector.Cmp(gB) != 0 {
t.Fatal("mismatch")
}
})
t.Run("Random", func(t *testing.T) {
b, gB, gAB, err := genGAB(dhPrime, g, gA, mathrand.New(mathrand.NewSource(239)))
if err != nil {
t.Fatal(err)
}
if err := CheckDHParams(dhPrime, g, gA, gB); err != nil {
t.Fatal(err)
}
bExpected, ok := big.NewInt(0).SetString("f6850b9cff75091df2d4d02d068868e95c0ec92e07c0dcee572705fcc"+
"3ac766370b6a8d4fa17bf628247135c962156aa4ab6e173b699e2fa6cb607a0b3d35205ffd36635dd37572d132f7f16952c"+
"0feb626be13e5165fae18d10cee45ecc9c83883a039903d354e46492d011953ddd8b619dbd4ad6c8fc9f3fab2d0cbcfacea"+
"d28fc18fd7ee1310e9c0f98066204648ba1296e82c691cf87eaa9c2ef0d2775fb4c5d41432d77028c5640ae91e2d8b0033a7"+
"dd4cee0aa87b57798b5afecee8c35a08ca9adcd3a753b3937f21b5363938e6efa5cca7ae45710c874a9b180a634eb2d6f7f7"+
"7cd3e93418a5badfe2cea621a5f3e85f9bea16273b9dbf1924a6eb845", 16)
require.True(t, ok)
require.Zero(t, bExpected.Cmp(b), "b mismatch")
gABExpected, ok := big.NewInt(0).SetString("35cc5280773903e351c5991b3dbbae8677cf73ea16fe08d2b52c88bac"+
"5aa883f16d30013c55379419aac65487dc3e97afada383afcb41c424e393cf1acc7c347202ba74a94cf049c26639016f1dc60"+
"6c0d29e31bf147f13528e34183a9ad26a4c6537cff68bd0e82bff69b9fbe118bba3732581fa9f372ef5228a8529fc5f4ed8ff"+
"2fe0c5877e42ab45efaa9da36f1d2c6ffbd4c8d32f34a20579405ddc867b6da09f52499c20ac7def55938bfbcbe0b0047e5c1"+
"d241cc83c65bab84f4997a5025b447d15e57f9a7b13ed9397de0d236ce50da1b55c51d91ed1214de5cad9654129b1ada3caf8"+
"8028510209a30aa37ab81fdc7eb17160ba54c772a888fcd2549", 16)
require.True(t, ok)
require.Zero(t, gABExpected.Cmp(gAB), "g_ab mismatch")
})
}
+5
View File
@@ -0,0 +1,5 @@
// Package crypto implements cryptographical primitives for MTproto.
//
// Reference:
// - https://core.telegram.org/mtproto/auth_key
package crypto
+57
View File
@@ -0,0 +1,57 @@
package crypto
import "go.mau.fi/mautrix-telegram/pkg/gotd/bin"
// EncryptedMessage of protocol.
type EncryptedMessage struct {
AuthKeyID [8]byte
MsgKey bin.Int128
EncryptedData []byte
}
// Decode implements bin.Decoder.
func (e *EncryptedMessage) Decode(b *bin.Buffer) error {
if err := b.ConsumeN(e.AuthKeyID[:], 8); err != nil {
return err
}
{
v, err := b.Int128()
if err != nil {
return err
}
e.MsgKey = v
}
// Consuming the rest of the buffer.
e.EncryptedData = append(e.EncryptedData[:0], make([]byte, b.Len())...)
if err := b.ConsumeN(e.EncryptedData, b.Len()); err != nil {
return err
}
return nil
}
// DecodeWithoutCopy is like Decode, but EncryptedData references to given buffer instead of
// copying.
func (e *EncryptedMessage) DecodeWithoutCopy(b *bin.Buffer) error {
if err := b.ConsumeN(e.AuthKeyID[:], 8); err != nil {
return err
}
{
v, err := b.Int128()
if err != nil {
return err
}
e.MsgKey = v
}
// Consuming the rest of the buffer.
e.EncryptedData = b.Buf
return nil
}
// Encode implements bin.Encoder.
func (e EncryptedMessage) Encode(b *bin.Buffer) error {
b.Put(e.AuthKeyID[:])
b.PutInt128(e.MsgKey)
b.Put(e.EncryptedData)
return nil
}
+156
View File
@@ -0,0 +1,156 @@
package crypto
import (
"github.com/go-faster/errors"
"go.mau.fi/mautrix-telegram/pkg/gotd/bin"
)
// EncryptedMessageData is stored in EncryptedMessage.EncryptedData.
type EncryptedMessageData struct {
Salt int64
SessionID int64
MessageID int64
SeqNo int32
MessageDataLen int32
MessageDataWithPadding []byte
// Message to encode to MessageDataWithPadding.
// Needed to prevent unnecessary allocations in EncodeWithoutCopy.
Message bin.Encoder
}
// Encode implements bin.Encoder.
func (e EncryptedMessageData) Encode(b *bin.Buffer) error {
b.PutLong(e.Salt)
b.PutLong(e.SessionID)
b.PutLong(e.MessageID)
b.PutInt32(e.SeqNo)
b.PutInt32(e.MessageDataLen)
b.Put(e.MessageDataWithPadding)
return nil
}
// EncodeWithoutCopy is like Encode, but tries to encode Message and uses only one buffer
// to encode. If Message is nil, fallbacks to Encode.
func (e EncryptedMessageData) EncodeWithoutCopy(b *bin.Buffer) error {
if e.Message == nil {
return e.Encode(b)
}
b.PutLong(e.Salt)
b.PutLong(e.SessionID)
b.PutLong(e.MessageID)
b.PutInt32(e.SeqNo)
lengthOffset := b.Len()
b.PutInt32(0)
originalLength := b.Len()
if err := b.Encode(e.Message); err != nil {
return errors.Wrap(err, "encode inner message")
}
msgLen := b.Len() - originalLength
(&bin.Buffer{Buf: b.Buf[lengthOffset:lengthOffset]}).PutInt(msgLen)
return nil
}
// Decode implements bin.Decoder.
func (e *EncryptedMessageData) Decode(b *bin.Buffer) error {
{
v, err := b.Long()
if err != nil {
return err
}
e.Salt = v
}
{
v, err := b.Long()
if err != nil {
return err
}
e.SessionID = v
}
{
v, err := b.Long()
if err != nil {
return err
}
e.MessageID = v
}
{
v, err := b.Int32()
if err != nil {
return err
}
e.SeqNo = v
}
{
v, err := b.Int32()
if err != nil {
return err
}
e.MessageDataLen = v
}
e.MessageDataWithPadding = append(e.MessageDataWithPadding[:0], b.Buf...)
if int(e.MessageDataLen) > len(e.MessageDataWithPadding) {
return errors.Errorf(
"MessageDataLen field is bigger then MessageDataWithPadding length (%d > %d)",
int(e.MessageDataLen), len(e.MessageDataWithPadding),
)
}
return nil
}
// DecodeWithoutCopy is like Decode, but MessageDataWithPadding references to given buffer instead of
// copying.
func (e *EncryptedMessageData) DecodeWithoutCopy(b *bin.Buffer) error {
{
v, err := b.Long()
if err != nil {
return err
}
e.Salt = v
}
{
v, err := b.Long()
if err != nil {
return err
}
e.SessionID = v
}
{
v, err := b.Long()
if err != nil {
return err
}
e.MessageID = v
}
{
v, err := b.Int32()
if err != nil {
return err
}
e.SeqNo = v
}
{
v, err := b.Int32()
if err != nil {
return err
}
e.MessageDataLen = v
}
e.MessageDataWithPadding = b.Buf
if int(e.MessageDataLen) > len(e.MessageDataWithPadding) {
return errors.Errorf(
"MessageDataLen field is bigger then MessageDataWithPadding length (%d > %d)",
int(e.MessageDataLen), len(e.MessageDataWithPadding),
)
}
return nil
}
// Data returns message data without hash.
func (e *EncryptedMessageData) Data() []byte {
return e.MessageDataWithPadding[:e.MessageDataLen]
}
@@ -0,0 +1,37 @@
package crypto
import (
"testing"
"go.mau.fi/mautrix-telegram/pkg/gotd/bin"
"github.com/stretchr/testify/require"
)
func TestEncryptedMessageData_Encode(t *testing.T) {
const padding = 16
paddingRequired := func(l int) int {
return padding + (padding - (l % padding))
}
d := EncryptedMessageData{
Salt: 1034,
SeqNo: 1,
MessageID: 3401235566,
SessionID: 2345512351,
MessageDataLen: 5,
MessageDataWithPadding: []byte{1, 2, 3, 100, 112},
}
b := new(bin.Buffer)
if err := d.Encode(b); err != nil {
t.Fatal(err)
}
b.Buf = append(b.Buf, make([]byte, paddingRequired(b.Len()))...)
decoded := EncryptedMessageData{}
if err := decoded.Decode(b); err != nil {
t.Fatal(err)
}
decoded.MessageDataWithPadding = decoded.MessageDataWithPadding[:decoded.MessageDataLen]
require.Equal(t, d, decoded)
}
+30
View File
@@ -0,0 +1,30 @@
package crypto
import (
"math/big"
"testing"
"go.mau.fi/mautrix-telegram/pkg/gotd/bin"
)
func TestEncryptedMessage_Encode(t *testing.T) {
k, ok := big.NewInt(0).SetString(`644475571b8fac33f5072049f29d3eeb4493cea84e925d0601c31c1edbb79567adf23c7b97f7882d70f23cff5b8d62eff66399cd32f35b1882ac602e76f30701975c73ad70937169d840b9483e306ab49e656826b2aedc4451d20d65fe96120ecd97ccc16e6ef8ce12cb90c37db21f9c1700ee282f2fba088af1491a3b7d93a2f7abb496e5015779d8c107c2a61d8f992c909b52d29be44ac55d4d077351c96591bfa44a3482d90080ad4bd1417300c88c715f28b03c7b7f1e6ddffd0f321df64adcfdf6f99c756f2df8a7bf9f55110b7353342e050ffb1353afc9a888d10a0287b7a5d94368ba2eb6f39730745905ce42c63d3950e97acd190bd20cc030182e`, 16)
if !ok {
t.Fatal(ok)
}
payload := []byte{1, 2, 3, 4}
var authKey Key
k.FillBytes(authKey[:])
d := EncryptedMessage{
EncryptedData: payload,
MsgKey: bin.Int128{0, 0, 0, 0},
AuthKeyID: authKey.ID(),
}
b := new(bin.Buffer)
if err := d.Encode(b); err != nil {
t.Fatal(err)
}
}
+72
View File
@@ -0,0 +1,72 @@
package crypto
import (
"crypto/aes"
"crypto/sha1" // #nosec
"io"
"github.com/go-faster/errors"
"github.com/gotd/ige"
"go.mau.fi/mautrix-telegram/pkg/gotd/bin"
)
// DecryptExchangeAnswer decrypts messages created during key exchange.
func DecryptExchangeAnswer(data, key, iv []byte) (dst []byte, err error) {
// Decrypting inner data.
cipher, err := aes.NewCipher(key)
if err != nil {
return nil, errors.Wrap(err, "create aes cipher")
}
dataWithHash := make([]byte, len(data))
// Checking length. Invalid length will lead to panic in CryptBlocks.
if len(dataWithHash)%cipher.BlockSize() != 0 {
return nil, errors.Errorf("invalid len of data_with_hash (%d %% 16 != 0)", len(dataWithHash))
}
ige.DecryptBlocks(cipher, iv, dataWithHash, data)
dst = GuessDataWithHash(dataWithHash)
if data == nil {
// Most common cause of this error is invalid crypto implementation,
// i.e. invalid keys are used to decrypt payload which lead to
// decrypt failure, so data does not match sha1 with any padding.
return nil, errors.New("guess data from data_with_hash")
}
return
}
// EncryptExchangeAnswer encrypts messages created during key exchange.
func EncryptExchangeAnswer(rand io.Reader, answer, key, iv []byte) (dst []byte, err error) {
cipher, err := aes.NewCipher(key)
if err != nil {
return nil, errors.Wrap(err, "create aes cipher")
}
answerWithHash, err := DataWithHash(answer, rand)
if err != nil {
return nil, errors.Wrap(err, "get answer with hash")
}
dst = make([]byte, len(answerWithHash))
ige.EncryptBlocks(cipher, iv, dst, answerWithHash)
return
}
// NonceHash1 computes nonce_hash_1.
// See https://core.telegram.org/mtproto/auth_key#dh-key-exchange-complete.
func NonceHash1(newNonce bin.Int256, key Key) (r bin.Int128) {
var buf []byte
buf = append(buf, newNonce[:]...)
buf = append(buf, 1)
buf = append(buf, sha(key[:])[0:8]...)
buf = sha(buf)[4:20]
copy(r[:], buf)
return
}
func sha(v []byte) []byte {
h := sha1.Sum(v) // #nosec
return h[:]
}
+15
View File
@@ -0,0 +1,15 @@
package crypto
import "math/big"
// FillBytes is safe version of (*big.Int).FillBytes.
// Returns false if to length is not exact equal to big.Int's.
// Otherwise fills to using b and returns true.
func FillBytes(b *big.Int, to []byte) bool {
bits := b.BitLen()
if (bits+7)/8 > len(to) {
return false
}
b.FillBytes(to)
return true
}
+30
View File
@@ -0,0 +1,30 @@
package crypto
import (
"crypto/rand"
"testing"
"github.com/stretchr/testify/require"
)
func TestFillBytes(t *testing.T) {
tests := []struct {
name string
bitSize int
result bool
}{
{"Smaller", 255, true},
{"Equal", 256, true},
{"Bigger", 512, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := require.New(t)
b, err := rand.Prime(rand.Reader, tt.bitSize)
a.NoError(err)
var to [256 / 8]byte
a.Equal(tt.result, FillBytes(b, to[:]))
})
}
}
+39
View File
@@ -0,0 +1,39 @@
//go:build go1.18
package crypto
import (
"bytes"
"math/rand"
"testing"
"go.mau.fi/mautrix-telegram/pkg/gotd/testutil"
)
func FuzzRSA(f *testing.F) {
f.Add([]byte{1, 2, 3})
f.Add([]byte{3, 2, 3, 1, 10})
f.Fuzz(func(t *testing.T, data []byte) {
source := rand.NewSource(0)
if len(data) > rsaDataLen {
data = data[:rsaDataLen]
}
reader := rand.New(source)
k := testutil.RSAPrivateKey()
encrypted, err := RSAEncryptHashed(data, &k.PublicKey, reader)
if err != nil {
t.Fatal(err)
}
decrypted, err := RSADecryptHashed(encrypted, k)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(data, decrypted) {
t.Fatal("mismatch")
}
})
}
+132
View File
@@ -0,0 +1,132 @@
package crypto
import (
"crypto/sha1" // #nosec
"encoding/binary"
"encoding/hex"
"fmt"
"github.com/go-faster/errors"
"github.com/go-faster/jx"
"go.uber.org/zap/zapcore"
)
// See https://core.telegram.org/mtproto/description#defining-aes-key-and-initialization-vector
// Key represents 2048-bit authorization key value.
type Key [256]byte
func (k Key) String() string {
// Never print key.
return "(redacted)"
}
// Zero reports whether Key is zero value.
func (k Key) Zero() bool {
return k == Key{}
}
// ID returns auth_key_id.
func (k Key) ID() [8]byte {
raw := sha1.Sum(k[:]) // #nosec
var id [8]byte
copy(id[:], raw[12:])
return id
}
// AuxHash returns aux_hash value of key.
func (k Key) AuxHash() [8]byte {
raw := sha1.Sum(k[:]) // #nosec
var id [8]byte
copy(id[:], raw[0:8])
return id
}
// WithID creates new AuthKey from Key.
func (k Key) WithID() AuthKey {
return AuthKey{
Value: k,
ID: k.ID(),
}
}
// AuthKey is a Key with cached id.
type AuthKey struct {
Value Key
ID [8]byte
}
// DecodeJSON decode AuthKey from object with base64-encoded key and integer ID.
func (a *AuthKey) DecodeJSON(d *jx.Decoder) error {
return d.ObjBytes(func(d *jx.Decoder, key []byte) error {
switch string(key) {
case "value":
data, err := d.Base64()
if err != nil {
return errors.Wrap(err, "decode value")
}
copy(a.Value[:], data)
case "id":
id, err := d.Int64()
if err != nil {
return errors.Wrap(err, "decode id")
}
a.SetIntID(id)
default:
return d.Skip()
}
return nil
})
}
// UnmarshalJSON implements json.Unmarshaler.
func (a *AuthKey) UnmarshalJSON(data []byte) error {
return a.DecodeJSON(jx.DecodeBytes(data))
}
// EncodeJSON encodes AuthKey as object with base64-encoded key and integer ID.
func (a AuthKey) EncodeJSON(e *jx.Encoder) error {
e.ObjStart()
e.FieldStart("value")
e.Base64(a.Value[:])
e.FieldStart("id")
e.Int64(a.IntID())
e.ObjEnd()
return nil
}
// MarshalJSON implements json.Marshaler.
func (a AuthKey) MarshalJSON() ([]byte, error) {
e := jx.GetEncoder()
if err := a.EncodeJSON(e); err != nil {
return nil, err
}
return e.Bytes(), nil
}
// Zero reports whether Key is zero value.
func (a AuthKey) Zero() bool {
return a == AuthKey{}
}
// IntID returns key fingerprint (ID) as int64.
func (a AuthKey) IntID() int64 {
return int64(binary.LittleEndian.Uint64(a.ID[:]))
}
// SetIntID sets key fingerprint (ID) as int64.
func (a *AuthKey) SetIntID(v int64) {
binary.LittleEndian.PutUint64(a.ID[:], uint64(v))
}
// String implements fmt.Stringer.
func (a AuthKey) String() string {
return fmt.Sprintf("Key(id: %x)", a.ID)
}
// MarshalLogObject implements zap.ObjectMarshaler.
func (a AuthKey) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("id", hex.EncodeToString(a.ID[:]))
return nil
}
+108
View File
@@ -0,0 +1,108 @@
package crypto
import (
"bytes"
"encoding/json"
"testing"
"github.com/stretchr/testify/require"
"go.mau.fi/mautrix-telegram/pkg/gotd/bin"
)
func TestAuthKeyID(t *testing.T) {
var k Key
for i := 0; i < 256; i++ {
k[i] = byte(i)
}
if k.ID() != [8]byte{50, 209, 88, 110, 164, 87, 223, 200} {
t.Error("bad id")
}
if k.AuxHash() != [8]byte{73, 22, 214, 189, 183, 247, 142, 104} {
t.Error("bad aux hash")
}
}
func TestCalcKey(t *testing.T) {
var k Key
for i := 0; i < 256; i++ {
k[i] = byte(i)
}
var m bin.Int128
for i := 0; i < 16; i++ {
m[i] = byte(i)
}
t.Run("Client", func(t *testing.T) {
key, iv := Keys(k, m, Client)
if key != [32]byte{
112, 78, 208, 156, 139, 65, 102, 138, 232, 249, 157, 36, 71, 56, 247, 29,
189, 220, 68, 70, 155, 107, 189, 74, 168, 87, 61, 208, 66, 189, 5, 158,
} {
t.Error("bad key")
}
if iv != [32]byte{
77, 38, 96, 0, 165, 80, 237, 171, 191, 76, 124, 228, 15, 208, 4, 60, 201, 34, 48,
24, 76, 211, 23, 165, 204, 156, 36, 130, 253, 59, 147, 24,
} {
t.Error("bad iv")
}
})
t.Run("Server", func(t *testing.T) {
key, iv := Keys(k, m, Server)
if key != [32]byte{
33, 119, 37, 121, 155, 36, 88, 6, 69, 129, 116, 161, 252, 251, 200, 131, 144, 104,
7, 177, 80, 51, 253, 208, 234, 43, 77, 105, 207, 156, 54, 78,
} {
t.Error("bad key")
}
if iv != [32]byte{
102, 154, 101, 56, 145, 122, 79, 165, 108, 163, 35, 96, 164, 49, 201, 22, 11, 228,
173, 136, 113, 64, 152, 13, 171, 145, 206, 123, 220, 71, 255, 188,
} {
t.Error("bad iv")
}
})
}
func TestAuthKeyJSON(t *testing.T) {
a := require.New(t)
var k Key
for i := 0; i < 256; i++ {
k[i] = byte(i)
}
key := k.WithID()
var buf bytes.Buffer
a.NoError(json.NewEncoder(&buf).Encode(key))
var got AuthKey
a.NoError(json.NewDecoder(&buf).Decode(&got))
a.Equal(key, got)
}
func TestAuthKey_UnmarshalJSON(t *testing.T) {
tests := []struct {
name string
data string
want AuthKey
wantErr bool
}{
{"BadValue", `{"value":1, "id":1}`, AuthKey{}, true},
{"BadID", `{"unknown": false, "value":"Cg==", "id":true}`, AuthKey{}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := require.New(t)
var key AuthKey
err := key.UnmarshalJSON([]byte(tt.data))
if tt.wantErr {
a.Error(err)
} else {
a.NoError(err)
a.Equal(tt.want, key)
}
})
}
}
+129
View File
@@ -0,0 +1,129 @@
package crypto
import (
"crypto/sha256"
"go.mau.fi/mautrix-telegram/pkg/gotd/bin"
)
// Side on which encryption is performed.
type Side byte
const (
// Client side of encryption (e.g. messages from client).
Client Side = 0
// Server side of encryption (e.g. RPC responses).
Server Side = 1
)
// DecryptSide returns Side for decryption.
func (s Side) DecryptSide() Side {
return s ^ 1 // flips bit, so 0 becomes 1, 1 becomes 0
}
func getX(mode Side) int {
switch mode {
case Client:
return 0
case Server:
return 8
default:
return 0
}
}
// Message keys are defined here:
// * https://core.telegram.org/mtproto/description#defining-aes-key-and-initialization-vector
// msgKeyLarge returns msg_key_large value.
func msgKeyLarge(r []byte, authKey Key, plaintextPadded []byte, mode Side) []byte {
h := sha256.New()
x := getX(mode)
_, _ = h.Write(authKey[88+x : 32+88+x])
_, _ = h.Write(plaintextPadded)
return h.Sum(r)
}
// messageKey returns msg_key = substr (msg_key_large, 8, 16).
func messageKey(messageKeyLarge []byte) (v bin.Int128) {
b := messageKeyLarge[8 : 16+8]
copy(v[:len(b)], b)
return v
}
// sha256a returns sha256_a value.
//
// sha256_a = SHA256 (msg_key + substr (auth_key, x, 36));
func sha256a(r []byte, authKey *Key, msgKey *bin.Int128, x int) []byte {
h := sha256.New()
_, _ = h.Write(msgKey[:])
_, _ = h.Write(authKey[x : x+36])
return h.Sum(r)
}
// sha256b returns sha256_b value.
//
// sha256_b = SHA256 (substr (auth_key, 40+x, 36) + msg_key);
func sha256b(r []byte, authKey *Key, msgKey *bin.Int128, x int) []byte {
h := sha256.New()
_, _ = h.Write(authKey[40+x : 40+x+36])
_, _ = h.Write(msgKey[:])
return h.Sum(r)
}
// aesKey returns aes_key value.
//
// aes_key = substr (sha256_a, 0, 8) + substr (sha256_b, 8, 16) + substr (sha256_a, 24, 8);
func aesKey(sha256a, sha256b []byte, v *bin.Int256) {
copy(v[:8], sha256a[:8])
copy(v[8:], sha256b[8:16+8])
copy(v[24:], sha256a[24:24+8])
}
// aesIV returns aes_iv value.
//
// aes_iv = substr (sha256_b, 0, 8) + substr (sha256_a, 8, 16) + substr (sha256_b, 24, 8);
func aesIV(sha256a, sha256b []byte, v *bin.Int256) {
// Same as aes_key, but with swapped params.
aesKey(sha256b, sha256a, v)
}
// Keys returns (aes_key, aes_iv) pair for AES-IGE.
//
// See https://core.telegram.org/mtproto/description#defining-aes-key-and-initialization-vector
//
// Example:
//
// key, iv := crypto.Keys(authKey, messageKey, crypto.Client)
// cipher, err := aes.NewCipher(key[:])
// if err != nil {
// return nil, err
// }
// encryptor := ige.NewIGEEncrypter(cipher, iv[:])
func Keys(authKey Key, msgKey bin.Int128, mode Side) (key, iv bin.Int256) {
x := getX(mode)
r := make([]byte, 512)
// `sha256_a = SHA256 (msg_key + substr (auth_key, x, 36));`
a := sha256a(r[0:0], &authKey, &msgKey, x)
// `sha256_b = SHA256 (substr (auth_key, 40+x, 36) + msg_key);`
b := sha256b(r[256:256], &authKey, &msgKey, x)
aesKey(a, b, &key)
aesIV(a, b, &iv)
return key, iv
}
// MessageKey computes message key for provided auth_key and padded payload.
func MessageKey(authKey Key, plaintextPadded []byte, mode Side) bin.Int128 {
r := make([]byte, 0, 256)
// `msg_key_large = SHA256 (substr (auth_key, 88+x, 32) + plaintext + random_padding);`
msgKeyLarge := msgKeyLarge(r, authKey, plaintextPadded, mode)
// `msg_key = substr (msg_key_large, 8, 16);`
return messageKey(msgKeyLarge)
}
+99
View File
@@ -0,0 +1,99 @@
package crypto
import (
"crypto/sha1" // #nosec G505
"go.mau.fi/mautrix-telegram/pkg/gotd/bin"
)
// sha1a returns sha1_a value.
//
// sha1_a = SHA1 (msg_key + substr (auth_key, x, 32));
func sha1a(r []byte, authKey Key, msgKey bin.Int128, x int) []byte {
h := sha1.New() // #nosec G401
_, _ = h.Write(msgKey[:])
_, _ = h.Write(authKey[x : x+32])
return h.Sum(r)
}
// sha1b returns sha1_b value.
//
// sha1_b = SHA1 (substr (auth_key, 32+x, 16) + msg_key + substr (auth_key, 48+x, 16));
func sha1b(r []byte, authKey Key, msgKey bin.Int128, x int) []byte {
h := sha1.New() // #nosec G401
_, _ = h.Write(authKey[32+x : 32+x+16])
_, _ = h.Write(msgKey[:])
_, _ = h.Write(authKey[48+x : 48+x+16])
return h.Sum(r)
}
// sha1c returns sha1_c value.
//
// sha1_c = SHA1 (substr (auth_key, 64+x, 32) + msg_key);
func sha1c(r []byte, authKey Key, msgKey bin.Int128, x int) []byte {
h := sha1.New() // #nosec G401
_, _ = h.Write(authKey[64+x : 64+x+32])
_, _ = h.Write(msgKey[:])
return h.Sum(r)
}
// sha1d returns sha1_d value.
//
// sha1_d = SHA1 (msg_key + substr (auth_key, 96+x, 32));
func sha1d(r []byte, authKey Key, msgKey bin.Int128, x int) []byte {
h := sha1.New() // #nosec G401
_, _ = h.Write(msgKey[:])
_, _ = h.Write(authKey[96+x : 96+x+32])
return h.Sum(r)
}
// OldKeys returns (aes_key, aes_iv) pair for AES-IGE.
//
// See https://core.telegram.org/mtproto/description_v1#defining-aes-key-and-initialization-vector
//
// Example:
//
// key, iv := crypto.OldKeys(authKey, messageKey, crypto.Client)
// cipher, err := aes.NewCipher(key[:])
// if err != nil {
// return nil, err
// }
// encryptor := ige.NewIGEEncrypter(cipher, iv[:])
//
// Warning: MTProto 1.0 is deprecated.
func OldKeys(authKey Key, msgKey bin.Int128, mode Side) (key, iv bin.Int256) {
x := getX(mode)
aesKey := func(sha1a, sha1b, sha1c []byte) (v bin.Int256) {
// aes_key = substr (sha1_a, 0, 8) + substr (sha1_b, 8, 12) + substr (sha1_c, 4, 12);
n := copy(v[:], sha1a[:8])
n += copy(v[n:], sha1b[8:8+12])
copy(v[n:], sha1c[4:4+12])
return v
}
aesIV := func(sha1a, sha1b, sha1c, sha1d []byte) (v bin.Int256) {
// aes_iv = substr(sha1_a, 8, 12) + substr(sha1_b, 0, 8) + substr(sha1_c, 16, 4) + substr(sha1_d, 0, 8);
n := copy(v[:], sha1a[8:8+12])
n += copy(v[n:], sha1b[:8])
n += copy(v[n:], sha1c[16:16+4])
copy(v[n:], sha1d[:8])
return v
}
r := make([]byte, sha1.Size*4)
a := sha1a(r[0:0], authKey, msgKey, x)
b := sha1b(r[sha1.Size:sha1.Size], authKey, msgKey, x)
c := sha1c(r[2*sha1.Size:2*sha1.Size], authKey, msgKey, x)
d := sha1d(r[3*sha1.Size:3*sha1.Size], authKey, msgKey, x)
return aesKey(a, b, c), aesIV(a, b, c, d)
}
+45
View File
@@ -0,0 +1,45 @@
package crypto
import (
"bytes"
"crypto/aes"
"testing"
"github.com/stretchr/testify/require"
"github.com/gotd/ige"
"go.mau.fi/mautrix-telegram/pkg/gotd/bin"
)
func TestOldKeys(t *testing.T) {
a := require.New(t)
var (
authKey Key
msgKey bin.Int128
)
for i := range authKey {
authKey[i] = byte(i)
}
for i := range msgKey {
msgKey[i] = byte(i)
}
data := bytes.Repeat([]byte("aboba"), 16)
encrypted := make([]byte, len(data))
{
key, iv := OldKeys(authKey, msgKey, Server)
a.Equal(key, bin.Int256{
0xbb, 0x17, 0xb0, 0x7e, 0xb9, 0x11, 0x10, 0x64, 0x70, 0x98, 0xb0, 0x69, 0xbd, 0x1a, 0x9b, 0x6f,
0xe5, 0xc4, 0xbc, 0xc3, 0xc3, 0x1f, 0x8e, 0x67, 0xe8, 0x31, 0xd0, 0x7a, 0x61, 0x08, 0x5f, 0x68,
})
a.Equal(iv, bin.Int256{
0x51, 0x97, 0xfc, 0x1e, 0x25, 0xb4, 0x1f, 0xe3, 0x6f, 0x18, 0xb5, 0xa3, 0xa8, 0xb2, 0xb3, 0x6c,
0xb2, 0xcb, 0x06, 0x1f, 0x1f, 0x15, 0x7b, 0x35, 0x14, 0xfe, 0x42, 0xe7, 0x4f, 0xb5, 0x83, 0x59,
})
block, err := aes.NewCipher(key[:])
a.NoError(err)
ige.EncryptBlocks(block, iv[:], encrypted, data)
}
}
+69
View File
@@ -0,0 +1,69 @@
package crypto
import (
"crypto/rand"
"io"
"testing"
"go.mau.fi/mautrix-telegram/pkg/gotd/bin"
"go.mau.fi/mautrix-telegram/pkg/gotd/testutil"
)
func genMessageAndAuthKeys() (Key, bin.Int128) {
var k Key
for i := 0; i < 256; i++ {
k[i] = byte(i)
}
var m bin.Int128
for i := 0; i < 16; i++ {
m[i] = byte(i)
}
return k, m
}
func BenchmarkKeys(b *testing.B) {
k, m := genMessageAndAuthKeys()
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = Keys(k, m, Client)
}
}
func TestKeys(t *testing.T) {
k, m := genMessageAndAuthKeys()
testutil.ZeroAlloc(t, func() {
_, _ = Keys(k, m, Client)
})
}
func BenchmarkMessageKey(b *testing.B) {
k, _ := genMessageAndAuthKeys()
payload := make([]byte, 1024)
if _, err := io.ReadFull(rand.Reader, payload); err != nil {
b.Error(err)
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = MessageKey(k, payload, Client)
}
}
func TestMessageKey(t *testing.T) {
k, _ := genMessageAndAuthKeys()
payload := make([]byte, 1024)
if _, err := io.ReadFull(rand.Reader, payload); err != nil {
t.Error(err)
}
testutil.ZeroAlloc(t, func() {
_ = MessageKey(k, payload, Client)
})
}
+123
View File
@@ -0,0 +1,123 @@
package crypto
import (
"crypto/rand"
"io"
"math/big"
)
// This piece of software can be found in multiple projects, referenced as "splitPQ":
// * https://github.com/sdidyk/mtproto/blob/cf2cb57ade6932a7e0854f2e3246492a2028d369/math.go#L44
// * https://github.com/cjongseok/mtproto/blob/dab4e1a6538c990d8a4450d700559197c344a63b/crypto.go#L48
// * https://gist.github.com/asaelko/6f27c8ccd2edfab4d1efadf80a5221a4
// * https://github.com/xelaj/mtproto/blob/13ca2c1b5bfaa249ec925921760be0dc05735915/math.go#L39
//
// Also there is StackOverflow question about this part of telegram auth:
// * https://stackoverflow.com/questions/31953836/decompose-a-number-into-2-prime-co-factors
//
// General information can be found in telegram auth docs:
// * https://core.telegram.org/mtproto/auth_key#dh-exchange-initiation
// * https://core.telegram.org/mtproto/samples-auth_key#4-encrypted-data-generation
//
// Looks like this is some kind of integer factorization algorithm implementation
// which is used as Proof of Work by Telegram Server for some reason.
//
// TODO(ernado): Try https://en.wikipedia.org/wiki/Pollard%27s_rho_algorithm
// DecomposePQ decomposes pq into prime factors such that p < q.
func DecomposePQ(pq *big.Int, randSource io.Reader) (p, q *big.Int, err error) { // nolint:gocognit
var (
value0 = big.NewInt(0)
value1 = big.NewInt(1)
value15 = big.NewInt(15)
value17 = big.NewInt(17)
rndMax = big.NewInt(0).SetBit(big.NewInt(0), 64, 1)
y = big.NewInt(0)
whatNext = big.NewInt(0)
a = big.NewInt(0)
b = big.NewInt(0)
c = big.NewInt(0)
b2 = big.NewInt(0)
z = big.NewInt(0)
)
what := big.NewInt(0).Set(pq)
g := big.NewInt(0)
i := 0
for !(g.Cmp(value1) == 1 && g.Cmp(what) == -1) {
v, err := rand.Int(randSource, rndMax)
if err != nil {
return nil, nil, err
}
v = v.And(v, value15)
v = v.Add(v, value17)
v = v.Mod(v, what)
x, err := rand.Int(randSource, rndMax)
if err != nil {
return nil, nil, err
}
whatNext.Sub(what, value1)
x = x.Mod(x, whatNext)
x = x.Add(x, value1)
y.Set(x)
lim := 1 << (uint(i) + 18)
j := 1
flag := true
for j < lim && flag {
a.Set(x)
b.Set(x)
c.Set(v)
for b.Cmp(value0) == 1 {
b2.SetInt64(0)
if b2.And(b, value1).Cmp(value0) == 1 {
c.Add(c, a)
if c.Cmp(what) >= 0 {
c.Sub(c, what)
}
}
a.Add(a, a)
if a.Cmp(what) >= 0 {
a.Sub(a, what)
}
b.Rsh(b, 1)
}
x.Set(c)
z.SetInt64(0)
if x.Cmp(y) == -1 {
z.Add(what, x)
z.Sub(z, y)
} else {
z.Sub(x, y)
}
g.GCD(nil, nil, z, what)
if (j & (j - 1)) == 0 {
y.Set(x)
}
j++
if g.Cmp(value1) != 0 {
flag = false
}
}
i++
}
p = big.NewInt(0).Set(g)
q = big.NewInt(0).Div(what, g)
if p.Cmp(q) == 1 {
p, q = q, p
}
return p, q, nil
}
+53
View File
@@ -0,0 +1,53 @@
package crypto
import (
"math/big"
"math/rand"
"testing"
)
func TestDecomposePQ(t *testing.T) {
for _, tt := range []struct {
pq, p, q *big.Int
}{
// Testing vectors taken from one of SplitPQ implementations.
// * https://github.com/sdidyk/mtproto/blob/cf2cb57ade6932a7e0854f2e3246492a2028d369/math_test.go
{big.NewInt(1724114033281923457), big.NewInt(1229739323), big.NewInt(1402015859)},
{big.NewInt(378221), big.NewInt(613), big.NewInt(617)},
{big.NewInt(15), big.NewInt(3), big.NewInt(5)},
// Testing vector taken from telegram docs.
// * https://core.telegram.org/mtproto/samples-auth_key#4-encrypted-data-generation
{big.NewInt(0x17ED48941A08F981), big.NewInt(0x494C553B), big.NewInt(0x53911073)},
} {
rnd := rand.New(rand.NewSource(239))
p, q, err := DecomposePQ(tt.pq, rnd)
if err != nil {
t.Fatal(err)
}
if tt.p.Cmp(p) != 0 || tt.q.Cmp(q) != 0 {
t.Errorf("PQ mismatch: %v %v, want %v %v", p, q, tt.p, tt.q)
}
}
}
func BenchmarkDecomposePQ(b *testing.B) {
// DecomposePQ is used as Proof of Work so it is not required to be
// very fast, leaving this benchmark here as a reference.
//
// This runs at ~300ms on Intel 8700k and allocates ~30mb, probably we
// can at least reduce allocations.
b.ReportAllocs()
pq := big.NewInt(0x17ED48941A08F981)
rnd := rand.New(rand.NewSource(239))
for i := 0; i < b.N; i++ {
p, q, err := DecomposePQ(pq, rnd)
if err != nil {
b.Fatal(err)
}
if p == nil || q == nil {
b.Fatal("nil")
}
}
}
+16
View File
@@ -0,0 +1,16 @@
package crypto
import "math/big"
// Prime checks that given number is prime.
func Prime(p *big.Int) bool {
// TODO(tdakkota): maybe it should be smaller?
// 1 - 1/4^64 is equal to 0.9999999999999999999999999999999999999970612641229442812300781587
//
// TDLib uses nchecks = 64
// See https://github.com/tdlib/td/blob/d161323858a782bc500d188b9ae916982526c262/tdutils/td/utils/BigNum.cpp#L155.
const probabilityN = 64
// ProbablyPrime is mutating, so we need a copy
return p.ProbablyPrime(probabilityN)
}
+48
View File
@@ -0,0 +1,48 @@
package crypto
import (
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"github.com/go-faster/errors"
)
// ParseRSAPublicKeys parses data as list of PEM-encdoed public keys.
func ParseRSAPublicKeys(data []byte) ([]*rsa.PublicKey, error) {
var keys []*rsa.PublicKey
for {
block, rest := pem.Decode(data)
if block == nil {
break
}
key, err := ParseRSA(block.Bytes)
if err != nil {
return nil, errors.Wrap(err, "parse RSA from PEM")
}
keys = append(keys, key)
data = rest
}
return keys, nil
}
// ParseRSA parses data RSA key in PKCS1 or PKIX forms.
func ParseRSA(data []byte) (*rsa.PublicKey, error) {
key, err := x509.ParsePKCS1PublicKey(data)
if err == nil {
return key, nil
}
k, err := x509.ParsePKIXPublicKey(data)
if err != nil {
return nil, err
}
publicKey, ok := k.(*rsa.PublicKey)
if !ok {
return nil, errors.Errorf("parsed unexpected key type %T", k)
}
return publicKey, nil
}
+53
View File
@@ -0,0 +1,53 @@
package crypto
import (
"io"
"go.mau.fi/mautrix-telegram/pkg/gotd/bin"
)
// RandInt64n returns random int64 from randSource in [0; n).
func RandInt64n(randSource io.Reader, n int64) (int64, error) {
v, err := RandInt64(randSource)
if err != nil {
return 0, err
}
if v < 0 {
v *= -1
}
return v % n, nil
}
// RandInt64 returns random int64 from randSource.
func RandInt64(randSource io.Reader) (int64, error) {
var buf [bin.Word * 2]byte
if _, err := io.ReadFull(randSource, buf[:]); err != nil {
return 0, err
}
b := &bin.Buffer{Buf: buf[:]}
return b.Long()
}
// RandInt128 generates and returns new random 128-bit integer.
//
// Use crypto/rand.Reader as randSource in production.
func RandInt128(randSource io.Reader) (bin.Int128, error) {
var buf [bin.Word * 4]byte
if _, err := io.ReadFull(randSource, buf[:]); err != nil {
return bin.Int128{}, err
}
b := &bin.Buffer{Buf: buf[:]}
return b.Int128()
}
// RandInt256 generates and returns new random 256-bit integer.
//
// Use crypto/rand.Reader as randSource in production.
func RandInt256(randSource io.Reader) (bin.Int256, error) {
var buf [bin.Word * 8]byte
if _, err := io.ReadFull(randSource, buf[:]); err != nil {
return bin.Int256{}, err
}
b := &bin.Buffer{Buf: buf[:]}
return b.Int256()
}
+21
View File
@@ -0,0 +1,21 @@
package crypto
import (
"bufio"
"crypto/rand"
"io"
"sync"
)
var defaultRand struct {
sync.Once
reader io.Reader
}
// DefaultRand returns default entropy source.
func DefaultRand() io.Reader {
defaultRand.Do(func() {
defaultRand.reader = bufio.NewReaderSize(rand.Reader, 1024)
})
return rand.Reader
}
+14
View File
@@ -0,0 +1,14 @@
//go:build !js
// +build !js
package crypto
import (
"crypto/rand"
"io"
)
// DefaultRand returns default entropy source.
func DefaultRand() io.Reader {
return rand.Reader
}
+53
View File
@@ -0,0 +1,53 @@
package crypto
import (
"crypto/rsa"
// #nosec
//
// Allowing sha1 because it is used in MTProto itself.
"crypto/sha1"
"math/big"
)
// RSAKeyBits is RSA key size.
//
// Can be used as rsa.GenerateKey(src, RSAKeyBits).
const RSAKeyBits = 2048
const (
rsaLen = 256
rsaWithHashLen = 255
rsaDataLen = rsaWithHashLen - sha1.Size
)
// RSAPublicDecrypt recovers the message digest from the raw signature
// using the signers RSA public key.
//
// See also OpenSSLs RSA_public_decrypt with RSA_NO_PADDING.
func RSAPublicDecrypt(pub *rsa.PublicKey, sig []byte) ([]byte, error) {
k := pub.Size()
if k < 11 || k != len(sig) {
return nil, rsa.ErrVerification
}
c := new(big.Int).SetBytes(sig)
e := big.NewInt(int64(pub.E))
m := new(big.Int).Exp(c, e, pub.N)
return m.Bytes(), nil
}
func rsaEncrypt(data []byte, key *rsa.PublicKey) []byte {
z := new(big.Int).SetBytes(data)
e := big.NewInt(int64(key.E))
c := new(big.Int).Exp(z, e, key.N)
res := make([]byte, rsaLen)
c.FillBytes(res)
return res
}
func rsaDecrypt(data []byte, key *rsa.PrivateKey, to []byte) bool {
c := new(big.Int).SetBytes(data)
m := new(big.Int).Exp(c, key.D, key.N)
return FillBytes(m, to)
}
+27
View File
@@ -0,0 +1,27 @@
package crypto
import (
"crypto/rsa"
"crypto/sha1" // #nosec
"encoding/binary"
"math/big"
"go.mau.fi/mautrix-telegram/pkg/gotd/bin"
)
// RSAFingerprint returns fingerprint of RSA public key as defined in MTProto.
func RSAFingerprint(key *rsa.PublicKey) int64 {
e := big.NewInt(int64(key.E))
// See "Creating an Authorization Key" for reference:
// * https://core.telegram.org/mtproto/auth_key#dh-exchange-initiation
// rsa_public_key n:string e:string = RSAPublicKey
buf := new(bin.Buffer)
buf.PutBytes(key.N.Bytes())
buf.PutBytes(e.Bytes())
h := sha1.New() // #nosec
_, _ = h.Write(buf.Buf)
result := h.Sum(nil)[12:sha1.Size]
return int64(binary.LittleEndian.Uint64(result))
}
+26
View File
@@ -0,0 +1,26 @@
package crypto
import (
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"testing"
)
func TestRSAFingerprint(t *testing.T) {
testKey := []byte(`-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCJhOrkbrOi7/fFQRN2+8W5+Inx
bdxo7XH5DDKgFvXPDDe8cQINO3/kFat7KlpC2n2sS8ApZQkmREANg0XpImL9lCHB
v1FgQmL0xtnaURKo7FzaoaL4jCf5556NQr1th9F3oeN67mR4+BF0vPP9Gu6GY5Z1
BSqi+FEREW/2aWSgSwIDAQAB
-----END PUBLIC KEY-----`)
b, _ := pem.Decode(testKey)
keyParsed, err := x509.ParsePKIXPublicKey(b.Bytes)
if err != nil {
t.Fatal(err)
}
key := keyParsed.(*rsa.PublicKey)
if RSAFingerprint(key) != 1914007313702140277 {
t.Fatal(RSAFingerprint(key))
}
}
+62
View File
@@ -0,0 +1,62 @@
package crypto
import (
"bytes"
"crypto/rsa"
"crypto/sha1" // #nosec G505
"io"
"github.com/go-faster/errors"
)
// RSAEncryptHashed encrypts given data with RSA, prefixing with a hash.
func RSAEncryptHashed(data []byte, key *rsa.PublicKey, randomSource io.Reader) ([]byte, error) {
// Preparing `data_with_hash`.
// data_with_hash := SHA1(data) + data + (any random bytes);
// such that the length equals 255 bytes;
var dataWithHash [rsaWithHashLen]byte
if len(data) > rsaDataLen {
return nil, errors.Errorf("data length %d is too big", len(data))
}
// Filling data_with_hash with random bytes.
if _, err := io.ReadFull(randomSource, dataWithHash[:]); err != nil {
return nil, err
}
h := sha1.Sum(data) // #nosec
// Replacing first 20 bytes with sha1(data).
copy(dataWithHash[:sha1.Size], h[:])
// Replacing other bytes with data itself.
copy(dataWithHash[sha1.Size:], data)
// Encrypting "dataWithHash" with RSA.
res := rsaEncrypt(dataWithHash[:], key)
return res, nil
}
// RSADecryptHashed decrypts given data with RSA.
func RSADecryptHashed(data []byte, key *rsa.PrivateKey) ([]byte, error) {
var dataWithHash [rsaWithHashLen]byte
if !rsaDecrypt(data, key, dataWithHash[:]) {
return nil, errors.New("invalid data_with_hash")
}
hash := dataWithHash[:sha1.Size]
paddedData := dataWithHash[sha1.Size:]
// Guessing such data that sha1(data) == hash.
for i := 0; i <= len(paddedData); i++ {
data := paddedData[:len(paddedData)-i]
h := sha1.Sum(data) // #nosec
if bytes.Equal(h[:], hash) {
// Found.
return data, nil
}
}
// This can be caused by invalid keys or implementation bug.
return nil, errors.New("hash mismatch")
}
+96
View File
@@ -0,0 +1,96 @@
package crypto
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"encoding/base64"
mathrand "math/rand"
"testing"
"github.com/stretchr/testify/require"
)
func TestRSAEncryptHashed(t *testing.T) {
testKey := []byte(`-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCJhOrkbrOi7/fFQRN2+8W5+Inx
bdxo7XH5DDKgFvXPDDe8cQINO3/kFat7KlpC2n2sS8ApZQkmREANg0XpImL9lCHB
v1FgQmL0xtnaURKo7FzaoaL4jCf5556NQr1th9F3oeN67mR4+BF0vPP9Gu6GY5Z1
BSqi+FEREW/2aWSgSwIDAQAB
-----END PUBLIC KEY-----`)
keys, err := ParseRSAPublicKeys(testKey)
if err != nil {
t.Fatal(err)
}
key := keys[0]
rnd := mathrand.New(mathrand.NewSource(239))
result, err := RSAEncryptHashed([]byte("hello world"), key, rnd)
if err != nil {
t.Fatal(err)
}
expectedBase64 := "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/GJXcAWGFq4zkN+CjnRXNgFL1" +
"PFMxY/KZT5wq3UcHCD3A+0hxePcHZywJFJ0JIJyOkFYno0uTZAWy/7sgR5Ucpd6SlUFDVp" +
"TinnaM9hKWpBmzT8HNgrGd7HAPqqNiq9cmLrQsPhLd+Xt4JCdWiIvbm+HrgXdrZx1iiIVB" +
"eDHu3g=="
gotBase64 := base64.StdEncoding.EncodeToString(result)
if expectedBase64 != gotBase64 {
t.Error(gotBase64)
}
if _, err := RSAEncryptHashed(bytes.Repeat([]byte{1, 2, 3}, 1000), key, rnd); err == nil {
t.Error("should error")
}
}
func TestRSADecryptHashed(t *testing.T) {
a := require.New(t)
src := rand.Reader
k, err := rsa.GenerateKey(src, RSAKeyBits)
a.NoError(err)
plaintext := []byte("abcd")
encrypted, err := RSAEncryptHashed(plaintext, &k.PublicKey, src)
a.NoError(err)
decrypted, err := RSADecryptHashed(encrypted, k)
a.NoError(err)
a.Equal(plaintext, decrypted)
}
func TestRSAEncryptHashedCorpus(t *testing.T) {
reader := mathrand.New(mathrand.NewSource(0))
k, err := rsa.GenerateKey(reader, RSAKeyBits)
require.NoError(t, err)
for _, s := range []string{
"\xbd\xbf\xef\x1e\x11p",
} {
t.Run(s, func(t *testing.T) {
data := []byte(s)
encrypted, err := RSAEncryptHashed(data, &k.PublicKey, reader)
require.NoError(t, err)
decrypted, err := RSADecryptHashed(encrypted, k)
require.NoError(t, err)
require.Equal(t, data, decrypted)
})
}
}
func TestRSAEncryptHashedFuzz(t *testing.T) {
src := mathrand.New(mathrand.NewSource(1))
k, err := rsa.GenerateKey(src, RSAKeyBits)
require.NoError(t, err)
for i := 0; i < 100; i++ {
n, err := RandInt64n(src, rsaDataLen)
require.NoError(t, err)
data := make([]byte, int(n))
encrypted, err := RSAEncryptHashed(data, &k.PublicKey, src)
require.NoError(t, err)
decrypted, err := RSADecryptHashed(encrypted, k)
require.NoError(t, err)
require.Equal(t, data, decrypted)
}
}
+161
View File
@@ -0,0 +1,161 @@
package crypto
import (
"bytes"
"crypto/aes"
"crypto/rsa"
"crypto/sha256"
"io"
"math/big"
"github.com/go-faster/errors"
"github.com/go-faster/xor"
"github.com/gotd/ige"
"go.mau.fi/mautrix-telegram/pkg/gotd/bin"
)
func reverseBytes(s []byte) {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
}
const (
rsaPadDataLimit = 144
dataWithPaddingLength = 192
dataWithHashLength = dataWithPaddingLength + sha256.Size
tempKeySize = 32
)
// RSAPad encrypts given data with RSA, prefixing with a hash.
//
// See https://core.telegram.org/mtproto/auth_key#presenting-proof-of-work-server-authentication.
func RSAPad(data []byte, key *rsa.PublicKey, randomSource io.Reader) ([]byte, error) {
// 1) data_with_padding := data + random_padding_bytes; — where random_padding_bytes are
// chosen so that the resulting length of data_with_padding is precisely 192 bytes, and
// data is the TL-serialized data to be encrypted as before.
//
// One has to check that data is not longer than 144 bytes.
if len(data) > rsaPadDataLimit {
return nil, errors.Errorf("data length is bigger that 144 (%d)", len(data))
}
dataWithPadding := make([]byte, dataWithPaddingLength)
copy(dataWithPadding, data)
// Filling data_with_padding with random bytes.
if _, err := io.ReadFull(randomSource, dataWithPadding[len(data):]); err != nil {
return nil, errors.Wrap(err, "pad data with random")
}
// Make a copy.
dataPadReversed := make([]byte, dataWithPaddingLength)
copy(dataPadReversed, dataWithPadding)
// 2) data_pad_reversed := BYTE_REVERSE(data_with_padding);
reverseBytes(dataPadReversed)
for {
// 3) A random 32-byte temp_key is generated.
tempKey := make([]byte, tempKeySize)
if _, err := io.ReadFull(randomSource, tempKey); err != nil {
return nil, errors.Wrap(err, "generate temp_key")
}
// 4) data_with_hash := data_pad_reversed + SHA256(temp_key + data_with_padding);
// — after this assignment, data_with_hash is exactly 224 bytes long.
dataWithHash := make([]byte, 0, dataWithHashLength)
dataWithHash = append(dataWithHash, dataPadReversed...)
{
h := sha256.New()
_, _ = h.Write(tempKey)
_, _ = h.Write(dataWithPadding)
dataWithHash = h.Sum(dataWithHash)
dataWithHash = dataWithHash[:dataWithHashLength]
}
// 5) aes_encrypted := AES256_IGE(data_with_hash, temp_key, 0); — AES256-IGE encryption with zero IV.
aesEncrypted := make([]byte, len(dataWithHash))
{
aesBlock, err := aes.NewCipher(tempKey)
if err != nil {
return nil, errors.Wrap(err, "create cipher")
}
var zeroIV bin.Int256
ige.EncryptBlocks(aesBlock, zeroIV[:], aesEncrypted, dataWithHash)
}
// 6) temp_key_xor := temp_key XOR SHA256(aes_encrypted); — adjusted key, 32 bytes
tempKeyXor := make([]byte, tempKeySize)
{
aesEncryptedHash := sha256.Sum256(aesEncrypted)
xor.Bytes(tempKeyXor, tempKey, aesEncryptedHash[:])
}
// 7) key_aes_encrypted := temp_key_xor + aes_encrypted; — exactly 256 bytes (2048 bits) long.
keyAESEncrypted := make([]byte, 0, tempKeySize+dataWithHashLength)
keyAESEncrypted = append(keyAESEncrypted, tempKeyXor...)
keyAESEncrypted = append(keyAESEncrypted, aesEncrypted...)
// 8) The value of key_aes_encrypted is compared with the RSA-modulus of server_pubkey
// as a big-endian 2048-bit (256-byte) unsigned integer. If key_aes_encrypted turns out to be
// greater than or equal to the RSA modulus, the previous steps starting from the generation
// of new random temp_key are repeated.
keyAESEncryptedBig := big.NewInt(0).SetBytes(keyAESEncrypted)
if keyAESEncryptedBig.Cmp(key.N) >= 0 {
continue
}
// Otherwise the final step is performed:
// 9) encrypted_data := RSA(key_aes_encrypted, server_pubkey);
// — 256-byte big-endian integer is elevated to the requisite power from the RSA public key
// modulo the RSA modulus, and the result is stored as a big-endian integer consisting of
// exactly 256 bytes (with leading zero bytes if required).
//
// Encrypting "key_aes_encrypted" with RSA.
res := rsaEncrypt(keyAESEncrypted, key)
return res, nil
}
}
// DecodeRSAPad implements server-side decoder of RSAPad.
func DecodeRSAPad(data []byte, key *rsa.PrivateKey) ([]byte, error) {
var encryptedData [256]byte
if !rsaDecrypt(data, key, encryptedData[:]) {
return nil, errors.New("invalid encrypted_data")
}
tempKeyXor := encryptedData[:tempKeySize]
aesEncrypted := encryptedData[tempKeySize:]
tempKey := make([]byte, tempKeySize)
{
aesEncryptedHash := sha256.Sum256(aesEncrypted)
xor.Bytes(tempKey, tempKeyXor, aesEncryptedHash[:])
}
dataWithHash := make([]byte, len(aesEncrypted))
{
aesBlock, err := aes.NewCipher(tempKey)
if err != nil {
return nil, errors.Wrap(err, "create cipher")
}
var zeroIV bin.Int256
ige.DecryptBlocks(aesBlock, zeroIV[:], dataWithHash, aesEncrypted)
}
dataWithPadding := dataWithHash[:dataWithPaddingLength]
reverseBytes(dataWithPadding)
hash := dataWithHash[dataWithPaddingLength:]
{
h := sha256.New()
_, _ = h.Write(tempKey)
_, _ = h.Write(dataWithPadding)
if !bytes.Equal(hash, h.Sum(nil)) {
return nil, errors.New("hash mismatch")
}
}
return dataWithPadding, nil
}
+84
View File
@@ -0,0 +1,84 @@
package crypto
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"encoding/hex"
"io"
"testing"
"github.com/stretchr/testify/require"
"go.mau.fi/mautrix-telegram/pkg/gotd/testutil"
)
func TestRSAPad(t *testing.T) {
a := require.New(t)
keys, err := ParseRSAPublicKeys([]byte(`
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEA6LszBcC1LGzyr992NzE0ieY+BSaOW622Aa9Bd4ZHLl+TuFQ4lo4g
5nKaMBwK/BIb9xUfg0Q29/2mgIR6Zr9krM7HjuIcCzFvDtr+L0GQjae9H0pRB2OO
62cECs5HKhT5DZ98K33vmWiLowc621dQuwKWSQKjWf50XYFw42h21P2KXUGyp2y/
+aEyZ+uVgLLQbRA1dEjSDZ2iGRy12Mk5gpYc397aYp438fsJoHIgJ2lgMv5h7WY9
t6N/byY9Nw9p21Og3AoXSL2q/2IJ1WRUhebgAdGVMlV1fkuOQoEzR7EdpqtQD9Cs
5+bfo3Nhmcyvk5ftB0WkJ9z6bNZ7yxrP8wIDAQAB
-----END RSA PUBLIC KEY-----`))
a.NoError(err)
data := bytes.Repeat([]byte{'a'}, 144)
encrypted, err := RSAPad(data, keys[0], testutil.ZeroRand{})
a.NoError(err)
a.Len(encrypted, 256)
hexResult := "bf68719e836806b040cd261ecaf66eb3c4ba19f3bbea3031b2e6cf29167bab647201d101b291dc" +
"5b716a42e789a38d947fe59e9bcce8f30ef46a946743ea8b6babbce7fc0afc46b802aa453e83471d82a4dfad83f971f35" +
"0b4b4fb474cd1c48fdf427e4b5fecce9ec3178ae7dac3985856fdefa21d6fdc5e0e0fd8a57bc4f51580d637d372be8d87" +
"c9aa3fde8e6f8287bcb3be846aadcdd59465375479e248f62ed438f9804fbe36d41ca906243a5f740f3937949aa149ba8" +
"a8b8e68b3f3e1e3cd3f946387520e21eee55845e1f015a919a22f6a72bfaecd2cae946c91983b41f9ffabe97963bbde8f" +
"30eaf5fd3c5b8cecab8711bd269e441b6084f385726ff0"
expected, err := hex.DecodeString(hexResult)
a.NoError(err)
a.Equal(expected, encrypted)
}
func TestDecodeRSAPad(t *testing.T) {
a := require.New(t)
r := rand.Reader
key, err := rsa.GenerateKey(r, RSAKeyBits)
a.NoError(err)
size := 144
data := make([]byte, size)
_, err = io.ReadFull(r, data)
a.NoError(err)
encrypted, err := RSAPad(data, &key.PublicKey, r)
a.NoError(err)
a.Len(encrypted, 256)
decrypted, err := DecodeRSAPad(encrypted, key)
a.NoError(err)
a.Equal(data, decrypted[:size])
}
func BenchmarkRSAPad(b *testing.B) {
key := testutil.RSAPrivateKey()
data := make([]byte, 144)
if _, err := io.ReadFull(rand.Reader, data); err != nil {
b.Fatal(err)
}
b.SetBytes(int64(len(data)))
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if _, err := RSAPad(data, &key.PublicKey, rand.Reader); err != nil {
b.Fatal(err)
}
}
}
+101
View File
@@ -0,0 +1,101 @@
package crypto
import (
"crypto/rsa"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRSAPublicDecrypt(t *testing.T) {
testKey := []byte(`-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAyr+18Rex2ohtVy8sroGPBwXD3DOoKCSpjDqYoXgCqB7ioln4eDCF
fOBUlfXUEvM/fnKCpF46VkAftlb4VuPDeQSS/ZxZYEGqHaywlroVnXHIjgqoxiAd
192xRGreuXIaUKmkwlM9JID9WS2jUsTpzQ91L8MEPLJ/4zrBwZua8W5fECwCCh2c
9G5IzzBm+otMS/YKwmR1olzRCyEkyAEjXWqBI9Ftv5eG8m0VkBzOG655WIYdyV0H
fDK/NWcvGqa0w/nriMD6mDjKOryamw0OP9QuYgMN0C9xMW9y8SmP4h92OAWodTYg
Y1hZCxdv6cs5UnW9+PWvS+WIbkh+GaWYxwIDAQAB
-----END RSA PUBLIC KEY-----`)
data := []uint8{
0x02, 0x05, 0x1a, 0x4a, 0x58, 0x12, 0xc3, 0xa6,
0xb1, 0xa4, 0xb5, 0x61, 0x2e, 0x45, 0x29, 0x12,
0xaa, 0xeb, 0x77, 0xc2, 0x12, 0x19, 0x37, 0x64,
0x3d, 0xb7, 0xfa, 0x6a, 0x7c, 0xe0, 0xaa, 0xc3,
0xc4, 0xcc, 0xe6, 0x6c, 0xdb, 0x55, 0xa6, 0x33,
0x8d, 0x38, 0x45, 0xb6, 0x35, 0xb0, 0x61, 0x10,
0x87, 0x28, 0x33, 0x40, 0x2a, 0x24, 0x22, 0x47,
0xda, 0x84, 0x5a, 0x9f, 0x16, 0x8e, 0xd2, 0x07,
0x08, 0x25, 0x32, 0xc8, 0x55, 0x42, 0x20, 0x7f,
0x58, 0x52, 0x2f, 0x33, 0x1e, 0x46, 0x4f, 0x86,
0x76, 0x00, 0xa3, 0x49, 0x15, 0xe9, 0x6e, 0xff,
0x41, 0x5f, 0xc9, 0xbd, 0xee, 0x1e, 0x6c, 0x12,
0x49, 0x82, 0xe0, 0x0c, 0xe6, 0x96, 0x3b, 0x06,
0xa3, 0x59, 0xc4, 0x66, 0x12, 0xfd, 0xd6, 0x36,
0xdd, 0x88, 0xe0, 0x90, 0x2e, 0x45, 0x26, 0x3e,
0x83, 0xbf, 0xec, 0xe0, 0x65, 0xbf, 0x1c, 0xef,
0xf0, 0x67, 0x27, 0x5b, 0x9b, 0xe0, 0x6c, 0x15,
0xd1, 0xa6, 0x46, 0x5b, 0x36, 0x70, 0x12, 0x85,
0x44, 0x19, 0x9a, 0x28, 0xd4, 0x49, 0xd3, 0x19,
0xc6, 0x04, 0xfc, 0xd8, 0xca, 0x1a, 0xc3, 0x6a,
0xcd, 0x55, 0x84, 0x19, 0xaa, 0x0d, 0x6f, 0x3d,
0x74, 0x91, 0xaf, 0xa3, 0x3a, 0x80, 0x1a, 0x23,
0x46, 0x0c, 0xfe, 0x3d, 0xda, 0x6a, 0xa9, 0x00,
0x3c, 0xb8, 0x1b, 0x3a, 0x2a, 0xcd, 0xb1, 0x6c,
0x2b, 0xef, 0xa6, 0x83, 0x1f, 0x94, 0x04, 0x2c,
0x35, 0x65, 0x31, 0xd0, 0x5e, 0x72, 0x84, 0xdd,
0xda, 0x70, 0x64, 0x76, 0x08, 0xa7, 0x85, 0x5b,
0x0b, 0xd9, 0xc4, 0xe8, 0x1a, 0x7b, 0x2d, 0xea,
0xd4, 0x40, 0x39, 0xed, 0xd6, 0x9a, 0xd3, 0xa6,
0x0e, 0x9d, 0x40, 0x3c, 0xb1, 0x26, 0x63, 0x6a,
0xed, 0xfc, 0xfb, 0xe5, 0x97, 0x5c, 0x6b, 0x25,
0x9c, 0x2e, 0xc3, 0xd3, 0x49, 0xd0, 0x15, 0x80,
}
expected := []byte{
0x2f, 0x1a, 0x77, 0xcc, 0x57, 0xa4, 0xad, 0xb2,
0x8f, 0xad, 0x2e, 0x49, 0x05, 0xaf, 0x88, 0x4a,
0x3e, 0x15, 0x0a, 0xc7, 0xf4, 0x96, 0xdc, 0xe5,
0x05, 0x7e, 0xc9, 0xd3, 0x5e, 0x3c, 0x86, 0x62,
0x81, 0xb5, 0xd4, 0x11, 0x45, 0x3d, 0xf2, 0xa7,
0x14, 0xe9, 0x15, 0xa4, 0x56, 0x60, 0x85, 0xd8,
0xb2, 0xd2, 0x0f, 0x5c, 0xd6, 0x12, 0xfb, 0x85,
0x98, 0xf2, 0x04, 0x5c, 0x75, 0xc3, 0x36, 0x69,
0x42, 0x63, 0x72, 0xbc, 0xe6, 0xe0, 0x54, 0x86,
0x6f, 0x42, 0xce, 0x36, 0xeb, 0xbc, 0x51, 0xa2,
0x81, 0xeb, 0xcf, 0xd9, 0x74, 0x69, 0x05, 0x03,
0x2c, 0xbe, 0xad, 0xc2, 0xd0, 0x08, 0x49, 0xd5,
0x19, 0x5b, 0x8d, 0x35, 0x83, 0x45, 0x84, 0x3e,
0xa5, 0x9b, 0x2b, 0x79, 0x76, 0x59, 0x7b, 0x3e,
0x2a, 0x8f, 0xc1, 0xe0, 0x3e, 0x70, 0x68, 0x1f,
0x7f, 0x0d, 0x92, 0x6f, 0x10, 0x85, 0x0c, 0x5c,
0xa2, 0xdf, 0x74, 0x8a, 0x7d, 0x9c, 0x04, 0xa8,
0xd0, 0x96, 0x39, 0xf1, 0xf4, 0xdb, 0x15, 0x22,
0x38, 0xa0, 0x67, 0x6e, 0x87, 0x36, 0x2e, 0xce,
0xef, 0x63, 0x15, 0xd9, 0x37, 0x7c, 0x59, 0xb1,
0x62, 0x06, 0x83, 0x7c, 0x2e, 0xe2, 0x93, 0x65,
0xad, 0xdb, 0x67, 0x1b, 0x0f, 0xe9, 0x81, 0x61,
0xbb, 0x0d, 0x8c, 0x2b, 0x6f, 0x25, 0x58, 0x0d,
0x24, 0x59, 0xff, 0xb2, 0x56, 0x62, 0x84, 0x0b,
0xa6, 0x20, 0xee, 0xe3, 0x03, 0xb1, 0x7d, 0x39,
0x91, 0x65, 0x6b, 0x5d, 0x3a, 0x94, 0xc8, 0xd1,
0x01, 0x73, 0xa2, 0xd7, 0x38, 0x28, 0x9d, 0x0f,
0x75, 0x0a, 0xad, 0xf4, 0x2e, 0x6f, 0x65, 0x2f,
0x5f, 0xaa, 0x4c, 0x6e, 0x8b, 0x61, 0x60, 0xfb,
0xab, 0xb1, 0x48, 0xa1, 0x32, 0x76, 0xa0, 0x88,
0x42, 0x6c, 0xf5, 0x1e, 0x23, 0x3d, 0x03, 0x47,
0x48, 0x09, 0x3d, 0xf9, 0x92, 0x1d, 0xcd, 0x66,
}
keys, err := ParseRSAPublicKeys(testKey)
require.NoError(t, err)
require.Len(t, keys, 1)
key := keys[0]
actual, err := RSAPublicDecrypt(key, data)
if assert.NoError(t, err) {
assert.Equal(t, expected, actual)
}
_, err = RSAPublicDecrypt(key, nil)
assert.ErrorIs(t, err, rsa.ErrVerification)
}
+17
View File
@@ -0,0 +1,17 @@
package crypto
import (
"encoding/binary"
"github.com/go-faster/xor"
"go.mau.fi/mautrix-telegram/pkg/gotd/bin"
)
// ServerSalt computes server salt.
func ServerSalt(newNonce bin.Int256, serverNonce bin.Int128) (salt int64) {
var serverSalt [8]byte
copy(serverSalt[:], newNonce[:8])
xor.Bytes(serverSalt[:], serverSalt[:], serverNonce[:8])
return int64(binary.LittleEndian.Uint64(serverSalt[:]))
}
+17
View File
@@ -0,0 +1,17 @@
package crypto
import (
"encoding/binary"
"io"
)
// NewSessionID generates new random int64 from reader.
//
// Use crypto/rand.Reader if session id should be cryptographically safe.
func NewSessionID(reader io.Reader) (int64, error) {
bytes := make([]byte, 8)
if _, err := io.ReadFull(reader, bytes); err != nil {
return 0, err
}
return int64(binary.LittleEndian.Uint64(bytes)), nil
}
+25
View File
@@ -0,0 +1,25 @@
package crypto
import (
"crypto/rand"
mathrand "math/rand"
"testing"
)
func TestNewSessionID(t *testing.T) {
t.Run("Crypto", func(t *testing.T) {
if _, err := NewSessionID(rand.Reader); err != nil {
t.Fatal(err)
}
})
t.Run("Math", func(t *testing.T) {
rnd := mathrand.New(mathrand.NewSource(239))
n, err := NewSessionID(rnd)
if err != nil {
t.Fatal(err)
}
if n != 2092333242585417206 {
t.Fatal("mismatch")
}
})
}
+29
View File
@@ -0,0 +1,29 @@
package crypto
import (
"crypto/sha256"
"hash"
"sync"
)
// SHA256 returns SHA256 hash.
func SHA256(from ...[]byte) []byte {
h := getSHA256()
defer sha256Pool.Put(h)
for _, b := range from {
_, _ = h.Write(b)
}
return h.Sum(nil)
}
var sha256Pool = &sync.Pool{
New: func() interface{} {
return sha256.New()
},
}
func getSHA256() hash.Hash {
h := sha256Pool.Get().(hash.Hash)
h.Reset()
return h
}
+118
View File
@@ -0,0 +1,118 @@
package srp
import (
"crypto/sha256"
"crypto/sha512"
"math/big"
"github.com/go-faster/errors"
"golang.org/x/crypto/pbkdf2"
)
// Hash computes user password hash using parameters from server.
//
// See https://core.telegram.org/api/srp#checking-the-password-with-srp.
func (s SRP) Hash(password, srpB, random []byte, i Input) (Answer, error) {
p := s.bigFromBytes(i.P)
if err := checkInput(i.G, p); err != nil {
return Answer{}, errors.Wrap(err, "validate algo")
}
g := big.NewInt(int64(i.G))
// It is safe to use FillBytes directly because we know that 64-bit G always smaller than
// 256-bit destination array.
var gBytes [256]byte
g.FillBytes(gBytes[:])
// random 2048-bit number a
a := s.bigFromBytes(random)
// `g_a = pow(g, a) mod p`
ga, ok := s.pad256FromBig(s.bigExp(g, a, p))
if !ok {
return Answer{}, errors.New("g_a is too big")
}
// `g_b = srp_B`
gb := s.pad256(srpB)
// `u = H(g_a | g_b)`
u := s.bigFromBytes(s.hash(ga[:], gb[:]))
// `x = PH2(password, salt1, salt2)`
// `v = pow(g, x) mod p`
x, v := s.computeXV(password, i.Salt1, i.Salt2, g, p)
// `k = (k * v) mod p`
k := s.bigFromBytes(s.hash(i.P, gBytes[:]))
// `k_v = (k * v) % p`
kv := k.Mul(k, v).Mod(k, p)
// `t = (g_b - k_v) % p`
t := s.bigFromBytes(srpB)
if t.Sub(t, kv).Cmp(big.NewInt(0)) == -1 {
t.Add(t, p)
}
// `s_a = pow(t, a + u * x) mod p`
sa, ok := s.pad256FromBig(s.bigExp(t, u.Mul(u, x).Add(u, a), p))
if !ok {
return Answer{}, errors.New("s_a is too big")
}
// `k_a = H(s_a)`
ka := sha256.Sum256(sa[:])
// `M1 = H(H(p) xor H(g) | H2(salt1) | H2(salt2) | g_a | g_b | k_a)`
xorHpHg := xor32(sha256.Sum256(i.P), sha256.Sum256(gBytes[:]))
M1 := s.hash(
xorHpHg[:],
s.hash(i.Salt1),
s.hash(i.Salt2),
ga[:],
gb[:],
ka[:],
)
return Answer{
A: ga[:],
M1: M1,
}, nil
}
// The main hashing function H is sha256:
//
// H(data) := sha256(data)
func (s SRP) hash(data ...[]byte) []byte {
h := sha256.New()
for i := range data {
h.Write(data[i])
}
return h.Sum(nil)
}
// The salting hashing function SH is defined as follows:
//
// SH(data, salt) := H(salt | data | salt)
func (s SRP) saltHash(data, salt []byte) []byte {
return s.hash(salt, data, salt)
}
// The primary password hashing function is defined as follows:
//
// PH1(password, salt1, salt2) := SH(SH(password, salt1), salt2)
func (s SRP) primary(password, salt1, salt2 []byte) []byte {
return s.saltHash(s.saltHash(password, salt1), salt2)
}
// The secondary password hashing function is defined as follows:
//
// PH2(password, salt1, salt2) := SH(pbkdf2(sha512, PH1(password, salt1, salt2), salt1, 100000), salt2)
func (s SRP) secondary(password, salt1, salt2 []byte) []byte {
return s.saltHash(s.pbkdf2(s.primary(password, salt1, salt2), salt1, 100000), salt2)
}
func (s SRP) pbkdf2(ph1, salt1 []byte, n int) []byte {
return pbkdf2.Key(ph1, salt1, n, 64, sha512.New)
}
+54
View File
@@ -0,0 +1,54 @@
package srp
import (
"io"
"math/big"
"github.com/go-faster/errors"
)
// computeXV computes following numbers
//
// `x = PH2(password, salt1, salt2)`
// `v = pow(g, x) mod p`
//
// TDLib uses terms `clientSalt` for `salt1` and `serverSalt` for `salt2`.
func (s SRP) computeXV(password, clientSalt, serverSalt []byte, g, p *big.Int) (x, v *big.Int) {
// `x = PH2(password, salt1, salt2)`
x = new(big.Int).SetBytes(s.secondary(password, clientSalt, serverSalt))
// `v = pow(g, x) mod p`
v = new(big.Int).Exp(g, x, p)
return x, v
}
// NewHash computes new user password hash using parameters from server.
//
// See https://core.telegram.org/api/srp#setting-a-new-2fa-password.
//
// TDLib implementation:
// See https://github.com/tdlib/td/blob/fa8feefed70d64271945e9d5fd010b957d93c8cd/td/telegram/PasswordManager.cpp#L57.
//
// TDesktop implementation:
// See https://github.com/telegramdesktop/tdesktop/blob/v3.4.8/Telegram/SourceFiles/core/core_cloud_password.cpp#L68.
func (s SRP) NewHash(password []byte, i Input) (hash, newSalt []byte, _ error) {
// Generate a new new_password_hash using the KDF algorithm specified in the new_settings,
// just append 32 sufficiently random bytes to the salt1, first. Proceed as for checking passwords with SRP,
// just stop at the generation of the v parameter, and use it as new_password_hash:
p := new(big.Int).SetBytes(i.P)
if err := checkInput(i.G, p); err != nil {
return nil, nil, errors.Wrap(err, "validate algo")
}
// Make a copy.
newClientSalt := append([]byte(nil), i.Salt1...)
newClientSalt = append(newClientSalt, make([]byte, 32)...)
// ... append 32 sufficiently random bytes to the salt1 ...
if _, err := io.ReadFull(s.random, newClientSalt[len(newClientSalt)-32:]); err != nil {
return nil, nil, err
}
_, v := s.computeXV(password, newClientSalt, i.Salt2, big.NewInt(int64(i.G)), p)
// As usual in big endian form, padded to 2048 bits.
padded, _ := s.pad256FromBig(v)
return padded[:], newClientSalt, nil
}
+72
View File
@@ -0,0 +1,72 @@
package srp
import (
"testing"
"github.com/stretchr/testify/require"
"go.mau.fi/mautrix-telegram/pkg/gotd/testutil"
)
func TestSRP_NewHash(t *testing.T) {
password := []uint8{
110, 101, 119, 80, 97, 115, 115, 119, 111, 114, 100,
}
i := Input{
Salt1: []uint8{
230, 200, 149, 125, 223, 152, 141, 72,
},
Salt2: []uint8{
159, 99, 68, 130, 43, 9, 108, 255, 135, 239, 164, 38, 245, 120, 87, 182,
},
G: 3,
P: []uint8{
199, 28, 174, 185, 198, 177, 201, 4, 142, 108, 82, 47, 112, 241, 63, 115,
152, 13, 64, 35, 142, 62, 33, 193, 73, 52, 208, 55, 86, 61, 147, 15,
72, 25, 138, 10, 167, 193, 64, 88, 34, 148, 147, 210, 37, 48, 244, 219,
250, 51, 111, 110, 10, 201, 37, 19, 149, 67, 174, 212, 76, 206, 124, 55,
32, 253, 81, 246, 148, 88, 112, 90, 198, 140, 212, 254, 107, 107, 19, 171,
220, 151, 70, 81, 41, 105, 50, 132, 84, 241, 143, 175, 140, 89, 95, 100,
36, 119, 254, 150, 187, 42, 148, 29, 91, 205, 29, 74, 200, 204, 73, 136,
7, 8, 250, 155, 55, 142, 60, 79, 58, 144, 96, 190, 230, 124, 249, 164,
164, 166, 149, 129, 16, 81, 144, 126, 22, 39, 83, 181, 107, 15, 107, 65,
13, 186, 116, 216, 168, 75, 42, 20, 179, 20, 78, 14, 241, 40, 71, 84,
253, 23, 237, 149, 13, 89, 101, 180, 185, 221, 70, 88, 45, 177, 23, 141,
22, 156, 107, 196, 101, 176, 214, 255, 156, 163, 146, 143, 239, 91, 154, 228,
228, 24, 252, 21, 232, 62, 190, 160, 248, 127, 169, 255, 94, 237, 112, 5,
13, 237, 40, 73, 244, 123, 249, 89, 217, 86, 133, 12, 233, 41, 133, 31,
13, 129, 21, 246, 53, 177, 5, 238, 46, 78, 21, 208, 75, 36, 84, 191,
111, 79, 173, 240, 52, 177, 4, 3, 17, 156, 216, 227, 185, 47, 204, 91,
},
}
expectedHash := []uint8{
24, 106, 193, 141, 204, 87, 144, 191, 107, 186, 33, 189, 149, 141, 55, 94,
229, 72, 26, 240, 2, 158, 155, 215, 169, 198, 142, 201, 38, 189, 81, 150,
216, 31, 140, 216, 181, 142, 224, 108, 138, 16, 173, 234, 204, 127, 86, 232,
25, 255, 81, 72, 37, 222, 177, 91, 31, 173, 236, 106, 174, 23, 162, 68,
203, 35, 72, 141, 23, 52, 156, 212, 38, 26, 139, 164, 218, 123, 156, 44,
229, 196, 0, 20, 221, 158, 54, 39, 80, 172, 243, 172, 137, 184, 184, 245,
198, 24, 240, 182, 165, 114, 195, 143, 255, 58, 85, 77, 136, 24, 160, 184,
231, 182, 1, 94, 24, 54, 18, 138, 30, 78, 45, 92, 249, 151, 29, 29,
208, 72, 170, 24, 29, 134, 17, 82, 234, 231, 21, 83, 150, 38, 128, 99,
35, 135, 184, 154, 193, 134, 95, 222, 215, 200, 195, 218, 166, 78, 211, 141,
194, 80, 54, 102, 63, 160, 207, 119, 72, 197, 46, 161, 156, 24, 126, 112,
167, 82, 168, 5, 62, 64, 157, 72, 148, 33, 138, 66, 147, 147, 208, 51,
130, 228, 30, 80, 183, 65, 91, 59, 138, 208, 146, 253, 7, 144, 248, 141,
137, 78, 132, 220, 167, 143, 71, 244, 33, 137, 55, 215, 170, 153, 216, 140,
135, 192, 155, 203, 141, 168, 144, 229, 53, 2, 102, 35, 206, 166, 252, 139,
61, 37, 219, 112, 203, 66, 170, 164, 131, 35, 146, 125, 135, 168, 252, 241,
}
expectedNewSalt := []uint8{
230, 200, 149, 125, 223, 152, 141, 72, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
}
a := require.New(t)
s := NewSRP(testutil.ZeroRand{})
hash, newSalt, err := s.NewHash(password, i)
a.NoError(err)
a.Equal(expectedHash, hash)
a.Equal(expectedNewSalt, newSalt)
}
+23
View File
@@ -0,0 +1,23 @@
package srp
import (
"math/big"
"go.mau.fi/mautrix-telegram/pkg/gotd/crypto"
)
func (s SRP) pad256FromBig(i *big.Int) (b [256]byte, r bool) {
r = crypto.FillBytes(i, b[:])
return b, r
}
func (s SRP) pad256(b []byte) [256]byte {
if len(b) >= 256 {
return *(*[256]byte)(b[len(b)-256:])
}
var tmp [256]byte
copy(tmp[256-len(b):], b)
return tmp
}
+63
View File
@@ -0,0 +1,63 @@
// Package srp contains implementation of Secure Remote Password protocol.
package srp
import (
"crypto/sha256"
"io"
"math/big"
"github.com/go-faster/xor"
"go.mau.fi/mautrix-telegram/pkg/gotd/crypto"
)
// SRP is client implementation of Secure Remote Password protocol.
//
// See https://core.telegram.org/api/srp.
type SRP struct {
random io.Reader
}
// NewSRP creates new SRP instance.
func NewSRP(random io.Reader) SRP {
return SRP{random: random}
}
// Input is hashing algorithm parameters from server.
//
// Copy of tg.PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow
type Input struct {
// One of two salts used by the derivation function (see SRP 2FA login)
Salt1 []byte
// One of two salts used by the derivation function (see SRP 2FA login)
Salt2 []byte
// Base (see SRP 2FA login)
G int
// 2048-bit modulus (see SRP 2FA login)
P []byte
}
// Answer is result of SRP algorithm.
type Answer struct {
// A parameter (see SRP)
A []byte
// M1 parameter (see SRP)
M1 []byte
}
func xor32(a, b [sha256.Size]byte) (res [sha256.Size]byte) {
xor.Bytes(res[:], a[:], b[:])
return res
}
func (s SRP) bigFromBytes(b []byte) *big.Int {
return new(big.Int).SetBytes(b)
}
func (s SRP) bigExp(x, y, m *big.Int) *big.Int {
return new(big.Int).Exp(x, y, m)
}
func checkInput(g int, p *big.Int) error {
return crypto.CheckDH(g, p)
}
+122
View File
@@ -0,0 +1,122 @@
package srp
import (
"crypto/rand"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"fmt"
"math/big"
"testing"
"github.com/stretchr/testify/assert"
)
type srpArgs struct {
password []byte
srpB []byte
mp Input
}
func testSRPInput(t testing.TB) srpArgs {
return srpArgs{
password: []byte("123123"),
srpB: getHex(t, "9C52401A6A8084EC82F01C3725D3FB448BD2F0C909F9D97726EAC4B7A74172D9"+
"52F02466BE6734FA274D2B7429E27397F10372D66B400B80A5C5AE3F28B17BF3"+
"105D7A2D2A885998CDC2DEFC208AEC217AB58859A9ABC2374AD93DC285F4B3FB"+
"CAFF4143D7888F2425BD2FB711B25609CEB21757D935B1EF2F042173AD0CE2FE"+
"0E474DAC53914BD25A8A9AED4AEA8953D55CB88621DB37B871EA0D04393AC098"+
"7F68094CCC9DE8239251375D8FFFD263316CD528C097B7BC9FB919FBEDB76C52"+
"5DF3413C374EE076D97A1E6D352BB7CC80FD13651B04B32E2E48C5268150842C"+
"FD07CF855958B1B5EA9C36FDAD697FE3AEC8DCC6B1EFEC36874AF226204676CF"),
mp: Input{
Salt1: getHex(t, "4D11FB6BEC38F9D2546BB0F61E4F1C99A1BC0DB8F0D5F35B1291B37B213123D7ED48F3C6794D495B"),
Salt2: getHex(t, "A1B181AAFE88188680AE32860D60BB01"),
G: 3,
P: getHex(t, "C71CAEB9C6B1C9048E6C522F70F13F73980D40238E3E21C14934D037563D930F"+
"48198A0AA7C14058229493D22530F4DBFA336F6E0AC925139543AED44CCE7C37"+
"20FD51F69458705AC68CD4FE6B6B13ABDC9746512969328454F18FAF8C595F64"+
"2477FE96BB2A941D5BCD1D4AC8CC49880708FA9B378E3C4F3A9060BEE67CF9A4"+
"A4A695811051907E162753B56B0F6B410DBA74D8A84B2A14B3144E0EF1284754"+
"FD17ED950D5965B4B9DD46582DB1178D169C6BC465B0D6FF9CA3928FEF5B9AE4"+
"E418FC15E83EBEA0F87FA9FF5EED70050DED2849F47BF959D956850CE929851F"+
"0D8115F635B105EE2E4E15D04B2454BF6F4FADF034B10403119CD8E3B92FCC5B"),
},
}
}
func TestSRP(t *testing.T) {
tests := []struct {
args srpArgs
want Answer
expectError assert.ErrorAssertionFunc
}{
{
args: testSRPInput(t),
want: Answer{
A: setByte(256, 3),
M1: getHex(t, "999DF906BDA2C6CBB52F503406EBA2D0D0503ACE0CC302C38F13EE5010AD4051"),
},
expectError: assert.NoError,
},
}
for i := range tests {
tcase := tests[i]
t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) {
random := setByte(256, 1)
srp := NewSRP(rand.Reader)
got, err := srp.Hash(tcase.args.password, tcase.args.srpB, random, tcase.args.mp)
if !tcase.expectError(t, err) {
return
}
if !assert.Equal(t, tcase.want, got) {
return
}
})
}
}
func getHex(t testing.TB, in string) []byte {
res, err := hex.DecodeString(in)
if err != nil {
t.Fatal("failed to get hex", err)
}
return res
}
func setByte(size, value int) []byte {
res := make([]byte, size)
binary.BigEndian.PutUint32(res[size-4:], uint32(value))
return res
}
const pBase64 = "xxyuucaxyQSObFIvcPE_c5gNQCOOPiHBSTTQN1Y9kw9IGYoKp8FAWCKUk9IlMPTb-jNvbgrJJROVQ67UTM58NyD9UfaUWHBaxozU_" +
"mtrE6vcl0ZRKWkyhFTxj6-MWV9kJHf-lrsqlB1bzR1KyMxJiAcI-ps3jjxPOpBgvuZ8-aSkppWBEFGQfhYnU7VrD2tBDbp02KhLKhSzFE4O8ShHVP0" +
"X7ZUNWWW0ud1GWC2xF40WnGvEZbDW_5yjko_vW5rk5Bj8Feg-vqD4f6n_Xu1wBQ3tKEn0e_lZ2VaFDOkphR8NgRX2NbEF7i5OFdBLJFS_b0-" +
"t8DSxBAMRnNjjuS_MWw=="
func Test_checkInput(t *testing.T) {
p, err := base64.URLEncoding.DecodeString(pBase64)
if err != nil {
t.Fatal("no err expected", err)
}
err = checkInput(3, big.NewInt(0).SetBytes(p))
if err != nil {
t.Fatal("no err expected", err)
}
}
func BenchmarkSRP_Auth(b *testing.B) {
input := testSRPInput(b)
srp := NewSRP(rand.Reader)
random := setByte(256, 1)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = srp.Hash(input.password, input.srpB, random, input.mp)
}
}
+36
View File
@@ -0,0 +1,36 @@
package crypto
import (
"crypto/sha1" // #nosec
"math/big"
)
// sha1BigInt returns SHA1(a + b).
func sha1BigInt(a, b *big.Int) []byte {
var buf []byte
buf = append(buf, a.Bytes()...)
buf = append(buf, b.Bytes()...)
h := sha1.Sum(buf) // #nosec
return h[:]
}
// TempAESKeys returns tmp_aes_key and tmp_aes_iv based on new_nonce and
// server_nonce as defined in "Creating an Authorization Key".
func TempAESKeys(newNonce, serverNonce *big.Int) (key, iv []byte) {
// See https://core.telegram.org/mtproto/auth_key#presenting-proof-of-work-server-authentication
// 5. Server responds in one of two ways: [...]
// tmp_aes_key := SHA1(new_nonce + server_nonce) + substr (SHA1(server_nonce + new_nonce), 0, 12);
// SHA1(new_nonce + server_nonce)
key = append(key, sha1BigInt(newNonce, serverNonce)...)
// substr (SHA1(server_nonce + new_nonce), 0, 12);
key = append(key, sha1BigInt(serverNonce, newNonce)[:12]...)
// tmp_aes_iv := substr (SHA1(server_nonce + new_nonce), 12, 8) + SHA1(new_nonce + new_nonce) + substr (new_nonce, 0, 4);
iv = append(iv, sha1BigInt(serverNonce, newNonce)[12:12+8]...)
iv = append(iv, sha1BigInt(newNonce, newNonce)...)
iv = append(iv, newNonce.Bytes()[:4]...)
return key, iv
}
+78
View File
@@ -0,0 +1,78 @@
package crypto
import (
"bytes"
"crypto/aes"
"math/big"
"testing"
"github.com/gotd/ige"
)
func hexInt(hexValue string) *big.Int {
n, ok := big.NewInt(0).SetString(hexValue, 16)
if !ok {
panic(ok)
}
return n
}
func TestTempAESKeys(t *testing.T) {
// https://core.telegram.org/mtproto/samples-auth_key#conversion-of-encrypted-answer-into-answer
var (
newNonce = hexInt("311C85DB234AA2640AFC4A76A735CF5B1F0FD68BD17FA181E1229AD867CC024D")
serverNonce = hexInt("A5CF4D33F4A11EA877BA4AA573907330")
keyExpected = hexInt("F011280887C7BB01DF0FC4E17830E0B91FBB8BE4B2267CB985AE25F33B527253")
ivExpected = hexInt("3212D579EE35452ED23E0D0C92841AA7D31B2E9BDEF2151E80D15860311C85DB")
)
key, iv := TempAESKeys(newNonce, serverNonce)
if !bytes.Equal(key, keyExpected.Bytes()) {
t.Error("invalid key")
}
if !bytes.Equal(iv, ivExpected.Bytes()) {
t.Error("invalid iv")
}
encryptedAnswer := hexInt("28A92FE20173B347A8BB324B5FAB2667C9A8BBCE6468D5B509A4CB" +
"DDC186240AC912CF7006AF8926DE606A2E74C0493CAA57741E6C82451F54D3E068F5CCC49B4444124B966" +
"6FFB405AAB564A3D01E67F6E912867C8D20D9882707DC330B17B4E0DD57CB53BFAAFA9EF5BE76AE6C1B9B6" +
"C51E2D6502A47C883095C46C81E3BE25F62427B585488BB3BF239213BF48EB8FE34C9A026CC8413934043" +
"974DB03556633038392CECB51F94824E140B98637730A4BE79A8F9DAFA39BAE81E1095849EA4C83467C9" +
"2A3A17D997817C8A7AC61C3FF414DA37B7D66E949C0AEC858F048224210FCC61F11C3A910B431CCBD104" +
"CCCC8DC6D29D4A5D133BE639A4C32BBFF153E63ACA3AC52F2E4709B8AE01844B142C1EE89D075D64F69A" +
"399FEB04E656FE3675A6F8F412078F3D0B58DA15311C1A9F8E53B3CD6BB5572C294904B726D0BE337E2E2" +
"1977DA26DD6E33270251C2CA29DFCC70227F0755F84CFDA9AC4B8DD5F84F1D1EB36BA45CDDC70444D8C21" +
"3E4BD8F63B8AB95A2D0B4180DC91283DC063ACFB92D6A4E407CDE7C8C69689F77A007441D4A6A8384B666" +
"502D9B77FC68B5B43CC607E60A146223E110FCB43BC3C942EF981930CDC4A1D310C0B64D5E55D308D86325" +
"1AB90502C3E46CC599E886A927CDA963B9EB16CE62603B68529EE98F9F5206419E03FB458EC4BD9454AA8F6" +
"BA777573CC54B328895B1DF25EAD9FB4CD5198EE022B2B81F388D281D5E5BC580107CA01A50665C32B55271" +
"5F335FD76264FAD00DDD5AE45B94832AC79CE7C511D194BC42B70EFA850BB15C2012C5215CABFE97CE66B8D8" +
"734D0EE759A638AF013").Bytes()
cipher, err := aes.NewCipher(key)
if err != nil {
t.Fatal(err)
}
d := ige.NewIGEDecrypter(cipher, iv)
decrypted := make([]byte, len(encryptedAnswer))
d.CryptBlocks(decrypted, encryptedAnswer)
expectedAnswer := hexInt("BA0D89B53E0549828CCA27E966B301A48FECE2FCA5CF4D33F4A1" +
"1EA877BA4AA57390733002000000FE000100C71CAEB9C6B1C9048E6C522F70F13F73980D40238E3E2" +
"1C14934D037563D930F48198A0AA7C14058229493D22530F4DBFA336F6E0AC925139543AED44CCE7C3" +
"720FD51F69458705AC68CD4FE6B6B13ABDC9746512969328454F18FAF8C595F642477FE96BB2A941D5" +
"BCD1D4AC8CC49880708FA9B378E3C4F3A9060BEE67CF9A4A4A695811051907E162753B56B0F6B410DBA" +
"74D8A84B2A14B3144E0EF1284754FD17ED950D5965B4B9DD46582DB1178D169C6BC465B0D6FF9CA392" +
"8FEF5B9AE4E418FC15E83EBEA0F87FA9FF5EED70050DED2849F47BF959D956850CE929851F0D8115F63" +
"5B105EE2E4E15D04B2454BF6F4FADF034B10403119CD8E3B92FCC5BFE000100262AABA621CC4DF587D" +
"C94CF8252258C0B9337DFB47545A49CDD5C9B8EAE7236C6CADC40B24E88590F1CC2CC762EBF1CF11DC" +
"C0B393CAAD6CEE4EE5848001C73ACBB1D127E4CB93072AA3D1C8151B6FB6AA6124B7CD782EAF981BDCF" +
"CE9D7A00E423BD9D194E8AF78EF6501F415522E44522281C79D906DDB79C72E9C63D83FB2A940FF77" +
"9DFB5F2FD786FB4AD71C9F08CF48758E534E9815F634F1E3A80A5E1C2AF210C5AB762755AD4B2126DF" +
"A61A77FA9DA967D65DFD0AFB5CDF26C4D4E1A88B180F4E0D0B45BA1484F95CB2712B50BF3F5968D9D5" +
"5C99C0FB9FB67BFF56D7D4481B634514FBA3488C4CDA2FC0659990E8E868B28632875A9AA703BCDCE8FCB7AE551").Bytes()
answer := GuessDataWithHash(decrypted)
if !bytes.Equal(answer, expectedAnswer) {
t.Fatal("mismatch")
}
}