ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
openpgp.c File Reference

OpenPGP (RFC 4880) packet format parser implementation. More...

Go to the source code of this file.

Functions

asciichat_error_t openpgp_base64_decode (const char *base64, size_t base64_len, uint8_t **binary_out, size_t *binary_len)
 
asciichat_error_t openpgp_parse_packet_header (const uint8_t *data, size_t data_len, openpgp_packet_header_t *header)
 
asciichat_error_t openpgp_extract_ed25519_from_mpi (const uint8_t *mpi, size_t mpi_len, uint8_t ed25519_pk[32])
 
asciichat_error_t openpgp_parse_public_key_packet (const uint8_t *packet_body, size_t body_len, openpgp_public_key_t *pubkey)
 
asciichat_error_t openpgp_parse_armored_pubkey (const char *armored_text, uint8_t ed25519_pk[32])
 
asciichat_error_t openpgp_parse_secret_key_packet (const uint8_t *packet_body, size_t body_len, openpgp_secret_key_t *seckey)
 
asciichat_error_t openpgp_parse_armored_seckey (const char *armored_text, uint8_t ed25519_pk[32], uint8_t ed25519_sk[32])
 

Detailed Description

OpenPGP (RFC 4880) packet format parser implementation.

Definition in file openpgp.c.

Function Documentation

◆ openpgp_base64_decode()

asciichat_error_t openpgp_base64_decode ( const char *  base64,
size_t  base64_len,
uint8_t **  binary_out,
size_t *  binary_len 
)

Definition at line 32 of file openpgp.c.

33 {
34 if (!base64 || !binary_out || !binary_len) {
35 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for base64 decode");
36 }
37
38 // Remove whitespace from base64 input (PGP armor has newlines)
39 char *clean_base64 = SAFE_MALLOC(base64_len + 1, char *);
40 char *clean_ptr = clean_base64;
41 for (size_t i = 0; i < base64_len; i++) {
42 if (base64[i] != '\n' && base64[i] != '\r' && base64[i] != ' ' && base64[i] != '\t') {
43 *clean_ptr++ = base64[i];
44 }
45 }
46 *clean_ptr = '\0';
47 size_t clean_len = (size_t)(clean_ptr - clean_base64);
48
49 // Allocate max possible output size
50 *binary_out = SAFE_MALLOC(clean_len, uint8_t *);
51
52 const char *end;
53 int result = sodium_base642bin(*binary_out, clean_len, clean_base64, clean_len, NULL, binary_len, &end,
54 sodium_base64_VARIANT_ORIGINAL);
55
56 SAFE_FREE(clean_base64);
57
58 if (result != 0) {
59 SAFE_FREE(*binary_out);
60 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to decode base64 PGP armored data");
61 }
62
63 return ASCIICHAT_OK;
64}

Referenced by openpgp_parse_armored_pubkey(), and openpgp_parse_armored_seckey().

◆ openpgp_extract_ed25519_from_mpi()

asciichat_error_t openpgp_extract_ed25519_from_mpi ( const uint8_t *  mpi,
size_t  mpi_len,
uint8_t  ed25519_pk[32] 
)

Definition at line 167 of file openpgp.c.

167 {
168 if (!mpi || !ed25519_pk || mpi_len < 35) {
169 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for MPI extraction (need at least 35 bytes)");
170 }
171
172 // MPI format:
173 // - 2 bytes: bit count (big-endian)
174 // - 1 byte: 0x40 prefix (Ed25519 marker)
175 // - 32 bytes: Ed25519 public key
176
177 uint16_t bit_count = ((uint16_t)mpi[0] << 8) | mpi[1];
178 log_debug("MPI bit count: %u", bit_count);
179
180 // Ed25519 with 0x40 prefix is typically 263 bits (0x0107)
181 if (bit_count < 256 || bit_count > 270) {
182 return SET_ERRNO(ERROR_CRYPTO_KEY, "Unexpected MPI bit count for Ed25519: %u (expected ~263)", bit_count);
183 }
184
185 // Check for 0x40 prefix byte
186 if (mpi[2] != 0x40) {
187 return SET_ERRNO(ERROR_CRYPTO_KEY, "Missing 0x40 prefix byte in Ed25519 MPI (found 0x%02x)", mpi[2]);
188 }
189
190 // Extract 32-byte Ed25519 public key
191 memcpy(ed25519_pk, mpi + 3, 32);
192
193 return ASCIICHAT_OK;
194}

