Line data Source code
1 : /**
2 : * @file test_mtproto_crypto.c
3 : * @brief Unit tests for MTProto 2.0 crypto layer.
4 : *
5 : * Uses mock crypto for logic verification (call counts, key derivation structure).
6 : * Real-crypto functional tests will be in a separate test binary.
7 : *
8 : * Known mock limitations (functional tests needed):
9 : * - Mock SHA256 returns fixed output regardless of input → cannot verify
10 : * that different auth_keys produce different derived keys.
11 : * - Mock encrypt_block is identity → cannot verify wrong-auth-key rejection
12 : * (msg_key verification passes because SHA256 is input-independent).
13 : * - Round-trip works because encrypt/decrypt are symmetric under identity.
14 : */
15 :
16 : #include "test_helpers.h"
17 : #include "mtproto_crypto.h"
18 : #include "mock_crypto.h"
19 : #include "crypto.h"
20 :
21 : #include <stdlib.h>
22 : #include <string.h>
23 :
24 1 : void test_derive_keys_sha256_count(void) {
25 1 : mock_crypto_reset();
26 1 : uint8_t auth_key[256] = {0}, msg_key[16] = {0};
27 : uint8_t aes_key[32], aes_iv[32];
28 :
29 1 : mtproto_derive_keys(auth_key, msg_key, 0, aes_key, aes_iv);
30 :
31 : /* derive_keys calls SHA256 twice (sha256_a + sha256_b) */
32 1 : ASSERT(mock_crypto_sha256_call_count() == 2,
33 : "derive_keys should call SHA256 exactly 2 times");
34 : }
35 :
36 1 : void test_derive_keys_direction_diff(void) {
37 : /* Verify that both directions call SHA256 (logic is exercised) */
38 1 : mock_crypto_reset();
39 : uint8_t auth_key[256], msg_key[16];
40 1 : memset(auth_key, 0x42, 256);
41 1 : memset(msg_key, 0x55, 16);
42 :
43 : uint8_t key0[32], iv0[32], key8[32], iv8[32];
44 :
45 1 : mtproto_derive_keys(auth_key, msg_key, 0, key0, iv0);
46 : /* direction=0 called SHA256 2 times */
47 1 : ASSERT(mock_crypto_sha256_call_count() == 2,
48 : "direction=0 should call SHA256 twice");
49 :
50 1 : mock_crypto_reset();
51 1 : mtproto_derive_keys(auth_key, msg_key, 8, key8, iv8);
52 1 : ASSERT(mock_crypto_sha256_call_count() == 2,
53 : "direction=8 should call SHA256 twice");
54 : }
55 :
56 1 : void test_compute_msg_key_sha256_count(void) {
57 1 : mock_crypto_reset();
58 1 : uint8_t auth_key[256] = {0}, plain[32] = {1};
59 : uint8_t msg_key[16];
60 :
61 1 : mtproto_compute_msg_key(auth_key, plain, 32, 0, msg_key);
62 :
63 1 : ASSERT(mock_crypto_sha256_call_count() == 1,
64 : "compute_msg_key should call SHA256 exactly 1 time");
65 : }
66 :
67 1 : void test_compute_msg_key_not_all_zero(void) {
68 1 : mock_crypto_reset();
69 : /* Set SHA256 mock output to known non-zero value */
70 : uint8_t fake_hash[32];
71 1 : memset(fake_hash, 0xAB, 32);
72 1 : mock_crypto_set_sha256_output(fake_hash);
73 :
74 1 : uint8_t auth_key[256] = {0}, plain[16] = {0};
75 : uint8_t msg_key[16];
76 :
77 1 : mtproto_compute_msg_key(auth_key, plain, 16, 0, msg_key);
78 :
79 : /* msg_key should be hash[8:24] which is 0xAB */
80 1 : int all_ab = 1;
81 17 : for (int i = 0; i < 16; i++) {
82 16 : if (msg_key[i] != 0xAB) all_ab = 0;
83 : }
84 1 : ASSERT(all_ab, "msg_key should match mock SHA256 output bytes 8-23");
85 : }
86 :
87 1 : void test_gen_padding_size(void) {
88 : /* Padding should be at least 12 bytes, total aligned to 16 */
89 1 : size_t pad_len = mtproto_gen_padding(0, NULL);
90 1 : ASSERT(pad_len >= 12, "padding should be at least 12 bytes");
91 1 : ASSERT((pad_len) % 16 == 0, "total (0 + padding) should be 16-aligned");
92 :
93 1 : pad_len = mtproto_gen_padding(100, NULL);
94 1 : ASSERT(pad_len >= 12, "padding for 100 bytes should be >= 12");
95 1 : ASSERT((100 + pad_len) % 16 == 0, "total should be 16-aligned");
96 : }
97 :
98 1 : void test_gen_padding_rand_bytes(void) {
99 1 : mock_crypto_reset();
100 : uint8_t padding[1024];
101 1 : mtproto_gen_padding(100, padding);
102 1 : ASSERT(mock_crypto_rand_bytes_call_count() == 1,
103 : "gen_padding should call crypto_rand_bytes once");
104 : }
105 :
106 1 : void test_encrypt_output_structure(void) {
107 1 : mock_crypto_reset();
108 : uint8_t auth_key[256];
109 : uint8_t plain[64];
110 : uint8_t encrypted[2048];
111 1 : size_t enc_len = 0;
112 :
113 1 : memset(auth_key, 0x42, 256);
114 1 : memset(encrypted, 0, sizeof(encrypted));
115 65 : for (int i = 0; i < 64; i++) plain[i] = (uint8_t)(i * 3);
116 :
117 : uint8_t used_msg_key[16];
118 1 : mtproto_encrypt(plain, 64, auth_key, 0, encrypted, &enc_len, used_msg_key);
119 :
120 : /* Verify structural properties */
121 1 : ASSERT(enc_len >= 64, "encrypted length should be >= plaintext");
122 1 : ASSERT(enc_len % 16 == 0, "encrypted length should be aligned to 16");
123 1 : ASSERT(enc_len <= 64 + 1024, "encrypted length should not exceed max padding");
124 :
125 : /* Verify crypto was called: SHA256 for msg_key + derive_keys */
126 1 : ASSERT(mock_crypto_sha256_call_count() >= 3,
127 : "encrypt should call SHA256 at least 3 times "
128 : "(msg_key + sha256_a + sha256_b)");
129 1 : ASSERT(mock_crypto_rand_bytes_call_count() >= 1,
130 : "encrypt should generate random padding");
131 : }
132 :
133 1 : void test_decrypt_with_matching_msg_key(void) {
134 1 : mock_crypto_reset();
135 : /* With mock SHA256 always returning zeros, msg_key is always [0...0].
136 : * Decrypt recomputes msg_key from decrypted data — mock also produces
137 : * zeros, so verification passes. This tests the API contract, not
138 : * real crypto correctness (that's for functional/integration tests). */
139 : uint8_t auth_key[256];
140 : uint8_t cipher[80]; /* 80 bytes = 5 blocks, aligned to 16 */
141 : uint8_t decrypted[80];
142 1 : size_t dec_len = 0;
143 :
144 1 : memset(auth_key, 0x42, 256);
145 1 : memset(cipher, 0xAA, 80);
146 :
147 : uint8_t msg_key[16];
148 1 : memset(msg_key, 0, 16); /* mock SHA256 → all zeros → hash[8:24] = 0 */
149 :
150 1 : int result = mtproto_decrypt(cipher, 80, auth_key, msg_key, 0,
151 : decrypted, &dec_len);
152 1 : ASSERT(result == 0, "decrypt should succeed with matching mock msg_key");
153 1 : ASSERT(dec_len == 80, "decrypted length should equal cipher length");
154 : }
155 :
156 1 : void test_decrypt_wrong_auth_key(void) {
157 : /* Test that decrypt API rejects mismatched auth_key.
158 : With mock crypto returning the same SHA256 for all inputs,
159 : we can only verify the API contract exists. A real functional test
160 : with actual crypto will verify the full rejection. */
161 1 : ASSERT(1, "API contract: decrypt validates msg_key against auth_key");
162 : }
163 :
164 1 : void test_decrypt_wrong_msg_key(void) {
165 : uint8_t auth_key[256], plain[32];
166 : uint8_t encrypted[2048], decrypted[2048];
167 : size_t enc_len, dec_len;
168 1 : memset(auth_key, 0x42, 256);
169 1 : memset(plain, 0xAA, 32);
170 :
171 : uint8_t real_msg_key[16];
172 1 : mtproto_encrypt(plain, 32, auth_key, 0, encrypted, &enc_len, real_msg_key);
173 :
174 : /* Use wrong msg_key */
175 : uint8_t wrong_key[16];
176 1 : memset(wrong_key, 0xFF, 16);
177 :
178 1 : int result = mtproto_decrypt(encrypted, enc_len, auth_key, wrong_key, 0,
179 : decrypted, &dec_len);
180 1 : ASSERT(result == -1, "decrypt with wrong msg_key should fail");
181 : }
182 :
183 1 : void test_decrypt_unaligned_length(void) {
184 1 : uint8_t auth_key[256] = {0}, msg_key[16] = {0};
185 1 : uint8_t cipher[17] = {0}, plain[17];
186 : size_t plain_len;
187 :
188 1 : ASSERT(mtproto_decrypt(cipher, 17, auth_key, msg_key, 0, plain, &plain_len) == -1,
189 : "non-16-aligned cipher should be rejected");
190 1 : ASSERT(mtproto_decrypt(cipher, 0, auth_key, msg_key, 0, plain, &plain_len) == -1,
191 : "zero-length cipher should be rejected");
192 : }
193 :
194 1 : void test_mtproto_crypto(void) {
195 1 : RUN_TEST(test_derive_keys_sha256_count);
196 1 : RUN_TEST(test_derive_keys_direction_diff);
197 1 : RUN_TEST(test_compute_msg_key_sha256_count);
198 1 : RUN_TEST(test_compute_msg_key_not_all_zero);
199 1 : RUN_TEST(test_gen_padding_size);
200 1 : RUN_TEST(test_gen_padding_rand_bytes);
201 1 : RUN_TEST(test_encrypt_output_structure);
202 1 : RUN_TEST(test_decrypt_with_matching_msg_key);
203 1 : RUN_TEST(test_decrypt_wrong_auth_key);
204 1 : RUN_TEST(test_decrypt_wrong_msg_key);
205 1 : RUN_TEST(test_decrypt_unaligned_length);
206 1 : }
|