ascii-chat 0.6.0
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
gpg_keys.c
Go to the documentation of this file.
1
7#include "gpg_keys.h"
8#include "../keys_validation.h"
9#include "common.h"
10#include "asciichat_errno.h"
11#include "platform/string.h"
12#include "export.h" // For gpg_get_public_key()
13#include <sodium.h>
14#include <string.h>
15#include <stdlib.h>
16#include <stdio.h>
17#include <time.h>
18
19// Platform-specific popen/pclose
20#ifdef _WIN32
21#define SAFE_POPEN _popen
22#define SAFE_PCLOSE _pclose
23#else
24#define SAFE_POPEN popen
25#define SAFE_PCLOSE pclose
26#endif
27
28// =============================================================================
29// GPG Key Parsing Implementation
30// =============================================================================
31
32asciichat_error_t parse_gpg_key(const char *gpg_key_id, public_key_t *key_out) {
33 if (!gpg_key_id || !key_out) {
34 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: gpg_key_id=%p, key_out=%p", gpg_key_id, key_out);
36 }
37
38 // Validate key ID format (must be 8, 16, or 40 hex characters, optionally with 0x prefix)
39 // - 8 chars: short key ID (last 8 chars of fingerprint)
40 // - 16 chars: long key ID (last 16 chars of fingerprint)
41 // - 40 chars: full fingerprint
42 const char *key_id = gpg_key_id;
43 if (strncmp(key_id, "0x", 2) == 0 || strncmp(key_id, "0X", 2) == 0) {
44 key_id += 2; // Skip 0x prefix
45 }
46
47 size_t key_id_len = strlen(key_id);
48 if (key_id_len != 8 && key_id_len != 16 && key_id_len != 40) {
49 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid GPG key ID length: %zu (expected 8, 16, or 40 hex chars)", key_id_len);
50 }
51
52 // Validate hex characters
53 for (size_t i = 0; i < key_id_len; i++) {
54 char c = key_id[i];
55 if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) {
56 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid GPG key ID: contains non-hex character '%c'", c);
57 }
58 }
59
60 // Extract Ed25519 public key from GPG keyring using gpg --list-keys
61 uint8_t ed25519_pk[32];
62 asciichat_error_t extract_result = extract_ed25519_from_gpg(key_id, ed25519_pk);
63 if (extract_result != ASCIICHAT_OK) {
64 return extract_result;
65 }
66
67 // Initialize the public key structure
68 memset(key_out, 0, sizeof(public_key_t));
69 key_out->type = KEY_TYPE_GPG;
70 memcpy(key_out->key, ed25519_pk, 32);
71
72 // Set comment for display
73 safe_snprintf(key_out->comment, sizeof(key_out->comment), "GPG key %s", key_id);
74
75 return ASCIICHAT_OK;
76}
77
78asciichat_error_t parse_gpg_key_binary(const uint8_t *gpg_key_binary, size_t key_size, public_key_t *key_out) {
79 if (!gpg_key_binary || !key_out) {
80 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: gpg_key_binary=%p, key_out=%p", gpg_key_binary, key_out);
82 }
83
84 if (key_size == 0) {
85 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid key size: %zu", key_size);
87 }
88
89 // TODO: Implement binary GPG key parsing
90 // This requires parsing the OpenPGP packet format
91 SET_ERRNO(ERROR_CRYPTO_KEY, "Binary GPG key parsing not yet implemented");
92 return ERROR_CRYPTO_KEY;
93}
94
95asciichat_error_t extract_ed25519_from_gpg(const char *gpg_key_id, uint8_t ed25519_pk[32]) {
96 if (!gpg_key_id || !ed25519_pk) {
97 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: gpg_key_id=%p, ed25519_pk=%p", gpg_key_id, ed25519_pk);
99 }
100
101 // Use gpg_get_public_key() to extract Ed25519 public key from GPG keyring
102 // Note: gpg_key_id should be the key ID (e.g., "7FE90A79F2E80ED3")
103 char keygrip[64];
104 if (gpg_get_public_key(gpg_key_id, ed25519_pk, keygrip) != 0) {
105 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to extract Ed25519 public key from GPG for key ID: %s", gpg_key_id);
106 }
107
108 return ASCIICHAT_OK;
109}
110
111asciichat_error_t gpg_to_x25519_public(const char *gpg_key_text, uint8_t x25519_pk[32]) {
112 if (!gpg_key_text || !x25519_pk) {
113 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: gpg_key_text=%p, x25519_pk=%p", gpg_key_text, x25519_pk);
114 return ERROR_INVALID_PARAM;
115 }
116
117 // First extract the Ed25519 public key from the GPG key
118 uint8_t ed25519_pk[32];
119 asciichat_error_t extract_result = extract_ed25519_from_gpg(gpg_key_text, ed25519_pk);
120 if (extract_result != ASCIICHAT_OK) {
121 return extract_result;
122 }
123
124 // Convert Ed25519 to X25519
125 if (crypto_sign_ed25519_pk_to_curve25519(x25519_pk, ed25519_pk) != 0) {
126 SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to convert Ed25519 to X25519");
127 return ERROR_CRYPTO_KEY;
128 }
129
130 return ASCIICHAT_OK;
131}
132
133// =============================================================================
134// GPG Key Operations
135// =============================================================================
136
137asciichat_error_t get_gpg_fingerprint(const char *gpg_key_text, uint8_t fingerprint_out[20]) {
138 if (!gpg_key_text || !fingerprint_out) {
139 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: gpg_key_text=%p, fingerprint_out=%p", gpg_key_text,
140 fingerprint_out);
141 return ERROR_INVALID_PARAM;
142 }
143
144 // TODO: Implement GPG fingerprint extraction
145 // This requires parsing the OpenPGP packet structure and computing SHA-1
146
147 SET_ERRNO(ERROR_CRYPTO_KEY, "GPG fingerprint extraction not yet implemented");
148 return ERROR_CRYPTO_KEY;
149}
150
151asciichat_error_t get_gpg_key_id(const char *gpg_key_text, uint8_t key_id_out[8]) {
152 if (!gpg_key_text || !key_id_out) {
153 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: gpg_key_text=%p, key_id_out=%p", gpg_key_text, key_id_out);
154 return ERROR_INVALID_PARAM;
155 }
156
157 // TODO: Implement GPG key ID extraction
158 // This is typically the last 8 bytes of the fingerprint
159
160 SET_ERRNO(ERROR_CRYPTO_KEY, "GPG key ID extraction not yet implemented");
161 return ERROR_CRYPTO_KEY;
162}
163
164asciichat_error_t check_gpg_key_expiry(const char *gpg_key_text, bool *is_expired) {
165 if (!gpg_key_text || !is_expired) {
166 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: gpg_key_text=%p, is_expired=%p", gpg_key_text, is_expired);
167 return ERROR_INVALID_PARAM;
168 }
169
170 // For GPG key expiry checking, we need a key ID
171 // If gpg_key_text is a key ID (8, 16, or 40 hex chars), use it directly
172 // Otherwise, we'd need to parse the GPG armored text (complex)
173 size_t key_len = strlen(gpg_key_text);
174 const char *key_id = gpg_key_text;
175
176 // Check if it looks like a key ID (hex characters only)
177 bool is_hex = true;
178 for (size_t i = 0; i < key_len; i++) {
179 if (!((gpg_key_text[i] >= '0' && gpg_key_text[i] <= '9') || (gpg_key_text[i] >= 'A' && gpg_key_text[i] <= 'F') ||
180 (gpg_key_text[i] >= 'a' && gpg_key_text[i] <= 'f'))) {
181 is_hex = false;
182 break;
183 }
184 }
185
186 // Only support key ID format (8, 16, or 40 hex chars)
187 if (!is_hex || (key_len != 8 && key_len != 16 && key_len != 40)) {
188 log_warn("check_gpg_key_expiry: Input is not a key ID format (expected 8/16/40 hex chars)");
189 *is_expired = false; // Assume not expired if we can't check
190 return ASCIICHAT_OK;
191 }
192
193 // Use gpg --list-keys with colon-separated output to check expiry
194 char cmd[512];
195 safe_snprintf(cmd, sizeof(cmd), "gpg --list-keys --with-colons %s 2>/dev/null", key_id);
196
197 FILE *fp = SAFE_POPEN(cmd, "r");
198 if (!fp) {
199 log_error("Failed to run gpg --list-keys for key %s", key_id);
200 *is_expired = false; // Assume not expired if we can't check
201 return ASCIICHAT_OK;
202 }
203
204 char line[1024];
205 bool found_pub = false;
206 *is_expired = false;
207
208 // Parse colon-separated output
209 // Format: pub:trust:keylen:algo:keyid:creation:expiry:...
210 // Field 6 is expiry timestamp (seconds since epoch), or empty if no expiry
211 while (fgets(line, sizeof(line), fp)) {
212 if (strncmp(line, "pub:", 4) == 0) {
213 found_pub = true;
214
215 // Split line by colons
216 char *fields[12];
217 int field_count = 0;
218 char *ptr = line;
219 char *field_start = ptr;
220
221 while (*ptr && field_count < 12) {
222 if (*ptr == ':' || *ptr == '\n') {
223 *ptr = '\0';
224 fields[field_count++] = field_start;
225 field_start = ptr + 1;
226 }
227 ptr++;
228 }
229
230 // Field 6 is expiry date (index 6)
231 if (field_count >= 7 && strlen(fields[6]) > 0) {
232 // Parse expiry timestamp
233 long expiry_timestamp = atol(fields[6]);
234 time_t now = time(NULL);
235
236 if (expiry_timestamp > 0 && expiry_timestamp < now) {
237 *is_expired = true;
238 log_warn("GPG key %s has expired (expiry: %ld, now: %ld)", key_id, expiry_timestamp, (long)now);
239 } else if (expiry_timestamp > 0) {
240 log_debug("GPG key %s expires at timestamp %ld (valid)", key_id, expiry_timestamp);
241 } else {
242 log_debug("GPG key %s has no expiration date", key_id);
243 }
244 } else {
245 log_debug("GPG key %s has no expiration date (field empty)", key_id);
246 }
247
248 break; // Found the pub line, stop parsing
249 }
250 }
251
252 SAFE_PCLOSE(fp);
253
254 if (!found_pub) {
255 log_warn("Could not find GPG key %s in keyring", key_id);
256 *is_expired = false; // Assume not expired if key not found
257 }
258
259 return ASCIICHAT_OK;
260}
261
262// =============================================================================
263// GPG Key Formatting
264// =============================================================================
265
266asciichat_error_t format_gpg_key_display(const char *gpg_key_text, char *output, size_t output_size) {
267 if (!gpg_key_text || !output) {
268 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: gpg_key_text=%p, output=%p", gpg_key_text, output);
269 return ERROR_INVALID_PARAM;
270 }
271
272 if (output_size < 64) {
273 SET_ERRNO(ERROR_INVALID_PARAM, "Output buffer too small: %zu (minimum 64)", output_size);
274 return ERROR_INVALID_PARAM;
275 }
276
277 // Extract key ID for display
278 uint8_t key_id[8];
279 asciichat_error_t key_id_result = get_gpg_key_id(gpg_key_text, key_id);
280 if (key_id_result != ASCIICHAT_OK) {
281 // Fallback to generic GPG key display
282 SAFE_STRNCPY(output, "GPG key (key ID extraction failed)", output_size - 1);
283 return ASCIICHAT_OK;
284 }
285
286 // Format as hex key ID
287 char hex_key_id[17];
288 for (int i = 0; i < 8; i++) {
289 safe_snprintf(hex_key_id + i * 2, 3, "%02x", key_id[i]);
290 }
291 hex_key_id[16] = '\0';
292
293 // Create display string
294 int result = safe_snprintf(output, output_size, "GPG key ID: %s", hex_key_id);
295 if (result < 0 || result >= (int)output_size) {
296 SET_ERRNO(ERROR_STRING, "Failed to format GPG key display");
297 return ERROR_STRING;
298 }
299
300 return ASCIICHAT_OK;
301}
302
303asciichat_error_t extract_gpg_key_comment(const char *gpg_key_text, char *comment_out, size_t comment_size) {
304 if (!gpg_key_text || !comment_out) {
305 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: gpg_key_text=%p, comment_out=%p", gpg_key_text, comment_out);
306 return ERROR_INVALID_PARAM;
307 }
308
309 if (comment_size == 0) {
310 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid comment size: %zu", comment_size);
311 return ERROR_INVALID_PARAM;
312 }
313
314 // TODO: Implement GPG key comment extraction
315 // This requires parsing the OpenPGP packet structure and extracting user ID packets
316
317 // For now, use a generic comment
318 SAFE_STRNCPY(comment_out, "GPG key", comment_size - 1);
319 return ASCIICHAT_OK;
320}
⚠️‼️ Error and/or exit() when things go bad.
GPG public key export interface.
#define SAFE_PCLOSE
Definition gpg_keys.c:25
#define SAFE_POPEN
Definition gpg_keys.c:24
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:358
unsigned char uint8_t
Definition common.h:56
int gpg_get_public_key(const char *key_id, uint8_t *public_key_out, char *keygrip_out)
Get public key from GPG keyring by key ID.
Definition export.c:251
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:46
@ ERROR_CRYPTO_KEY
Definition error_codes.h:89
@ ASCIICHAT_OK
Definition error_codes.h:48
@ ERROR_STRING
@ ERROR_INVALID_PARAM
uint8_t key[32]
Definition key_types.h:71
asciichat_error_t check_gpg_key_expiry(const char *gpg_key_text, bool *is_expired)
Check if GPG key is expired.
Definition gpg_keys.c:164
char comment[256]
Definition key_types.h:72
asciichat_error_t extract_ed25519_from_gpg(const char *gpg_key_id, uint8_t ed25519_pk[32])
Extract Ed25519 public key from GPG key.
Definition gpg_keys.c:95
asciichat_error_t extract_gpg_key_comment(const char *gpg_key_text, char *comment_out, size_t comment_size)
Extract key comment/email from GPG key.
Definition gpg_keys.c:303
asciichat_error_t get_gpg_key_id(const char *gpg_key_text, uint8_t key_id_out[8])
Get GPG key ID (short fingerprint)
Definition gpg_keys.c:151
asciichat_error_t parse_gpg_key(const char *gpg_key_id, public_key_t *key_out)
Parse GPG key from armored text format.
Definition gpg_keys.c:32
asciichat_error_t get_gpg_fingerprint(const char *gpg_key_text, uint8_t fingerprint_out[20])
Get GPG key fingerprint.
Definition gpg_keys.c:137
asciichat_error_t parse_gpg_key_binary(const uint8_t *gpg_key_binary, size_t key_size, public_key_t *key_out)
Parse GPG key from binary format.
Definition gpg_keys.c:78
asciichat_error_t format_gpg_key_display(const char *gpg_key_text, char *output, size_t output_size)
Format GPG key for display.
Definition gpg_keys.c:266
key_type_t type
Definition key_types.h:70
asciichat_error_t gpg_to_x25519_public(const char *gpg_key_text, uint8_t x25519_pk[32])
Convert GPG key to X25519 for key exchange.
Definition gpg_keys.c:111
@ KEY_TYPE_GPG
Definition key_types.h:54
#define log_warn(...)
Log a WARN message.
#define log_error(...)
Log an ERROR message.
#define log_debug(...)
Log a DEBUG message.
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe version of snprintf that ensures null termination.
Platform-independent safe string functions.
Public key structure.
Definition key_types.h:69
⏱️ High-precision timing utilities using sokol_time.h and uthash
🔤 String Manipulation and Shell Escaping Utilities