◆ openpgp_parse_armored_pubkey()

asciichat_error_t openpgp_parse_armored_pubkey ( const char *  armored_text,
uint8_t  ed25519_pk[32] 
)

Definition at line 290 of file openpgp.c.

290 {
291 if (!armored_text || !ed25519_pk) {
292 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for armored pubkey parsing");
293 }
294
295 // Find the BEGIN marker
296 const char *begin = strstr(armored_text, "-----BEGIN PGP PUBLIC KEY BLOCK-----");
297 if (!begin) {
298 return SET_ERRNO(ERROR_CRYPTO_KEY, "Missing PGP PUBLIC KEY BLOCK BEGIN marker");
299 }
300
301 // Skip to the end of the BEGIN line
302 const char *base64_start = strchr(begin, '\n');
303 if (!base64_start) {
304 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid PGP armored format: no newline after BEGIN marker");
305 }
306 base64_start++; // Skip the newline
307
308 // Find the END marker
309 const char *end = strstr(base64_start, "-----END PGP PUBLIC KEY BLOCK-----");
310 if (!end) {
311 return SET_ERRNO(ERROR_CRYPTO_KEY, "Missing PGP PUBLIC KEY BLOCK END marker");
312 }
313
314 // Find the checksum line (starts with '=') and exclude it
315 const char *base64_end = end;
316 const char *checksum = base64_end;
317 while (checksum > base64_start && *checksum != '=') {
318 checksum--;
319 }
320 if (*checksum == '=') {
321 // Move back to before the checksum line
322 while (checksum > base64_start && (checksum[-1] == '\n' || checksum[-1] == '\r')) {
323 checksum--;
324 }
325 base64_end = checksum;
326 }
327
328 size_t base64_len = (size_t)(base64_end - base64_start);
329
330 log_debug("Extracting base64 data from PGP armor (%zu bytes)", base64_len);
331
332 // Decode base64 to binary OpenPGP packets
333 uint8_t *binary_data;
334 size_t binary_len;
335 asciichat_error_t decode_result = openpgp_base64_decode(base64_start, base64_len, &binary_data, &binary_len);
336 if (decode_result != ASCIICHAT_OK) {
337 return decode_result;
338 }
339
340 log_debug("Decoded %zu bytes of OpenPGP packet data", binary_len);
341
342 // Parse OpenPGP packets to find the public key packet (tag 6)
343 size_t offset = 0;
344 bool found_pubkey = false;
345
346 while (offset < binary_len) {
347 openpgp_packet_header_t header;
348 asciichat_error_t header_result = openpgp_parse_packet_header(binary_data + offset, binary_len - offset, &header);
349 if (header_result != ASCIICHAT_OK) {
350 SAFE_FREE(binary_data);
351 return header_result;
352 }
353
354 log_debug("Packet at offset %zu: tag=%u, length=%zu", offset, header.tag, header.length);
355
356 // Check if this is a public key packet (tag 6)
357 if (header.tag == OPENPGP_TAG_PUBLIC_KEY) {
358 openpgp_public_key_t pubkey;
359 asciichat_error_t parse_result =
360 openpgp_parse_public_key_packet(binary_data + offset + header.header_len, header.length, &pubkey);
361
362 if (parse_result == ASCIICHAT_OK) {
363 memcpy(ed25519_pk, pubkey.pubkey, 32);
364 found_pubkey = true;
365 log_debug("Extracted Ed25519 public key from OpenPGP armored block");
366 break;
367 } else {
368 // Not an Ed25519 key, try next packet
369 log_debug("Skipping non-Ed25519 public key packet");
370 }
371 }
372
373 // Move to next packet
374 offset += header.header_len + header.length;
375 }
376
377 SAFE_FREE(binary_data);
378
379 if (!found_pubkey) {
380 return SET_ERRNO(ERROR_CRYPTO_KEY, "No Ed25519 public key found in PGP armored block");
381 }
382
383 return ASCIICHAT_OK;
384}
asciichat_error_t openpgp_base64_decode(const char *base64, size_t base64_len, uint8_t **binary_out, size_t *binary_len)
Definition openpgp.c:32
asciichat_error_t openpgp_parse_packet_header(const uint8_t *data, size_t data_len, openpgp_packet_header_t *header)
Definition openpgp.c:70
asciichat_error_t openpgp_parse_public_key_packet(const uint8_t *packet_body, size_t body_len, openpgp_public_key_t *pubkey)
Definition openpgp.c:200

