Line data Source code
1 : /* SPDX-License-Identifier: GPL-3.0-or-later */
2 : /* Copyright 2026 Peter Csaszar */
3 :
4 : /**
5 : * @file app/auth_flow.c
6 : * @brief High-level login flow with DC migration.
7 : */
8 :
9 : #include "app/auth_flow.h"
10 : #include "app/dc_config.h"
11 : #include "app/session_store.h"
12 :
13 : #include "auth_session.h"
14 : #include "infrastructure/auth_2fa.h"
15 : #include "mtproto_auth.h"
16 : #include "mtproto_rpc.h"
17 : #include "logger.h"
18 :
19 : #include <stdio.h>
20 : #include <string.h>
21 :
22 : /** Maximum redirects we follow before giving up. Telegram usually needs 1. */
23 : #define AUTH_MAX_MIGRATIONS 3
24 :
25 : /**
26 : * Connect to the physical endpoint of @p phys_dc but use @p dh_dc in the
27 : * DH p_q_inner_data_dc field. Pass dh_dc=0 for the Telegram test-DC
28 : * wildcard (the test server rejects any non-zero value there).
29 : */
30 3 : static int connect_dc_ex(int phys_dc, int dh_dc,
31 : Transport *t, MtProtoSession *s) {
32 3 : const DcEndpoint *ep = dc_lookup(phys_dc);
33 3 : if (!ep) {
34 1 : logger_log(LOG_ERROR, "auth_flow: unknown DC id %d", phys_dc);
35 1 : return -1;
36 : }
37 :
38 2 : if (transport_connect(t, ep->host, ep->port) != 0) {
39 1 : logger_log(LOG_ERROR, "auth_flow: connect failed for DC%d (%s:%d)",
40 1 : phys_dc, ep->host, ep->port);
41 1 : return -1;
42 : }
43 1 : t->dc_id = dh_dc; /* DH uses this for p_q_inner_data_dc */
44 :
45 1 : if (mtproto_auth_key_gen(t, s) != 0) {
46 1 : logger_log(LOG_ERROR, "auth_flow: DH auth key gen failed on DC%d",
47 : phys_dc);
48 1 : transport_close(t);
49 1 : return -1;
50 : }
51 0 : t->dc_id = phys_dc; /* restore physical DC id after DH */
52 0 : logger_log(LOG_INFO, "auth_flow: connected to DC%d, auth key ready", phys_dc);
53 0 : return 0;
54 : }
55 :
56 4 : int auth_flow_connect_dc(int dc_id, Transport *t, MtProtoSession *s) {
57 4 : if (!t || !s) return -1;
58 2 : return connect_dc_ex(dc_id, dc_id, t, s);
59 : }
60 :
61 : /** Full migration: tear down session+transport, reconnect, redo DH. */
62 0 : static int migrate(int new_dc, int dh_dc, Transport *t, MtProtoSession *s) {
63 0 : logger_log(LOG_INFO, "auth_flow: migrating to DC%d (dh_dc=%d)",
64 : new_dc, dh_dc);
65 0 : transport_close(t);
66 0 : mtproto_session_init(s);
67 0 : return connect_dc_ex(new_dc, dh_dc, t, s);
68 : }
69 :
70 : /**
71 : * Soft migrate: reuse the existing auth key, just reconnect TCP to the
72 : * target DC endpoint and update t->dc_id. Used when the server accepts
73 : * only dc=0 for DH (Telegram test DC) so a full re-DH would fail.
74 : */
75 0 : static int migrate_soft(int new_dc, Transport *t) {
76 0 : logger_log(LOG_INFO, "auth_flow: soft-migrating to DC%d (keep auth key)",
77 : new_dc);
78 0 : transport_close(t);
79 0 : const DcEndpoint *ep = dc_lookup(new_dc);
80 0 : if (!ep) {
81 0 : logger_log(LOG_ERROR, "auth_flow: unknown DC id %d", new_dc);
82 0 : return -1;
83 : }
84 0 : if (transport_connect(t, ep->host, ep->port) != 0) {
85 0 : logger_log(LOG_ERROR, "auth_flow: connect failed for DC%d (%s:%d)",
86 0 : new_dc, ep->host, ep->port);
87 0 : return -1;
88 : }
89 0 : t->dc_id = new_dc;
90 0 : return 0;
91 : }
92 :
93 8 : int auth_flow_login(const ApiConfig *cfg,
94 : const AuthFlowCallbacks *cb,
95 : Transport *t, MtProtoSession *s,
96 : AuthFlowResult *out) {
97 8 : if (!cfg || !cb || !t || !s) return -1;
98 4 : if (!cb->get_phone || !cb->get_code) {
99 2 : logger_log(LOG_ERROR, "auth_flow: get_phone/get_code callbacks required");
100 2 : return -1;
101 : }
102 :
103 2 : if (out) memset(out, 0, sizeof(*out));
104 :
105 : /* Fast path: restore a persisted session. */
106 : {
107 2 : int saved_dc = 0;
108 2 : mtproto_session_init(s);
109 2 : if (session_store_load(s, &saved_dc) == 0 && s->has_auth_key) {
110 : /* New session_id per TCP connection; server resets seqno tracking. */
111 1 : mtproto_session_renew_id(s);
112 1 : const DcEndpoint *ep = dc_lookup(saved_dc);
113 1 : if (ep && transport_connect(t, ep->host, ep->port) == 0) {
114 1 : t->dc_id = saved_dc;
115 1 : logger_log(LOG_INFO,
116 : "auth_flow: reusing persisted session on DC%d",
117 : saved_dc);
118 1 : if (out) { out->dc_id = saved_dc; out->user_id = 0; }
119 1 : return 0;
120 : }
121 0 : logger_log(LOG_WARN,
122 : "auth_flow: persisted session unusable, re-login");
123 0 : if (ep) transport_close(t);
124 0 : mtproto_session_init(s);
125 : }
126 : }
127 :
128 1 : int current_dc = cfg->start_dc_set ? cfg->start_dc : DEFAULT_DC_ID;
129 1 : if (connect_dc_ex(current_dc, current_dc, t, s) != 0) return -1;
130 :
131 : char phone[64];
132 0 : if (cb->get_phone(cb->user, phone, sizeof(phone)) != 0) {
133 0 : logger_log(LOG_ERROR, "auth_flow: phone number input failed");
134 0 : return -1;
135 : }
136 :
137 : /* ---- auth.sendCode (with migration) ---- */
138 0 : AuthSentCode sent = {0};
139 0 : int migrations = 0;
140 0 : for (;;) {
141 0 : RpcError err = {0};
142 0 : err.migrate_dc = -1;
143 0 : int rc = auth_send_code(cfg, s, t, phone, &sent, &err);
144 0 : if (rc == 0) break;
145 0 : if (err.migrate_dc > 0 && migrations < AUTH_MAX_MIGRATIONS) {
146 0 : migrations++;
147 0 : int target = (current_dc < 0) ? -err.migrate_dc : err.migrate_dc;
148 0 : if (migrate(target, target, t, s) == 0) {
149 0 : current_dc = target;
150 0 : continue;
151 : }
152 : /* Full re-DH failed (e.g. test DC only accepts dc=0).
153 : * Fall back: reuse existing auth key, just reconnect TCP. */
154 0 : logger_log(LOG_WARN,
155 : "auth_flow: full migrate failed, trying soft migrate");
156 0 : if (migrate_soft(target, t) != 0) return -1;
157 0 : current_dc = target;
158 0 : continue;
159 : }
160 0 : logger_log(LOG_ERROR, "auth_flow: sendCode failed (%d: %s)",
161 : err.error_code, err.error_msg);
162 0 : return -1;
163 : }
164 :
165 : /* If the code was routed to a Telegram app session (sentCodeTypeApp) but the
166 : * next delivery method is SMS, request a resend so the code arrives via SMS.
167 : * This is the standard path for real phone numbers that have no active
168 : * test-DC client to receive the app notification. */
169 0 : if (sent.code_type == CRC_auth_sentCodeTypeApp
170 0 : && sent.next_type == CRC_codeTypeSms) {
171 0 : logger_log(LOG_INFO,
172 : "auth_flow: sentCodeTypeApp with SMS fallback — resending via SMS");
173 0 : RpcError rerr = {0};
174 0 : rerr.migrate_dc = -1;
175 0 : if (auth_resend_code(cfg, s, t, phone, &sent, &rerr) == 0) {
176 0 : logger_log(LOG_INFO, "auth_flow: code resent via SMS");
177 : } else {
178 0 : logger_log(LOG_WARN,
179 : "auth_flow: resend failed (%d: %s); "
180 : "code may still arrive in Telegram app",
181 : rerr.error_code, rerr.error_msg);
182 : }
183 : }
184 :
185 : /* ---- user enters code ---- */
186 : char code[32];
187 0 : if (cb->get_code(cb->user, code, sizeof(code)) != 0) {
188 0 : logger_log(LOG_ERROR, "auth_flow: code input failed");
189 0 : return -1;
190 : }
191 :
192 : /* ---- auth.signIn (with migration; 2FA; sign-up for new accounts) ---- */
193 0 : int64_t uid = 0;
194 : for (;;) {
195 0 : RpcError err = {0};
196 0 : err.migrate_dc = -1;
197 0 : int signup_required = 0;
198 0 : int rc = auth_sign_in(cfg, s, t, phone, sent.phone_code_hash, code,
199 : &uid, &signup_required, &err);
200 0 : if (rc == 0) break;
201 :
202 : /* New account: no existing record for this phone on this DC. */
203 0 : if (signup_required) {
204 0 : logger_log(LOG_INFO,
205 : "auth_flow: new account — registering with signUp");
206 0 : RpcError su_err = {0};
207 0 : if (auth_sign_up(cfg, s, t, phone, sent.phone_code_hash,
208 : "Test", "Account", &uid, &su_err) != 0) {
209 0 : logger_log(LOG_ERROR,
210 : "auth_flow: signUp failed (%d: %s)",
211 : su_err.error_code, su_err.error_msg);
212 0 : return -1;
213 : }
214 0 : break;
215 : }
216 :
217 0 : if (err.migrate_dc > 0 && migrations < AUTH_MAX_MIGRATIONS) {
218 0 : migrations++;
219 0 : int target = (current_dc < 0) ? -err.migrate_dc : err.migrate_dc;
220 0 : if (migrate(target, target, t, s) != 0) return -1;
221 0 : current_dc = target;
222 0 : logger_log(LOG_ERROR,
223 : "auth_flow: unexpected migration during signIn — "
224 : "restart login flow");
225 0 : return -1;
226 : }
227 0 : if (strcmp(err.error_msg, "SESSION_PASSWORD_NEEDED") == 0) {
228 0 : if (out) out->needs_password = 1;
229 0 : if (!cb->get_password) {
230 0 : logger_log(LOG_ERROR,
231 : "auth_flow: 2FA required but no get_password callback");
232 0 : return -1;
233 : }
234 0 : logger_log(LOG_INFO,
235 : "auth_flow: 2FA password required — running SRP flow");
236 :
237 0 : Account2faPassword params = {0};
238 0 : RpcError gp_err = {0};
239 0 : if (auth_2fa_get_password(cfg, s, t, ¶ms, &gp_err) != 0) {
240 0 : logger_log(LOG_ERROR,
241 : "auth_flow: account.getPassword failed (%d: %s)",
242 : gp_err.error_code, gp_err.error_msg);
243 0 : return -1;
244 : }
245 0 : if (!params.has_password) {
246 0 : logger_log(LOG_ERROR,
247 : "auth_flow: SESSION_PASSWORD_NEEDED but server "
248 : "reports no password configured");
249 0 : return -1;
250 : }
251 : char pwd[128];
252 0 : if (cb->get_password(cb->user, pwd, sizeof(pwd)) != 0) {
253 0 : logger_log(LOG_ERROR, "auth_flow: password input failed");
254 0 : return -1;
255 : }
256 0 : RpcError cp_err = {0};
257 0 : int64_t cp_uid = 0;
258 0 : if (auth_2fa_check_password(cfg, s, t, ¶ms, pwd,
259 : &cp_uid, &cp_err) != 0) {
260 0 : logger_log(LOG_ERROR,
261 : "auth_flow: auth.checkPassword failed (%d: %s)",
262 : cp_err.error_code, cp_err.error_msg);
263 0 : return -1;
264 : }
265 0 : uid = cp_uid;
266 0 : break;
267 : }
268 0 : logger_log(LOG_ERROR, "auth_flow: signIn failed (%d: %s)",
269 : err.error_code, err.error_msg);
270 0 : return -1;
271 : }
272 :
273 0 : if (out) {
274 0 : out->dc_id = current_dc;
275 0 : out->user_id = uid;
276 : }
277 0 : logger_log(LOG_INFO, "auth_flow: login complete on DC%d, user_id=%lld",
278 : current_dc, (long long)uid);
279 :
280 : /* Persist so the next run can skip sendCode + signIn. */
281 0 : if (session_store_save(s, current_dc) != 0) {
282 0 : logger_log(LOG_WARN,
283 : "auth_flow: failed to persist session (non-fatal)");
284 : }
285 0 : return 0;
286 : }
|