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