References openpgp_base64_decode(), openpgp_parse_packet_header(), and openpgp_parse_public_key_packet().

Referenced by parse_gpg_key_binary().

◆ openpgp_parse_armored_seckey()

asciichat_error_t openpgp_parse_armored_seckey ( const char *  armored_text,
uint8_t  ed25519_pk[32],
uint8_t  ed25519_sk[32] 
)

Definition at line 649 of file openpgp.c.

650 {
651 if (!armored_text || !ed25519_pk || !ed25519_sk) {
652 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for armored seckey parsing");
653 }
654
655 // Find the BEGIN marker (try both "PRIVATE KEY" and "SECRET KEY" formats)
656 const char *begin = strstr(armored_text, "-----BEGIN PGP PRIVATE KEY BLOCK-----");
657 if (!begin) {
658 begin = strstr(armored_text, "-----BEGIN PGP SECRET KEY BLOCK-----");
659 }
660 if (!begin) {
661 return SET_ERRNO(ERROR_CRYPTO_KEY, "Missing PGP PRIVATE/SECRET KEY BLOCK BEGIN marker");
662 }
663
664 // Skip to the end of the BEGIN line
665 const char *base64_start = strchr(begin, '\n');
666 if (!base64_start) {
667 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid PGP armored format: no newline after BEGIN marker");
668 }
669 base64_start++; // Skip the newline
670
671 // Find the END marker (try both formats)
672 const char *end = strstr(base64_start, "-----END PGP PRIVATE KEY BLOCK-----");
673 if (!end) {
674 end = strstr(base64_start, "-----END PGP SECRET KEY BLOCK-----");
675 }
676 if (!end) {
677 return SET_ERRNO(ERROR_CRYPTO_KEY, "Missing PGP PRIVATE/SECRET KEY BLOCK END marker");
678 }
679
680 // Find the checksum line (starts with '=') and exclude it
681 const char *base64_end = end;
682 const char *checksum = base64_end;
683 while (checksum > base64_start && *checksum != '=') {
684 checksum--;
685 }
686 if (*checksum == '=') {
687 // Move back to before the checksum line
688 while (checksum > base64_start && (checksum[-1] == '\n' || checksum[-1] == '\r')) {
689 checksum--;
690 }
691 base64_end = checksum;
692 }
693
694 size_t base64_len = (size_t)(base64_end - base64_start);
695
696 log_debug("Extracting base64 data from PGP secret key armor (%zu bytes)", base64_len);
697
698 // Decode base64 to binary OpenPGP packets
699 uint8_t *binary_data;
700 size_t binary_len;
701 asciichat_error_t decode_result = openpgp_base64_decode(base64_start, base64_len, &binary_data, &binary_len);
702 if (decode_result != ASCIICHAT_OK) {
703 return decode_result;
704 }
705
706 log_debug("Decoded %zu bytes of OpenPGP secret key packet data", binary_len);
707
708 // Parse OpenPGP packets to find the secret key packet (tag 5)
709 size_t offset = 0;
710 bool found_seckey = false;
711
712 while (offset < binary_len) {
713 openpgp_packet_header_t header;
714 asciichat_error_t header_result = openpgp_parse_packet_header(binary_data + offset, binary_len - offset, &header);
715 if (header_result != ASCIICHAT_OK) {
716 SAFE_FREE(binary_data);
717 return header_result;
718 }
719
720 log_debug("Packet at offset %zu: tag=%u, length=%zu", offset, header.tag, header.length);
721
722 // Check if this is a secret key packet (tag 5)
723 if (header.tag == OPENPGP_TAG_SECRET_KEY) {
724 openpgp_secret_key_t seckey;
725 asciichat_error_t parse_result =
726 openpgp_parse_secret_key_packet(binary_data + offset + header.header_len, header.length, &seckey);
727
728 if (parse_result == ASCIICHAT_OK) {
729 // Check if key is encrypted
730 if (seckey.is_encrypted) {
731 SAFE_FREE(binary_data);
732 log_debug("Detected encrypted GPG key, attempting to decrypt with passphrase");
733
734 // Decrypt the key using gpg binary
735 char *decrypted_text = NULL;
736 asciichat_error_t decrypt_result = openpgp_decrypt_with_gpg(armored_text, &decrypted_text);
737 if (decrypt_result != ASCIICHAT_OK) {
738 return decrypt_result;
739 }
740
741 // Recursively parse the decrypted key
742 asciichat_error_t recursive_result = openpgp_parse_armored_seckey(decrypted_text, ed25519_pk, ed25519_sk);
743 SAFE_FREE(decrypted_text);
744 return recursive_result;
745 }
746
747 // Unencrypted key - extract directly
748 memcpy(ed25519_pk, seckey.pubkey, 32);
749 memcpy(ed25519_sk, seckey.seckey, 32);
750 found_seckey = true;
751 log_debug("Extracted Ed25519 keypair from OpenPGP armored secret key block");
752 break;
753 } else {
754 // Not an Ed25519 key, try next packet
755 log_debug("Skipping non-Ed25519 secret key packet");
756 }
757 }
758
759 // Move to next packet
760 offset += header.header_len + header.length;
761 }
762
763 SAFE_FREE(binary_data);
764
765 if (!found_seckey) {
766 return SET_ERRNO(ERROR_CRYPTO_KEY, "No Ed25519 secret key found in PGP armored block");
767 }
768
769 return ASCIICHAT_OK;
770}
asciichat_error_t openpgp_parse_armored_seckey(const char *armored_text, uint8_t ed25519_pk[32], uint8_t ed25519_sk[32])
Definition openpgp.c:649
asciichat_error_t openpgp_parse_secret_key_packet(const uint8_t *packet_body, size_t body_len, openpgp_secret_key_t *seckey)
Definition openpgp.c:390

