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