LCOV - code coverage report
Current view: top level - tests/unit - test_auth_2fa.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 100.0 % 235 235
Test Date: 2026-04-20 19:54:22 Functions: 100.0 % 18 18

            Line data    Source code
       1              : /**
       2              :  * @file test_auth_2fa.c
       3              :  * @brief Unit tests for P3-03 2FA login (account.getPassword +
       4              :  *        auth.checkPassword SRP proof).
       5              :  *
       6              :  * Uses the mock crypto/socket backends. SRP math is only exercised for
       7              :  * call-count / argument-shape correctness here — known-answer verification
       8              :  * against OpenSSL lives in tests/functional/test_srp_functional.c.
       9              :  */
      10              : 
      11              : #include "test_helpers.h"
      12              : #include "infrastructure/auth_2fa.h"
      13              : #include "tl_serial.h"
      14              : #include "tl_registry.h"
      15              : #include "mock_socket.h"
      16              : #include "mock_crypto.h"
      17              : #include "mtproto_session.h"
      18              : #include "transport.h"
      19              : #include "api_call.h"
      20              : 
      21              : #include <stdlib.h>
      22              : #include <string.h>
      23              : 
      24            7 : static void build_fake_encrypted_response(const uint8_t *payload, size_t plen,
      25              :                                           uint8_t *out, size_t *out_len) {
      26            7 :     TlWriter w; tl_writer_init(&w);
      27            7 :     uint8_t zeros24[24] = {0}; tl_write_raw(&w, zeros24, 24);
      28            7 :     uint8_t header[32] = {0};
      29            7 :     uint32_t plen32 = (uint32_t)plen;
      30            7 :     memcpy(header + 28, &plen32, 4);
      31            7 :     tl_write_raw(&w, header, 32);
      32            7 :     tl_write_raw(&w, payload, plen);
      33            7 :     size_t enc = w.len - 24;
      34            7 :     if (enc % 16 != 0) {
      35            6 :         uint8_t pad[16] = {0}; tl_write_raw(&w, pad, 16 - (enc % 16));
      36              :     }
      37              :     /* abridged transport framing: length in dwords. If < 0x7F we emit one
      38              :      * byte; otherwise 0x7F + 3-byte LE dword count (matches transport.c). */
      39            7 :     size_t dwords = w.len / 4;
      40            7 :     size_t off = 0;
      41            7 :     if (dwords < 0x7F) {
      42            4 :         out[0] = (uint8_t)dwords;
      43            4 :         off = 1;
      44              :     } else {
      45            3 :         out[0] = 0x7F;
      46            3 :         out[1] = (uint8_t)(dwords);
      47            3 :         out[2] = (uint8_t)(dwords >> 8);
      48            3 :         out[3] = (uint8_t)(dwords >> 16);
      49            3 :         off = 4;
      50              :     }
      51            7 :     memcpy(out + off, w.data, w.len);
      52            7 :     *out_len = off + w.len;
      53            7 :     tl_writer_free(&w);
      54            7 : }
      55              : 
      56            8 : static void fix_session(MtProtoSession *s) {
      57            8 :     mtproto_session_init(s);
      58            8 :     s->session_id = 0; /* match the zero session_id in fake encrypted frames */
      59            8 :     uint8_t k[256] = {0}; mtproto_session_set_auth_key(s, k);
      60            8 :     mtproto_session_set_salt(s, 0xBADCAFEDEADBEEFULL);
      61            8 : }
      62            8 : static void fix_transport(Transport *t) {
      63            8 :     transport_init(t); t->fd = 42; t->connected = 1; t->dc_id = 1;
      64            8 : }
      65            8 : static void fix_cfg(ApiConfig *cfg) {
      66            8 :     api_config_init(cfg); cfg->api_id = 12345; cfg->api_hash = "deadbeef";
      67            8 : }
      68              : 
      69              : /* Build an account.password payload with has_password set. */
      70              : #define CRC_account_password TL_account_password
      71              : #define CRC_KdfAlgo TL_passwordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow
      72            2 : static size_t make_account_password(uint8_t *buf, size_t max,
      73              :                                     int has_pw, int64_t srp_id) {
      74            2 :     TlWriter w; tl_writer_init(&w);
      75            2 :     tl_write_uint32(&w, CRC_account_password);
      76            2 :     uint32_t flags = has_pw ? (1u << 2) : 0;
      77            2 :     tl_write_uint32(&w, flags);
      78              : 
      79            2 :     if (has_pw) {
      80              :         /* current_algo: passwordKdfAlgoSHA... salt1:bytes salt2:bytes g:int p:bytes */
      81            1 :         tl_write_uint32(&w, CRC_KdfAlgo);
      82            1 :         uint8_t salt1[16] = {0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18,
      83              :                              0x19,0x1a,0x1b,0x1c,0x1d,0x1e,0x1f,0x20};
      84            1 :         uint8_t salt2[16] = {0x21,0x22,0x23,0x24,0x25,0x26,0x27,0x28,
      85              :                              0x29,0x2a,0x2b,0x2c,0x2d,0x2e,0x2f,0x30};
      86            1 :         tl_write_bytes(&w, salt1, sizeof(salt1));
      87            1 :         tl_write_bytes(&w, salt2, sizeof(salt2));
      88            1 :         tl_write_int32(&w, 2); /* g */
      89              :         uint8_t p[256];
      90          257 :         for (int i = 0; i < 256; i++) p[i] = (uint8_t)(0x80 + (i & 0x7f));
      91            1 :         tl_write_bytes(&w, p, sizeof(p));
      92              : 
      93              :         /* srp_B:bytes */
      94              :         uint8_t srpB[256];
      95          257 :         for (int i = 0; i < 256; i++) srpB[i] = (uint8_t)(i ^ 0x5A);
      96            1 :         tl_write_bytes(&w, srpB, sizeof(srpB));
      97              : 
      98              :         /* srp_id:long */
      99            1 :         tl_write_int64(&w, srp_id);
     100              :     }
     101              : 
     102            2 :     size_t n = w.len < max ? w.len : max;
     103            2 :     memcpy(buf, w.data, n);
     104            2 :     tl_writer_free(&w);
     105            2 :     return n;
     106              : }
     107              : 
     108            1 : static void test_get_password_parses_srp_params(void) {
     109            1 :     mock_socket_reset(); mock_crypto_reset();
     110              : 
     111              :     uint8_t payload[1024];
     112            1 :     size_t plen = make_account_password(payload, sizeof(payload), 1,
     113              :                                          0x1122334455667788LL);
     114            1 :     uint8_t resp[2048]; size_t rlen = 0;
     115            1 :     build_fake_encrypted_response(payload, plen, resp, &rlen);
     116            1 :     mock_socket_set_response(resp, rlen);
     117              : 
     118              :     MtProtoSession s; Transport t; ApiConfig cfg;
     119            1 :     fix_session(&s); fix_transport(&t); fix_cfg(&cfg);
     120              : 
     121            1 :     Account2faPassword pw = {0};
     122            1 :     RpcError err = {0};
     123            1 :     int rc = auth_2fa_get_password(&cfg, &s, &t, &pw, &err);
     124            1 :     ASSERT(rc == 0, "getPassword parses ok");
     125            1 :     ASSERT(pw.has_password == 1, "has_password flag set");
     126            1 :     ASSERT(pw.srp_id == 0x1122334455667788LL, "srp_id captured");
     127            1 :     ASSERT(pw.g == 2, "g captured");
     128            1 :     ASSERT(pw.salt1_len == 16, "salt1 length");
     129            1 :     ASSERT(pw.salt2_len == 16, "salt2 length");
     130            1 :     ASSERT(pw.p[0] == 0x80, "prime first byte captured");
     131            1 :     ASSERT(pw.srp_B[0] == 0x5A, "srp_B first byte captured (0 ^ 0x5A)");
     132              : }
     133              : 
     134            1 : static void test_get_password_no_2fa(void) {
     135            1 :     mock_socket_reset(); mock_crypto_reset();
     136              : 
     137              :     uint8_t payload[128];
     138            1 :     size_t plen = make_account_password(payload, sizeof(payload), 0, 0);
     139            1 :     uint8_t resp[512]; size_t rlen = 0;
     140            1 :     build_fake_encrypted_response(payload, plen, resp, &rlen);
     141            1 :     mock_socket_set_response(resp, rlen);
     142              : 
     143              :     MtProtoSession s; Transport t; ApiConfig cfg;
     144            1 :     fix_session(&s); fix_transport(&t); fix_cfg(&cfg);
     145              : 
     146            1 :     Account2faPassword pw = {0};
     147            1 :     int rc = auth_2fa_get_password(&cfg, &s, &t, &pw, NULL);
     148            1 :     ASSERT(rc == 0, "no-2FA getPassword parses");
     149            1 :     ASSERT(pw.has_password == 0, "has_password not set");
     150              : }
     151              : 
     152            1 : static void test_get_password_rpc_error(void) {
     153            1 :     mock_socket_reset(); mock_crypto_reset();
     154              : 
     155            1 :     TlWriter w; tl_writer_init(&w);
     156            1 :     tl_write_uint32(&w, TL_rpc_error);
     157            1 :     tl_write_int32 (&w, 500);
     158            1 :     tl_write_string(&w, "PASSWORD_TOO_FRESH_3600");
     159            1 :     uint8_t payload[128]; memcpy(payload, w.data, w.len);
     160            1 :     size_t plen = w.len; tl_writer_free(&w);
     161              : 
     162            1 :     uint8_t resp[512]; size_t rlen = 0;
     163            1 :     build_fake_encrypted_response(payload, plen, resp, &rlen);
     164            1 :     mock_socket_set_response(resp, rlen);
     165              : 
     166              :     MtProtoSession s; Transport t; ApiConfig cfg;
     167            1 :     fix_session(&s); fix_transport(&t); fix_cfg(&cfg);
     168              : 
     169            1 :     Account2faPassword pw = {0};
     170            1 :     RpcError err = {0};
     171            1 :     int rc = auth_2fa_get_password(&cfg, &s, &t, &pw, &err);
     172            1 :     ASSERT(rc != 0, "RPC error propagates");
     173            1 :     ASSERT(err.error_code == 500, "error code captured");
     174              : }
     175              : 
     176            1 : static void test_check_password_rejects_missing_password_flag(void) {
     177              :     MtProtoSession s; Transport t; ApiConfig cfg;
     178            1 :     fix_session(&s); fix_transport(&t); fix_cfg(&cfg);
     179              : 
     180            1 :     Account2faPassword pw = {0}; pw.has_password = 0;
     181            1 :     RpcError err = {0};
     182            1 :     int rc = auth_2fa_check_password(&cfg, &s, &t, &pw, "hunter2",
     183              :                                       NULL, &err);
     184            1 :     ASSERT(rc == -1, "check rejects has_password=0");
     185              : }
     186              : 
     187            1 : static void test_check_password_uses_pbkdf2_and_bn(void) {
     188            1 :     mock_socket_reset(); mock_crypto_reset();
     189              : 
     190              :     /* Mock auth.authorization response so the round-trip succeeds and we
     191              :      * can count how many times the mocked primitives were invoked. */
     192            1 :     TlWriter w; tl_writer_init(&w);
     193            1 :     tl_write_uint32(&w, TL_auth_authorization);
     194            1 :     tl_write_uint32(&w, 0);                  /* flags */
     195            1 :     tl_write_uint32(&w, TL_user);
     196            1 :     tl_write_uint32(&w, 0);                  /* user flags */
     197            1 :     tl_write_int64 (&w, 42LL);               /* user id */
     198            1 :     uint8_t payload[128]; memcpy(payload, w.data, w.len);
     199            1 :     size_t plen = w.len; tl_writer_free(&w);
     200              : 
     201            1 :     uint8_t resp[512]; size_t rlen = 0;
     202            1 :     build_fake_encrypted_response(payload, plen, resp, &rlen);
     203            1 :     mock_socket_set_response(resp, rlen);
     204              : 
     205              :     MtProtoSession s; Transport t; ApiConfig cfg;
     206            1 :     fix_session(&s); fix_transport(&t); fix_cfg(&cfg);
     207              : 
     208            1 :     Account2faPassword pw = {0};
     209            1 :     pw.has_password = 1;
     210            1 :     pw.srp_id = 0xAABBCCDDEEFF0011LL;
     211            1 :     pw.g = 2;
     212            1 :     pw.salt1_len = 4; pw.salt2_len = 4;
     213            1 :     memcpy(pw.salt1, "s1s1", 4);
     214            1 :     memcpy(pw.salt2, "s2s2", 4);
     215          257 :     for (int i = 0; i < 256; i++) { pw.p[i] = 0xFF; pw.srp_B[i] = 0x7F; }
     216              : 
     217            1 :     int64_t uid = 0;
     218            1 :     RpcError err = {0};
     219            1 :     int rc = auth_2fa_check_password(&cfg, &s, &t, &pw, "hunter2",
     220              :                                       &uid, &err);
     221            1 :     ASSERT(rc == 0, "checkPassword returns ok with mocked auth.authorization");
     222            1 :     ASSERT(uid == 42, "user id extracted");
     223            1 :     ASSERT(mock_crypto_pbkdf2_call_count() == 1,
     224              :            "PBKDF2 invoked exactly once during x derivation");
     225            1 :     ASSERT(mock_crypto_bn_mod_exp_call_count() >= 3,
     226              :            "mod_exp used for A, v and the base^a/x chain");
     227              : }
     228              : 
     229            1 : static void test_null_args(void) {
     230            1 :     ASSERT(auth_2fa_get_password(NULL, NULL, NULL, NULL, NULL) == -1,
     231              :            "getPassword null args");
     232            1 :     ASSERT(auth_2fa_check_password(NULL, NULL, NULL, NULL, NULL, NULL, NULL) == -1,
     233              :            "checkPassword null args");
     234              : }
     235              : 
     236              : /* Build account.password with salt1 length = SRP_SALT_MAX + 1 (too large). */
     237            1 : static size_t make_account_password_bad_salt(uint8_t *buf, size_t max) {
     238            1 :     TlWriter w; tl_writer_init(&w);
     239            1 :     tl_write_uint32(&w, CRC_account_password);
     240            1 :     tl_write_uint32(&w, (1u << 2)); /* has_password */
     241              : 
     242            1 :     tl_write_uint32(&w, CRC_KdfAlgo);
     243              :     /* salt1 length = SRP_SALT_MAX + 1 — should trigger the guard. */
     244              :     uint8_t big_salt[SRP_SALT_MAX + 1];
     245            1 :     memset(big_salt, 0xAB, sizeof(big_salt));
     246            1 :     tl_write_bytes(&w, big_salt, sizeof(big_salt));
     247            1 :     uint8_t salt2[16] = {0};
     248            1 :     tl_write_bytes(&w, salt2, sizeof(salt2));
     249            1 :     tl_write_int32(&w, 2); /* g */
     250            1 :     uint8_t p[256]; memset(p, 0x80, sizeof(p));
     251            1 :     tl_write_bytes(&w, p, sizeof(p));
     252            1 :     uint8_t srpB[256]; memset(srpB, 0x5A, sizeof(srpB));
     253            1 :     tl_write_bytes(&w, srpB, sizeof(srpB));
     254            1 :     tl_write_int64(&w, 0x1234567890ABCDEFLL);
     255              : 
     256            1 :     size_t n = w.len < max ? w.len : max;
     257            1 :     memcpy(buf, w.data, n);
     258            1 :     tl_writer_free(&w);
     259            1 :     return n;
     260              : }
     261              : 
     262              : /* Build account.password with p length != 256 (e.g. 128 bytes). */
     263            1 : static size_t make_account_password_bad_prime_len(uint8_t *buf, size_t max) {
     264            1 :     TlWriter w; tl_writer_init(&w);
     265            1 :     tl_write_uint32(&w, CRC_account_password);
     266            1 :     tl_write_uint32(&w, (1u << 2)); /* has_password */
     267              : 
     268            1 :     tl_write_uint32(&w, CRC_KdfAlgo);
     269            1 :     uint8_t salt1[16] = {0}; tl_write_bytes(&w, salt1, sizeof(salt1));
     270            1 :     uint8_t salt2[16] = {0}; tl_write_bytes(&w, salt2, sizeof(salt2));
     271            1 :     tl_write_int32(&w, 2); /* g */
     272              :     /* prime is only 128 bytes — wrong length; must be SRP_PRIME_LEN (256). */
     273            1 :     uint8_t p[128]; memset(p, 0x80, sizeof(p));
     274            1 :     tl_write_bytes(&w, p, sizeof(p));
     275            1 :     uint8_t srpB[256]; memset(srpB, 0x5A, sizeof(srpB));
     276            1 :     tl_write_bytes(&w, srpB, sizeof(srpB));
     277            1 :     tl_write_int64(&w, 0x1234567890ABCDEFLL);
     278              : 
     279            1 :     size_t n = w.len < max ? w.len : max;
     280            1 :     memcpy(buf, w.data, n);
     281            1 :     tl_writer_free(&w);
     282            1 :     return n;
     283              : }
     284              : 
     285              : /* Build account.password with has_password=true but zero-length srp_B. */
     286            1 : static size_t make_account_password_empty_srpB(uint8_t *buf, size_t max) {
     287            1 :     TlWriter w; tl_writer_init(&w);
     288            1 :     tl_write_uint32(&w, CRC_account_password);
     289            1 :     tl_write_uint32(&w, (1u << 2)); /* has_password */
     290              : 
     291            1 :     tl_write_uint32(&w, CRC_KdfAlgo);
     292            1 :     uint8_t salt1[16] = {0}; tl_write_bytes(&w, salt1, sizeof(salt1));
     293            1 :     uint8_t salt2[16] = {0}; tl_write_bytes(&w, salt2, sizeof(salt2));
     294            1 :     tl_write_int32(&w, 2); /* g */
     295            1 :     uint8_t p[256]; memset(p, 0x80, sizeof(p));
     296            1 :     tl_write_bytes(&w, p, sizeof(p));
     297              :     /* srp_B is empty (zero-length bytes). */
     298            1 :     tl_write_bytes(&w, NULL, 0);
     299            1 :     tl_write_int64(&w, 0x1234567890ABCDEFLL);
     300              : 
     301            1 :     size_t n = w.len < max ? w.len : max;
     302            1 :     memcpy(buf, w.data, n);
     303            1 :     tl_writer_free(&w);
     304            1 :     return n;
     305              : }
     306              : 
     307            1 : static void test_get_password_rejects_oversized_salt(void) {
     308            1 :     mock_socket_reset(); mock_crypto_reset();
     309              : 
     310              :     uint8_t payload[2048];
     311            1 :     size_t plen = make_account_password_bad_salt(payload, sizeof(payload));
     312            1 :     uint8_t resp[4096]; size_t rlen = 0;
     313            1 :     build_fake_encrypted_response(payload, plen, resp, &rlen);
     314            1 :     mock_socket_set_response(resp, rlen);
     315              : 
     316              :     MtProtoSession s; Transport t; ApiConfig cfg;
     317            1 :     fix_session(&s); fix_transport(&t); fix_cfg(&cfg);
     318              : 
     319            1 :     Account2faPassword pw = {0};
     320            1 :     int rc = auth_2fa_get_password(&cfg, &s, &t, &pw, NULL);
     321            1 :     ASSERT(rc == -1, "getPassword rejects salt1 > SRP_SALT_MAX");
     322              : }
     323              : 
     324            1 : static void test_get_password_rejects_wrong_prime_len(void) {
     325            1 :     mock_socket_reset(); mock_crypto_reset();
     326              : 
     327              :     uint8_t payload[1024];
     328            1 :     size_t plen = make_account_password_bad_prime_len(payload, sizeof(payload));
     329            1 :     uint8_t resp[2048]; size_t rlen = 0;
     330            1 :     build_fake_encrypted_response(payload, plen, resp, &rlen);
     331            1 :     mock_socket_set_response(resp, rlen);
     332              : 
     333              :     MtProtoSession s; Transport t; ApiConfig cfg;
     334            1 :     fix_session(&s); fix_transport(&t); fix_cfg(&cfg);
     335              : 
     336            1 :     Account2faPassword pw = {0};
     337            1 :     int rc = auth_2fa_get_password(&cfg, &s, &t, &pw, NULL);
     338            1 :     ASSERT(rc == -1, "getPassword rejects p length != SRP_PRIME_LEN");
     339              : }
     340              : 
     341            1 : static void test_get_password_rejects_empty_srpB(void) {
     342            1 :     mock_socket_reset(); mock_crypto_reset();
     343              : 
     344              :     uint8_t payload[1024];
     345            1 :     size_t plen = make_account_password_empty_srpB(payload, sizeof(payload));
     346            1 :     uint8_t resp[2048]; size_t rlen = 0;
     347            1 :     build_fake_encrypted_response(payload, plen, resp, &rlen);
     348            1 :     mock_socket_set_response(resp, rlen);
     349              : 
     350              :     MtProtoSession s; Transport t; ApiConfig cfg;
     351            1 :     fix_session(&s); fix_transport(&t); fix_cfg(&cfg);
     352              : 
     353            1 :     Account2faPassword pw = {0};
     354            1 :     int rc = auth_2fa_get_password(&cfg, &s, &t, &pw, NULL);
     355            1 :     ASSERT(rc == -1, "getPassword rejects zero-length srp_B");
     356              : }
     357              : 
     358            1 : void run_auth_2fa_tests(void) {
     359            1 :     RUN_TEST(test_get_password_parses_srp_params);
     360            1 :     RUN_TEST(test_get_password_no_2fa);
     361            1 :     RUN_TEST(test_get_password_rpc_error);
     362            1 :     RUN_TEST(test_check_password_rejects_missing_password_flag);
     363            1 :     RUN_TEST(test_check_password_uses_pbkdf2_and_bn);
     364            1 :     RUN_TEST(test_null_args);
     365            1 :     RUN_TEST(test_get_password_rejects_oversized_salt);
     366            1 :     RUN_TEST(test_get_password_rejects_wrong_prime_len);
     367            1 :     RUN_TEST(test_get_password_rejects_empty_srpB);
     368            1 : }
        

Generated by: LCOV version 2.0-1