References openpgp_base64_decode(), openpgp_parse_armored_seckey(), openpgp_parse_packet_header(), and openpgp_parse_secret_key_packet().

Referenced by openpgp_parse_armored_seckey(), and parse_private_key().

◆ openpgp_parse_packet_header()

asciichat_error_t openpgp_parse_packet_header ( const uint8_t *  data,
size_t  data_len,
openpgp_packet_header_t *  header 
)

Definition at line 70 of file openpgp.c.

70 {
71 if (!data || !header || data_len == 0) {
72 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for packet header parsing");
73 }
74
75 memset(header, 0, sizeof(openpgp_packet_header_t));
76
77 uint8_t ctb = data[0]; // Cipher Type Byte
78
79 // Check if bit 7 is set (all packets must have bit 7 = 1)
80 if ((ctb & 0x80) == 0) {
81 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid OpenPGP packet: bit 7 not set in CTB");
82 }
83
84 // Check if new format (bit 6 = 1) or old format (bit 6 = 0)
85 if (ctb & 0x40) {
86 // New format: bits 5-0 = tag
87 header->new_format = true;
88 header->tag = ctb & 0x3F;
89
90 if (data_len < 2) {
91 return SET_ERRNO(ERROR_CRYPTO_KEY, "Insufficient data for new format packet header");
92 }
93
94 uint8_t len_byte = data[1];
95
96 if (len_byte < 192) {
97 // One-octet length
98 header->length = len_byte;
99 header->header_len = 2;
100 } else if (len_byte < 224) {
101 // Two-octet length
102 if (data_len < 3) {
103 return SET_ERRNO(ERROR_CRYPTO_KEY, "Insufficient data for two-octet length");
104 }
105 header->length = ((len_byte - 192) << 8) + data[2] + 192;
106 header->header_len = 3;
107 } else if (len_byte == 255) {
108 // Five-octet length
109 if (data_len < 6) {
110 return SET_ERRNO(ERROR_CRYPTO_KEY, "Insufficient data for five-octet length");
111 }
112 header->length = ((size_t)data[2] << 24) | ((size_t)data[3] << 16) | ((size_t)data[4] << 8) | data[5];
113 header->header_len = 6;
114 } else {
115 // Partial body length (not supported for our use case)
116 return SET_ERRNO(ERROR_CRYPTO_KEY, "Partial body length not supported");
117 }
118 } else {
119 // Old format: bits 5-2 = tag, bits 1-0 = length type
120 header->new_format = false;
121 header->tag = (ctb >> 2) & 0x0F;
122 uint8_t length_type = ctb & 0x03;
123
124 switch (length_type) {
125 case 0: // One-octet length
126 if (data_len < 2) {
127 return SET_ERRNO(ERROR_CRYPTO_KEY, "Insufficient data for one-octet length");
128 }
129 header->length = data[1];
130 header->header_len = 2;
131 break;
132
133 case 1: // Two-octet length
134 if (data_len < 3) {
135 return SET_ERRNO(ERROR_CRYPTO_KEY, "Insufficient data for two-octet length");
136 }
137 header->length = ((size_t)data[1] << 8) | data[2];
138 header->header_len = 3;
139 break;
140
141 case 2: // Four-octet length
142 if (data_len < 5) {
143 return SET_ERRNO(ERROR_CRYPTO_KEY, "Insufficient data for four-octet length");
144 }
145 header->length = ((size_t)data[1] << 24) | ((size_t)data[2] << 16) | ((size_t)data[3] << 8) | data[4];
146 header->header_len = 5;
147 break;
148
149 case 3: // Indeterminate length (not supported)
150 return SET_ERRNO(ERROR_CRYPTO_KEY, "Indeterminate length not supported");
151
152 default:
153 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid length type: %u", length_type);
154 }
155 }
156
157 log_debug("OpenPGP packet: tag=%u, length=%zu, header_len=%zu, new_format=%d", header->tag, header->length,
158 header->header_len, header->new_format);
159
160 return ASCIICHAT_OK;
161}

