WTelegramClient/src/Encryption.cs

571 lines
26 KiB
C#
Raw Normal View History

2021-08-04 00:40:09 +02:00
using System;
2021-08-06 01:54:29 +02:00
using System.Buffers.Binary;
2021-08-04 00:40:09 +02:00
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
2022-10-07 03:00:57 +02:00
using System.Runtime.InteropServices;
2021-08-04 00:40:09 +02:00
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using TL;
using static WTelegram.Compat;
2021-08-04 00:40:09 +02:00
namespace WTelegram
{
public static class Encryption
2021-08-04 00:40:09 +02:00
{
private static readonly Dictionary<long, RSAPublicKey> PublicKeys = [];
2024-03-26 12:07:03 +01:00
internal static readonly RandomNumberGenerator RNG = RandomNumberGenerator.Create();
2022-10-07 03:00:57 +02:00
internal static readonly Aes AesECB = Aes.Create();
2022-01-07 01:14:16 +01:00
static Encryption()
{
AesECB.Mode = CipherMode.ECB;
AesECB.Padding = PaddingMode.Zeros;
if (AesECB.BlockSize != 128) throw new WTException("AES Blocksize is not 16 bytes");
2022-01-07 01:14:16 +01:00
}
2021-08-04 00:40:09 +02:00
2021-09-23 09:27:52 +02:00
internal static async Task CreateAuthorizationKey(Client client, Session.DCSession session)
2021-08-04 00:40:09 +02:00
{
if (PublicKeys.Count == 0) LoadDefaultPublicKeys();
var sha1 = SHA1.Create();
var sha256 = SHA256.Create();
2021-08-04 00:40:09 +02:00
//1)
2024-03-26 12:07:03 +01:00
var nonce = new TL.Int128(RNG);
2022-10-01 13:56:43 +02:00
var resPQ = await client.ReqPqMulti(nonce);
2021-08-04 00:40:09 +02:00
//2)
if (resPQ.nonce != nonce) throw new WTException("Nonce mismatch");
2021-08-04 00:40:09 +02:00
var fingerprint = resPQ.server_public_key_fingerprints.FirstOrDefault(PublicKeys.ContainsKey);
if (fingerprint == 0) throw new WTException("Couldn't match any server_public_key_fingerprints");
var publicKey = PublicKeys[fingerprint];
2021-08-04 10:11:07 +02:00
Helpers.Log(2, $"Selected public key with fingerprint {fingerprint:X}");
2021-08-04 00:40:09 +02:00
//3)
long retry_id = 0;
ulong pq = Helpers.FromBigEndian(resPQ.pq);
ulong p = Helpers.PQFactorize(pq);
ulong q = pq / p;
//4)
2021-08-10 14:40:41 +02:00
var pqInnerData = new PQInnerDataDc
2021-08-04 00:40:09 +02:00
{
pq = resPQ.pq,
p = Helpers.ToBigEndian(p),
q = Helpers.ToBigEndian(q),
nonce = nonce,
2021-08-04 00:40:09 +02:00
server_nonce = resPQ.server_nonce,
new_nonce = new Int256(RNG),
2021-08-10 14:40:41 +02:00
dc = session.DataCenter?.id ?? 0
};
2022-09-11 15:34:38 +02:00
if (client.TLConfig?.test_mode == true) pqInnerData.dc += 10000;
2022-06-19 18:08:19 +02:00
if (session.DataCenter?.flags.HasFlag(DcOption.Flags.media_only) == true) pqInnerData.dc = -pqInnerData.dc;
byte[] encrypted_data = null;
{
//4.1) RSA_PAD(data, server_public_key)
using var clearStream = new MemoryStream(256);
2022-10-04 00:52:44 +02:00
using var writer = new BinaryWriter(clearStream);
byte[] aes_key = new byte[32], zero_iv = new byte[32];
var n = BigEndianInteger(publicKey.n);
2022-08-13 01:20:53 +02:00
do
{
RNG.GetBytes(aes_key);
clearStream.Position = 0;
clearStream.Write(aes_key, 0, 32); // write aes_key as prefix for initial Sha256 computation
writer.WriteTLObject(pqInnerData);
int clearLength = (int)clearStream.Position - 32; // length before padding
if (clearLength > 144) throw new WTException("PQInnerData too big");
byte[] clearBuffer = clearStream.GetBuffer();
RNG.GetBytes(clearBuffer, 32 + clearLength, 192 - clearLength);
sha256.ComputeHash(clearBuffer, 0, 32 + 192).CopyTo(clearBuffer, 224); // append Sha256
Array.Reverse(clearBuffer, 32, 192);
var aes_encrypted = AES_IGE_EncryptDecrypt(clearBuffer.AsSpan(32, 224), aes_key, zero_iv, true);
var hash_aes = sha256.ComputeHash(aes_encrypted);
for (int i = 0; i < 32; i++) // prefix aes_encrypted with temp_key_xor
clearBuffer[i] = (byte)(aes_key[i] ^ hash_aes[i]);
aes_encrypted.CopyTo(clearBuffer, 32);
var x = BigEndianInteger(clearBuffer);
if (x < n) // if good result, encrypt with RSA key:
encrypted_data = BigInteger.ModPow(x, BigEndianInteger(publicKey.e), n).To256Bytes();
2022-08-13 01:20:53 +02:00
} while (encrypted_data == null); // otherwise, repeat the steps
}
var serverDHparams = await client.ReqDHParams(pqInnerData.nonce, pqInnerData.server_nonce, pqInnerData.p, pqInnerData.q, fingerprint, encrypted_data);
2021-08-04 00:40:09 +02:00
//5)
var localTime = DateTimeOffset.UtcNow;
if (serverDHparams is not ServerDHParamsOk serverDHparamsOk) throw new WTException("not server_DH_params_ok");
if (serverDHparamsOk.nonce != nonce) throw new WTException("Nonce mismatch");
if (serverDHparamsOk.server_nonce != resPQ.server_nonce) throw new WTException("Server Nonce mismatch");
var (tmp_aes_key, tmp_aes_iv) = ConstructTmpAESKeyIV(sha1, resPQ.server_nonce, pqInnerData.new_nonce);
2021-08-04 00:40:09 +02:00
var answer = AES_IGE_EncryptDecrypt(serverDHparamsOk.encrypted_answer, tmp_aes_key, tmp_aes_iv, false);
using var answerReader = new BinaryReader(new MemoryStream(answer));
2022-08-13 01:20:53 +02:00
var answerHash = answerReader.ReadBytes(20);
var answerObj = answerReader.ReadTLObject();
if (answerObj is not ServerDHInnerData serverDHinnerData) throw new WTException("not server_DH_inner_data");
2022-08-13 01:20:53 +02:00
long padding = answerReader.BaseStream.Length - answerReader.BaseStream.Position;
if (padding >= 16) throw new WTException("Too much pad");
if (!Enumerable.SequenceEqual(sha1.ComputeHash(answer, 20, answer.Length - (int)padding - 20), answerHash))
throw new WTException("Answer SHA1 mismatch");
if (serverDHinnerData.nonce != nonce) throw new WTException("Nonce mismatch");
if (serverDHinnerData.server_nonce != resPQ.server_nonce) throw new WTException("Server Nonce mismatch");
var g_a = BigEndianInteger(serverDHinnerData.g_a);
var dh_prime = BigEndianInteger(serverDHinnerData.dh_prime);
2022-10-04 00:52:44 +02:00
CheckGoodPrime(dh_prime, serverDHinnerData.g);
session.lastSentMsgId = 0;
session.serverTicksOffset = (serverDHinnerData.server_time - localTime).Ticks;
Helpers.Log(1, $"Time offset: {session.serverTicksOffset} | Server: {serverDHinnerData.server_time.TimeOfDay} UTC | Local: {localTime.TimeOfDay} UTC");
2021-08-04 00:40:09 +02:00
//6)
2022-10-04 00:52:44 +02:00
var salt = new byte[256];
RNG.GetBytes(salt);
var b = BigEndianInteger(salt);
2021-08-04 00:40:09 +02:00
var g_b = BigInteger.ModPow(serverDHinnerData.g, b, dh_prime);
2022-10-04 00:52:44 +02:00
CheckGoodGaAndGb(g_a, dh_prime);
CheckGoodGaAndGb(g_b, dh_prime);
var clientDHinnerData = new ClientDHInnerData
2021-08-04 00:40:09 +02:00
{
nonce = nonce,
2021-08-04 00:40:09 +02:00
server_nonce = resPQ.server_nonce,
retry_id = retry_id,
g_b = g_b.ToByteArray(true, true)
};
{
using var clearStream = new MemoryStream(384);
clearStream.Position = 20; // skip SHA1 area (to be patched)
2022-10-04 00:52:44 +02:00
using var writer = new BinaryWriter(clearStream);
writer.WriteTLObject(clientDHinnerData);
int clearLength = (int)clearStream.Length; // length before padding (= 20 + message_data_length)
int paddingToAdd = (0x7FFFFFF0 - clearLength) % 16;
clearStream.SetLength(clearLength + paddingToAdd);
byte[] clearBuffer = clearStream.GetBuffer();
RNG.GetBytes(clearBuffer, clearLength, paddingToAdd);
sha1.ComputeHash(clearBuffer, 20, clearLength - 20).CopyTo(clearBuffer, 0);
encrypted_data = AES_IGE_EncryptDecrypt(clearBuffer.AsSpan(0, clearLength + paddingToAdd), tmp_aes_key, tmp_aes_iv, true);
}
var setClientDHparamsAnswer = await client.SetClientDHParams(clientDHinnerData.nonce, clientDHinnerData.server_nonce, encrypted_data);
2021-08-04 00:40:09 +02:00
//7)
var gab = BigInteger.ModPow(g_a, b, dh_prime);
var authKey = gab.ToByteArray(true, true);
//8)
var authKeyHash = sha1.ComputeHash(authKey);
2021-08-06 01:54:29 +02:00
retry_id = BinaryPrimitives.ReadInt64LittleEndian(authKeyHash); // (auth_key_aux_hash)
//9)
if (setClientDHparamsAnswer is not DhGenOk dhGenOk) throw new WTException("not dh_gen_ok");
if (dhGenOk.nonce != nonce) throw new WTException("Nonce mismatch");
if (dhGenOk.server_nonce != resPQ.server_nonce) throw new WTException("Server Nonce mismatch");
2021-08-04 00:40:09 +02:00
var expected_new_nonceN = new byte[32 + 1 + 8];
pqInnerData.new_nonce.raw.CopyTo(expected_new_nonceN, 0);
2021-08-04 00:40:09 +02:00
expected_new_nonceN[32] = 1;
Array.Copy(authKeyHash, 0, expected_new_nonceN, 33, 8); // (auth_key_aux_hash)
if (!Enumerable.SequenceEqual(dhGenOk.new_nonce_hash1.raw, sha1.ComputeHash(expected_new_nonceN).Skip(4)))
throw new WTException("setClientDHparamsAnswer.new_nonce_hashN mismatch");
2021-08-04 00:40:09 +02:00
session.authKeyID = BinaryPrimitives.ReadInt64LittleEndian(authKeyHash.AsSpan(12));
2021-08-04 00:40:09 +02:00
session.AuthKey = authKey;
session.Salt = BinaryPrimitives.ReadInt64LittleEndian(pqInnerData.new_nonce.raw) ^ BinaryPrimitives.ReadInt64LittleEndian(resPQ.server_nonce.raw);
session.OldSalt = session.Salt;
}
2021-08-04 00:40:09 +02:00
public static (byte[] key, byte[] iv) ConstructTmpAESKeyIV(SHA1 sha1, TL.Int128 server_nonce, Int256 new_nonce)
{
byte[] tmp_aes_key = new byte[32], tmp_aes_iv = new byte[32];
sha1.TransformBlock(new_nonce, 0, 32, null, 0);
sha1.TransformFinalBlock(server_nonce, 0, 16);
sha1.Hash.CopyTo(tmp_aes_key, 0); // tmp_aes_key := SHA1(new_nonce + server_nonce)
sha1.Initialize();
sha1.TransformBlock(server_nonce, 0, 16, null, 0);
sha1.TransformFinalBlock(new_nonce, 0, 32);
Array.Copy(sha1.Hash, 0, tmp_aes_key, 20, 12); // + SHA1(server_nonce, new_nonce)[0:12]
Array.Copy(sha1.Hash, 12, tmp_aes_iv, 0, 8); // tmp_aes_iv != SHA1(server_nonce, new_nonce)[12:8]
sha1.Initialize();
sha1.TransformBlock(new_nonce, 0, 32, null, 0);
sha1.TransformFinalBlock(new_nonce, 0, 32);
sha1.Hash.CopyTo(tmp_aes_iv, 8); // + SHA(new_nonce + new_nonce)
Array.Copy(new_nonce, 0, tmp_aes_iv, 28, 4); // + new_nonce[0:4]
sha1.Initialize();
return (tmp_aes_key, tmp_aes_iv);
2021-08-04 00:40:09 +02:00
}
2022-10-04 00:52:44 +02:00
internal static void CheckGoodPrime(BigInteger p, int g)
2021-08-04 00:40:09 +02:00
{
Helpers.Log(2, "Verifying encryption key safety... (this should happen only once per DC)");
2021-08-04 00:40:09 +02:00
// check that 2^2047 <= p < 2^2048
if (p.GetBitLength() != 2048) throw new WTException("p is not 2048-bit number");
2021-08-04 00:40:09 +02:00
// check that g generates a cyclic subgroup of prime order (p - 1) / 2, i.e. is a quadratic residue mod p.
if (g switch
{
2 => p % 8 != 7,
3 => p % 3 != 2,
4 => false,
2022-10-04 00:52:44 +02:00
5 => (int)(p % 5) is not 1 and not 4,
6 => (int)(p % 24) is not 19 and not 23,
7 => (int)(p % 7) is not 3 and not 5 and not 6,
2021-08-04 00:40:09 +02:00
_ => true,
})
throw new WTException("Bad prime mod 4g");
// check whether p is a safe prime (meaning that both p and (p - 1) / 2 are prime)
2022-10-01 13:56:43 +02:00
if (SafePrimes.Contains(p)) return;
if (!p.IsProbablePrime()) throw new WTException("p is not a prime number");
if (!((p - 1) / 2).IsProbablePrime()) throw new WTException("(p - 1) / 2 is not a prime number");
2022-10-01 13:56:43 +02:00
SafePrimes.Add(p);
}
2024-03-19 11:48:45 +01:00
private static readonly HashSet<BigInteger> SafePrimes = [ new( // C71CAEB9C6B1C904...
[
2022-10-01 13:56:43 +02:00
0x5B, 0xCC, 0x2F, 0xB9, 0xE3, 0xD8, 0x9C, 0x11, 0x03, 0x04, 0xB1, 0x34, 0xF0, 0xAD, 0x4F, 0x6F,
0xBF, 0x54, 0x24, 0x4B, 0xD0, 0x15, 0x4E, 0x2E, 0xEE, 0x05, 0xB1, 0x35, 0xF6, 0x15, 0x81, 0x0D,
0x1F, 0x85, 0x29, 0xE9, 0x0C, 0x85, 0x56, 0xD9, 0x59, 0xF9, 0x7B, 0xF4, 0x49, 0x28, 0xED, 0x0D,
0x05, 0x70, 0xED, 0x5E, 0xFF, 0xA9, 0x7F, 0xF8, 0xA0, 0xBE, 0x3E, 0xE8, 0x15, 0xFC, 0x18, 0xE4,
0xE4, 0x9A, 0x5B, 0xEF, 0x8F, 0x92, 0xA3, 0x9C, 0xFF, 0xD6, 0xB0, 0x65, 0xC4, 0x6B, 0x9C, 0x16,
0x8D, 0x17, 0xB1, 0x2D, 0x58, 0x46, 0xDD, 0xB9, 0xB4, 0x65, 0x59, 0x0D, 0x95, 0xED, 0x17, 0xFD,
0x54, 0x47, 0x28, 0xF1, 0x0E, 0x4E, 0x14, 0xB3, 0x14, 0x2A, 0x4B, 0xA8, 0xD8, 0x74, 0xBA, 0x0D,
0x41, 0x6B, 0x0F, 0x6B, 0xB5, 0x53, 0x27, 0x16, 0x7E, 0x90, 0x51, 0x10, 0x81, 0x95, 0xA6, 0xA4,
0xA4, 0xF9, 0x7C, 0xE6, 0xBE, 0x60, 0x90, 0x3A, 0x4F, 0x3C, 0x8E, 0x37, 0x9B, 0xFA, 0x08, 0x07,
0x88, 0x49, 0xCC, 0xC8, 0x4A, 0x1D, 0xCD, 0x5B, 0x1D, 0x94, 0x2A, 0xBB, 0x96, 0xFE, 0x77, 0x24,
0x64, 0x5F, 0x59, 0x8C, 0xAF, 0x8F, 0xF1, 0x54, 0x84, 0x32, 0x69, 0x29, 0x51, 0x46, 0x97, 0xDC,
0xAB, 0x13, 0x6B, 0x6B, 0xFE, 0xD4, 0x8C, 0xC6, 0x5A, 0x70, 0x58, 0x94, 0xF6, 0x51, 0xFD, 0x20,
0x37, 0x7C, 0xCE, 0x4C, 0xD4, 0xAE, 0x43, 0x95, 0x13, 0x25, 0xC9, 0x0A, 0x6E, 0x6F, 0x33, 0xFA,
0xDB, 0xF4, 0x30, 0x25, 0xD2, 0x93, 0x94, 0x22, 0x58, 0x40, 0xC1, 0xA7, 0x0A, 0x8A, 0x19, 0x48,
0x0F, 0x93, 0x3D, 0x56, 0x37, 0xD0, 0x34, 0x49, 0xC1, 0x21, 0x3E, 0x8E, 0x23, 0x40, 0x0D, 0x98,
0x73, 0x3F, 0xF1, 0x70, 0x2F, 0x52, 0x6C, 0x8E, 0x04, 0xC9, 0xB1, 0xC6, 0xB9, 0xAE, 0x1C, 0xC7, 0x00
2024-03-19 11:48:45 +01:00
])];
2022-10-01 13:56:43 +02:00
2022-10-04 00:52:44 +02:00
internal static void CheckGoodGaAndGb(BigInteger g, BigInteger dh_prime)
{
// check that g, g_a and g_b are greater than 1 and less than dh_prime - 1.
2021-08-04 00:40:09 +02:00
// We recommend checking that g_a and g_b are between 2^{2048-64} and dh_prime - 2^{2048-64} as well.
2022-10-04 00:52:44 +02:00
if (g.GetBitLength() < 2048 - 64 || (dh_prime - g).GetBitLength() < 2048 - 64)
throw new WTException("g^a or g^b is not between 2^{2048-64} and dh_prime - 2^{2048-64}");
2021-08-04 00:40:09 +02:00
}
/// <summary>Load a specific Telegram server public key</summary>
/// <param name="pem">A string starting with <c>-----BEGIN RSA PUBLIC KEY-----</c></param>
2021-08-04 00:40:09 +02:00
public static void LoadPublicKey(string pem)
{
using var rsa = RSA.Create();
using var sha1 = SHA1.Create();
2021-08-04 00:40:09 +02:00
rsa.ImportFromPem(pem);
var rsaParam = rsa.ExportParameters(false);
if (rsaParam.Modulus[0] == 0) rsaParam.Modulus = rsaParam.Modulus[1..];
2021-08-04 00:40:09 +02:00
var publicKey = new RSAPublicKey { n = rsaParam.Modulus, e = rsaParam.Exponent };
var bareData = publicKey.ToBytes();
var fingerprint = BinaryPrimitives.ReadInt64LittleEndian(sha1.ComputeHash(bareData, 4, bareData.Length - 4).AsSpan(12)); // 64 lower-order bits of SHA1
2021-08-04 00:40:09 +02:00
PublicKeys[fingerprint] = publicKey;
2021-08-04 10:11:07 +02:00
Helpers.Log(1, $"Loaded a public key with fingerprint {fingerprint:X}");
2021-08-04 00:40:09 +02:00
}
2022-10-01 13:56:43 +02:00
private static void LoadDefaultPublicKeys()
2021-08-04 00:40:09 +02:00
{
// Production Public Key (D09D1D85DE64FD85)
LoadPublicKey(@"-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEA6LszBcC1LGzyr992NzE0ieY+BSaOW622Aa9Bd4ZHLl+TuFQ4lo4g
5nKaMBwK/BIb9xUfg0Q29/2mgIR6Zr9krM7HjuIcCzFvDtr+L0GQjae9H0pRB2OO
62cECs5HKhT5DZ98K33vmWiLowc621dQuwKWSQKjWf50XYFw42h21P2KXUGyp2y/
+aEyZ+uVgLLQbRA1dEjSDZ2iGRy12Mk5gpYc397aYp438fsJoHIgJ2lgMv5h7WY9
t6N/byY9Nw9p21Og3AoXSL2q/2IJ1WRUhebgAdGVMlV1fkuOQoEzR7EdpqtQD9Cs
5+bfo3Nhmcyvk5ftB0WkJ9z6bNZ7yxrP8wIDAQAB
-----END RSA PUBLIC KEY-----");
// Test Public Key (B25898DF208D2603)
LoadPublicKey(@"-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAyMEdY1aR+sCR3ZSJrtztKTKqigvO/vBfqACJLZtS7QMgCGXJ6XIR
yy7mx66W0/sOFa7/1mAZtEoIokDP3ShoqF4fVNb6XeqgQfaUHd8wJpDWHcR2OFwv
plUUI1PLTktZ9uW2WE23b+ixNwJjJGwBDJPQEQFBE+vfmH0JP503wr5INS1poWg/
j25sIWeYPHYeOrFp/eXaqhISP6G+q2IeTaWTXpwZj4LzXq5YOpk4bYEQ6mvRq7D1
aHWfYmlEGepfaYR8Q0YqvvhYtMte3ITnuSJs171+GDqpdKcSwHnd6FudwGO4pcCO
j4WcDuXc2CTHgH8gFTNhp/Y8/SpDOhvn9QIDAQAB
-----END RSA PUBLIC KEY-----");
2021-08-04 00:40:09 +02:00
}
public static byte[] EncryptDecryptMessage(Span<byte> input, bool encrypt, int x, byte[] authKey, byte[] msgKey, int msgKeyOffset, SHA256 sha256)
{
// first, construct AES key & IV
byte[] aes_key = new byte[32], aes_iv = new byte[32];
2021-09-23 05:37:00 +02:00
sha256.TransformBlock(msgKey, msgKeyOffset, 16, null, 0); // msgKey
sha256.TransformFinalBlock(authKey, x, 36); // authKey[x:36]
2021-08-10 14:40:41 +02:00
var sha256_a = sha256.Hash;
sha256.Initialize();
2021-09-23 05:37:00 +02:00
sha256.TransformBlock(authKey, 40 + x, 36, null, 0); // authKey[40+x:36]
sha256.TransformFinalBlock(msgKey, msgKeyOffset, 16); // msgKey
2021-08-10 14:40:41 +02:00
var sha256_b = sha256.Hash;
sha256.Initialize();
2021-08-10 14:40:41 +02:00
Array.Copy(sha256_a, 0, aes_key, 0, 8);
Array.Copy(sha256_b, 8, aes_key, 8, 16);
Array.Copy(sha256_a, 24, aes_key, 24, 8);
Array.Copy(sha256_b, 0, aes_iv, 0, 8);
Array.Copy(sha256_a, 8, aes_iv, 8, 16);
Array.Copy(sha256_b, 24, aes_iv, 24, 8);
2021-08-04 00:40:09 +02:00
return AES_IGE_EncryptDecrypt(input, aes_key, aes_iv, encrypt);
}
public static byte[] AES_IGE_EncryptDecrypt(Span<byte> input, byte[] aes_key, byte[] aes_iv, bool encrypt)
2021-08-04 00:40:09 +02:00
{
if (input.Length % 16 != 0) throw new WTException("AES_IGE input size not divisible by 16");
2021-08-04 00:40:09 +02:00
2022-02-11 02:43:48 +01:00
using var aesCrypto = encrypt ? AesECB.CreateEncryptor(aes_key, null) : AesECB.CreateDecryptor(aes_key, null);
2022-10-07 03:00:57 +02:00
var output = new byte[input.Length];
var prevBytes = (byte[])aes_iv.Clone();
var span = MemoryMarshal.Cast<byte, long>(input);
var sout = MemoryMarshal.Cast<byte, long>(output);
var prev = MemoryMarshal.Cast<byte, long>(prevBytes);
if (!encrypt) { (prev[2], prev[0]) = (prev[0], prev[2]); (prev[3], prev[1]) = (prev[1], prev[3]); }
for (int i = 0, count = input.Length / 8; i < count;)
2021-08-04 00:40:09 +02:00
{
2022-10-07 03:00:57 +02:00
sout[i] = span[i] ^ prev[0]; sout[i + 1] = span[i + 1] ^ prev[1];
aesCrypto.TransformBlock(output, i * 8, 16, output, i * 8);
prev[0] = sout[i] ^= prev[2]; prev[1] = sout[i + 1] ^= prev[3];
prev[2] = span[i++]; prev[3] = span[i++];
2021-08-04 00:40:09 +02:00
}
return output;
}
2021-08-10 08:25:37 +02:00
#if OBFUSCATION
public sealed class AesCtr(byte[] key, byte[] ivec) : IDisposable
{
readonly ICryptoTransform _encryptor = AesECB.CreateEncryptor(key, null);
2023-07-08 01:34:31 +02:00
readonly byte[] _ecount = new byte[16];
int _num;
2023-07-08 01:34:31 +02:00
public void Dispose() => _encryptor.Dispose();
public void EncryptDecrypt(Span<byte> buffer)
{
for (int i = 0; i < buffer.Length; i++)
{
2023-07-08 01:34:31 +02:00
if (_num == 0)
{
_encryptor.TransformBlock(ivec, 0, 16, _ecount, 0);
for (int n = 15; n >= 0; n--) // increment big-endian counter
if (++ivec[n] != 0) break;
}
2023-07-08 01:34:31 +02:00
buffer[i] ^= _ecount[_num];
_num = (_num + 1) % 16;
}
}
}
// see https://core.telegram.org/mtproto/mtproto-transports#transport-obfuscation
internal static (AesCtr, AesCtr, byte[]) InitObfuscation(byte[] secret, byte protocolId, int dcId)
{
byte[] preamble = new byte[64];
do
RNG.GetBytes(preamble, 0, 58);
while (preamble[0] == 0xef ||
BinaryPrimitives.ReadUInt32LittleEndian(preamble) is 0x44414548 or 0x54534f50 or 0x20544547 or 0x4954504f or 0x02010316 or 0xdddddddd or 0xeeeeeeee ||
BinaryPrimitives.ReadInt32LittleEndian(preamble.AsSpan(4)) == 0);
preamble[62] = preamble[56]; preamble[63] = preamble[57];
preamble[56] = preamble[57] = preamble[58] = preamble[59] = protocolId;
preamble[60] = (byte)dcId; preamble[61] = (byte)(dcId >> 8);
byte[] recvKey = preamble[8..40], recvIV = preamble[40..56];
Array.Reverse(preamble, 8, 48);
byte[] sendKey = preamble[8..40], sendIV = preamble[40..56];
if (secret != null)
{
using var sha256 = SHA256.Create();
sha256.TransformBlock(sendKey, 0, 32, null, 0);
sha256.TransformFinalBlock(secret, 0, 16);
sendKey = sha256.Hash;
sha256.Initialize();
sha256.TransformBlock(recvKey, 0, 32, null, 0);
sha256.TransformFinalBlock(secret, 0, 16);
recvKey = sha256.Hash;
}
2022-01-07 01:14:16 +01:00
var sendCtr = new AesCtr(sendKey, sendIV);
var recvCtr = new AesCtr(recvKey, recvIV);
var encrypted = (byte[])preamble.Clone();
sendCtr.EncryptDecrypt(encrypted);
for (int i = 56; i < 64; i++)
preamble[i] = encrypted[i];
return (sendCtr, recvCtr, preamble);
}
#endif
internal static async Task<InputCheckPasswordSRP> Check2FA(Account_Password accountPassword, Func<Task<string>> getPassword)
2021-08-10 08:25:37 +02:00
{
if (accountPassword.current_algo is not PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow algo)
if (accountPassword.current_algo == null && (algo = accountPassword.new_algo as PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow) != null)
{
int salt1len = algo.salt1.Length;
Array.Resize(ref algo.salt1, salt1len + 32);
RNG.GetBytes(algo.salt1, salt1len, 32);
}
else
throw new WTException("2FA authentication uses an unsupported algo: " + accountPassword.current_algo?.GetType().Name);
2021-08-10 08:25:37 +02:00
var g = new BigInteger(algo.g);
var p = BigEndianInteger(algo.p);
2022-10-04 00:52:44 +02:00
var validTask = Task.Run(() => CheckGoodPrime(p, algo.g));
System.Threading.Thread.Sleep(100);
Helpers.Log(3, $"This account has enabled 2FA. A password is needed. {accountPassword.hint}");
var passwordBytes = Encoding.UTF8.GetBytes(await getPassword());
2021-08-10 08:25:37 +02:00
using var sha256 = SHA256.Create();
sha256.TransformBlock(algo.salt1, 0, algo.salt1.Length, null, 0);
sha256.TransformBlock(passwordBytes, 0, passwordBytes.Length, null, 0);
sha256.TransformFinalBlock(algo.salt1, 0, algo.salt1.Length);
var hash = sha256.Hash;
sha256.Initialize();
sha256.TransformBlock(algo.salt2, 0, algo.salt2.Length, null, 0);
sha256.TransformBlock(hash, 0, 32, null, 0);
sha256.TransformFinalBlock(algo.salt2, 0, algo.salt2.Length);
hash = sha256.Hash;
#if NETCOREAPP2_0_OR_GREATER
using var derive = new Rfc2898DeriveBytes(hash, algo.salt1, 100000, HashAlgorithmName.SHA512);
var pbkdf2 = derive.GetBytes(64);
#else
var pbkdf2 = PBKDF2_SHA512(hash, algo.salt1, 100000, 64);
#endif
sha256.Initialize();
sha256.TransformBlock(algo.salt2, 0, algo.salt2.Length, null, 0);
sha256.TransformBlock(pbkdf2, 0, 64, null, 0);
sha256.TransformFinalBlock(algo.salt2, 0, algo.salt2.Length);
var x = BigEndianInteger(sha256.Hash);
2021-08-10 08:25:37 +02:00
var v = BigInteger.ModPow(g, x, p);
if (accountPassword.current_algo == null) // we're computing a new password
{
await validTask;
return new InputCheckPasswordSRP { A = v.To256Bytes() };
}
var g_b = BigEndianInteger(accountPassword.srp_B);
var g_b_256 = g_b.To256Bytes();
var g_256 = g.To256Bytes();
sha256.Initialize();
sha256.TransformBlock(algo.p, 0, 256, null, 0);
sha256.TransformFinalBlock(g_256, 0, 256);
var k = BigEndianInteger(sha256.Hash);
2021-08-10 08:25:37 +02:00
var k_v = (k * v) % p;
var a = BigEndianInteger(new Int256(RNG).raw);
2021-08-10 08:25:37 +02:00
var g_a = BigInteger.ModPow(g, a, p);
var g_a_256 = g_a.To256Bytes();
sha256.Initialize();
sha256.TransformBlock(g_a_256, 0, 256, null, 0);
sha256.TransformFinalBlock(g_b_256, 0, 256);
var u = BigEndianInteger(sha256.Hash);
2021-08-10 08:25:37 +02:00
var t = (g_b - k_v) % p; //(positive modulo, if the result is negative increment by p)
if (t.Sign < 0) t += p;
var s_a = BigInteger.ModPow(t, a + u * x, p);
sha256.Initialize();
var k_a = sha256.ComputeHash(s_a.To256Bytes());
2021-08-10 08:25:37 +02:00
hash = sha256.ComputeHash(algo.p);
var h2 = sha256.ComputeHash(g_256);
2021-08-10 08:25:37 +02:00
for (int i = 0; i < 32; i++) hash[i] ^= h2[i];
var hs1 = sha256.ComputeHash(algo.salt1);
var hs2 = sha256.ComputeHash(algo.salt2);
sha256.Initialize();
sha256.TransformBlock(hash, 0, 32, null, 0);
sha256.TransformBlock(hs1, 0, 32, null, 0);
sha256.TransformBlock(hs2, 0, 32, null, 0);
sha256.TransformBlock(g_a_256, 0, 256, null, 0);
sha256.TransformBlock(g_b_256, 0, 256, null, 0);
sha256.TransformFinalBlock(k_a, 0, 32);
var m1 = sha256.Hash;
2021-08-10 08:25:37 +02:00
await validTask;
2021-08-10 08:25:37 +02:00
return new InputCheckPasswordSRP { A = g_a_256, M1 = m1, srp_id = accountPassword.srp_id };
}
#if !NETCOREAPP2_0_OR_GREATER
// adapted from https://github.com/dotnet/aspnetcore/blob/main/src/DataProtection/Cryptography.KeyDerivation/src/PBKDF2/ManagedPbkdf2Provider.cs
2021-10-31 02:40:10 +01:00
private static byte[] PBKDF2_SHA512(byte[] password, byte[] salt, int iterationCount, int numBytesRequested)
{
// PBKDF2 is defined in NIST SP800-132, Sec. 5.3: http://csrc.nist.gov/publications/nistpubs/800-132/nist-sp800-132.pdf
byte[] retVal = new byte[numBytesRequested];
int numBytesWritten = 0;
int numBytesRemaining = numBytesRequested;
// For each block index, U_0 := Salt || block_index
byte[] saltWithBlockIndex = new byte[checked(salt.Length + sizeof(uint))];
Buffer.BlockCopy(salt, 0, saltWithBlockIndex, 0, salt.Length);
using var hashAlgorithm = new HMACSHA512(password);
for (uint blockIndex = 1; numBytesRemaining > 0; blockIndex++)
{
// write the block index out as big-endian
saltWithBlockIndex[^4] = (byte)(blockIndex >> 24);
saltWithBlockIndex[^3] = (byte)(blockIndex >> 16);
saltWithBlockIndex[^2] = (byte)(blockIndex >> 8);
saltWithBlockIndex[^1] = (byte)blockIndex;
// U_1 = PRF(U_0) = PRF(Salt || block_index)
// T_blockIndex = U_1
byte[] U_iter = hashAlgorithm.ComputeHash(saltWithBlockIndex); // this is U_1
byte[] T_blockIndex = U_iter;
for (int iter = 1; iter < iterationCount; iter++)
{
U_iter = hashAlgorithm.ComputeHash(U_iter);
for (int j = U_iter.Length - 1; j >= 0; j--)
T_blockIndex[j] ^= U_iter[j];
// At this point, the 'U_iter' variable actually contains U_{iter+1} (due to indexing differences).
}
// At this point, we're done iterating on this block, so copy the transformed block into retVal.
int numBytesToCopy = Math.Min(numBytesRemaining, T_blockIndex.Length);
Buffer.BlockCopy(T_blockIndex, 0, retVal, numBytesWritten, numBytesToCopy);
numBytesWritten += numBytesToCopy;
numBytesRemaining -= numBytesToCopy;
}
return retVal; // retVal := T_1 || T_2 || ... || T_n, where T_n may be truncated to meet the desired output length
}
#endif
2021-08-04 00:40:09 +02:00
}
2022-10-07 03:00:57 +02:00
2024-03-26 02:30:16 +01:00
internal sealed class AES_IGE_Stream : Helpers.IndirectStream
2022-10-07 03:00:57 +02:00
{
2023-07-08 01:34:31 +02:00
private readonly ICryptoTransform _aesCrypto;
private readonly byte[] _prevBytes;
2022-10-07 03:00:57 +02:00
public AES_IGE_Stream(Stream stream, long size, byte[] key, byte[] iv) : this(stream, key, iv, false) { ContentLength = size; }
public AES_IGE_Stream(Stream stream, byte[] key, byte[] iv, bool encrypt) : base(stream)
2022-10-07 03:00:57 +02:00
{
2023-07-08 01:34:31 +02:00
_aesCrypto = encrypt ? Encryption.AesECB.CreateEncryptor(key, null) : Encryption.AesECB.CreateDecryptor(key, null);
if (encrypt) _prevBytes = (byte[])iv.Clone();
else { _prevBytes = new byte[32]; Array.Copy(iv, 0, _prevBytes, 16, 16); Array.Copy(iv, 16, _prevBytes, 0, 16); }
2022-10-07 03:00:57 +02:00
}
public override long Length => base.Length + 15 & ~15;
public override bool CanSeek => false;
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
2022-10-07 03:00:57 +02:00
public override int Read(byte[] buffer, int offset, int count)
{
count = _innerStream.Read(buffer, offset, count);
if (count == 0) return 0;
Process(buffer, offset, count);
if (ContentLength.HasValue && _innerStream.Position == _innerStream.Length)
return count - (int)(_innerStream.Position - ContentLength.Value);
return count + 15 & ~15;
2022-10-07 03:00:57 +02:00
}
public override void Write(byte[] buffer, int offset, int count)
{
Process(buffer, offset, count);
if (ContentLength.HasValue && _innerStream.Position + count > ContentLength)
count -= (int)(_innerStream.Position + count - ContentLength.Value);
_innerStream.Write(buffer, offset, count);
}
public void Process(byte[] buffer, int offset, int count)
{
count = count + 15 & ~15;
2022-10-07 03:00:57 +02:00
var span = MemoryMarshal.Cast<byte, long>(buffer.AsSpan(offset, count));
2023-07-08 01:34:31 +02:00
var prev = MemoryMarshal.Cast<byte, long>(_prevBytes);
2022-10-07 03:00:57 +02:00
for (offset = 0, count /= 8; offset < count;)
{
prev[0] ^= span[offset]; prev[1] ^= span[offset + 1];
2023-07-08 01:34:31 +02:00
_aesCrypto.TransformBlock(_prevBytes, 0, 16, _prevBytes, 0);
2022-10-07 03:00:57 +02:00
prev[0] ^= prev[2]; prev[1] ^= prev[3];
prev[2] = span[offset]; prev[3] = span[offset + 1];
span[offset++] = prev[0]; span[offset++] = prev[1];
}
}
}
2021-08-04 00:40:09 +02:00
}