LCOV - code coverage report
Current view: top level - src/infrastructure - auth_2fa.c (source / functions) Coverage Total Hit
Test: coverage-functional.info Lines: 85.6 % 201 172
Test Date: 2026-04-20 19:54:24 Functions: 100.0 % 9 9

            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              : }
        

Generated by: LCOV version 2.0-1