Line data Source code
1 : /* SPDX-License-Identifier: GPL-3.0-or-later */
2 : /* Copyright 2026 Peter Csaszar */
3 :
4 : /**
5 : * @file auth_2fa.c
6 : * @brief account.getPassword + SRP-based auth.checkPassword (P3-03).
7 : *
8 : * SRP spec: https://core.telegram.org/api/srp
9 : */
10 :
11 : #include "infrastructure/auth_2fa.h"
12 :
13 : #include "tl_serial.h"
14 : #include "tl_registry.h"
15 : #include "tl_skip.h"
16 : #include "mtproto_rpc.h"
17 : #include "api_call.h"
18 : #include "crypto.h"
19 : #include "logger.h"
20 : #include "raii.h"
21 :
22 : #include <stdio.h>
23 : #include <stdlib.h>
24 : #include <string.h>
25 :
26 : /* ---- TL CRCs ---- */
27 : #define CRC_account_getPassword 0x548a30f5u
28 : #define CRC_auth_checkPassword 0xd18b4d16u
29 : #define CRC_inputCheckPasswordSRP TL_inputCheckPasswordSRP
30 : #define CRC_account_password TL_account_password
31 : #define CRC_KdfAlgoPBKDF2 \
32 : TL_passwordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow
33 : #define CRC_KdfAlgoUnknown TL_passwordKdfAlgoUnknown
34 : #define CRC_SecureKdfUnknown 0x4a8537u
35 : #define CRC_securePasswordKdfAlgoPBKDF2 0xbbf2dda0u
36 : #define CRC_securePasswordKdfAlgoSHA512 0x86471d92u
37 : #define CRC_securePasswordKdfAlgoUnknown 0x004a8537u
38 :
39 : /* ---- helpers ---- */
40 :
41 9 : static void bytes_xor_eq(uint8_t *dst, const uint8_t *src, size_t n) {
42 297 : for (size_t i = 0; i < n; i++) dst[i] ^= src[i];
43 9 : }
44 :
45 : /* H(salt | data | salt) — "SH" in the Telegram spec. */
46 27 : static void sh_sha256(const unsigned char *salt, size_t salt_len,
47 : const unsigned char *data, size_t data_len,
48 : unsigned char out[32]) {
49 27 : size_t total = salt_len * 2 + data_len;
50 27 : RAII_STRING uint8_t *buf = (uint8_t *)malloc(total);
51 27 : if (!buf) { memset(out, 0, 32); return; }
52 27 : memcpy(buf, salt, salt_len);
53 27 : memcpy(buf + salt_len, data, data_len);
54 27 : memcpy(buf + salt_len + data_len, salt, salt_len);
55 27 : crypto_sha256(buf, total, out);
56 : }
57 :
58 : /* ---- account.getPassword ---- */
59 :
60 5 : static int parse_password_kdf_algo(TlReader *r, Account2faPassword *out) {
61 5 : if (!tl_reader_ok(r) || r->len - r->pos < 4) return -1;
62 5 : uint32_t crc = tl_read_uint32(r);
63 5 : if (crc == CRC_KdfAlgoUnknown) {
64 0 : logger_log(LOG_WARN, "auth_2fa: server uses unknown KDF algo");
65 0 : return -1;
66 : }
67 5 : if (crc != CRC_KdfAlgoPBKDF2) {
68 0 : logger_log(LOG_ERROR, "auth_2fa: unsupported KDF algo 0x%08x", crc);
69 0 : return -1;
70 : }
71 5 : size_t s1 = 0, s2 = 0;
72 10 : RAII_STRING uint8_t *salt1 = tl_read_bytes(r, &s1);
73 10 : RAII_STRING uint8_t *salt2 = tl_read_bytes(r, &s2);
74 5 : if ((!salt1 && s1) || (!salt2 && s2)) return -1;
75 5 : if (s1 > SRP_SALT_MAX || s2 > SRP_SALT_MAX) {
76 0 : logger_log(LOG_ERROR, "auth_2fa: salt too large (%zu / %zu)", s1, s2);
77 0 : return -1;
78 : }
79 5 : if (r->len - r->pos < 4) return -1;
80 5 : int32_t g = tl_read_int32(r);
81 5 : size_t p_len = 0;
82 10 : RAII_STRING uint8_t *p = tl_read_bytes(r, &p_len);
83 5 : if ((!p && p_len) || p_len != SRP_PRIME_LEN) {
84 0 : logger_log(LOG_ERROR, "auth_2fa: unexpected prime length %zu", p_len);
85 0 : return -1;
86 : }
87 5 : if (out) {
88 5 : if (salt1) memcpy(out->salt1, salt1, s1);
89 5 : if (salt2) memcpy(out->salt2, salt2, s2);
90 5 : out->salt1_len = s1; out->salt2_len = s2;
91 5 : out->g = g;
92 5 : memcpy(out->p, p, p_len);
93 : }
94 5 : return 0;
95 : }
96 :
97 6 : int auth_2fa_get_password(const ApiConfig *cfg,
98 : MtProtoSession *s, Transport *t,
99 : Account2faPassword *out, RpcError *err) {
100 6 : if (!cfg || !s || !t || !out) return -1;
101 6 : memset(out, 0, sizeof(*out));
102 :
103 : uint8_t query[8];
104 6 : TlWriter w; tl_writer_init(&w);
105 6 : tl_write_uint32(&w, CRC_account_getPassword);
106 6 : size_t qlen = w.len;
107 6 : memcpy(query, w.data, qlen);
108 6 : tl_writer_free(&w);
109 :
110 6 : uint8_t resp[2048]; size_t resp_len = 0;
111 6 : if (api_call(cfg, s, t, query, qlen, resp, sizeof(resp), &resp_len) != 0) {
112 0 : logger_log(LOG_ERROR, "auth_2fa: account.getPassword api_call failed");
113 0 : return -1;
114 : }
115 6 : if (resp_len < 4) return -1;
116 :
117 : uint32_t top;
118 6 : memcpy(&top, resp, 4);
119 6 : if (top == TL_rpc_error) {
120 0 : if (err) rpc_parse_error(resp, resp_len, err);
121 0 : return -1;
122 : }
123 6 : if (top != CRC_account_password) {
124 0 : logger_log(LOG_ERROR, "auth_2fa: unexpected top 0x%08x", top);
125 0 : return -1;
126 : }
127 :
128 : /* account.password#957b50fb flags:#
129 : * has_recovery:flags.0?true
130 : * has_secure_values:flags.1?true
131 : * has_password:flags.2?true
132 : * current_algo:flags.2?PasswordKdfAlgo
133 : * srp_B:flags.2?bytes
134 : * srp_id:flags.2?long
135 : * hint:flags.3?string
136 : * email_unconfirmed_pattern:flags.4?string
137 : * new_algo:PasswordKdfAlgo
138 : * new_secure_algo:SecurePasswordKdfAlgo
139 : * secure_random:bytes
140 : * pending_reset_date:flags.5?int
141 : * login_email_pattern:flags.6?string
142 : */
143 6 : TlReader r = tl_reader_init(resp, resp_len);
144 6 : tl_read_uint32(&r); /* top */
145 6 : uint32_t flags = tl_read_uint32(&r);
146 :
147 6 : out->has_password = (flags & (1u << 2)) ? 1 : 0;
148 :
149 6 : if (out->has_password) {
150 5 : if (parse_password_kdf_algo(&r, out) != 0) return -1;
151 :
152 5 : size_t srpB_len = 0;
153 10 : RAII_STRING uint8_t *srpB = tl_read_bytes(&r, &srpB_len);
154 5 : if (!srpB || srpB_len == 0 || srpB_len > SRP_PRIME_LEN) {
155 0 : logger_log(LOG_ERROR, "auth_2fa: bad srp_B length %zu", srpB_len);
156 0 : return -1;
157 : }
158 5 : memset(out->srp_B, 0, SRP_PRIME_LEN);
159 5 : memcpy(out->srp_B + (SRP_PRIME_LEN - srpB_len), srpB, srpB_len);
160 :
161 5 : if (r.len - r.pos < 8) return -1;
162 5 : out->srp_id = tl_read_int64(&r);
163 : }
164 6 : if (flags & (1u << 3)) { if (tl_skip_string(&r) != 0) return -1; }
165 6 : if (flags & (1u << 4)) { if (tl_skip_string(&r) != 0) return -1; }
166 :
167 : /* new_algo and new_secure_algo are not used during check; skip/ignore. */
168 : /* We don't strictly need to parse the rest to drive checkPassword. */
169 6 : return 0;
170 : }
171 :
172 : /* ---- SRP math ---- */
173 :
174 : /* Compute x = PH2(password, salt1, salt2). Result is 32 bytes (SHA-256). */
175 9 : static int compute_x(const char *password,
176 : const uint8_t *salt1, size_t s1,
177 : const uint8_t *salt2, size_t s2,
178 : uint8_t out_x[32]) {
179 9 : size_t plen = strlen(password);
180 :
181 : /* PH1 = SH(SH(password, salt1), salt2). */
182 : uint8_t inner[32];
183 9 : sh_sha256(salt1, s1, (const uint8_t *)password, plen, inner);
184 : uint8_t ph1[32];
185 9 : sh_sha256(salt2, s2, inner, 32, ph1);
186 :
187 : /* PH2 = SH(pbkdf2(ph1, salt1, 100000), salt2). */
188 : uint8_t pbkdf2_out[64];
189 9 : if (crypto_pbkdf2_hmac_sha512(ph1, 32, salt1, s1,
190 : 100000, pbkdf2_out, 64) != 0)
191 0 : return -1;
192 9 : sh_sha256(salt2, s2, pbkdf2_out, 64, out_x);
193 9 : return 0;
194 : }
195 :
196 : /* Pack g (int) into left-padded 256-byte big-endian representation. */
197 9 : static void pack_g(int32_t g, uint8_t out[SRP_PRIME_LEN]) {
198 9 : memset(out, 0, SRP_PRIME_LEN);
199 9 : uint32_t gu = (uint32_t)g;
200 9 : out[SRP_PRIME_LEN - 4] = (uint8_t)(gu >> 24);
201 9 : out[SRP_PRIME_LEN - 3] = (uint8_t)(gu >> 16);
202 9 : out[SRP_PRIME_LEN - 2] = (uint8_t)(gu >> 8);
203 9 : out[SRP_PRIME_LEN - 1] = (uint8_t)(gu );
204 9 : }
205 :
206 : /* Compute the SRP proof. Writes A[256] and M1[32] on success.
207 : * When @p a_in is non-NULL the caller pins the 256-byte client private
208 : * exponent (used by functional tests). Otherwise we pull from
209 : * crypto_rand_bytes. */
210 9 : static int srp_compute(const Account2faPassword *p, const char *password,
211 : const uint8_t *a_in,
212 : uint8_t A_out[SRP_PRIME_LEN], uint8_t M1_out[32]) {
213 9 : uint8_t g_bytes[SRP_PRIME_LEN]; pack_g(p->g, g_bytes);
214 :
215 : uint8_t a[SRP_PRIME_LEN];
216 9 : if (a_in) {
217 5 : memcpy(a, a_in, SRP_PRIME_LEN);
218 4 : } else if (crypto_rand_bytes(a, SRP_PRIME_LEN) != 0) {
219 0 : return -1;
220 : }
221 :
222 9 : CryptoBnCtx *ctx = crypto_bn_ctx_new();
223 9 : if (!ctx) return -1;
224 :
225 : /* A = g^a mod p */
226 9 : size_t A_len = SRP_PRIME_LEN;
227 9 : if (crypto_bn_mod_exp(A_out, &A_len, g_bytes, SRP_PRIME_LEN,
228 9 : a, SRP_PRIME_LEN, p->p, SRP_PRIME_LEN, ctx) != 0)
229 0 : goto fail;
230 :
231 : /* x = PH2(password, salt1, salt2) */
232 : uint8_t x[32];
233 9 : if (compute_x(password, p->salt1, p->salt1_len,
234 9 : p->salt2, p->salt2_len, x) != 0) goto fail;
235 :
236 : /* v = g^x mod p */
237 9 : uint8_t v[SRP_PRIME_LEN]; size_t v_len = SRP_PRIME_LEN;
238 9 : if (crypto_bn_mod_exp(v, &v_len, g_bytes, SRP_PRIME_LEN,
239 9 : x, 32, p->p, SRP_PRIME_LEN, ctx) != 0) goto fail;
240 :
241 : /* k = H(p | g) */
242 : uint8_t kbuf[SRP_PRIME_LEN * 2]; uint8_t k[32];
243 9 : memcpy(kbuf, p->p, SRP_PRIME_LEN);
244 9 : memcpy(kbuf + SRP_PRIME_LEN, g_bytes, SRP_PRIME_LEN);
245 9 : crypto_sha256(kbuf, sizeof(kbuf), k);
246 :
247 : /* u = H(A | B) */
248 : uint8_t ubuf[SRP_PRIME_LEN * 2]; uint8_t u[32];
249 9 : memcpy(ubuf, A_out, SRP_PRIME_LEN);
250 9 : memcpy(ubuf + SRP_PRIME_LEN, p->srp_B, SRP_PRIME_LEN);
251 9 : crypto_sha256(ubuf, sizeof(ubuf), u);
252 :
253 : /* kv = k*v mod p */
254 9 : uint8_t kv[SRP_PRIME_LEN]; size_t kv_len = SRP_PRIME_LEN;
255 9 : if (crypto_bn_mod_mul(kv, &kv_len, k, 32, v, SRP_PRIME_LEN,
256 9 : p->p, SRP_PRIME_LEN, ctx) != 0) goto fail;
257 :
258 : /* base = (B - kv) mod p */
259 9 : uint8_t base[SRP_PRIME_LEN]; size_t base_len = SRP_PRIME_LEN;
260 9 : if (crypto_bn_mod_sub(base, &base_len, p->srp_B, SRP_PRIME_LEN,
261 : kv, SRP_PRIME_LEN,
262 9 : p->p, SRP_PRIME_LEN, ctx) != 0) goto fail;
263 :
264 : /* ux = u*x (not reduced mod p — exponent can exceed p).
265 : * We want a + u*x as a raw integer for modular exponentiation, so
266 : * compute it modulo (p - 1) is NOT required; OpenSSL BN_mod_exp
267 : * handles arbitrary exponents. We just need the correct value. */
268 : /* Use mod_mul with modulus = p as an approximation? No — a + u*x
269 : * mod p would change the result. Instead reduce via mod (p-1) per
270 : * Fermat, but Telegram's server expects the exponent itself. The
271 : * standard trick: compute S = (B - k*v)^(a + u*x) mod p by
272 : * chaining BN_mul + BN_add with a regular context, not modular.
273 : *
274 : * We implement that directly here using two temp BN ops: first
275 : * (u*x) without modular reduction, then (a + ux). We expose an
276 : * internal helper below. */
277 : /* For simplicity, compute S via two BN_mod_exp steps using
278 : * mathematical identity:
279 : * S = pow(base, a) * pow(base, u*x) mod p
280 : * = pow(base, a) mod p * pow(pow(base, x), u) mod p mod p
281 : *
282 : * This avoids dealing with the full 256*256 = 512-byte product. */
283 9 : uint8_t base_a[SRP_PRIME_LEN]; size_t ba_len = SRP_PRIME_LEN;
284 9 : if (crypto_bn_mod_exp(base_a, &ba_len, base, SRP_PRIME_LEN,
285 9 : a, SRP_PRIME_LEN, p->p, SRP_PRIME_LEN, ctx) != 0)
286 0 : goto fail;
287 :
288 9 : uint8_t base_x[SRP_PRIME_LEN]; size_t bx_len = SRP_PRIME_LEN;
289 9 : if (crypto_bn_mod_exp(base_x, &bx_len, base, SRP_PRIME_LEN,
290 9 : x, 32, p->p, SRP_PRIME_LEN, ctx) != 0) goto fail;
291 :
292 9 : uint8_t base_xu[SRP_PRIME_LEN]; size_t bxu_len = SRP_PRIME_LEN;
293 9 : if (crypto_bn_mod_exp(base_xu, &bxu_len, base_x, SRP_PRIME_LEN,
294 9 : u, 32, p->p, SRP_PRIME_LEN, ctx) != 0) goto fail;
295 :
296 9 : uint8_t S[SRP_PRIME_LEN]; size_t S_len = SRP_PRIME_LEN;
297 9 : if (crypto_bn_mod_mul(S, &S_len, base_a, SRP_PRIME_LEN,
298 : base_xu, SRP_PRIME_LEN,
299 9 : p->p, SRP_PRIME_LEN, ctx) != 0) goto fail;
300 :
301 : /* K = H(S) */
302 : uint8_t K[32];
303 9 : crypto_sha256(S, SRP_PRIME_LEN, K);
304 :
305 : /* M1 = H(H(p) XOR H(g) | H(salt1) | H(salt2) | A | B | K) */
306 : uint8_t h_p[32], h_g[32];
307 9 : crypto_sha256(p->p, SRP_PRIME_LEN, h_p);
308 9 : crypto_sha256(g_bytes, SRP_PRIME_LEN, h_g);
309 9 : bytes_xor_eq(h_p, h_g, 32);
310 :
311 : uint8_t h_s1[32], h_s2[32];
312 9 : crypto_sha256(p->salt1, p->salt1_len, h_s1);
313 9 : crypto_sha256(p->salt2, p->salt2_len, h_s2);
314 :
315 : uint8_t m1_buf[32 + 32 + 32 + SRP_PRIME_LEN + SRP_PRIME_LEN + 32];
316 9 : size_t off = 0;
317 9 : memcpy(m1_buf + off, h_p, 32); off += 32;
318 9 : memcpy(m1_buf + off, h_s1, 32); off += 32;
319 9 : memcpy(m1_buf + off, h_s2, 32); off += 32;
320 9 : memcpy(m1_buf + off, A_out, SRP_PRIME_LEN); off += SRP_PRIME_LEN;
321 9 : memcpy(m1_buf + off, p->srp_B, SRP_PRIME_LEN); off += SRP_PRIME_LEN;
322 9 : memcpy(m1_buf + off, K, 32); off += 32;
323 9 : crypto_sha256(m1_buf, off, M1_out);
324 :
325 9 : crypto_bn_ctx_free(ctx);
326 9 : return 0;
327 :
328 0 : fail:
329 0 : crypto_bn_ctx_free(ctx);
330 0 : return -1;
331 : }
332 :
333 : /* ---- auth.checkPassword ---- */
334 :
335 5 : int auth_2fa_check_password(const ApiConfig *cfg,
336 : MtProtoSession *s, Transport *t,
337 : const Account2faPassword *params,
338 : const char *password,
339 : int64_t *user_id_out, RpcError *err) {
340 5 : if (!cfg || !s || !t || !params || !password) return -1;
341 5 : if (!params->has_password) {
342 1 : logger_log(LOG_ERROR, "auth_2fa: no password configured on account");
343 1 : return -1;
344 : }
345 :
346 : uint8_t A[SRP_PRIME_LEN], M1[32];
347 4 : if (srp_compute(params, password, NULL, A, M1) != 0) return -1;
348 :
349 4 : TlWriter w; tl_writer_init(&w);
350 4 : tl_write_uint32(&w, CRC_auth_checkPassword);
351 4 : tl_write_uint32(&w, CRC_inputCheckPasswordSRP);
352 4 : tl_write_int64 (&w, params->srp_id);
353 4 : tl_write_bytes (&w, A, SRP_PRIME_LEN);
354 4 : tl_write_bytes (&w, M1, 32);
355 :
356 : uint8_t query[512];
357 4 : if (w.len > sizeof(query)) { tl_writer_free(&w); return -1; }
358 4 : memcpy(query, w.data, w.len);
359 4 : size_t qlen = w.len;
360 4 : tl_writer_free(&w);
361 :
362 4 : uint8_t resp[2048]; size_t resp_len = 0;
363 4 : if (api_call(cfg, s, t, query, qlen, resp, sizeof(resp), &resp_len) != 0) {
364 0 : logger_log(LOG_ERROR, "auth_2fa: auth.checkPassword api_call failed");
365 0 : return -1;
366 : }
367 4 : if (resp_len < 4) return -1;
368 :
369 : uint32_t top;
370 4 : memcpy(&top, resp, 4);
371 4 : if (top == TL_rpc_error) {
372 3 : if (err) rpc_parse_error(resp, resp_len, err);
373 3 : return -1;
374 : }
375 1 : if (top != TL_auth_authorization) {
376 0 : logger_log(LOG_ERROR, "auth_2fa: unexpected top 0x%08x", top);
377 0 : return -1;
378 : }
379 :
380 : /* auth.authorization — extract user.id (flags.0=setup_password_required). */
381 1 : TlReader r = tl_reader_init(resp, resp_len);
382 1 : tl_read_uint32(&r); /* top */
383 1 : uint32_t flags = tl_read_uint32(&r);
384 1 : if (flags & (1u << 1)) { if (r.len - r.pos < 4) return -1; tl_read_int32(&r); }
385 :
386 1 : uint32_t user_crc = tl_read_uint32(&r);
387 1 : if (user_crc == TL_user || user_crc == TL_userFull) {
388 1 : tl_read_uint32(&r); /* user flags */
389 1 : if (r.len - r.pos >= 8) {
390 1 : int64_t uid = tl_read_int64(&r);
391 1 : if (user_id_out) *user_id_out = uid;
392 : }
393 0 : } else if (user_id_out) {
394 0 : *user_id_out = 0;
395 : }
396 1 : return 0;
397 : }
398 :
399 8 : int auth_2fa_srp_compute(const Account2faPassword *params,
400 : const char *password,
401 : const unsigned char *a_priv_in,
402 : unsigned char A_out[SRP_PRIME_LEN],
403 : unsigned char M1_out[32]) {
404 8 : if (!params || !password || !A_out || !M1_out) return -1;
405 6 : if (!params->has_password) return -1;
406 5 : return srp_compute(params, password, a_priv_in, A_out, M1_out);
407 : }
|