From ba90b47831b54addd68de3d27f4e4cebe91d5142 Mon Sep 17 00:00:00 2001 From: Wizou Date: Tue, 10 Aug 2021 08:25:37 +0200 Subject: [PATCH] Added 2FA support --- README.md | 7 +++--- src/Client.cs | 7 ++++-- src/Encryption.cs | 61 +++++++++++++++++++++++++++++++++++++++++++++++ src/Helpers.cs | 9 +++++++ src/Session.cs | 2 +- 5 files changed, 80 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 6f673b9..ad1e942 100644 --- a/README.md +++ b/README.md @@ -45,8 +45,9 @@ static string Config(string what) if (what == "api_hash") return "YOUR_API_HASH"; if (what == "phone_number") return "+12025550156"; if (what == "verification_code") { Console.Write("Code: "); return Console.ReadLine(); } - if (what == "first_name") return "John"; // if sign-up is required - if (what == "last_name") return "Doe"; // if sign-up is required + if (what == "first_name") return "John"; // if sign-up is required + if (what == "last_name") return "Doe"; // if sign-up is required + if (what == "password") return "secret!"; // if user has enabled 2FA return null; } ... @@ -116,6 +117,6 @@ Here are the main expected developments: - [x] Convert API functions classes to real methods and serialize structures without using Reflection - [ ] Separate task/thread for reading/handling update messages independently from CallAsync - [ ] Support MTProto 2.0 -- [ ] Support users with 2FA enabled +- [x] Support users with 2FA enabled - [ ] Support secret chats end-to-end encryption & PFS - [ ] Support all service messages diff --git a/src/Client.cs b/src/Client.cs index da4a1d1..5824ee6 100644 --- a/src/Client.cs +++ b/src/Client.cs @@ -411,9 +411,12 @@ namespace WTelegram { authorization = await Auth_SignIn(phone_number, sentCode.phone_code_hash, verification_code); } - catch (RpcException e) when (e.Code == 400 && e.Message == "SESSION_PASSWORD_NEEDED") + catch (RpcException e) when (e.Code == 401 && e.Message == "SESSION_PASSWORD_NEEDED") { - throw new NotImplementedException("Library does not support 2FA yet"); //TODO: support 2FA + var accountPassword = await Account_GetPassword(); + Helpers.Log(3, $"This account has enabled 2FA. A password is needed. {accountPassword.hint}"); + var checkPasswordSRP = Check2FA(accountPassword, Config("password")); + authorization = await Auth_CheckPassword(checkPasswordSRP); } if (authorization is Auth_AuthorizationSignUpRequired signUpRequired) { diff --git a/src/Encryption.cs b/src/Encryption.cs index d835972..d1ed155 100644 --- a/src/Encryption.cs +++ b/src/Encryption.cs @@ -257,5 +257,66 @@ Slv8kg9qv1m6XHVQY3PnEw+QQtqSIXklHwIDAQAB } return output; } + + internal static InputCheckPasswordSRPBase Check2FA(Account_Password accountPassword, string password) + { + if (accountPassword.current_algo is not PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow algo) + throw new ApplicationException("2FA authentication uses an unsupported algo: " + accountPassword.current_algo?.GetType().Name); + + var passwordBytes = Encoding.UTF8.GetBytes(password); + var g = new BigInteger(algo.g); + var p = new BigInteger(algo.p, true, true); + var g_b = new BigInteger(accountPassword.srp_B, true, true); + var g_b_256 = g_b.To256Bytes(); + var g_256 = g.To256Bytes(); + ValidityChecks(p, algo.g); + + 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.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; + var pbkdf2 = new Rfc2898DeriveBytes(hash, algo.salt1, 100000, HashAlgorithmName.SHA512).GetBytes(64); + 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 = new BigInteger(sha256.Hash, true, true); + + sha256.TransformBlock(algo.p, 0, 256, null, 0); + sha256.TransformFinalBlock(g_256, 0, 256); + var k = new BigInteger(sha256.Hash, true, true); + + var v = BigInteger.ModPow(g, x, p); + var k_v = (k * v) % p; + var a = new BigInteger(new Int256(RNG).raw, true, true); + var g_a = BigInteger.ModPow(g, a, p); + var g_a_256 = g_a.To256Bytes(); + + sha256.TransformBlock(g_a_256, 0, 256, null, 0); + sha256.TransformFinalBlock(g_b_256, 0, 256); + var u = new BigInteger(sha256.Hash, true, true); + + 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); + var k_a = SHA256.HashData(s_a.To256Bytes()); + + hash = SHA256.HashData(algo.p); + var h2 = SHA256.HashData(g_256); + for (int i = 0; i < 32; i++) hash[i] ^= h2[i]; + sha256.TransformBlock(hash, 0, 32, null, 0); + sha256.TransformBlock(SHA256.HashData(algo.salt1), 0, 32, null, 0); + sha256.TransformBlock(SHA256.HashData(algo.salt2), 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; + + return new InputCheckPasswordSRP { A = g_a_256, M1 = m1, srp_id = accountPassword.srp_id }; + } } } diff --git a/src/Helpers.cs b/src/Helpers.cs index a39348c..27592e5 100644 --- a/src/Helpers.cs +++ b/src/Helpers.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Numerics; namespace WTelegram { @@ -128,5 +129,13 @@ namespace WTelegram } } } + + internal static byte[] To256Bytes(this BigInteger bi) + { + var result = new byte[256]; + var bigEndian = bi.ToByteArray(true, true); + bigEndian.CopyTo(result, 256 - bigEndian.Length); + return result; + } } } diff --git a/src/Session.cs b/src/Session.cs index 18fc52a..ea24ac5 100644 --- a/src/Session.cs +++ b/src/Session.cs @@ -58,7 +58,7 @@ namespace WTelegram { var utf8Json = JsonSerializer.SerializeToUtf8Bytes(this, Helpers.JsonOptions); var finalBlock = new byte[16]; - var output = new byte[(16 + 32 + utf8Json.Length + 15) & ~15]; + var output = new byte[(16 + 32 + utf8Json.Length + 16) & ~15]; Encryption.RNG.GetBytes(output, 0, 16); using var aes = Aes.Create(); using var encryptor = aes.CreateEncryptor(_apiHash, output[0..16]);