claim169_core/
encode.rs

1//! Encoder builder for creating Claim 169 QR codes.
2//!
3//! The [`Encoder`] provides a fluent builder API for encoding identity credentials
4//! into QR-ready Base45 strings.
5//!
6//! # Basic Usage
7//!
8//! ```rust,ignore
9//! use claim169_core::{Encoder, Claim169, CwtMeta};
10//!
11//! // Create an unsigned credential (requires explicit opt-in)
12//! let qr_data = Encoder::new(claim169, cwt_meta)
13//!     .allow_unsigned()
14//!     .encode()?;
15//!
16//! // Create a signed credential with Ed25519
17//! let qr_data = Encoder::new(claim169, cwt_meta)
18//!     .sign_with_ed25519(&private_key)?
19//!     .encode()?;
20//!
21//! // Create a signed and encrypted credential
22//! let qr_data = Encoder::new(claim169, cwt_meta)
23//!     .sign_with_ed25519(&private_key)?
24//!     .encrypt_with_aes256(&aes_key)?
25//!     .encode()?;
26//! ```
27//!
28//! # HSM Integration
29//!
30//! For hardware security modules, use the generic `sign_with()` method:
31//!
32//! ```rust,ignore
33//! use claim169_core::{Encoder, Signer};
34//!
35//! struct HsmSigner { /* ... */ }
36//! impl Signer for HsmSigner { /* ... */ }
37//!
38//! let qr_data = Encoder::new(claim169, cwt_meta)
39//!     .sign_with(hsm_signer, iana::Algorithm::EdDSA)
40//!     .encode()?;
41//! ```
42
43use coset::iana;
44
45use crate::crypto::traits::{Encryptor, Signer};
46use crate::error::{Claim169Error, Result};
47use crate::model::{Claim169, CwtMeta};
48use crate::pipeline::encode::{encode_signed, encode_signed_and_encrypted, EncodeConfig};
49
50#[cfg(feature = "software-crypto")]
51use crate::crypto::software::AesGcmEncryptor;
52
53/// Configuration for encryption in the encoder
54struct EncryptConfig {
55    encryptor: Box<dyn Encryptor + Send + Sync>,
56    algorithm: iana::Algorithm,
57    nonce: Option<[u8; 12]>,
58}
59
60/// Builder for encoding Claim 169 credentials into QR-ready strings.
61///
62/// The encoder follows a builder pattern where configuration methods return `Self`
63/// and the final `encode()` method consumes the builder to produce the result.
64///
65/// # Operation Order
66///
67/// When both signing and encryption are configured, the credential is always
68/// **signed first, then encrypted** (sign-then-encrypt), regardless of the order
69/// in which builder methods are called.
70///
71/// # Security
72///
73/// - Unsigned encoding requires explicit opt-in via [`allow_unsigned()`](Self::allow_unsigned)
74/// - Nonces are generated randomly by default for encryption
75/// - Use explicit nonce methods only for testing or deterministic scenarios
76///
77/// # Example
78///
79/// ```rust,ignore
80/// use claim169_core::{Encoder, Claim169, CwtMeta};
81///
82/// let claim169 = Claim169::minimal("ID-001", "Jane Doe");
83/// let cwt_meta = CwtMeta::new()
84///     .with_issuer("https://issuer.example.com")
85///     .with_expires_at(1800000000);
86///
87/// // Sign with Ed25519
88/// let qr_data = Encoder::new(claim169, cwt_meta)
89///     .sign_with_ed25519(&private_key)?
90///     .encode()?;
91/// ```
92pub struct Encoder {
93    claim169: Claim169,
94    cwt_meta: CwtMeta,
95    signer: Option<Box<dyn Signer + Send + Sync>>,
96    sign_algorithm: Option<iana::Algorithm>,
97    encrypt_config: Option<EncryptConfig>,
98    allow_unsigned: bool,
99    skip_biometrics: bool,
100}
101
102impl Encoder {
103    /// Create a new encoder with the given claim and CWT metadata.
104    ///
105    /// # Arguments
106    ///
107    /// * `claim169` - The identity claim data to encode
108    /// * `cwt_meta` - CWT metadata including issuer, expiration, etc.
109    ///
110    /// # Example
111    ///
112    /// ```rust,ignore
113    /// let encoder = Encoder::new(claim169, cwt_meta);
114    /// ```
115    pub fn new(claim169: Claim169, cwt_meta: CwtMeta) -> Self {
116        Self {
117            claim169,
118            cwt_meta,
119            signer: None,
120            sign_algorithm: None,
121            encrypt_config: None,
122            allow_unsigned: false,
123            skip_biometrics: false,
124        }
125    }
126
127    /// Sign with a custom signer implementation.
128    ///
129    /// Use this method for HSM integration or custom cryptographic backends.
130    ///
131    /// # Arguments
132    ///
133    /// * `signer` - A type implementing the [`Signer`] trait
134    /// * `algorithm` - The COSE algorithm to use for signing
135    ///
136    /// # Example
137    ///
138    /// ```rust,ignore
139    /// let encoder = Encoder::new(claim169, cwt_meta)
140    ///     .sign_with(hsm_signer, iana::Algorithm::EdDSA);
141    /// ```
142    pub fn sign_with<S: Signer + 'static>(mut self, signer: S, algorithm: iana::Algorithm) -> Self {
143        self.signer = Some(Box::new(signer));
144        self.sign_algorithm = Some(algorithm);
145        self
146    }
147
148    /// Sign with an Ed25519 private key.
149    ///
150    /// The key is validated immediately and an error is returned if invalid.
151    ///
152    /// # Arguments
153    ///
154    /// * `private_key` - 32-byte Ed25519 private key
155    ///
156    /// # Errors
157    ///
158    /// Returns an error if the private key is not exactly 32 bytes.
159    ///
160    /// # Example
161    ///
162    /// ```rust,ignore
163    /// let encoder = Encoder::new(claim169, cwt_meta)
164    ///     .sign_with_ed25519(&private_key)?;
165    /// ```
166    #[cfg(feature = "software-crypto")]
167    pub fn sign_with_ed25519(self, private_key: &[u8]) -> Result<Self> {
168        use crate::crypto::software::Ed25519Signer;
169
170        let signer = Ed25519Signer::from_bytes(private_key)
171            .map_err(|e| Claim169Error::Crypto(e.to_string()))?;
172
173        Ok(self.sign_with(signer, iana::Algorithm::EdDSA))
174    }
175
176    /// Sign with an ECDSA P-256 private key.
177    ///
178    /// The key is validated immediately and an error is returned if invalid.
179    ///
180    /// # Arguments
181    ///
182    /// * `private_key` - 32-byte ECDSA P-256 private key (scalar)
183    ///
184    /// # Errors
185    ///
186    /// Returns an error if the private key format is invalid.
187    ///
188    /// # Example
189    ///
190    /// ```rust,ignore
191    /// let encoder = Encoder::new(claim169, cwt_meta)
192    ///     .sign_with_ecdsa_p256(&private_key)?;
193    /// ```
194    #[cfg(feature = "software-crypto")]
195    pub fn sign_with_ecdsa_p256(self, private_key: &[u8]) -> Result<Self> {
196        use crate::crypto::software::EcdsaP256Signer;
197
198        let signer = EcdsaP256Signer::from_bytes(private_key)
199            .map_err(|e| Claim169Error::Crypto(e.to_string()))?;
200
201        Ok(self.sign_with(signer, iana::Algorithm::ES256))
202    }
203
204    /// Encrypt with a custom encryptor implementation.
205    ///
206    /// Use this method for HSM integration or custom cryptographic backends.
207    /// A random 12-byte nonce is generated automatically.
208    ///
209    /// # Arguments
210    ///
211    /// * `encryptor` - A type implementing the [`Encryptor`] trait
212    /// * `algorithm` - The COSE algorithm to use for encryption
213    pub fn encrypt_with<E: Encryptor + 'static>(
214        mut self,
215        encryptor: E,
216        algorithm: iana::Algorithm,
217    ) -> Self {
218        self.encrypt_config = Some(EncryptConfig {
219            encryptor: Box::new(encryptor),
220            algorithm,
221            nonce: None, // Generate random nonce during encode
222        });
223        self
224    }
225
226    /// Encrypt with AES-256-GCM.
227    ///
228    /// A random 12-byte nonce is generated automatically during encoding.
229    ///
230    /// # Arguments
231    ///
232    /// * `key` - 32-byte AES-256 encryption key
233    ///
234    /// # Errors
235    ///
236    /// Returns an error if the key is not exactly 32 bytes.
237    ///
238    /// # Example
239    ///
240    /// ```rust,ignore
241    /// let encoder = Encoder::new(claim169, cwt_meta)
242    ///     .sign_with_ed25519(&sign_key)?
243    ///     .encrypt_with_aes256(&aes_key)?;
244    /// ```
245    #[cfg(feature = "software-crypto")]
246    pub fn encrypt_with_aes256(self, key: &[u8]) -> Result<Self> {
247        let encryptor =
248            AesGcmEncryptor::aes256(key).map_err(|e| Claim169Error::Crypto(e.to_string()))?;
249
250        Ok(self.encrypt_with(encryptor, iana::Algorithm::A256GCM))
251    }
252
253    /// Encrypt with AES-128-GCM.
254    ///
255    /// A random 12-byte nonce is generated automatically during encoding.
256    ///
257    /// # Arguments
258    ///
259    /// * `key` - 16-byte AES-128 encryption key
260    ///
261    /// # Errors
262    ///
263    /// Returns an error if the key is not exactly 16 bytes.
264    #[cfg(feature = "software-crypto")]
265    pub fn encrypt_with_aes128(self, key: &[u8]) -> Result<Self> {
266        let encryptor =
267            AesGcmEncryptor::aes128(key).map_err(|e| Claim169Error::Crypto(e.to_string()))?;
268
269        Ok(self.encrypt_with(encryptor, iana::Algorithm::A128GCM))
270    }
271
272    /// Encrypt with AES-256-GCM using an explicit nonce.
273    ///
274    /// **Warning**: Only use this for testing or deterministic scenarios.
275    /// Reusing nonces with the same key is a critical security vulnerability.
276    ///
277    /// # Arguments
278    ///
279    /// * `key` - 32-byte AES-256 encryption key
280    /// * `nonce` - 12-byte nonce/IV (must be unique per encryption)
281    #[cfg(feature = "software-crypto")]
282    pub fn encrypt_with_aes256_nonce(mut self, key: &[u8], nonce: &[u8; 12]) -> Result<Self> {
283        let encryptor =
284            AesGcmEncryptor::aes256(key).map_err(|e| Claim169Error::Crypto(e.to_string()))?;
285
286        self.encrypt_config = Some(EncryptConfig {
287            encryptor: Box::new(encryptor),
288            algorithm: iana::Algorithm::A256GCM,
289            nonce: Some(*nonce),
290        });
291        Ok(self)
292    }
293
294    /// Encrypt with AES-128-GCM using an explicit nonce.
295    ///
296    /// **Warning**: Only use this for testing or deterministic scenarios.
297    /// Reusing nonces with the same key is a critical security vulnerability.
298    ///
299    /// # Arguments
300    ///
301    /// * `key` - 16-byte AES-128 encryption key
302    /// * `nonce` - 12-byte nonce/IV (must be unique per encryption)
303    #[cfg(feature = "software-crypto")]
304    pub fn encrypt_with_aes128_nonce(mut self, key: &[u8], nonce: &[u8; 12]) -> Result<Self> {
305        let encryptor =
306            AesGcmEncryptor::aes128(key).map_err(|e| Claim169Error::Crypto(e.to_string()))?;
307
308        self.encrypt_config = Some(EncryptConfig {
309            encryptor: Box::new(encryptor),
310            algorithm: iana::Algorithm::A128GCM,
311            nonce: Some(*nonce),
312        });
313        Ok(self)
314    }
315
316    /// Allow encoding without a signature.
317    ///
318    /// **Security Warning**: Unsigned credentials cannot be verified for authenticity.
319    /// Only use this for testing or scenarios where signatures are not required.
320    ///
321    /// # Example
322    ///
323    /// ```rust,ignore
324    /// let encoder = Encoder::new(claim169, cwt_meta)
325    ///     .allow_unsigned()
326    ///     .encode()?;
327    /// ```
328    pub fn allow_unsigned(mut self) -> Self {
329        self.allow_unsigned = true;
330        self
331    }
332
333    /// Skip biometric fields during encoding.
334    ///
335    /// This reduces the QR code size by excluding fingerprint, iris, face,
336    /// palm, and voice biometric data.
337    ///
338    /// # Example
339    ///
340    /// ```rust,ignore
341    /// let encoder = Encoder::new(claim169, cwt_meta)
342    ///     .skip_biometrics()
343    ///     .sign_with_ed25519(&key)?
344    ///     .encode()?;
345    /// ```
346    pub fn skip_biometrics(mut self) -> Self {
347        self.skip_biometrics = true;
348        self
349    }
350
351    /// Encode the credential to a Base45 QR string.
352    ///
353    /// This method consumes the encoder and produces the final QR-ready string.
354    ///
355    /// # Pipeline
356    ///
357    /// ```text
358    /// Claim169 → CBOR → CWT → COSE_Sign1 → [COSE_Encrypt0] → zlib → Base45
359    /// ```
360    ///
361    /// # Errors
362    ///
363    /// Returns an error if:
364    /// - Neither a signer nor `allow_unsigned()` was configured
365    /// - Signing fails
366    /// - Encryption fails
367    /// - CBOR encoding fails
368    ///
369    /// # Example
370    ///
371    /// ```rust,ignore
372    /// let qr_data = Encoder::new(claim169, cwt_meta)
373    ///     .sign_with_ed25519(&key)?
374    ///     .encode()?;
375    ///
376    /// println!("QR content: {}", qr_data);
377    /// ```
378    pub fn encode(self) -> Result<String> {
379        // Validate configuration
380        if self.signer.is_none() && !self.allow_unsigned {
381            return Err(Claim169Error::EncodingConfig(
382                "either call sign_with_*() or allow_unsigned() before encode()".to_string(),
383            ));
384        }
385
386        let config = EncodeConfig {
387            skip_biometrics: self.skip_biometrics,
388        };
389
390        // Convert the boxed signer to a trait object reference
391        // The cast is needed because Box<dyn Signer + Send + Sync> doesn't automatically
392        // coerce to &dyn Signer, even though Signer: Send + Sync
393        let signer_ref: Option<&dyn Signer> =
394            self.signer.as_ref().map(|s| s.as_ref() as &dyn Signer);
395
396        match self.encrypt_config {
397            Some(encrypt_config) => {
398                // Get nonce - auto-generate if software-crypto is enabled, otherwise require explicit
399                #[cfg(feature = "software-crypto")]
400                let nonce = encrypt_config.nonce.unwrap_or_else(generate_nonce);
401
402                #[cfg(not(feature = "software-crypto"))]
403                let nonce = encrypt_config.nonce.ok_or_else(|| {
404                    Claim169Error::EncodingConfig(
405                        "explicit nonce required when software-crypto feature is disabled"
406                            .to_string(),
407                    )
408                })?;
409
410                encode_signed_and_encrypted(
411                    &self.claim169,
412                    &self.cwt_meta,
413                    signer_ref,
414                    self.sign_algorithm,
415                    encrypt_config.encryptor.as_ref(),
416                    encrypt_config.algorithm,
417                    &nonce,
418                    &config,
419                )
420            }
421            None => encode_signed(
422                &self.claim169,
423                &self.cwt_meta,
424                signer_ref,
425                self.sign_algorithm,
426                &config,
427            ),
428        }
429    }
430}
431
432/// Generate a random 12-byte nonce for AES-GCM encryption.
433#[cfg(feature = "software-crypto")]
434fn generate_nonce() -> [u8; 12] {
435    use rand::RngCore;
436    let mut nonce = [0u8; 12];
437    rand::thread_rng().fill_bytes(&mut nonce);
438    nonce
439}
440
441/// Generate a random nonce.
442///
443/// Returns a 12-byte random nonce suitable for AES-GCM encryption.
444///
445/// # Example
446///
447/// ```rust
448/// use claim169_core::generate_random_nonce;
449///
450/// let nonce = generate_random_nonce();
451/// assert_eq!(nonce.len(), 12);
452/// ```
453#[cfg(feature = "software-crypto")]
454pub fn generate_random_nonce() -> [u8; 12] {
455    generate_nonce()
456}
457
458#[cfg(test)]
459mod tests {
460    use super::*;
461
462    #[test]
463    fn test_encoder_requires_signer_or_allow_unsigned() {
464        let claim169 = Claim169::minimal("test-id", "Test User");
465        let cwt_meta = CwtMeta::default();
466
467        let result = Encoder::new(claim169, cwt_meta).encode();
468
469        assert!(result.is_err());
470        match result.unwrap_err() {
471            Claim169Error::EncodingConfig(msg) => {
472                assert!(msg.contains("allow_unsigned"));
473            }
474            e => panic!("Expected EncodingConfig error, got: {:?}", e),
475        }
476    }
477
478    #[test]
479    fn test_encoder_unsigned() {
480        let claim169 = Claim169::minimal("test-id", "Test User");
481        let cwt_meta = CwtMeta::new().with_issuer("test-issuer");
482
483        let result = Encoder::new(claim169, cwt_meta).allow_unsigned().encode();
484
485        assert!(result.is_ok());
486        let qr_data = result.unwrap();
487        assert!(!qr_data.is_empty());
488    }
489
490    #[cfg(feature = "software-crypto")]
491    #[test]
492    fn test_encoder_ed25519_signed() {
493        use crate::crypto::software::Ed25519Signer;
494
495        let claim169 = Claim169::minimal("signed-test", "Signed User");
496        let cwt_meta = CwtMeta::new()
497            .with_issuer("https://test.issuer")
498            .with_expires_at(1800000000);
499
500        let private_key = [0u8; 32]; // For test - generate real key in production
501        let test_signer = Ed25519Signer::from_bytes(&private_key).unwrap();
502
503        let result = Encoder::new(claim169, cwt_meta)
504            .sign_with(test_signer, iana::Algorithm::EdDSA)
505            .encode();
506
507        assert!(result.is_ok());
508    }
509
510    #[cfg(feature = "software-crypto")]
511    #[test]
512    fn test_encoder_ed25519_convenience() {
513        let claim169 = Claim169::minimal("signed-test", "Signed User");
514        let cwt_meta = CwtMeta::default();
515
516        // Use a fixed test key
517        let private_key = [1u8; 32];
518
519        let result = Encoder::new(claim169, cwt_meta)
520            .sign_with_ed25519(&private_key)
521            .and_then(|e| e.encode());
522
523        assert!(result.is_ok());
524    }
525
526    #[cfg(feature = "software-crypto")]
527    #[test]
528    fn test_encoder_with_encryption() {
529        let claim169 = Claim169::minimal("encrypted-test", "Encrypted User");
530        let cwt_meta = CwtMeta::new().with_issuer("test");
531
532        let sign_key = [2u8; 32];
533        let encrypt_key = [3u8; 32];
534        let nonce = [4u8; 12];
535
536        let result = Encoder::new(claim169, cwt_meta)
537            .sign_with_ed25519(&sign_key)
538            .and_then(|e| e.encrypt_with_aes256_nonce(&encrypt_key, &nonce))
539            .and_then(|e| e.encode());
540
541        assert!(result.is_ok());
542    }
543
544    #[cfg(feature = "software-crypto")]
545    #[test]
546    fn test_encoder_skip_biometrics() {
547        use crate::model::Biometric;
548
549        let mut claim169 = Claim169::minimal("bio-test", "Bio User");
550        claim169.face = Some(vec![Biometric::new(vec![1, 2, 3, 4, 5])]);
551        assert!(claim169.has_biometrics());
552
553        let cwt_meta = CwtMeta::default();
554        let sign_key = [5u8; 32];
555
556        // Encode with biometrics
557        let result_with_bio = Encoder::new(claim169.clone(), cwt_meta.clone())
558            .sign_with_ed25519(&sign_key)
559            .and_then(|e| e.encode())
560            .unwrap();
561
562        // Encode without biometrics
563        let result_without_bio = Encoder::new(claim169, cwt_meta)
564            .skip_biometrics()
565            .sign_with_ed25519(&sign_key)
566            .and_then(|e| e.encode())
567            .unwrap();
568
569        // Without biometrics should be smaller
570        assert!(result_without_bio.len() < result_with_bio.len());
571    }
572
573    #[cfg(feature = "software-crypto")]
574    #[test]
575    fn test_encoder_roundtrip() {
576        use crate::crypto::software::{AesGcmDecryptor, Ed25519Signer};
577        use crate::model::VerificationStatus;
578        use crate::pipeline::claim169::transform;
579        use crate::pipeline::{base45_decode, cose_parse, cwt_parse, decompress};
580
581        let original_claim = Claim169 {
582            id: Some("roundtrip-builder".to_string()),
583            full_name: Some("Builder Roundtrip".to_string()),
584            email: Some("builder@test.com".to_string()),
585            ..Default::default()
586        };
587
588        let cwt_meta = CwtMeta::new()
589            .with_issuer("https://builder.test")
590            .with_expires_at(1800000000);
591
592        // Generate keys
593        let signer = Ed25519Signer::generate();
594        let verifier = signer.verifying_key();
595
596        let encrypt_key = [10u8; 32];
597        let nonce = [11u8; 12];
598
599        // Encode using builder
600        let qr_data = Encoder::new(original_claim.clone(), cwt_meta.clone())
601            .sign_with(signer, iana::Algorithm::EdDSA)
602            .encrypt_with_aes256_nonce(&encrypt_key, &nonce)
603            .unwrap()
604            .encode()
605            .unwrap();
606
607        // Decode and verify
608        let compressed = base45_decode(&qr_data).unwrap();
609        let cose_bytes = decompress(&compressed, 65536).unwrap();
610        let decryptor = AesGcmDecryptor::aes256(&encrypt_key).unwrap();
611        let cose_result = cose_parse(&cose_bytes, Some(&verifier), Some(&decryptor)).unwrap();
612
613        assert_eq!(
614            cose_result.verification_status,
615            VerificationStatus::Verified
616        );
617
618        let cwt_result = cwt_parse(&cose_result.payload).unwrap();
619        let decoded_claim = transform(cwt_result.claim_169, false).unwrap();
620
621        assert_eq!(decoded_claim.id, original_claim.id);
622        assert_eq!(decoded_claim.full_name, original_claim.full_name);
623        assert_eq!(decoded_claim.email, original_claim.email);
624    }
625
626    #[test]
627    fn test_generate_random_nonce() {
628        let nonce1 = generate_random_nonce();
629        let nonce2 = generate_random_nonce();
630
631        assert_eq!(nonce1.len(), 12);
632        assert_eq!(nonce2.len(), 12);
633        assert_ne!(nonce1, nonce2);
634    }
635}