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:
@@ -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
|
||||
}
|
||||
@@ -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)))
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)))
|
||||
})
|
||||
}
|
||||
@@ -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}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// Package crypto implements cryptographical primitives for MTproto.
|
||||
//
|
||||
// Reference:
|
||||
// - https://core.telegram.org/mtproto/auth_key
|
||||
package crypto
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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[:]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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[:]))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 signer’s RSA public key.
|
||||
//
|
||||
// See also OpenSSL’s 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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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[:]))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user