chie_crypto/
openpgp.rs

1//! OpenPGP Key Format Compatibility
2//!
3//! This module provides basic OpenPGP (RFC 4880) key format support for Ed25519 keys.
4//! Supports key import/export and basic packet handling.
5//!
6//! # Examples
7//!
8//! ```
9//! use chie_crypto::openpgp::{OpenPgpPublicKey, OpenPgpSecretKey};
10//! use chie_crypto::signing::KeyPair;
11//!
12//! // Generate a keypair
13//! let keypair = KeyPair::generate();
14//!
15//! // Export as OpenPGP public key
16//! let pgp_pub = OpenPgpPublicKey::from_ed25519(&keypair.public_key(), "user@example.com");
17//! let armored = pgp_pub.to_armored();
18//!
19//! // Export as OpenPGP secret key
20//! let pgp_sec = OpenPgpSecretKey::from_ed25519(&keypair, "user@example.com");
21//! let armored_sec = pgp_sec.to_armored();
22//! ```
23
24use crate::signing::{KeyPair, PublicKey};
25use base64::{Engine, engine::general_purpose::STANDARD};
26use serde::{Deserialize, Serialize};
27use sha2::{Digest, Sha256};
28use std::fmt;
29use std::time::{SystemTime, UNIX_EPOCH};
30use thiserror::Error;
31
32/// OpenPGP key format errors
33#[derive(Debug, Error, Clone, PartialEq, Eq)]
34pub enum OpenPgpError {
35    /// Invalid packet format
36    #[error("Invalid packet format: {0}")]
37    InvalidPacket(String),
38
39    /// Invalid key length
40    #[error("Invalid key length: expected {expected}, got {actual}")]
41    InvalidLength { expected: usize, actual: usize },
42
43    /// Unsupported algorithm
44    #[error("Unsupported algorithm: {0}")]
45    UnsupportedAlgorithm(u8),
46
47    /// Base64 decode error
48    #[error("Base64 decode error: {0}")]
49    Base64Error(String),
50
51    /// Invalid armor format
52    #[error("Invalid armor format: {0}")]
53    InvalidArmor(String),
54
55    /// Unsupported packet type
56    #[error("Unsupported packet type: {0}")]
57    UnsupportedPacketType(u8),
58
59    /// Invalid secret key
60    #[error("Invalid secret key")]
61    InvalidSecretKey,
62}
63
64/// Result type for OpenPGP operations
65pub type OpenPgpResult<T> = Result<T, OpenPgpError>;
66
67// OpenPGP constants
68const PGP_PUBLIC_KEY_HEADER: &str = "-----BEGIN PGP PUBLIC KEY BLOCK-----";
69const PGP_PUBLIC_KEY_FOOTER: &str = "-----END PGP PUBLIC KEY BLOCK-----";
70const PGP_PRIVATE_KEY_HEADER: &str = "-----BEGIN PGP PRIVATE KEY BLOCK-----";
71const PGP_PRIVATE_KEY_FOOTER: &str = "-----END PGP PRIVATE KEY BLOCK-----";
72
73// Packet tags (old format)
74const TAG_PUBLIC_KEY: u8 = 6; // Public-Key Packet
75const TAG_USER_ID: u8 = 13; // User ID Packet
76const TAG_SECRET_KEY: u8 = 5; // Secret-Key Packet
77
78// Public key algorithm IDs
79const ALGO_EDDSA: u8 = 22; // EdDSA (RFC 6637)
80
81// EdDSA curve OID for Ed25519
82const ED25519_OID: &[u8] = &[0x2B, 0x06, 0x01, 0x04, 0x01, 0xDA, 0x47, 0x0F, 0x01];
83
84/// OpenPGP public key
85#[derive(Clone, Debug, Serialize, Deserialize)]
86pub struct OpenPgpPublicKey {
87    /// Creation timestamp
88    pub created: u32,
89    /// Public key material
90    pub key_material: Vec<u8>,
91    /// User ID
92    pub user_id: String,
93}
94
95impl OpenPgpPublicKey {
96    /// Create OpenPGP public key from Ed25519 public key
97    pub fn from_ed25519(public_key: &PublicKey, user_id: impl Into<String>) -> Self {
98        let created = SystemTime::now()
99            .duration_since(UNIX_EPOCH)
100            .unwrap()
101            .as_secs() as u32;
102
103        Self {
104            created,
105            key_material: public_key.to_vec(),
106            user_id: user_id.into(),
107        }
108    }
109
110    /// Encode as binary packet format
111    pub fn to_packet(&self) -> Vec<u8> {
112        let mut packet = Vec::new();
113
114        // Public key packet
115        let mut key_packet = Vec::new();
116
117        // Version (4)
118        key_packet.push(4);
119
120        // Creation time (4 bytes)
121        key_packet.extend_from_slice(&self.created.to_be_bytes());
122
123        // Algorithm (EdDSA)
124        key_packet.push(ALGO_EDDSA);
125
126        // OID length
127        key_packet.push(ED25519_OID.len() as u8);
128
129        // OID
130        key_packet.extend_from_slice(ED25519_OID);
131
132        // MPI of EdDSA point (0x40 prefix + 32 bytes)
133        write_mpi(&mut key_packet, &self.key_material);
134
135        // Add packet header
136        add_packet_header(&mut packet, TAG_PUBLIC_KEY, &key_packet);
137
138        // User ID packet
139        let user_id_bytes = self.user_id.as_bytes();
140        add_packet_header(&mut packet, TAG_USER_ID, user_id_bytes);
141
142        packet
143    }
144
145    /// Encode as ASCII-armored format
146    pub fn to_armored(&self) -> String {
147        let packet = self.to_packet();
148        let encoded = STANDARD.encode(&packet);
149
150        // Calculate CRC24 checksum
151        let crc = crc24(&packet);
152        let crc_bytes = [(crc >> 16) as u8, (crc >> 8) as u8, crc as u8];
153        let crc_encoded = STANDARD.encode(crc_bytes);
154
155        let mut result = String::new();
156        result.push_str(PGP_PUBLIC_KEY_HEADER);
157        result.push_str("\n\n");
158
159        // Split into 64-character lines
160        for chunk in encoded.as_bytes().chunks(64) {
161            result.push_str(std::str::from_utf8(chunk).unwrap());
162            result.push('\n');
163        }
164
165        result.push('=');
166        result.push_str(&crc_encoded);
167        result.push('\n');
168        result.push_str(PGP_PUBLIC_KEY_FOOTER);
169        result.push('\n');
170
171        result
172    }
173
174    /// Get key fingerprint (SHA-256 hash of key packet)
175    pub fn fingerprint(&self) -> [u8; 32] {
176        let packet = self.to_packet();
177        let mut hasher = Sha256::new();
178        hasher.update(&packet);
179        hasher.finalize().into()
180    }
181}
182
183impl fmt::Display for OpenPgpPublicKey {
184    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185        write!(f, "{}", self.to_armored())
186    }
187}
188
189/// OpenPGP secret key
190#[derive(Clone, Serialize, Deserialize)]
191pub struct OpenPgpSecretKey {
192    /// Creation timestamp
193    pub created: u32,
194    /// Public key material
195    pub public_key: Vec<u8>,
196    /// Secret key material (32 bytes for Ed25519)
197    pub secret_key: Vec<u8>,
198    /// User ID
199    pub user_id: String,
200}
201
202impl OpenPgpSecretKey {
203    /// Create OpenPGP secret key from Ed25519 keypair
204    pub fn from_ed25519(keypair: &KeyPair, user_id: impl Into<String>) -> Self {
205        let created = SystemTime::now()
206            .duration_since(UNIX_EPOCH)
207            .unwrap()
208            .as_secs() as u32;
209
210        Self {
211            created,
212            public_key: keypair.public_key().to_vec(),
213            secret_key: keypair.secret_key().to_vec(),
214            user_id: user_id.into(),
215        }
216    }
217
218    /// Encode as binary packet format
219    pub fn to_packet(&self) -> Vec<u8> {
220        let mut packet = Vec::new();
221
222        // Secret key packet
223        let mut key_packet = Vec::new();
224
225        // Version (4)
226        key_packet.push(4);
227
228        // Creation time (4 bytes)
229        key_packet.extend_from_slice(&self.created.to_be_bytes());
230
231        // Algorithm (EdDSA)
232        key_packet.push(ALGO_EDDSA);
233
234        // OID length
235        key_packet.push(ED25519_OID.len() as u8);
236
237        // OID
238        key_packet.extend_from_slice(ED25519_OID);
239
240        // MPI of EdDSA point (public key)
241        write_mpi(&mut key_packet, &self.public_key);
242
243        // String-to-key usage (0 = unencrypted)
244        key_packet.push(0);
245
246        // MPI of EdDSA secret scalar
247        write_mpi(&mut key_packet, &self.secret_key);
248
249        // Checksum (simple sum of secret key bytes)
250        let checksum: u16 = self.secret_key.iter().map(|&b| b as u16).sum();
251        key_packet.extend_from_slice(&checksum.to_be_bytes());
252
253        // Add packet header
254        add_packet_header(&mut packet, TAG_SECRET_KEY, &key_packet);
255
256        // User ID packet
257        let user_id_bytes = self.user_id.as_bytes();
258        add_packet_header(&mut packet, TAG_USER_ID, user_id_bytes);
259
260        packet
261    }
262
263    /// Encode as ASCII-armored format
264    pub fn to_armored(&self) -> String {
265        let packet = self.to_packet();
266        let encoded = STANDARD.encode(&packet);
267
268        // Calculate CRC24 checksum
269        let crc = crc24(&packet);
270        let crc_bytes = [(crc >> 16) as u8, (crc >> 8) as u8, crc as u8];
271        let crc_encoded = STANDARD.encode(crc_bytes);
272
273        let mut result = String::new();
274        result.push_str(PGP_PRIVATE_KEY_HEADER);
275        result.push_str("\n\n");
276
277        // Split into 64-character lines
278        for chunk in encoded.as_bytes().chunks(64) {
279            result.push_str(std::str::from_utf8(chunk).unwrap());
280            result.push('\n');
281        }
282
283        result.push('=');
284        result.push_str(&crc_encoded);
285        result.push('\n');
286        result.push_str(PGP_PRIVATE_KEY_FOOTER);
287        result.push('\n');
288
289        result
290    }
291
292    /// Convert to Ed25519 keypair
293    pub fn to_ed25519(&self) -> OpenPgpResult<KeyPair> {
294        if self.secret_key.len() != 32 {
295            return Err(OpenPgpError::InvalidLength {
296                expected: 32,
297                actual: self.secret_key.len(),
298            });
299        }
300
301        let mut secret = [0u8; 32];
302        secret.copy_from_slice(&self.secret_key);
303
304        KeyPair::from_secret_key(&secret).map_err(|_| OpenPgpError::InvalidSecretKey)
305    }
306
307    /// Get corresponding public key
308    pub fn public_key(&self) -> OpenPgpPublicKey {
309        OpenPgpPublicKey {
310            created: self.created,
311            key_material: self.public_key.clone(),
312            user_id: self.user_id.clone(),
313        }
314    }
315}
316
317// Helper functions
318
319/// Add OpenPGP packet header (old format)
320fn add_packet_header(buf: &mut Vec<u8>, tag: u8, body: &[u8]) {
321    let len = body.len();
322
323    if len < 256 {
324        // Old format, one-octet length
325        buf.push(0x80 | (tag << 2));
326        buf.push(len as u8);
327    } else if len < 65536 {
328        // Old format, two-octet length
329        buf.push(0x80 | (tag << 2) | 0x01);
330        buf.extend_from_slice(&(len as u16).to_be_bytes());
331    } else {
332        // Old format, four-octet length
333        buf.push(0x80 | (tag << 2) | 0x02);
334        buf.extend_from_slice(&(len as u32).to_be_bytes());
335    }
336
337    buf.extend_from_slice(body);
338}
339
340/// Write MPI (multiprecision integer) for EdDSA
341fn write_mpi(buf: &mut Vec<u8>, data: &[u8]) {
342    // MPI bit count (for 32-byte Ed25519 key, this is 0x0100 = 256 bits, but we use 0x0107 = 263 bits to account for the 0x40 prefix)
343    let bit_count = (data.len() * 8 + 7) as u16;
344    buf.extend_from_slice(&bit_count.to_be_bytes());
345
346    // EdDSA point is prefixed with 0x40
347    buf.push(0x40);
348    buf.extend_from_slice(data);
349}
350
351/// Calculate CRC24 checksum for OpenPGP
352fn crc24(data: &[u8]) -> u32 {
353    const CRC24_INIT: u32 = 0xB704CE;
354    const CRC24_POLY: u32 = 0x1864CFB;
355
356    let mut crc = CRC24_INIT;
357
358    for &byte in data {
359        crc ^= (byte as u32) << 16;
360        for _ in 0..8 {
361            crc <<= 1;
362            if crc & 0x1000000 != 0 {
363                crc ^= CRC24_POLY;
364            }
365        }
366    }
367
368    crc & 0xFFFFFF
369}
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374
375    #[test]
376    fn test_pgp_public_key_creation() {
377        let keypair = KeyPair::generate();
378        let pgp_pub = OpenPgpPublicKey::from_ed25519(&keypair.public_key(), "test@example.com");
379
380        assert_eq!(pgp_pub.key_material.len(), 32);
381        assert_eq!(pgp_pub.user_id, "test@example.com");
382    }
383
384    #[test]
385    fn test_pgp_public_key_packet() {
386        let keypair = KeyPair::generate();
387        let pgp_pub = OpenPgpPublicKey::from_ed25519(&keypair.public_key(), "test@example.com");
388
389        let packet = pgp_pub.to_packet();
390        assert!(!packet.is_empty());
391
392        // Should start with packet header
393        assert_eq!(packet[0] & 0x80, 0x80); // Old format bit
394    }
395
396    #[test]
397    fn test_pgp_public_key_armor() {
398        let keypair = KeyPair::generate();
399        let pgp_pub = OpenPgpPublicKey::from_ed25519(&keypair.public_key(), "test@example.com");
400
401        let armored = pgp_pub.to_armored();
402        assert!(armored.contains(PGP_PUBLIC_KEY_HEADER));
403        assert!(armored.contains(PGP_PUBLIC_KEY_FOOTER));
404        assert!(armored.contains("=")); // CRC24 checksum line
405    }
406
407    #[test]
408    fn test_pgp_secret_key_creation() {
409        let keypair = KeyPair::generate();
410        let pgp_sec = OpenPgpSecretKey::from_ed25519(&keypair, "test@example.com");
411
412        assert_eq!(pgp_sec.public_key.len(), 32);
413        assert_eq!(pgp_sec.secret_key.len(), 32);
414        assert_eq!(pgp_sec.user_id, "test@example.com");
415    }
416
417    #[test]
418    fn test_pgp_secret_key_armor() {
419        let keypair = KeyPair::generate();
420        let pgp_sec = OpenPgpSecretKey::from_ed25519(&keypair, "test@example.com");
421
422        let armored = pgp_sec.to_armored();
423        assert!(armored.contains(PGP_PRIVATE_KEY_HEADER));
424        assert!(armored.contains(PGP_PRIVATE_KEY_FOOTER));
425    }
426
427    #[test]
428    fn test_pgp_secret_to_keypair() {
429        let keypair = KeyPair::generate();
430        let pgp_sec = OpenPgpSecretKey::from_ed25519(&keypair, "test@example.com");
431
432        let recovered = pgp_sec.to_ed25519().unwrap();
433        assert_eq!(recovered.public_key(), keypair.public_key());
434        assert_eq!(recovered.secret_key(), keypair.secret_key());
435    }
436
437    #[test]
438    fn test_pgp_public_from_secret() {
439        let keypair = KeyPair::generate();
440        let pgp_sec = OpenPgpSecretKey::from_ed25519(&keypair, "test@example.com");
441        let pgp_pub = pgp_sec.public_key();
442
443        assert_eq!(pgp_pub.key_material, pgp_sec.public_key);
444        assert_eq!(pgp_pub.user_id, pgp_sec.user_id);
445    }
446
447    #[test]
448    fn test_fingerprint() {
449        let keypair = KeyPair::generate();
450        let pgp_pub = OpenPgpPublicKey::from_ed25519(&keypair.public_key(), "test@example.com");
451
452        let fp1 = pgp_pub.fingerprint();
453        let fp2 = pgp_pub.fingerprint();
454
455        // Fingerprint should be deterministic
456        assert_eq!(fp1, fp2);
457        assert_eq!(fp1.len(), 32);
458    }
459
460    #[test]
461    fn test_crc24() {
462        let data = b"hello";
463        let crc = crc24(data);
464
465        // CRC24 should be 24 bits
466        assert!(crc < 0x1000000);
467
468        // CRC should be deterministic
469        assert_eq!(crc, crc24(data));
470    }
471
472    #[test]
473    fn test_armor_line_length() {
474        let keypair = KeyPair::generate();
475        let pgp_pub = OpenPgpPublicKey::from_ed25519(&keypair.public_key(), "test@example.com");
476
477        let armored = pgp_pub.to_armored();
478
479        // Check line lengths (should be <= 64 chars for data lines)
480        for line in armored.lines() {
481            if !line.contains("BEGIN") && !line.contains("END") && !line.starts_with('=') {
482                assert!(line.len() <= 64, "Line too long: {}", line.len());
483            }
484        }
485    }
486
487    #[test]
488    fn test_mpi_encoding() {
489        let mut buf = Vec::new();
490        let data = &[0x12, 0x34, 0x56, 0x78];
491
492        write_mpi(&mut buf, data);
493
494        // Should have bit count (2 bytes) + 0x40 prefix + data
495        assert_eq!(buf.len(), 2 + 1 + data.len());
496        assert_eq!(buf[2], 0x40); // EdDSA prefix
497    }
498
499    #[test]
500    fn test_packet_header() {
501        let mut buf = Vec::new();
502        let body = b"test";
503
504        add_packet_header(&mut buf, TAG_USER_ID, body);
505
506        // Old format bit should be set
507        assert_eq!(buf[0] & 0x80, 0x80);
508
509        // Tag should be encoded
510        assert_eq!((buf[0] >> 2) & 0x0F, TAG_USER_ID);
511    }
512
513    #[test]
514    fn test_serialization() {
515        let keypair = KeyPair::generate();
516        let pgp_pub = OpenPgpPublicKey::from_ed25519(&keypair.public_key(), "test@example.com");
517
518        let serialized = crate::codec::encode(&pgp_pub).unwrap();
519        let deserialized: OpenPgpPublicKey = crate::codec::decode(&serialized).unwrap();
520
521        assert_eq!(deserialized.key_material, pgp_pub.key_material);
522        assert_eq!(deserialized.user_id, pgp_pub.user_id);
523    }
524}