Line data Source code
1 : /* SPDX-License-Identifier: GPL-3.0-or-later */
2 : /* Copyright 2026 Peter Csaszar */
3 :
4 : /**
5 : * @file app/credentials.c
6 : * @brief Env + INI-file api_id/api_hash loader.
7 : *
8 : * The INI parser tolerates (US-33):
9 : * - CRLF line endings (\r stripped in trim).
10 : * - UTF-8 BOM (EF BB BF) at file start.
11 : * - Comment lines starting with `#` or `;` (ignored entirely).
12 : * - Leading/trailing whitespace around both key and value.
13 : * - Double-quoted values: api_hash="abcdef..." → quotes stripped.
14 : * - Duplicate keys: last occurrence wins, one LOG_WARN per duplicate.
15 : * - Empty values (`api_id=`) → treated as missing, targeted diagnostic.
16 : *
17 : * Partial credentials produce explicit diagnostics pointing at the wizard:
18 : * - Only api_id missing → "api_id not found ..."
19 : * - Only api_hash missing → "api_hash not found ..."
20 : * - Both missing → combined message (legacy wording).
21 : *
22 : * api_hash must be a 32-char lowercase hex string; any other length is
23 : * rejected with a dedicated LOG_ERROR so a truncated copy-paste never
24 : * silently becomes the production credential.
25 : */
26 :
27 : #include "app/credentials.h"
28 :
29 : #include "logger.h"
30 : #include "platform/path.h"
31 : #include "raii.h"
32 :
33 : #include <ctype.h>
34 : #include <stdio.h>
35 : #include <stdlib.h>
36 : #include <string.h>
37 :
38 : #define API_HASH_MAX 64
39 : #define API_HASH_EXPECT 32 /* Telegram api_hash is 32 lowercase hex chars. */
40 :
41 : static char g_api_hash_buf[API_HASH_MAX + 1];
42 :
43 : /** Strip trailing whitespace / CR / LF in place. */
44 23 : static void rtrim_inplace(char *s) {
45 23 : size_t n = strlen(s);
46 51 : while (n > 0 && (s[n - 1] == '\n' || s[n - 1] == '\r' ||
47 25 : s[n - 1] == ' ' || s[n - 1] == '\t')) {
48 28 : s[--n] = '\0';
49 : }
50 23 : }
51 :
52 : /** Strip one pair of matched double quotes surrounding @p s, in place. */
53 23 : static void unquote_inplace(char *s) {
54 23 : size_t n = strlen(s);
55 23 : if (n >= 2 && s[0] == '"' && s[n - 1] == '"') {
56 1 : memmove(s, s + 1, n - 2);
57 1 : s[n - 2] = '\0';
58 : }
59 23 : }
60 :
61 : /**
62 : * Reads the value of @p key from the INI at @p path into @p out.
63 : *
64 : * @return 0 on success (value copied),
65 : * -1 if the file cannot be opened,
66 : * -2 if the key is not present,
67 : * -3 if the key exists but the value is empty.
68 : *
69 : * Emits LOG_WARN if the key appears more than once; the last occurrence
70 : * wins.
71 : */
72 28 : static int read_ini_key(const char *path, const char *key,
73 : char *out, size_t cap) {
74 56 : RAII_FILE FILE *fp = fopen(path, "r");
75 28 : if (!fp) return -1;
76 :
77 : char line[512];
78 26 : size_t klen = strlen(key);
79 26 : int found = 0;
80 26 : int empty_value = 0;
81 :
82 : /* UTF-8 BOM (EF BB BF) at file start is consumed on the first read so
83 : * that the first key on line 1 is still recognised. */
84 26 : int first_line = 1;
85 :
86 80 : while (fgets(line, sizeof(line), fp)) {
87 54 : char *p = line;
88 :
89 54 : if (first_line) {
90 24 : first_line = 0;
91 24 : if ((unsigned char)p[0] == 0xEF &&
92 2 : (unsigned char)p[1] == 0xBB &&
93 2 : (unsigned char)p[2] == 0xBF) {
94 2 : p += 3;
95 : }
96 : }
97 :
98 : /* Skip leading whitespace. */
99 60 : while (*p == ' ' || *p == '\t') p++;
100 :
101 : /* Blank line or comment (# or ;) — skip entirely. */
102 54 : if (*p == '\0' || *p == '\n' || *p == '\r' ||
103 54 : *p == '#' || *p == ';') {
104 8 : continue;
105 : }
106 :
107 46 : if (strncmp(p, key, klen) != 0) continue;
108 23 : p += klen;
109 25 : while (*p == ' ' || *p == '\t') p++;
110 23 : if (*p != '=') continue;
111 23 : p++;
112 26 : while (*p == ' ' || *p == '\t') p++;
113 :
114 : /* Copy the remainder then normalise: trim tail, strip quotes. */
115 23 : size_t n = strlen(p);
116 23 : if (n >= cap) n = cap - 1;
117 :
118 23 : if (found) {
119 1 : logger_log(LOG_WARN,
120 : "credentials: duplicate '%s=' in %s — using the "
121 : "last occurrence", key, path);
122 : }
123 :
124 23 : memcpy(out, p, n);
125 23 : out[n] = '\0';
126 23 : rtrim_inplace(out);
127 23 : unquote_inplace(out);
128 :
129 23 : empty_value = (out[0] == '\0');
130 23 : found = 1;
131 : /* Do not early-return: keep scanning so we detect duplicates and
132 : * honour last-wins semantics. */
133 : }
134 :
135 26 : if (!found) return -2;
136 22 : if (empty_value) return -3;
137 21 : return 0;
138 : }
139 :
140 : /** Return 1 if @p s is a 32-char lowercase-hex string, else 0. */
141 12 : static int is_valid_api_hash(const char *s) {
142 12 : if (!s) return 0;
143 12 : size_t n = strlen(s);
144 12 : if (n != API_HASH_EXPECT) return 0;
145 330 : for (size_t i = 0; i < n; i++) {
146 320 : unsigned char c = (unsigned char)s[i];
147 320 : if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) return 0;
148 : }
149 10 : return 1;
150 : }
151 :
152 15 : int credentials_load(ApiConfig *out) {
153 15 : if (!out) return -1;
154 15 : api_config_init(out);
155 :
156 : /* -- api_id -- */
157 15 : int api_id = 0;
158 15 : const char *env_id = getenv("TG_CLI_API_ID");
159 15 : if (env_id && *env_id) {
160 1 : api_id = atoi(env_id);
161 : }
162 :
163 : /* -- api_hash from env -- */
164 15 : const char *env_hash = getenv("TG_CLI_API_HASH");
165 15 : if (env_hash && *env_hash) {
166 1 : size_t n = strlen(env_hash);
167 1 : if (n > API_HASH_MAX) n = API_HASH_MAX;
168 1 : memcpy(g_api_hash_buf, env_hash, n);
169 1 : g_api_hash_buf[n] = '\0';
170 : } else {
171 14 : g_api_hash_buf[0] = '\0';
172 : }
173 :
174 : /* -- Fall back to ~/.config/tg-cli/config.ini for missing values -- */
175 15 : int hash_len_rejected = 0;
176 29 : if (api_id == 0 || g_api_hash_buf[0] == '\0') {
177 14 : const char *cfg_dir = platform_config_dir();
178 14 : if (cfg_dir) {
179 : char path[1024];
180 14 : snprintf(path, sizeof(path), "%s/tg-cli/config.ini", cfg_dir);
181 :
182 14 : if (api_id == 0) {
183 : char buf[32];
184 14 : int rc = read_ini_key(path, "api_id", buf, sizeof(buf));
185 14 : if (rc == 0) {
186 10 : api_id = atoi(buf);
187 4 : } else if (rc == -3) {
188 1 : logger_log(LOG_WARN,
189 : "credentials: api_id is set to an empty "
190 : "value in %s — ignoring", path);
191 : }
192 : }
193 14 : if (g_api_hash_buf[0] == '\0') {
194 14 : int rc = read_ini_key(path, "api_hash",
195 : g_api_hash_buf,
196 : sizeof(g_api_hash_buf));
197 14 : if (rc == -3) {
198 0 : logger_log(LOG_WARN,
199 : "credentials: api_hash is set to an empty "
200 : "value in %s — ignoring", path);
201 0 : g_api_hash_buf[0] = '\0';
202 14 : } else if (rc == 0 && !is_valid_api_hash(g_api_hash_buf)) {
203 2 : logger_log(LOG_ERROR,
204 : "credentials: api_hash in %s is not a 32-"
205 : "character lowercase hex string — rejecting",
206 : path);
207 2 : g_api_hash_buf[0] = '\0';
208 2 : hash_len_rejected = 1;
209 : }
210 : }
211 : }
212 1 : } else if (!is_valid_api_hash(g_api_hash_buf)) {
213 : /* Env-var supplied a bad-length hash. */
214 0 : logger_log(LOG_ERROR,
215 : "credentials: TG_CLI_API_HASH is not a 32-character "
216 : "lowercase hex string — rejecting");
217 0 : g_api_hash_buf[0] = '\0';
218 0 : hash_len_rejected = 1;
219 : }
220 :
221 15 : int id_missing = (api_id == 0);
222 15 : int hash_missing = (g_api_hash_buf[0] == '\0');
223 :
224 15 : if (id_missing && hash_missing) {
225 2 : logger_log(LOG_ERROR,
226 : "credentials: api_id/api_hash not found. Set "
227 : "TG_CLI_API_ID and TG_CLI_API_HASH env vars, or add "
228 : "api_id=/api_hash= lines to ~/.config/tg-cli/config.ini "
229 : "(run `tg-cli config --wizard` to generate it).");
230 2 : return -1;
231 : }
232 13 : if (id_missing) {
233 2 : logger_log(LOG_ERROR,
234 : "credentials: api_id not found. Set TG_CLI_API_ID or "
235 : "add `api_id=...` to ~/.config/tg-cli/config.ini "
236 : "(run `tg-cli config --wizard`).");
237 2 : return -1;
238 : }
239 11 : if (hash_missing) {
240 3 : if (hash_len_rejected) {
241 2 : logger_log(LOG_ERROR,
242 : "credentials: api_hash rejected (wrong length or "
243 : "non-hex). Obtain a 32-char lowercase hex hash "
244 : "from https://my.telegram.org and re-run "
245 : "`tg-cli config --wizard`.");
246 : } else {
247 1 : logger_log(LOG_ERROR,
248 : "credentials: api_hash not found. Set "
249 : "TG_CLI_API_HASH or add `api_hash=...` to "
250 : "~/.config/tg-cli/config.ini "
251 : "(run `tg-cli config --wizard`).");
252 : }
253 3 : return -1;
254 : }
255 :
256 8 : out->api_id = api_id;
257 8 : out->api_hash = g_api_hash_buf;
258 8 : return 0;
259 : }
|