Line data Source code
1 : /* SPDX-License-Identifier: GPL-3.0-or-later */
2 : /* Copyright 2026 Peter Csaszar */
3 :
4 : /**
5 : * @file test_pii_redact.c
6 : * @brief Unit tests for pii_redact helpers and log-redaction of auth PII.
7 : *
8 : * Verifies:
9 : * 1. redact_phone with various phone shapes (short, long, NULL).
10 : * 2. A log-capture test that confirms a send_code log line does NOT
11 : * contain the raw phone_code_hash value.
12 : */
13 :
14 : #include "test_helpers.h"
15 : #include "pii_redact.h"
16 : #include "logger.h"
17 : #include "auth_session.h"
18 : #include "mtproto_session.h"
19 : #include "transport.h"
20 : #include "tl_serial.h"
21 : #include "tl_registry.h"
22 : #include "mock_socket.h"
23 : #include "mock_crypto.h"
24 :
25 : #include <string.h>
26 : #include <stdio.h>
27 : #include <stdlib.h>
28 : #include <unistd.h>
29 :
30 : /* ------------------------------------------------------------------ */
31 : /* redact_phone tests */
32 : /* ------------------------------------------------------------------ */
33 :
34 1 : static void test_redact_phone_long(void) {
35 : char out[32];
36 1 : redact_phone("+15551234567", out, sizeof(out));
37 : /* Last 4 digits must be visible */
38 1 : ASSERT(strstr(out, "4567") != NULL,
39 : "redact_phone: last 4 digits must be present");
40 : /* Raw middle digits must NOT be present */
41 1 : ASSERT(strstr(out, "555123") == NULL,
42 : "redact_phone: middle digits must be masked");
43 : /* Must start with '+' */
44 1 : ASSERT(out[0] == '+', "redact_phone: must start with '+'");
45 : }
46 :
47 1 : static void test_redact_phone_short(void) {
48 : char out[32];
49 : /* A 4-digit bare number — too short to keep last 4 and still mask */
50 1 : redact_phone("1234", out, sizeof(out));
51 : /* Must not expose digits, output should be the full-mask form */
52 1 : ASSERT(strstr(out, "1234") == NULL,
53 : "redact_phone: short phone must be fully masked");
54 1 : ASSERT(out[0] == '+', "redact_phone: short phone must start with '+'");
55 : }
56 :
57 1 : static void test_redact_phone_null(void) {
58 : char out[32];
59 1 : redact_phone(NULL, out, sizeof(out));
60 1 : ASSERT(strstr(out, "null") != NULL,
61 : "redact_phone: NULL phone must produce '(null)' output");
62 : }
63 :
64 1 : static void test_redact_phone_empty(void) {
65 : char out[32];
66 1 : redact_phone("", out, sizeof(out));
67 1 : ASSERT(strstr(out, "null") != NULL,
68 : "redact_phone: empty phone must produce '(null)' output");
69 : }
70 :
71 1 : static void test_redact_phone_with_plus(void) {
72 : char out[32];
73 1 : redact_phone("+447700900123", out, sizeof(out));
74 1 : ASSERT(strstr(out, "0123") != NULL,
75 : "redact_phone: last 4 of +447700900123 must be 0123");
76 1 : ASSERT(strstr(out, "77009") == NULL,
77 : "redact_phone: internal digits must not appear");
78 : }
79 :
80 : /* ------------------------------------------------------------------ */
81 : /* Log-capture test: send_code must NOT log the raw hash */
82 : /* ------------------------------------------------------------------ */
83 :
84 : /* Reuse the fake-response builder from test_auth_session.c */
85 1 : static void build_fake_encrypted_response_pr(const uint8_t *payload, size_t plen,
86 : uint8_t *out, size_t *out_len) {
87 : TlWriter w;
88 1 : tl_writer_init(&w);
89 1 : uint8_t zeros24[24] = {0};
90 1 : tl_write_raw(&w, zeros24, 24);
91 1 : uint8_t header[32] = {0};
92 1 : uint32_t plen32 = (uint32_t)plen;
93 1 : memcpy(header + 28, &plen32, 4);
94 1 : tl_write_raw(&w, header, 32);
95 1 : tl_write_raw(&w, payload, plen);
96 1 : size_t total = w.len;
97 1 : size_t payload_start = 24;
98 1 : size_t enc_part = total - payload_start;
99 1 : if (enc_part % 16 != 0) {
100 1 : size_t pad = 16 - (enc_part % 16);
101 1 : uint8_t zeros[16] = {0};
102 1 : tl_write_raw(&w, zeros, pad);
103 : }
104 1 : size_t wire_bytes = w.len;
105 1 : size_t wire_units = wire_bytes / 4;
106 1 : uint8_t *result = (uint8_t *)malloc(1 + wire_bytes);
107 1 : result[0] = (uint8_t)wire_units;
108 1 : memcpy(result + 1, w.data, wire_bytes);
109 1 : *out_len = 1 + wire_bytes;
110 1 : memcpy(out, result, *out_len);
111 1 : free(result);
112 1 : tl_writer_free(&w);
113 1 : }
114 :
115 : /** The secret hash used in the log-capture test. */
116 : #define SECRET_HASH "SUPERSECRET_HASH_XYZ_9876"
117 :
118 1 : static void test_send_code_does_not_log_hash(void) {
119 1 : mock_socket_reset();
120 1 : mock_crypto_reset();
121 :
122 : /* Build a fake sentCode response containing the secret hash */
123 : TlWriter w;
124 1 : tl_writer_init(&w);
125 1 : tl_write_uint32(&w, CRC_auth_sentCode);
126 1 : tl_write_uint32(&w, 0); /* flags */
127 1 : tl_write_uint32(&w, CRC_auth_sentCodeTypeApp);
128 1 : tl_write_int32(&w, 5); /* length */
129 1 : tl_write_string(&w, SECRET_HASH);
130 : uint8_t payload[512];
131 1 : size_t plen = w.len < sizeof(payload) ? w.len : sizeof(payload);
132 1 : memcpy(payload, w.data, plen);
133 1 : tl_writer_free(&w);
134 :
135 : uint8_t resp_buf[1024];
136 1 : size_t resp_len = 0;
137 1 : build_fake_encrypted_response_pr(payload, plen, resp_buf, &resp_len);
138 1 : mock_socket_set_response(resp_buf, resp_len);
139 :
140 : /* Direct logging to a temp file so we can inspect it */
141 1 : const char *log_path = "/tmp/tg-cli-pii-test.log";
142 1 : unlink(log_path);
143 1 : logger_init(log_path, LOG_DEBUG);
144 :
145 : MtProtoSession s;
146 1 : mtproto_session_init(&s);
147 1 : s.session_id = 0;
148 1 : uint8_t fake_key[256] = {0};
149 1 : mtproto_session_set_auth_key(&s, fake_key);
150 1 : mtproto_session_set_salt(&s, 0x1122334455667788ULL);
151 :
152 : Transport t;
153 1 : transport_init(&t);
154 1 : t.fd = 42; t.connected = 1; t.dc_id = 1;
155 :
156 : ApiConfig cfg;
157 1 : api_config_init(&cfg);
158 1 : cfg.api_id = 12345; cfg.api_hash = "deadbeef";
159 :
160 : AuthSentCode result;
161 1 : memset(&result, 0, sizeof(result));
162 1 : int rc = auth_send_code(&cfg, &s, &t, "+15551234567", &result, NULL);
163 1 : logger_close();
164 :
165 1 : ASSERT(rc == 0, "log-capture: send_code must succeed");
166 :
167 : /* Read log and verify the secret hash is absent */
168 1 : FILE *f = fopen(log_path, "r");
169 1 : ASSERT(f != NULL, "log-capture: log file must exist");
170 1 : if (!f) return;
171 :
172 : char line[512];
173 1 : int found = 0;
174 3 : while (fgets(line, sizeof(line), f)) {
175 2 : if (strstr(line, SECRET_HASH)) { found = 1; break; }
176 : }
177 1 : fclose(f);
178 1 : unlink(log_path);
179 :
180 1 : ASSERT(found == 0,
181 : "log-capture: phone_code_hash must NOT appear in log output");
182 : }
183 :
184 : /* ------------------------------------------------------------------ */
185 : /* Entry point called from test_runner.c */
186 : /* ------------------------------------------------------------------ */
187 :
188 1 : void run_pii_redact_tests(void) {
189 1 : RUN_TEST(test_redact_phone_long);
190 1 : RUN_TEST(test_redact_phone_short);
191 1 : RUN_TEST(test_redact_phone_null);
192 1 : RUN_TEST(test_redact_phone_empty);
193 1 : RUN_TEST(test_redact_phone_with_plus);
194 1 : RUN_TEST(test_send_code_does_not_log_hash);
195 1 : }
|