Referenced by openpgp_parse_armored_pubkey(), and openpgp_parse_armored_seckey().

◆ openpgp_parse_public_key_packet()

asciichat_error_t openpgp_parse_public_key_packet ( const uint8_t *  packet_body,
size_t  body_len,
openpgp_public_key_t *  pubkey 
)

Definition at line 200 of file openpgp.c.

201 {
202 if (!packet_body || !pubkey || body_len < 6) {
203 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for public key packet parsing");
204 }
205
206 memset(pubkey, 0, sizeof(openpgp_public_key_t));
207
208 size_t offset = 0;
209
210 // Version (1 byte, must be 4)
211 pubkey->version = packet_body[offset++];
212 if (pubkey->version != 4) {
213 return SET_ERRNO(ERROR_CRYPTO_KEY, "Unsupported OpenPGP public key version: %u (only version 4 supported)",
214 pubkey->version);
215 }
216
217 // Creation time (4 bytes, big-endian Unix timestamp)
218 pubkey->created = ((uint32_t)packet_body[offset] << 24) | ((uint32_t)packet_body[offset + 1] << 16) |
219 ((uint32_t)packet_body[offset + 2] << 8) | packet_body[offset + 3];
220 offset += 4;
221
222 // Algorithm (1 byte)
223 pubkey->algorithm = packet_body[offset++];
224
225 log_debug("Public key packet: version=%u, created=%u, algorithm=%u", pubkey->version, pubkey->created,
226 pubkey->algorithm);
227
228 // Only support EdDSA (algorithm 22)
229 if (pubkey->algorithm != OPENPGP_ALGO_EDDSA) {
230 return SET_ERRNO(ERROR_CRYPTO_KEY, "Unsupported public key algorithm: %u (only EdDSA/22 supported)",
231 pubkey->algorithm);
232 }
233
234 // EdDSA (Ed25519) keys have a special encoding:
235 // - OID for the curve (variable length)
236 // - 0x40 prefix byte
237 // - 32 bytes of Ed25519 public key
238 //
239 // We search for the 0x40 prefix and extract the following 32 bytes
240
241 // Search for 0x40 prefix byte in the remaining packet data
242 bool found_prefix = false;
243 size_t key_offset = 0;
244
245 for (size_t i = offset; i < body_len - 32; i++) {
246 if (packet_body[i] == 0x40) {
247 key_offset = i + 1; // Point to first byte after 0x40
248 found_prefix = true;
249 log_debug("Found Ed25519 key prefix 0x40 at offset %zu", i);
250 break;
251 }
252 }
253
254 if (!found_prefix) {
255 return SET_ERRNO(ERROR_CRYPTO_KEY, "Ed25519 public key prefix (0x40) not found in packet");
256 }
257
258 if (key_offset + 32 > body_len) {
259 return SET_ERRNO(ERROR_CRYPTO_KEY, "Insufficient data for Ed25519 public key (need 32 bytes after 0x40 prefix)");
260 }
261
262 // Extract the 32-byte Ed25519 public key
263 memcpy(pubkey->pubkey, packet_body + key_offset, 32);
264
265 log_debug("Extracted Ed25519 public key (first 8 bytes): %02x%02x%02x%02x%02x%02x%02x%02x", pubkey->pubkey[0],
266 pubkey->pubkey[1], pubkey->pubkey[2], pubkey->pubkey[3], pubkey->pubkey[4], pubkey->pubkey[5],
267 pubkey->pubkey[6], pubkey->pubkey[7]);
268
269 // Calculate Key ID (last 8 bytes of SHA-1 fingerprint)
270 // For now, we'll skip fingerprint calculation and just extract from packet if available
271 // The keyid is typically at a fixed offset for Ed25519 keys
272 // We'll compute it properly by hashing the public key material
273
274 // For version 4 keys, fingerprint = SHA-1(0x99 || length || packet_body)
275 // Key ID = last 8 bytes of fingerprint
276 // For simplicity, we'll set keyid to 0 for now (not critical for our use case)
277 pubkey->keyid = 0;
278
279 log_debug("Extracted Ed25519 public key (first 8 bytes): %02x%02x%02x%02x%02x%02x%02x%02x", pubkey->pubkey[0],
280 pubkey->pubkey[1], pubkey->pubkey[2], pubkey->pubkey[3], pubkey->pubkey[4], pubkey->pubkey[5],
281 pubkey->pubkey[6], pubkey->pubkey[7]);
282
283 return ASCIICHAT_OK;
284}

