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 5 : int auth_flow_connect_dc(int dc_id, Transport *t, MtProtoSession *s) {
26 5 : if (!t || !s) return -1;
27 :
28 3 : const DcEndpoint *ep = dc_lookup(dc_id);
29 3 : if (!ep) {
30 1 : logger_log(LOG_ERROR, "auth_flow: unknown DC id %d", dc_id);
31 1 : return -1;
32 : }
33 :
34 2 : if (transport_connect(t, ep->host, ep->port) != 0) {
35 1 : logger_log(LOG_ERROR, "auth_flow: connect failed for DC%d (%s:%d)",
36 1 : dc_id, ep->host, ep->port);
37 1 : return -1;
38 : }
39 1 : t->dc_id = dc_id;
40 :
41 1 : if (mtproto_auth_key_gen(t, s) != 0) {
42 1 : logger_log(LOG_ERROR, "auth_flow: DH auth key gen failed on DC%d",
43 : dc_id);
44 1 : transport_close(t);
45 1 : return -1;
46 : }
47 0 : logger_log(LOG_INFO, "auth_flow: connected to DC%d, auth key ready", dc_id);
48 0 : return 0;
49 : }
50 :
51 : /** Tear down the current session/transport and reconnect to a new DC. */
52 0 : static int migrate(int new_dc, Transport *t, MtProtoSession *s) {
53 0 : logger_log(LOG_INFO, "auth_flow: migrating to DC%d", new_dc);
54 0 : transport_close(t);
55 : /* Re-init session — auth key is DC-scoped. */
56 0 : mtproto_session_init(s);
57 0 : return auth_flow_connect_dc(new_dc, t, s);
58 : }
59 :
60 8 : int auth_flow_login(const ApiConfig *cfg,
61 : const AuthFlowCallbacks *cb,
62 : Transport *t, MtProtoSession *s,
63 : AuthFlowResult *out) {
64 8 : if (!cfg || !cb || !t || !s) return -1;
65 4 : if (!cb->get_phone || !cb->get_code) {
66 2 : logger_log(LOG_ERROR, "auth_flow: get_phone/get_code callbacks required");
67 2 : return -1;
68 : }
69 :
70 2 : if (out) memset(out, 0, sizeof(*out));
71 :
72 : /* Fast path: restore a persisted session. */
73 : {
74 2 : int saved_dc = 0;
75 2 : mtproto_session_init(s);
76 2 : if (session_store_load(s, &saved_dc) == 0 && s->has_auth_key) {
77 1 : const DcEndpoint *ep = dc_lookup(saved_dc);
78 1 : if (ep && transport_connect(t, ep->host, ep->port) == 0) {
79 1 : t->dc_id = saved_dc;
80 1 : logger_log(LOG_INFO,
81 : "auth_flow: reusing persisted session on DC%d",
82 : saved_dc);
83 1 : if (out) { out->dc_id = saved_dc; out->user_id = 0; }
84 1 : return 0;
85 : }
86 0 : logger_log(LOG_WARN,
87 : "auth_flow: persisted session unusable, re-login");
88 0 : if (ep) transport_close(t);
89 0 : mtproto_session_init(s);
90 : }
91 : }
92 :
93 1 : int current_dc = DEFAULT_DC_ID;
94 1 : if (auth_flow_connect_dc(current_dc, t, s) != 0) return -1;
95 :
96 : char phone[64];
97 0 : if (cb->get_phone(cb->user, phone, sizeof(phone)) != 0) {
98 0 : logger_log(LOG_ERROR, "auth_flow: phone number input failed");
99 0 : return -1;
100 : }
101 :
102 : /* ---- auth.sendCode (with migration) ---- */
103 0 : AuthSentCode sent = {0};
104 0 : int migrations = 0;
105 0 : for (;;) {
106 0 : RpcError err = {0};
107 0 : err.migrate_dc = -1;
108 0 : int rc = auth_send_code(cfg, s, t, phone, &sent, &err);
109 0 : if (rc == 0) break;
110 0 : if (err.migrate_dc > 0 && migrations < AUTH_MAX_MIGRATIONS) {
111 0 : migrations++;
112 0 : if (migrate(err.migrate_dc, t, s) != 0) return -1;
113 0 : current_dc = err.migrate_dc;
114 0 : continue;
115 : }
116 0 : logger_log(LOG_ERROR, "auth_flow: sendCode failed (%d: %s)",
117 : err.error_code, err.error_msg);
118 0 : return -1;
119 : }
120 :
121 : /* ---- user enters code ---- */
122 : char code[32];
123 0 : if (cb->get_code(cb->user, code, sizeof(code)) != 0) {
124 0 : logger_log(LOG_ERROR, "auth_flow: code input failed");
125 0 : return -1;
126 : }
127 :
128 : /* ---- auth.signIn (with migration; 2FA detected but unimplemented) ---- */
129 0 : int64_t uid = 0;
130 : for (;;) {
131 0 : RpcError err = {0};
132 0 : err.migrate_dc = -1;
133 0 : int rc = auth_sign_in(cfg, s, t, phone, sent.phone_code_hash, code,
134 : &uid, &err);
135 0 : if (rc == 0) break;
136 0 : if (err.migrate_dc > 0 && migrations < AUTH_MAX_MIGRATIONS) {
137 0 : migrations++;
138 0 : if (migrate(err.migrate_dc, t, s) != 0) return -1;
139 0 : current_dc = err.migrate_dc;
140 : /* After migration we must re-send the code from scratch because
141 : * phone_code_hash is DC-scoped. Loop back via a recursive call
142 : * after the outer caller handles the new state. For now we
143 : * signal failure with a clear diagnostic. */
144 0 : logger_log(LOG_ERROR,
145 : "auth_flow: unexpected migration during signIn — "
146 : "restart login flow");
147 0 : return -1;
148 : }
149 0 : if (strcmp(err.error_msg, "SESSION_PASSWORD_NEEDED") == 0) {
150 0 : if (out) out->needs_password = 1;
151 0 : if (!cb->get_password) {
152 0 : logger_log(LOG_ERROR,
153 : "auth_flow: 2FA required but no get_password callback");
154 0 : return -1;
155 : }
156 0 : logger_log(LOG_INFO,
157 : "auth_flow: 2FA password required — running SRP flow");
158 :
159 0 : Account2faPassword params = {0};
160 0 : RpcError gp_err = {0};
161 0 : if (auth_2fa_get_password(cfg, s, t, ¶ms, &gp_err) != 0) {
162 0 : logger_log(LOG_ERROR,
163 : "auth_flow: account.getPassword failed (%d: %s)",
164 : gp_err.error_code, gp_err.error_msg);
165 0 : return -1;
166 : }
167 0 : if (!params.has_password) {
168 0 : logger_log(LOG_ERROR,
169 : "auth_flow: SESSION_PASSWORD_NEEDED but server "
170 : "reports no password configured");
171 0 : return -1;
172 : }
173 : char pwd[128];
174 0 : if (cb->get_password(cb->user, pwd, sizeof(pwd)) != 0) {
175 0 : logger_log(LOG_ERROR, "auth_flow: password input failed");
176 0 : return -1;
177 : }
178 0 : RpcError cp_err = {0};
179 0 : int64_t cp_uid = 0;
180 0 : if (auth_2fa_check_password(cfg, s, t, ¶ms, pwd,
181 : &cp_uid, &cp_err) != 0) {
182 0 : logger_log(LOG_ERROR,
183 : "auth_flow: auth.checkPassword failed (%d: %s)",
184 : cp_err.error_code, cp_err.error_msg);
185 0 : return -1;
186 : }
187 0 : uid = cp_uid;
188 0 : break;
189 : }
190 0 : logger_log(LOG_ERROR, "auth_flow: signIn failed (%d: %s)",
191 : err.error_code, err.error_msg);
192 0 : return -1;
193 : }
194 :
195 0 : if (out) {
196 0 : out->dc_id = current_dc;
197 0 : out->user_id = uid;
198 : }
199 0 : logger_log(LOG_INFO, "auth_flow: login complete on DC%d, user_id=%lld",
200 : current_dc, (long long)uid);
201 :
202 : /* Persist so the next run can skip sendCode + signIn. */
203 0 : if (session_store_save(s, current_dc) != 0) {
204 0 : logger_log(LOG_WARN,
205 : "auth_flow: failed to persist session (non-fatal)");
206 : }
207 0 : return 0;
208 : }
|