LCOV - code coverage report
Current view: top level - src/app - credentials.c (source / functions) Coverage Total Hit
Test: coverage-functional.info Lines: 95.5 % 112 107
Test Date: 2026-04-20 19:54:24 Functions: 100.0 % 5 5

            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              : }
        

Generated by: LCOV version 2.0-1