1use crate::crypto::traits::{Decryptor, SignatureVerifier};
44use crate::error::{Claim169Error, Result};
45use crate::model::VerificationStatus;
46use crate::pipeline;
47use crate::{DecodeResult, Warning, WarningCode};
48
49#[cfg(feature = "software-crypto")]
50use crate::crypto::software::{AesGcmDecryptor, EcdsaP256Verifier, Ed25519Verifier};
51
52use std::time::{SystemTime, UNIX_EPOCH};
53
54const DEFAULT_MAX_DECOMPRESSED_BYTES: usize = 65536;
56
57pub struct Decoder {
87 qr_text: String,
88 verifier: Option<Box<dyn SignatureVerifier + Send + Sync>>,
89 decryptor: Option<Box<dyn Decryptor + Send + Sync>>,
90 allow_unverified: bool,
91 skip_biometrics: bool,
92 validate_timestamps: bool,
93 clock_skew_tolerance_seconds: i64,
94 max_decompressed_bytes: usize,
95}
96
97impl Decoder {
98 pub fn new(qr_text: impl Into<String>) -> Self {
111 Self {
112 qr_text: qr_text.into(),
113 verifier: None,
114 decryptor: None,
115 allow_unverified: false,
116 skip_biometrics: false,
117 validate_timestamps: true,
118 clock_skew_tolerance_seconds: 0,
119 max_decompressed_bytes: DEFAULT_MAX_DECOMPRESSED_BYTES,
120 }
121 }
122
123 pub fn verify_with<V: SignatureVerifier + 'static>(mut self, verifier: V) -> Self {
138 self.verifier = Some(Box::new(verifier));
139 self
140 }
141
142 #[cfg(feature = "software-crypto")]
161 pub fn verify_with_ed25519(self, public_key: &[u8]) -> Result<Self> {
162 let verifier = Ed25519Verifier::from_bytes(public_key)
163 .map_err(|e| Claim169Error::Crypto(e.to_string()))?;
164
165 Ok(self.verify_with(verifier))
166 }
167
168 #[cfg(feature = "software-crypto")]
180 pub fn verify_with_ed25519_pem(self, pem: &str) -> Result<Self> {
181 let verifier =
182 Ed25519Verifier::from_pem(pem).map_err(|e| Claim169Error::Crypto(e.to_string()))?;
183
184 Ok(self.verify_with(verifier))
185 }
186
187 #[cfg(feature = "software-crypto")]
199 pub fn verify_with_ecdsa_p256(self, public_key: &[u8]) -> Result<Self> {
200 let verifier = EcdsaP256Verifier::from_sec1_bytes(public_key)
201 .map_err(|e| Claim169Error::Crypto(e.to_string()))?;
202
203 Ok(self.verify_with(verifier))
204 }
205
206 #[cfg(feature = "software-crypto")]
218 pub fn verify_with_ecdsa_p256_pem(self, pem: &str) -> Result<Self> {
219 let verifier =
220 EcdsaP256Verifier::from_pem(pem).map_err(|e| Claim169Error::Crypto(e.to_string()))?;
221
222 Ok(self.verify_with(verifier))
223 }
224
225 pub fn decrypt_with<D: Decryptor + 'static>(mut self, decryptor: D) -> Self {
233 self.decryptor = Some(Box::new(decryptor));
234 self
235 }
236
237 #[cfg(feature = "software-crypto")]
247 pub fn decrypt_with_aes256(self, key: &[u8]) -> Result<Self> {
248 let decryptor =
249 AesGcmDecryptor::aes256(key).map_err(|e| Claim169Error::Crypto(e.to_string()))?;
250
251 Ok(self.decrypt_with(decryptor))
252 }
253
254 #[cfg(feature = "software-crypto")]
264 pub fn decrypt_with_aes128(self, key: &[u8]) -> Result<Self> {
265 let decryptor =
266 AesGcmDecryptor::aes128(key).map_err(|e| Claim169Error::Crypto(e.to_string()))?;
267
268 Ok(self.decrypt_with(decryptor))
269 }
270
271 pub fn allow_unverified(mut self) -> Self {
284 self.allow_unverified = true;
285 self
286 }
287
288 pub fn skip_biometrics(mut self) -> Self {
293 self.skip_biometrics = true;
294 self
295 }
296
297 pub fn without_timestamp_validation(mut self) -> Self {
305 self.validate_timestamps = false;
306 self
307 }
308
309 pub fn clock_skew_tolerance(mut self, seconds: i64) -> Self {
318 self.clock_skew_tolerance_seconds = seconds;
319 self
320 }
321
322 pub fn max_decompressed_bytes(mut self, bytes: usize) -> Self {
331 self.max_decompressed_bytes = bytes;
332 self
333 }
334
335 pub fn decode(self) -> Result<DecodeResult> {
365 let mut warnings = Vec::new();
366
367 let verifier_ref: Option<&dyn SignatureVerifier> = self
369 .verifier
370 .as_ref()
371 .map(|v| v.as_ref() as &dyn SignatureVerifier);
372 let decryptor_ref: Option<&dyn Decryptor> = self
373 .decryptor
374 .as_ref()
375 .map(|d| d.as_ref() as &dyn Decryptor);
376
377 let compressed = pipeline::base45_decode(&self.qr_text)?;
379
380 let cose_bytes = pipeline::decompress(&compressed, self.max_decompressed_bytes)?;
382
383 let cose_result = pipeline::cose_parse(&cose_bytes, verifier_ref, decryptor_ref)?;
385
386 if !self.allow_unverified && cose_result.verification_status == VerificationStatus::Skipped
388 {
389 return Err(Claim169Error::DecodingConfig(
390 "verification required but no verifier provided - use allow_unverified() to skip"
391 .to_string(),
392 ));
393 }
394
395 if cose_result.verification_status == VerificationStatus::Failed {
397 return Err(Claim169Error::SignatureInvalid(
398 "signature verification failed".to_string(),
399 ));
400 }
401
402 let cwt_result = pipeline::cwt_parse(&cose_result.payload)?;
404
405 if self.validate_timestamps {
407 let now = SystemTime::now()
408 .duration_since(UNIX_EPOCH)
409 .map(|d| d.as_secs() as i64)
410 .map_err(|_| {
411 Claim169Error::DecodingConfig("system clock is before Unix epoch".to_string())
412 })?;
413
414 let skew = self.clock_skew_tolerance_seconds;
415
416 if let Some(exp) = cwt_result.meta.expires_at {
417 if now > exp + skew {
418 return Err(Claim169Error::Expired(exp));
419 }
420 }
421
422 if let Some(nbf) = cwt_result.meta.not_before {
423 if now + skew < nbf {
424 return Err(Claim169Error::NotYetValid(nbf));
425 }
426 }
427 } else {
428 warnings.push(Warning {
429 code: WarningCode::TimestampValidationSkipped,
430 message: "Timestamp validation was disabled".to_string(),
431 });
432 }
433
434 let claim169 = pipeline::claim169_transform(cwt_result.claim_169, self.skip_biometrics)?;
436
437 if self.skip_biometrics {
438 warnings.push(Warning {
439 code: WarningCode::BiometricsSkipped,
440 message: "Biometric data was skipped".to_string(),
441 });
442 }
443
444 if !claim169.unknown_fields.is_empty() {
445 warnings.push(Warning {
446 code: WarningCode::UnknownFields,
447 message: format!(
448 "Found {} unknown fields (keys: {:?})",
449 claim169.unknown_fields.len(),
450 claim169.unknown_fields.keys().collect::<Vec<_>>()
451 ),
452 });
453 }
454
455 Ok(DecodeResult {
456 claim169,
457 cwt_meta: cwt_result.meta,
458 verification_status: cose_result.verification_status,
459 x509_headers: cose_result.x509_headers,
460 warnings,
461 })
462 }
463}
464
465#[cfg(test)]
466mod tests {
467 use super::*;
468 use crate::model::{Claim169, CwtMeta, VerificationStatus};
469
470 fn create_test_qr() -> String {
472 use crate::Encoder;
473
474 let claim169 = Claim169 {
475 id: Some("test-123".to_string()),
476 full_name: Some("Test User".to_string()),
477 ..Default::default()
478 };
479
480 let cwt_meta = CwtMeta::new()
481 .with_issuer("https://test.issuer")
482 .with_expires_at(i64::MAX); Encoder::new(claim169, cwt_meta)
485 .allow_unsigned()
486 .encode()
487 .unwrap()
488 }
489
490 #[test]
491 fn test_decoder_requires_verifier_or_allow_unverified() {
492 let qr_text = create_test_qr();
493
494 let result = Decoder::new(&qr_text).decode();
495
496 assert!(result.is_err());
497 match result.unwrap_err() {
498 Claim169Error::DecodingConfig(msg) => {
499 assert!(msg.contains("allow_unverified"));
500 }
501 e => panic!("Expected DecodingConfig error, got: {:?}", e),
502 }
503 }
504
505 #[test]
506 fn test_decoder_allow_unverified() {
507 let qr_text = create_test_qr();
508
509 let result = Decoder::new(&qr_text).allow_unverified().decode();
510
511 assert!(result.is_ok());
512 let decoded = result.unwrap();
513 assert_eq!(decoded.claim169.id, Some("test-123".to_string()));
514 assert_eq!(decoded.claim169.full_name, Some("Test User".to_string()));
515 assert_eq!(decoded.verification_status, VerificationStatus::Skipped);
516 }
517
518 #[test]
519 fn test_decoder_accepts_string() {
520 let qr_text = create_test_qr();
521
522 let result = Decoder::new(qr_text.clone()).allow_unverified().decode();
524 assert!(result.is_ok());
525
526 let result = Decoder::new(&qr_text).allow_unverified().decode();
528 assert!(result.is_ok());
529 }
530
531 #[test]
532 fn test_decoder_skip_biometrics() {
533 let qr_text = create_test_qr();
534
535 let result = Decoder::new(&qr_text)
536 .allow_unverified()
537 .skip_biometrics()
538 .decode()
539 .unwrap();
540
541 assert!(result
543 .warnings
544 .iter()
545 .any(|w| w.code == WarningCode::BiometricsSkipped));
546 }
547
548 #[test]
549 fn test_decoder_without_timestamp_validation() {
550 let qr_text = create_test_qr();
551
552 let result = Decoder::new(&qr_text)
553 .allow_unverified()
554 .without_timestamp_validation()
555 .decode()
556 .unwrap();
557
558 assert!(result
560 .warnings
561 .iter()
562 .any(|w| w.code == WarningCode::TimestampValidationSkipped));
563 }
564
565 #[cfg(feature = "software-crypto")]
566 #[test]
567 fn test_decoder_roundtrip_ed25519() {
568 use crate::crypto::software::Ed25519Signer;
569 use crate::Encoder;
570 use coset::iana;
571
572 let claim169 = Claim169 {
573 id: Some("signed-test".to_string()),
574 full_name: Some("Signed User".to_string()),
575 email: Some("signed@example.com".to_string()),
576 ..Default::default()
577 };
578
579 let cwt_meta = CwtMeta::new()
580 .with_issuer("https://signed.test")
581 .with_expires_at(i64::MAX);
582
583 let signer = Ed25519Signer::generate();
585 let public_key = signer.public_key_bytes();
586
587 let qr_data = Encoder::new(claim169.clone(), cwt_meta)
589 .sign_with(signer, iana::Algorithm::EdDSA)
590 .encode()
591 .unwrap();
592
593 let result = Decoder::new(&qr_data)
595 .verify_with_ed25519(&public_key)
596 .unwrap()
597 .decode()
598 .unwrap();
599
600 assert_eq!(result.verification_status, VerificationStatus::Verified);
601 assert_eq!(result.claim169.id, claim169.id);
602 assert_eq!(result.claim169.full_name, claim169.full_name);
603 assert_eq!(result.claim169.email, claim169.email);
604 }
605
606 #[cfg(feature = "software-crypto")]
607 #[test]
608 fn test_decoder_roundtrip_encrypted() {
609 use crate::crypto::software::Ed25519Signer;
610 use crate::Encoder;
611 use coset::iana;
612
613 let claim169 = Claim169 {
614 id: Some("encrypted-test".to_string()),
615 full_name: Some("Encrypted User".to_string()),
616 ..Default::default()
617 };
618
619 let cwt_meta = CwtMeta::new()
620 .with_issuer("https://encrypted.test")
621 .with_expires_at(i64::MAX);
622
623 let signer = Ed25519Signer::generate();
625 let public_key = signer.public_key_bytes();
626 let aes_key = [42u8; 32];
627 let nonce = [7u8; 12];
628
629 let qr_data = Encoder::new(claim169.clone(), cwt_meta)
631 .sign_with(signer, iana::Algorithm::EdDSA)
632 .encrypt_with_aes256_nonce(&aes_key, &nonce)
633 .unwrap()
634 .encode()
635 .unwrap();
636
637 let result = Decoder::new(&qr_data)
639 .decrypt_with_aes256(&aes_key)
640 .unwrap()
641 .verify_with_ed25519(&public_key)
642 .unwrap()
643 .decode()
644 .unwrap();
645
646 assert_eq!(result.verification_status, VerificationStatus::Verified);
647 assert_eq!(result.claim169.id, claim169.id);
648 assert_eq!(result.claim169.full_name, claim169.full_name);
649 }
650
651 #[cfg(feature = "software-crypto")]
652 #[test]
653 fn test_decoder_wrong_key_fails() {
654 use crate::crypto::software::Ed25519Signer;
655 use crate::Encoder;
656 use coset::iana;
657
658 let claim169 = Claim169::minimal("test", "Test");
659 let cwt_meta = CwtMeta::default();
660
661 let signer = Ed25519Signer::generate();
662 let wrong_signer = Ed25519Signer::generate();
663 let wrong_public_key = wrong_signer.public_key_bytes();
664
665 let qr_data = Encoder::new(claim169, cwt_meta)
666 .sign_with(signer, iana::Algorithm::EdDSA)
667 .encode()
668 .unwrap();
669
670 let result = Decoder::new(&qr_data)
672 .verify_with_ed25519(&wrong_public_key)
673 .unwrap()
674 .decode();
675
676 assert!(result.is_err());
677 assert!(matches!(
678 result.unwrap_err(),
679 Claim169Error::SignatureInvalid(_)
680 ));
681 }
682
683 #[test]
684 fn test_decoder_invalid_base45() {
685 let result = Decoder::new("!!!invalid base45!!!")
686 .allow_unverified()
687 .decode();
688
689 assert!(result.is_err());
690 assert!(matches!(
691 result.unwrap_err(),
692 Claim169Error::Base45Decode(_)
693 ));
694 }
695
696 #[test]
697 fn test_decoder_max_decompressed_bytes() {
698 let qr_text = create_test_qr();
699
700 let result = Decoder::new(&qr_text)
702 .allow_unverified()
703 .max_decompressed_bytes(10)
704 .decode();
705
706 assert!(result.is_err());
707 assert!(matches!(
708 result.unwrap_err(),
709 Claim169Error::DecompressLimitExceeded { .. }
710 ));
711 }
712}