Referenced by openpgp_parse_armored_pubkey().

◆ openpgp_parse_secret_key_packet()

asciichat_error_t openpgp_parse_secret_key_packet ( const uint8_t *  packet_body,
size_t  body_len,
openpgp_secret_key_t *  seckey 
)

Definition at line 390 of file openpgp.c.

391 {
392 if (!packet_body || !seckey || body_len < 6) {
393 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for secret key packet parsing");
394 }
395
396 memset(seckey, 0, sizeof(openpgp_secret_key_t));
397
398 size_t offset = 0;
399
400 // Parse public key portion (same as public key packet)
401 // Version (1 byte, must be 4)
402 seckey->version = packet_body[offset++];
403 if (seckey->version != 4) {
404 return SET_ERRNO(ERROR_CRYPTO_KEY, "Unsupported OpenPGP secret key version: %u (only version 4 supported)",
405 seckey->version);
406 }
407
408 // Creation time (4 bytes, big-endian Unix timestamp)
409 seckey->created = ((uint32_t)packet_body[offset] << 24) | ((uint32_t)packet_body[offset + 1] << 16) |
410 ((uint32_t)packet_body[offset + 2] << 8) | packet_body[offset + 3];
411 offset += 4;
412
413 // Algorithm (1 byte)
414 seckey->algorithm = packet_body[offset++];
415
416 log_debug("Secret key packet: version=%u, created=%u, algorithm=%u", seckey->version, seckey->created,
417 seckey->algorithm);
418
419 // Only support EdDSA (algorithm 22)
420 if (seckey->algorithm != OPENPGP_ALGO_EDDSA) {
421 return SET_ERRNO(ERROR_CRYPTO_KEY, "Unsupported secret key algorithm: %u (only EdDSA/22 supported)",
422 seckey->algorithm);
423 }
424
425 // EdDSA public key: OID + 0x40 prefix + 32 bytes of Ed25519 public key
426 // Search for 0x40 prefix byte
427 bool found_prefix = false;
428 size_t pubkey_offset = 0;
429
430 for (size_t i = offset; i < body_len - 32; i++) {
431 if (packet_body[i] == 0x40) {
432 pubkey_offset = i + 1;
433 found_prefix = true;
434 log_debug("Found Ed25519 public key prefix 0x40 at offset %zu", i);
435 break;
436 }
437 }
438
439 if (!found_prefix) {
440 return SET_ERRNO(ERROR_CRYPTO_KEY, "Ed25519 public key prefix (0x40) not found in secret key packet");
441 }
442
443 if (pubkey_offset + 32 > body_len) {
444 return SET_ERRNO(ERROR_CRYPTO_KEY, "Insufficient data for Ed25519 public key (need 32 bytes after 0x40 prefix)");
445 }
446
447 // Extract the 32-byte Ed25519 public key
448 memcpy(seckey->pubkey, packet_body + pubkey_offset, 32);
449
450 log_debug("Extracted Ed25519 public key (first 8 bytes): %02x%02x%02x%02x%02x%02x%02x%02x", seckey->pubkey[0],
451 seckey->pubkey[1], seckey->pubkey[2], seckey->pubkey[3], seckey->pubkey[4], seckey->pubkey[5],
452 seckey->pubkey[6], seckey->pubkey[7]);
453
454 // Move offset past public key material
455 offset = pubkey_offset + 32;
456
457 // S2K usage byte (1 byte)
458 // 0x00 = secret key is not encrypted
459 // 0xFE or 0xFF = secret key is encrypted with S2K
460 if (offset >= body_len) {
461 return SET_ERRNO(ERROR_CRYPTO_KEY, "Missing S2K usage byte in secret key packet");
462 }
463
464 uint8_t s2k_usage = packet_body[offset++];
465 log_debug("S2K usage byte: 0x%02x", s2k_usage);
466
467 if (s2k_usage != 0x00) {
468 seckey->is_encrypted = true;
469 log_debug("Detected encrypted secret key (S2K usage = 0x%02x)", s2k_usage);
470 // Don't parse encrypted key material here - caller will need to decrypt with gpg
471 return ASCIICHAT_OK;
472 }
473
474 seckey->is_encrypted = false;
475
476 // For unencrypted keys (S2K usage = 0x00), secret key material follows directly
477 // For Ed25519: 32 bytes of secret key
478 if (offset + 32 > body_len) {
479 return SET_ERRNO(ERROR_CRYPTO_KEY, "Insufficient data for Ed25519 secret key (need 32 bytes)");
480 }
481
482 // Extract the 32-byte Ed25519 secret key
483 memcpy(seckey->seckey, packet_body + offset, 32);
484
485 log_debug("Extracted Ed25519 secret key (first 8 bytes): %02x%02x%02x%02x%02x%02x%02x%02x", seckey->seckey[0],
486 seckey->seckey[1], seckey->seckey[2], seckey->seckey[3], seckey->seckey[4], seckey->seckey[5],
487 seckey->seckey[6], seckey->seckey[7]);
488
489 return ASCIICHAT_OK;
490}

Referenced by openpgp_parse_armored_seckey().