1#![no_std]
22#![cfg_attr(docsrs, feature(doc_cfg))]
23#![deny(rustdoc::broken_intra_doc_links)]
25#![deny(unsafe_code)]
26use core::fmt::{self, Write};
29
30#[cfg(feature = "alloc")]
31extern crate alloc;
32#[cfg(feature = "alloc")]
33use alloc::vec::Vec;
34
35use chacha20::{
36 ChaCha20,
37 cipher::{KeyIvInit, StreamCipher, StreamCipherSeek},
38};
39use chacha20poly1305::{ChaCha20Poly1305, KeyInit, aead::AeadInOut};
40
41use rand_core::RngCore;
42use subtle::{Choice, ConstantTimeEq};
43
44#[cfg(feature = "alloc")]
45#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
46pub mod batch;
47
48pub const COMPACT_NOTE_SIZE: usize = 1 + 11 + 8 + 32; pub const NOTE_PLAINTEXT_SIZE: usize = COMPACT_NOTE_SIZE + 512;
55pub const OUT_PLAINTEXT_SIZE: usize = 32 + 32; const AEAD_TAG_SIZE: usize = 16;
59const NOTE_ENCRYPTION_NONCE: [u8; 12] = [0; 12];
60pub const ENC_CIPHERTEXT_SIZE: usize = NOTE_PLAINTEXT_SIZE + AEAD_TAG_SIZE;
62pub const OUT_CIPHERTEXT_SIZE: usize = OUT_PLAINTEXT_SIZE + AEAD_TAG_SIZE;
64
65pub struct OutgoingCipherKey(pub [u8; 32]);
68
69impl From<[u8; 32]> for OutgoingCipherKey {
70 fn from(ock: [u8; 32]) -> Self {
71 OutgoingCipherKey(ock)
72 }
73}
74
75impl AsRef<[u8]> for OutgoingCipherKey {
76 fn as_ref(&self) -> &[u8] {
77 &self.0
78 }
79}
80
81#[derive(Clone)]
85pub struct EphemeralKeyBytes(pub [u8; 32]);
86
87impl fmt::Debug for EphemeralKeyBytes {
88 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89 struct HexFmt<'b>(&'b [u8]);
90 impl<'b> fmt::Debug for HexFmt<'b> {
91 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92 f.write_char('"')?;
93 for b in self.0 {
94 f.write_fmt(format_args!("{:02x}", b))?;
95 }
96 f.write_char('"')
97 }
98 }
99
100 f.debug_tuple("EphemeralKeyBytes")
101 .field(&HexFmt(&self.0))
102 .finish()
103 }
104}
105
106impl AsRef<[u8]> for EphemeralKeyBytes {
107 fn as_ref(&self) -> &[u8] {
108 &self.0
109 }
110}
111
112impl From<[u8; 32]> for EphemeralKeyBytes {
113 fn from(value: [u8; 32]) -> EphemeralKeyBytes {
114 EphemeralKeyBytes(value)
115 }
116}
117
118impl ConstantTimeEq for EphemeralKeyBytes {
119 fn ct_eq(&self, other: &Self) -> Choice {
120 self.0.ct_eq(&other.0)
121 }
122}
123
124pub struct NotePlaintextBytes(pub [u8; NOTE_PLAINTEXT_SIZE]);
126pub struct OutPlaintextBytes(pub [u8; OUT_PLAINTEXT_SIZE]);
128
129#[derive(Copy, Clone, PartialEq, Eq)]
130enum NoteValidity {
131 Valid,
132 Invalid,
133}
134
135pub trait Domain {
141 type EphemeralSecretKey: ConstantTimeEq;
142 type EphemeralPublicKey;
143 type PreparedEphemeralPublicKey;
144 type SharedSecret;
145 type SymmetricKey: AsRef<[u8]>;
146 type Note;
147 type Recipient;
148 type DiversifiedTransmissionKey;
149 type IncomingViewingKey;
150 type OutgoingViewingKey;
151 type ValueCommitment;
152 type ExtractedCommitment;
153 type ExtractedCommitmentBytes: Eq + for<'a> From<&'a Self::ExtractedCommitment>;
154 type Memo;
155
156 fn derive_esk(note: &Self::Note) -> Option<Self::EphemeralSecretKey>;
164
165 fn get_pk_d(note: &Self::Note) -> Self::DiversifiedTransmissionKey;
167
168 fn prepare_epk(epk: Self::EphemeralPublicKey) -> Self::PreparedEphemeralPublicKey;
171
172 fn ka_derive_public(
174 note: &Self::Note,
175 esk: &Self::EphemeralSecretKey,
176 ) -> Self::EphemeralPublicKey;
177
178 fn ka_agree_enc(
181 esk: &Self::EphemeralSecretKey,
182 pk_d: &Self::DiversifiedTransmissionKey,
183 ) -> Self::SharedSecret;
184
185 fn ka_agree_dec(
189 ivk: &Self::IncomingViewingKey,
190 epk: &Self::PreparedEphemeralPublicKey,
191 ) -> Self::SharedSecret;
192
193 fn kdf(secret: Self::SharedSecret, ephemeral_key: &EphemeralKeyBytes) -> Self::SymmetricKey;
207
208 fn note_plaintext_bytes(note: &Self::Note, memo: &Self::Memo) -> NotePlaintextBytes;
210
211 fn derive_ock(
215 ovk: &Self::OutgoingViewingKey,
216 cv: &Self::ValueCommitment,
217 cmstar_bytes: &Self::ExtractedCommitmentBytes,
218 ephemeral_key: &EphemeralKeyBytes,
219 ) -> OutgoingCipherKey;
220
221 fn outgoing_plaintext_bytes(
223 note: &Self::Note,
224 esk: &Self::EphemeralSecretKey,
225 ) -> OutPlaintextBytes;
226
227 fn epk_bytes(epk: &Self::EphemeralPublicKey) -> EphemeralKeyBytes;
229
230 fn epk(ephemeral_key: &EphemeralKeyBytes) -> Option<Self::EphemeralPublicKey>;
235
236 fn cmstar(note: &Self::Note) -> Self::ExtractedCommitment;
238
239 fn parse_note_plaintext_without_memo_ivk(
259 &self,
260 ivk: &Self::IncomingViewingKey,
261 plaintext: &[u8],
262 ) -> Option<(Self::Note, Self::Recipient)>;
263
264 fn parse_note_plaintext_without_memo_ovk(
280 &self,
281 pk_d: &Self::DiversifiedTransmissionKey,
282 plaintext: &NotePlaintextBytes,
283 ) -> Option<(Self::Note, Self::Recipient)>;
284
285 fn extract_memo(&self, plaintext: &NotePlaintextBytes) -> Self::Memo;
293
294 fn extract_pk_d(out_plaintext: &OutPlaintextBytes) -> Option<Self::DiversifiedTransmissionKey>;
300
301 fn extract_esk(out_plaintext: &OutPlaintextBytes) -> Option<Self::EphemeralSecretKey>;
307}
308
309#[cfg(feature = "alloc")]
317#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
318pub trait BatchDomain: Domain {
319 fn batch_kdf<'a>(
325 items: impl Iterator<Item = (Option<Self::SharedSecret>, &'a EphemeralKeyBytes)>,
326 ) -> Vec<Option<Self::SymmetricKey>> {
327 items
329 .map(|(secret, ephemeral_key)| secret.map(|secret| Self::kdf(secret, ephemeral_key)))
330 .collect()
331 }
332
333 fn batch_epk(
343 ephemeral_keys: impl Iterator<Item = EphemeralKeyBytes>,
344 ) -> Vec<(Option<Self::PreparedEphemeralPublicKey>, EphemeralKeyBytes)> {
345 ephemeral_keys
347 .map(|ephemeral_key| {
348 (
349 Self::epk(&ephemeral_key).map(Self::prepare_epk),
350 ephemeral_key,
351 )
352 })
353 .collect()
354 }
355
356 fn batch_ka_agree_dec<'a>(
375 ivk: &Self::IncomingViewingKey,
376 epks: impl Iterator<Item = Option<&'a Self::PreparedEphemeralPublicKey>>,
377 ) -> Vec<Option<Self::SharedSecret>>
378 where
379 Self::PreparedEphemeralPublicKey: 'a,
380 {
381 epks.map(|epk| epk.map(|epk| Self::ka_agree_dec(ivk, epk)))
383 .collect()
384 }
385}
386
387pub trait ShieldedOutput<D: Domain, const CIPHERTEXT_SIZE: usize> {
396 fn ephemeral_key(&self) -> EphemeralKeyBytes;
398
399 fn cmstar_bytes(&self) -> D::ExtractedCommitmentBytes;
401
402 fn enc_ciphertext(&self) -> &[u8; CIPHERTEXT_SIZE];
404}
405
406pub struct NoteEncryption<D: Domain> {
419 epk: D::EphemeralPublicKey,
420 esk: D::EphemeralSecretKey,
421 note: D::Note,
422 memo: D::Memo,
423 ovk: Option<D::OutgoingViewingKey>,
425}
426
427impl<D: Domain> NoteEncryption<D> {
428 pub fn new(ovk: Option<D::OutgoingViewingKey>, note: D::Note, memo: D::Memo) -> Self {
431 let esk = D::derive_esk(¬e).expect("ZIP 212 is active.");
432 NoteEncryption {
433 epk: D::ka_derive_public(¬e, &esk),
434 esk,
435 note,
436 memo,
437 ovk,
438 }
439 }
440
441 #[cfg(feature = "pre-zip-212")]
447 #[cfg_attr(docsrs, doc(cfg(feature = "pre-zip-212")))]
448 pub fn new_with_esk(
449 esk: D::EphemeralSecretKey,
450 ovk: Option<D::OutgoingViewingKey>,
451 note: D::Note,
452 memo: D::Memo,
453 ) -> Self {
454 NoteEncryption {
455 epk: D::ka_derive_public(¬e, &esk),
456 esk,
457 note,
458 memo,
459 ovk,
460 }
461 }
462
463 pub fn esk(&self) -> &D::EphemeralSecretKey {
465 &self.esk
466 }
467
468 pub fn epk(&self) -> &D::EphemeralPublicKey {
471 &self.epk
472 }
473
474 pub fn encrypt_note_plaintext(&self) -> [u8; ENC_CIPHERTEXT_SIZE] {
476 let pk_d = D::get_pk_d(&self.note);
477 let shared_secret = D::ka_agree_enc(&self.esk, &pk_d);
478 let key = D::kdf(shared_secret, &D::epk_bytes(&self.epk));
479 let input = D::note_plaintext_bytes(&self.note, &self.memo);
480
481 let mut output = [0u8; ENC_CIPHERTEXT_SIZE];
482 output[..NOTE_PLAINTEXT_SIZE].copy_from_slice(&input.0);
483 let tag = ChaCha20Poly1305::new_from_slice(key.as_ref())
484 .expect("note encryption keys are 32 bytes")
485 .encrypt_inout_detached(
486 (&NOTE_ENCRYPTION_NONCE).into(),
487 &[],
488 (&mut output[..NOTE_PLAINTEXT_SIZE]).into(),
489 )
490 .unwrap();
491 output[NOTE_PLAINTEXT_SIZE..].copy_from_slice(&tag);
492
493 output
494 }
495
496 pub fn encrypt_outgoing_plaintext<R: RngCore>(
498 &self,
499 cv: &D::ValueCommitment,
500 cmstar: &D::ExtractedCommitment,
501 rng: &mut R,
502 ) -> [u8; OUT_CIPHERTEXT_SIZE] {
503 let (ock, input) = if let Some(ovk) = &self.ovk {
504 let ock = D::derive_ock(ovk, cv, &cmstar.into(), &D::epk_bytes(&self.epk));
505 let input = D::outgoing_plaintext_bytes(&self.note, &self.esk);
506
507 (ock, input)
508 } else {
509 let mut ock = OutgoingCipherKey([0; 32]);
511 let mut input = [0u8; OUT_PLAINTEXT_SIZE];
512
513 rng.fill_bytes(&mut ock.0);
514 rng.fill_bytes(&mut input);
515
516 (ock, OutPlaintextBytes(input))
517 };
518
519 let mut output = [0u8; OUT_CIPHERTEXT_SIZE];
520 output[..OUT_PLAINTEXT_SIZE].copy_from_slice(&input.0);
521 let tag = ChaCha20Poly1305::new_from_slice(ock.as_ref())
522 .expect("note encryption keys are 32 bytes")
523 .encrypt_inout_detached(
524 (&NOTE_ENCRYPTION_NONCE).into(),
525 &[],
526 (&mut output[..OUT_PLAINTEXT_SIZE]).into(),
527 )
528 .unwrap();
529 output[OUT_PLAINTEXT_SIZE..].copy_from_slice(&tag);
530
531 output
532 }
533}
534
535pub fn try_note_decryption<D: Domain, Output: ShieldedOutput<D, ENC_CIPHERTEXT_SIZE>>(
547 domain: &D,
548 ivk: &D::IncomingViewingKey,
549 output: &Output,
550) -> Option<(D::Note, D::Recipient, D::Memo)> {
551 let ephemeral_key = output.ephemeral_key();
552
553 let epk = D::prepare_epk(D::epk(&ephemeral_key)?);
554 let shared_secret = D::ka_agree_dec(ivk, &epk);
555 let key = D::kdf(shared_secret, &ephemeral_key);
556
557 try_note_decryption_inner(domain, ivk, &ephemeral_key, output, &key)
558}
559
560fn try_note_decryption_inner<D: Domain, Output: ShieldedOutput<D, ENC_CIPHERTEXT_SIZE>>(
561 domain: &D,
562 ivk: &D::IncomingViewingKey,
563 ephemeral_key: &EphemeralKeyBytes,
564 output: &Output,
565 key: &D::SymmetricKey,
566) -> Option<(D::Note, D::Recipient, D::Memo)> {
567 let enc_ciphertext = output.enc_ciphertext();
568
569 let mut plaintext =
570 NotePlaintextBytes(enc_ciphertext[..NOTE_PLAINTEXT_SIZE].try_into().unwrap());
571
572 ChaCha20Poly1305::new_from_slice(key.as_ref())
573 .expect("note encryption keys are 32 bytes")
574 .decrypt_inout_detached(
575 (&NOTE_ENCRYPTION_NONCE).into(),
576 &[],
577 (&mut plaintext.0[..]).into(),
578 enc_ciphertext[NOTE_PLAINTEXT_SIZE..]
579 .try_into()
580 .expect("fixed-size ciphertext includes a 16-byte AEAD tag"),
581 )
582 .ok()?;
583
584 let (note, to) = parse_note_plaintext_without_memo_ivk(
585 domain,
586 ivk,
587 ephemeral_key,
588 &output.cmstar_bytes(),
589 &plaintext.0,
590 )?;
591 let memo = domain.extract_memo(&plaintext);
592
593 Some((note, to, memo))
594}
595
596fn parse_note_plaintext_without_memo_ivk<D: Domain>(
597 domain: &D,
598 ivk: &D::IncomingViewingKey,
599 ephemeral_key: &EphemeralKeyBytes,
600 cmstar_bytes: &D::ExtractedCommitmentBytes,
601 plaintext: &[u8],
602) -> Option<(D::Note, D::Recipient)> {
603 let (note, to) = domain.parse_note_plaintext_without_memo_ivk(ivk, plaintext)?;
604
605 if let NoteValidity::Valid = check_note_validity::<D>(¬e, ephemeral_key, cmstar_bytes) {
606 Some((note, to))
607 } else {
608 None
609 }
610}
611
612fn check_note_validity<D: Domain>(
613 note: &D::Note,
614 ephemeral_key: &EphemeralKeyBytes,
615 cmstar_bytes: &D::ExtractedCommitmentBytes,
616) -> NoteValidity {
617 if &D::ExtractedCommitmentBytes::from(&D::cmstar(note)) == cmstar_bytes {
618 if let Some(derived_esk) = D::derive_esk(note) {
621 if D::epk_bytes(&D::ka_derive_public(note, &derived_esk))
622 .ct_eq(ephemeral_key)
623 .into()
624 {
625 NoteValidity::Valid
626 } else {
627 NoteValidity::Invalid
628 }
629 } else {
630 NoteValidity::Valid
632 }
633 } else {
634 NoteValidity::Invalid
636 }
637}
638
639pub fn try_compact_note_decryption<D: Domain, Output: ShieldedOutput<D, COMPACT_NOTE_SIZE>>(
651 domain: &D,
652 ivk: &D::IncomingViewingKey,
653 output: &Output,
654) -> Option<(D::Note, D::Recipient)> {
655 let ephemeral_key = output.ephemeral_key();
656
657 let epk = D::prepare_epk(D::epk(&ephemeral_key)?);
658 let shared_secret = D::ka_agree_dec(ivk, &epk);
659 let key = D::kdf(shared_secret, &ephemeral_key);
660
661 try_compact_note_decryption_inner(domain, ivk, &ephemeral_key, output, &key)
662}
663
664fn try_compact_note_decryption_inner<D: Domain, Output: ShieldedOutput<D, COMPACT_NOTE_SIZE>>(
665 domain: &D,
666 ivk: &D::IncomingViewingKey,
667 ephemeral_key: &EphemeralKeyBytes,
668 output: &Output,
669 key: &D::SymmetricKey,
670) -> Option<(D::Note, D::Recipient)> {
671 let mut plaintext = [0; COMPACT_NOTE_SIZE];
673 plaintext.copy_from_slice(output.enc_ciphertext());
674 let mut keystream = ChaCha20::new_from_slices(key.as_ref(), &NOTE_ENCRYPTION_NONCE)
675 .expect("note encryption keys are 32 bytes and nonces are 12 bytes");
676 keystream.seek(64);
677 keystream.apply_keystream(&mut plaintext);
678
679 parse_note_plaintext_without_memo_ivk(
680 domain,
681 ivk,
682 ephemeral_key,
683 &output.cmstar_bytes(),
684 &plaintext,
685 )
686}
687
688pub fn try_output_recovery_with_ovk<D: Domain, Output: ShieldedOutput<D, ENC_CIPHERTEXT_SIZE>>(
700 domain: &D,
701 ovk: &D::OutgoingViewingKey,
702 output: &Output,
703 cv: &D::ValueCommitment,
704 out_ciphertext: &[u8; OUT_CIPHERTEXT_SIZE],
705) -> Option<(D::Note, D::Recipient, D::Memo)> {
706 let ock = D::derive_ock(ovk, cv, &output.cmstar_bytes(), &output.ephemeral_key());
707 try_output_recovery_with_ock(domain, &ock, output, out_ciphertext)
708}
709
710pub fn try_output_recovery_with_ock<D: Domain, Output: ShieldedOutput<D, ENC_CIPHERTEXT_SIZE>>(
724 domain: &D,
725 ock: &OutgoingCipherKey,
726 output: &Output,
727 out_ciphertext: &[u8; OUT_CIPHERTEXT_SIZE],
728) -> Option<(D::Note, D::Recipient, D::Memo)> {
729 let mut op = OutPlaintextBytes([0; OUT_PLAINTEXT_SIZE]);
730 op.0.copy_from_slice(&out_ciphertext[..OUT_PLAINTEXT_SIZE]);
731
732 ChaCha20Poly1305::new_from_slice(ock.as_ref())
733 .expect("note encryption keys are 32 bytes")
734 .decrypt_inout_detached(
735 (&NOTE_ENCRYPTION_NONCE).into(),
736 &[],
737 (&mut op.0[..]).into(),
738 out_ciphertext[OUT_PLAINTEXT_SIZE..]
739 .try_into()
740 .expect("fixed-size ciphertext includes a 16-byte AEAD tag"),
741 )
742 .ok()?;
743
744 let pk_d = D::extract_pk_d(&op)?;
745 let esk = D::extract_esk(&op)?;
746
747 try_output_recovery_with_pkd_esk(domain, pk_d, esk, output)
748}
749
750pub fn try_output_recovery_with_pkd_esk<
764 D: Domain,
765 Output: ShieldedOutput<D, ENC_CIPHERTEXT_SIZE>,
766>(
767 domain: &D,
768 pk_d: D::DiversifiedTransmissionKey,
769 esk: D::EphemeralSecretKey,
770 output: &Output,
771) -> Option<(D::Note, D::Recipient, D::Memo)> {
772 let ephemeral_key = output.ephemeral_key();
773 let shared_secret = D::ka_agree_enc(&esk, &pk_d);
774 let key = D::kdf(shared_secret, &ephemeral_key);
778
779 let enc_ciphertext = output.enc_ciphertext();
780 let mut plaintext = NotePlaintextBytes([0; NOTE_PLAINTEXT_SIZE]);
781 plaintext
782 .0
783 .copy_from_slice(&enc_ciphertext[..NOTE_PLAINTEXT_SIZE]);
784
785 ChaCha20Poly1305::new_from_slice(key.as_ref())
786 .expect("note encryption keys are 32 bytes")
787 .decrypt_inout_detached(
788 (&NOTE_ENCRYPTION_NONCE).into(),
789 &[],
790 (&mut plaintext.0[..]).into(),
791 enc_ciphertext[NOTE_PLAINTEXT_SIZE..]
792 .try_into()
793 .expect("fixed-size ciphertext includes a 16-byte AEAD tag"),
794 )
795 .ok()?;
796
797 let (note, to) = domain.parse_note_plaintext_without_memo_ovk(&pk_d, &plaintext)?;
798 let memo = domain.extract_memo(&plaintext);
799
800 #[allow(clippy::collapsible_if)]
805 if let Some(derived_esk) = D::derive_esk(¬e) {
806 if (!derived_esk.ct_eq(&esk)).into() {
807 return None;
808 }
809 }
810
811 if let NoteValidity::Valid =
812 check_note_validity::<D>(¬e, &ephemeral_key, &output.cmstar_bytes())
813 {
814 Some((note, to, memo))
815 } else {
816 None
817 }
818}
819
820#[cfg(test)]
821mod tests {
822 #[test]
823 fn encryption_state_is_cleared_on_drop() {
824 fn assert_cleanup<T: cipher::zeroize::ZeroizeOnDrop>() {}
825
826 assert_cleanup::<chacha20::ChaCha20>();
827 assert_cleanup::<chacha20poly1305::ChaCha20Poly1305>();
828 }
829}