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 .unwrap_or(0);
411
412 let skew = self.clock_skew_tolerance_seconds;
413
414 if let Some(exp) = cwt_result.meta.expires_at {
415 if now > exp + skew {
416 return Err(Claim169Error::Expired(exp));
417 }
418 }
419
420 if let Some(nbf) = cwt_result.meta.not_before {
421 if now + skew < nbf {
422 return Err(Claim169Error::NotYetValid(nbf));
423 }
424 }
425 } else {
426 warnings.push(Warning {
427 code: WarningCode::TimestampValidationSkipped,
428 message: "Timestamp validation was disabled".to_string(),
429 });
430 }
431
432 let claim169 = pipeline::claim169_transform(cwt_result.claim_169, self.skip_biometrics)?;
434
435 if self.skip_biometrics {
436 warnings.push(Warning {
437 code: WarningCode::BiometricsSkipped,
438 message: "Biometric data was skipped".to_string(),
439 });
440 }
441
442 if !claim169.unknown_fields.is_empty() {
443 warnings.push(Warning {
444 code: WarningCode::UnknownFields,
445 message: format!(
446 "Found {} unknown fields (keys: {:?})",
447 claim169.unknown_fields.len(),
448 claim169.unknown_fields.keys().collect::<Vec<_>>()
449 ),
450 });
451 }
452
453 Ok(DecodeResult {
454 claim169,
455 cwt_meta: cwt_result.meta,
456 verification_status: cose_result.verification_status,
457 warnings,
458 })
459 }
460}
461
462#[cfg(test)]
463mod tests {
464 use super::*;
465 use crate::model::{Claim169, CwtMeta, VerificationStatus};
466
467 fn create_test_qr() -> String {
469 use crate::Encoder;
470
471 let claim169 = Claim169 {
472 id: Some("test-123".to_string()),
473 full_name: Some("Test User".to_string()),
474 ..Default::default()
475 };
476
477 let cwt_meta = CwtMeta::new()
478 .with_issuer("https://test.issuer")
479 .with_expires_at(i64::MAX); Encoder::new(claim169, cwt_meta)
482 .allow_unsigned()
483 .encode()
484 .unwrap()
485 }
486
487 #[test]
488 fn test_decoder_requires_verifier_or_allow_unverified() {
489 let qr_text = create_test_qr();
490
491 let result = Decoder::new(&qr_text).decode();
492
493 assert!(result.is_err());
494 match result.unwrap_err() {
495 Claim169Error::DecodingConfig(msg) => {
496 assert!(msg.contains("allow_unverified"));
497 }
498 e => panic!("Expected DecodingConfig error, got: {:?}", e),
499 }
500 }
501
502 #[test]
503 fn test_decoder_allow_unverified() {
504 let qr_text = create_test_qr();
505
506 let result = Decoder::new(&qr_text).allow_unverified().decode();
507
508 assert!(result.is_ok());
509 let decoded = result.unwrap();
510 assert_eq!(decoded.claim169.id, Some("test-123".to_string()));
511 assert_eq!(decoded.claim169.full_name, Some("Test User".to_string()));
512 assert_eq!(decoded.verification_status, VerificationStatus::Skipped);
513 }
514
515 #[test]
516 fn test_decoder_accepts_string() {
517 let qr_text = create_test_qr();
518
519 let result = Decoder::new(qr_text.clone()).allow_unverified().decode();
521 assert!(result.is_ok());
522
523 let result = Decoder::new(&qr_text).allow_unverified().decode();
525 assert!(result.is_ok());
526 }
527
528 #[test]
529 fn test_decoder_skip_biometrics() {
530 let qr_text = create_test_qr();
531
532 let result = Decoder::new(&qr_text)
533 .allow_unverified()
534 .skip_biometrics()
535 .decode()
536 .unwrap();
537
538 assert!(result
540 .warnings
541 .iter()
542 .any(|w| w.code == WarningCode::BiometricsSkipped));
543 }
544
545 #[test]
546 fn test_decoder_without_timestamp_validation() {
547 let qr_text = create_test_qr();
548
549 let result = Decoder::new(&qr_text)
550 .allow_unverified()
551 .without_timestamp_validation()
552 .decode()
553 .unwrap();
554
555 assert!(result
557 .warnings
558 .iter()
559 .any(|w| w.code == WarningCode::TimestampValidationSkipped));
560 }
561
562 #[cfg(feature = "software-crypto")]
563 #[test]
564 fn test_decoder_roundtrip_ed25519() {
565 use crate::crypto::software::Ed25519Signer;
566 use crate::Encoder;
567 use coset::iana;
568
569 let claim169 = Claim169 {
570 id: Some("signed-test".to_string()),
571 full_name: Some("Signed User".to_string()),
572 email: Some("signed@example.com".to_string()),
573 ..Default::default()
574 };
575
576 let cwt_meta = CwtMeta::new()
577 .with_issuer("https://signed.test")
578 .with_expires_at(i64::MAX);
579
580 let signer = Ed25519Signer::generate();
582 let public_key = signer.public_key_bytes();
583
584 let qr_data = Encoder::new(claim169.clone(), cwt_meta)
586 .sign_with(signer, iana::Algorithm::EdDSA)
587 .encode()
588 .unwrap();
589
590 let result = Decoder::new(&qr_data)
592 .verify_with_ed25519(&public_key)
593 .unwrap()
594 .decode()
595 .unwrap();
596
597 assert_eq!(result.verification_status, VerificationStatus::Verified);
598 assert_eq!(result.claim169.id, claim169.id);
599 assert_eq!(result.claim169.full_name, claim169.full_name);
600 assert_eq!(result.claim169.email, claim169.email);
601 }
602
603 #[cfg(feature = "software-crypto")]
604 #[test]
605 fn test_decoder_roundtrip_encrypted() {
606 use crate::crypto::software::Ed25519Signer;
607 use crate::Encoder;
608 use coset::iana;
609
610 let claim169 = Claim169 {
611 id: Some("encrypted-test".to_string()),
612 full_name: Some("Encrypted User".to_string()),
613 ..Default::default()
614 };
615
616 let cwt_meta = CwtMeta::new()
617 .with_issuer("https://encrypted.test")
618 .with_expires_at(i64::MAX);
619
620 let signer = Ed25519Signer::generate();
622 let public_key = signer.public_key_bytes();
623 let aes_key = [42u8; 32];
624 let nonce = [7u8; 12];
625
626 let qr_data = Encoder::new(claim169.clone(), cwt_meta)
628 .sign_with(signer, iana::Algorithm::EdDSA)
629 .encrypt_with_aes256_nonce(&aes_key, &nonce)
630 .unwrap()
631 .encode()
632 .unwrap();
633
634 let result = Decoder::new(&qr_data)
636 .decrypt_with_aes256(&aes_key)
637 .unwrap()
638 .verify_with_ed25519(&public_key)
639 .unwrap()
640 .decode()
641 .unwrap();
642
643 assert_eq!(result.verification_status, VerificationStatus::Verified);
644 assert_eq!(result.claim169.id, claim169.id);
645 assert_eq!(result.claim169.full_name, claim169.full_name);
646 }
647
648 #[cfg(feature = "software-crypto")]
649 #[test]
650 fn test_decoder_wrong_key_fails() {
651 use crate::crypto::software::Ed25519Signer;
652 use crate::Encoder;
653 use coset::iana;
654
655 let claim169 = Claim169::minimal("test", "Test");
656 let cwt_meta = CwtMeta::default();
657
658 let signer = Ed25519Signer::generate();
659 let wrong_signer = Ed25519Signer::generate();
660 let wrong_public_key = wrong_signer.public_key_bytes();
661
662 let qr_data = Encoder::new(claim169, cwt_meta)
663 .sign_with(signer, iana::Algorithm::EdDSA)
664 .encode()
665 .unwrap();
666
667 let result = Decoder::new(&qr_data)
669 .verify_with_ed25519(&wrong_public_key)
670 .unwrap()
671 .decode();
672
673 assert!(result.is_err());
674 assert!(matches!(
675 result.unwrap_err(),
676 Claim169Error::SignatureInvalid(_)
677 ));
678 }
679
680 #[test]
681 fn test_decoder_invalid_base45() {
682 let result = Decoder::new("!!!invalid base45!!!")
683 .allow_unverified()
684 .decode();
685
686 assert!(result.is_err());
687 assert!(matches!(
688 result.unwrap_err(),
689 Claim169Error::Base45Decode(_)
690 ));
691 }
692
693 #[test]
694 fn test_decoder_max_decompressed_bytes() {
695 let qr_text = create_test_qr();
696
697 let result = Decoder::new(&qr_text)
699 .allow_unverified()
700 .max_decompressed_bytes(10)
701 .decode();
702
703 assert!(result.is_err());
704 assert!(matches!(
705 result.unwrap_err(),
706 Claim169Error::DecompressLimitExceeded { .. }
707 ));
708 }
709}