Skip to main content

zcash_note_encryption/
lib.rs

1//! Note encryption for Zcash transactions.
2//!
3//! This crate implements the [in-band secret distribution scheme] for the
4//! Sapling and
5//! Orchard protocols. It provides reusable methods that implement common note
6//! encryption
7//! and trial decryption logic, and enforce protocol-agnostic verification
8//! requirements.
9//!
10//! Protocol-specific logic is handled via the [`Domain`] trait. Implementations
11//! of this
12//! trait are provided in the [`zcash_primitives`] (for Sapling) and [`orchard`]
13//! crates;
14//! users with their own existing types can similarly implement the trait
15//! themselves.
16//!
17//! [in-band secret distribution scheme]: https://zips.z.cash/protocol/protocol.pdf#saplingandorchardinband
18//! [`zcash_primitives`]: https://crates.io/crates/zcash_primitives
19//! [`orchard`]: https://crates.io/crates/orchard
20
21#![no_std]
22#![cfg_attr(docsrs, feature(doc_cfg))]
23// Catch documentation errors caused by code changes.
24#![deny(rustdoc::broken_intra_doc_links)]
25#![deny(unsafe_code)]
26// TODO: #![deny(missing_docs)]
27
28use 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
48/// The size of a compact note.
49pub const COMPACT_NOTE_SIZE: usize = 1 + // version
50    11 + // diversifier
51    8  + // value
52    32; // rseed (or rcm prior to ZIP 212)
53/// The size of [`NotePlaintextBytes`].
54pub const NOTE_PLAINTEXT_SIZE: usize = COMPACT_NOTE_SIZE + 512;
55/// The size of [`OutPlaintextBytes`].
56pub const OUT_PLAINTEXT_SIZE: usize = 32 + // pk_d
57    32; // esk
58const AEAD_TAG_SIZE: usize = 16;
59const NOTE_ENCRYPTION_NONCE: [u8; 12] = [0; 12];
60/// The size of an encrypted note plaintext.
61pub const ENC_CIPHERTEXT_SIZE: usize = NOTE_PLAINTEXT_SIZE + AEAD_TAG_SIZE;
62/// The size of an encrypted outgoing plaintext.
63pub const OUT_CIPHERTEXT_SIZE: usize = OUT_PLAINTEXT_SIZE + AEAD_TAG_SIZE;
64
65/// A symmetric key that can be used to recover a single Sapling or Orchard
66/// output.
67pub 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/// Newtype representing the byte encoding of an [`EphemeralPublicKey`].
82///
83/// [`EphemeralPublicKey`]: Domain::EphemeralPublicKey
84#[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
124/// Newtype representing the byte encoding of a note plaintext.
125pub struct NotePlaintextBytes(pub [u8; NOTE_PLAINTEXT_SIZE]);
126/// Newtype representing the byte encoding of a outgoing plaintext.
127pub struct OutPlaintextBytes(pub [u8; OUT_PLAINTEXT_SIZE]);
128
129#[derive(Copy, Clone, PartialEq, Eq)]
130enum NoteValidity {
131    Valid,
132    Invalid,
133}
134
135/// Trait that encapsulates protocol-specific note encryption types and logic.
136///
137/// This trait enables most of the note encryption logic to be shared between
138/// Sapling and
139/// Orchard, as well as between different implementations of those protocols.
140pub 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    /// Derives the `EphemeralSecretKey` corresponding to this note.
157    ///
158    /// Returns `None` if the note was created prior to [ZIP 212], and doesn't
159    /// have a
160    /// deterministic `EphemeralSecretKey`.
161    ///
162    /// [ZIP 212]: https://zips.z.cash/zip-0212
163    fn derive_esk(note: &Self::Note) -> Option<Self::EphemeralSecretKey>;
164
165    /// Extracts the `DiversifiedTransmissionKey` from the note.
166    fn get_pk_d(note: &Self::Note) -> Self::DiversifiedTransmissionKey;
167
168    /// Prepare an ephemeral public key for more efficient scalar
169    /// multiplication.
170    fn prepare_epk(epk: Self::EphemeralPublicKey) -> Self::PreparedEphemeralPublicKey;
171
172    /// Derives `EphemeralPublicKey` from `esk` and the note's diversifier.
173    fn ka_derive_public(
174        note: &Self::Note,
175        esk: &Self::EphemeralSecretKey,
176    ) -> Self::EphemeralPublicKey;
177
178    /// Derives the `SharedSecret` from the sender's information during note
179    /// encryption.
180    fn ka_agree_enc(
181        esk: &Self::EphemeralSecretKey,
182        pk_d: &Self::DiversifiedTransmissionKey,
183    ) -> Self::SharedSecret;
184
185    /// Derives the `SharedSecret` from the recipient's information during note
186    /// trial
187    /// decryption.
188    fn ka_agree_dec(
189        ivk: &Self::IncomingViewingKey,
190        epk: &Self::PreparedEphemeralPublicKey,
191    ) -> Self::SharedSecret;
192
193    /// Derives the `SymmetricKey` used to encrypt the note plaintext.
194    ///
195    /// `secret` is the `SharedSecret` obtained from [`Self::ka_agree_enc`] or
196    /// [`Self::ka_agree_dec`].
197    ///
198    /// `ephemeral_key` is the byte encoding of the [`EphemeralPublicKey`] used
199    /// to derive
200    /// `secret`. During encryption it is derived via [`Self::epk_bytes`];
201    /// during trial
202    /// decryption it is obtained from [`ShieldedOutput::ephemeral_key`].
203    ///
204    /// [`EphemeralPublicKey`]: Self::EphemeralPublicKey
205    /// [`EphemeralSecretKey`]: Self::EphemeralSecretKey
206    fn kdf(secret: Self::SharedSecret, ephemeral_key: &EphemeralKeyBytes) -> Self::SymmetricKey;
207
208    /// Encodes the given `Note` and `Memo` as a note plaintext.
209    fn note_plaintext_bytes(note: &Self::Note, memo: &Self::Memo) -> NotePlaintextBytes;
210
211    /// Derives the [`OutgoingCipherKey`] for an encrypted note, given the
212    /// note-specific
213    /// public data and an `OutgoingViewingKey`.
214    fn derive_ock(
215        ovk: &Self::OutgoingViewingKey,
216        cv: &Self::ValueCommitment,
217        cmstar_bytes: &Self::ExtractedCommitmentBytes,
218        ephemeral_key: &EphemeralKeyBytes,
219    ) -> OutgoingCipherKey;
220
221    /// Encodes the outgoing plaintext for the given note.
222    fn outgoing_plaintext_bytes(
223        note: &Self::Note,
224        esk: &Self::EphemeralSecretKey,
225    ) -> OutPlaintextBytes;
226
227    /// Returns the byte encoding of the given `EphemeralPublicKey`.
228    fn epk_bytes(epk: &Self::EphemeralPublicKey) -> EphemeralKeyBytes;
229
230    /// Attempts to parse `ephemeral_key` as an `EphemeralPublicKey`.
231    ///
232    /// Returns `None` if `ephemeral_key` is not a valid byte encoding of an
233    /// `EphemeralPublicKey`.
234    fn epk(ephemeral_key: &EphemeralKeyBytes) -> Option<Self::EphemeralPublicKey>;
235
236    /// Derives the `ExtractedCommitment` for this note.
237    fn cmstar(note: &Self::Note) -> Self::ExtractedCommitment;
238
239    /// Parses the given note plaintext from the recipient's perspective.
240    ///
241    /// The implementation of this method must check that:
242    /// - The note plaintext version is valid (for the given decryption domain's
243    ///   context,
244    ///   which may be passed via `self`).
245    /// - The note plaintext contains valid encodings of its various fields.
246    /// - Any domain-specific requirements are satisfied.
247    ///
248    /// `&self` is passed here to enable the implementation to enforce
249    /// contextual checks,
250    /// such as rules like [ZIP 212] that become active at a specific block
251    /// height.
252    ///
253    /// [ZIP 212]: https://zips.z.cash/zip-0212
254    ///
255    /// # Panics
256    ///
257    /// Panics if `plaintext` is shorter than [`COMPACT_NOTE_SIZE`].
258    fn parse_note_plaintext_without_memo_ivk(
259        &self,
260        ivk: &Self::IncomingViewingKey,
261        plaintext: &[u8],
262    ) -> Option<(Self::Note, Self::Recipient)>;
263
264    /// Parses the given note plaintext from the sender's perspective.
265    ///
266    /// The implementation of this method must check that:
267    /// - The note plaintext version is valid (for the given decryption domain's
268    ///   context,
269    ///   which may be passed via `self`).
270    /// - The note plaintext contains valid encodings of its various fields.
271    /// - Any domain-specific requirements are satisfied.
272    ///
273    /// `&self` is passed here to enable the implementation to enforce
274    /// contextual checks,
275    /// such as rules like [ZIP 212] that become active at a specific block
276    /// height.
277    ///
278    /// [ZIP 212]: https://zips.z.cash/zip-0212
279    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    /// Extracts the memo field from the given note plaintext.
286    ///
287    /// # Compatibility
288    ///
289    /// `&self` is passed here in anticipation of future changes to memo
290    /// handling, where
291    /// the memos may no longer be part of the note plaintext.
292    fn extract_memo(&self, plaintext: &NotePlaintextBytes) -> Self::Memo;
293
294    /// Parses the `DiversifiedTransmissionKey` field of the outgoing plaintext.
295    ///
296    /// Returns `None` if `out_plaintext` does not contain a valid byte encoding
297    /// of a
298    /// `DiversifiedTransmissionKey`.
299    fn extract_pk_d(out_plaintext: &OutPlaintextBytes) -> Option<Self::DiversifiedTransmissionKey>;
300
301    /// Parses the `EphemeralSecretKey` field of the outgoing plaintext.
302    ///
303    /// Returns `None` if `out_plaintext` does not contain a valid byte encoding
304    /// of an
305    /// `EphemeralSecretKey`.
306    fn extract_esk(out_plaintext: &OutPlaintextBytes) -> Option<Self::EphemeralSecretKey>;
307}
308
309/// Trait that encapsulates protocol-specific batch trial decryption logic.
310///
311/// Each batchable operation has a default implementation that calls through to
312/// the
313/// non-batched implementation. Domains can override whichever operations
314/// benefit from
315/// batched logic.
316#[cfg(feature = "alloc")]
317#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
318pub trait BatchDomain: Domain {
319    /// Computes `Self::kdf` on a batch of items.
320    ///
321    /// For each item in the batch, if the shared secret is `None`, this returns
322    /// `None` at
323    /// that position.
324    fn batch_kdf<'a>(
325        items: impl Iterator<Item = (Option<Self::SharedSecret>, &'a EphemeralKeyBytes)>,
326    ) -> Vec<Option<Self::SymmetricKey>> {
327        // Default implementation: do the non-batched thing.
328        items
329            .map(|(secret, ephemeral_key)| secret.map(|secret| Self::kdf(secret, ephemeral_key)))
330            .collect()
331    }
332
333    /// Computes `Self::epk` on a batch of ephemeral keys.
334    ///
335    /// This is useful for protocols where the underlying curve requires an
336    /// inversion to
337    /// parse an encoded point.
338    ///
339    /// For usability, this returns tuples of the ephemeral keys and the result
340    /// of parsing
341    /// them.
342    fn batch_epk(
343        ephemeral_keys: impl Iterator<Item = EphemeralKeyBytes>,
344    ) -> Vec<(Option<Self::PreparedEphemeralPublicKey>, EphemeralKeyBytes)> {
345        // Default implementation: do the non-batched thing.
346        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    /// Computes `Self::ka_agree_dec` on a batch of prepared ephemeral keys
357    /// against a
358    /// single incoming viewing key.
359    ///
360    /// For each item, if the prepared ephemeral key is `None` (i.e. its
361    /// encoding could
362    /// not be parsed), this returns `None` at that position.
363    ///
364    /// Trial decryption multiplies many ephemeral keys by the same viewing key,
365    /// so
366    /// domains for which same-scalar multiplications can share work (for
367    /// example,
368    /// lockstep ladders over a shared batched field inversion) can override
369    /// this to
370    /// reduce the cost of the scalar multiplications, which dominate batched
371    /// trial
372    /// decryption. The default implementation performs the per-item
373    /// computation.
374    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        // Default implementation: do the non-batched thing.
382        epks.map(|epk| epk.map(|epk| Self::ka_agree_dec(ivk, epk)))
383            .collect()
384    }
385}
386
387/// Trait that provides access to the components of an encrypted transaction
388/// output.
389///
390/// Implementations of this trait are required to define the length of their
391/// ciphertext
392/// field. In order to use the trial decryption APIs in this crate, the length
393/// must be
394/// either [`ENC_CIPHERTEXT_SIZE`] or [`COMPACT_NOTE_SIZE`].
395pub trait ShieldedOutput<D: Domain, const CIPHERTEXT_SIZE: usize> {
396    /// Exposes the `ephemeral_key` field of the output.
397    fn ephemeral_key(&self) -> EphemeralKeyBytes;
398
399    /// Exposes the `cmu_bytes` or `cmx_bytes` field of the output.
400    fn cmstar_bytes(&self) -> D::ExtractedCommitmentBytes;
401
402    /// Exposes the note ciphertext of the output.
403    fn enc_ciphertext(&self) -> &[u8; CIPHERTEXT_SIZE];
404}
405
406/// A struct containing context required for encrypting Sapling and Orchard
407/// notes.
408///
409/// This struct provides a safe API for encrypting Sapling and Orchard notes. In
410/// particular, it
411/// enforces that fresh ephemeral keys are used for every note, and that the
412/// ciphertexts are
413/// consistent with each other.
414///
415/// Implements section 4.19 of the
416/// [Zcash Protocol
417/// Specification](https://zips.z.cash/protocol/nu5.pdf#saplingandorchardinband)
418pub struct NoteEncryption<D: Domain> {
419    epk: D::EphemeralPublicKey,
420    esk: D::EphemeralSecretKey,
421    note: D::Note,
422    memo: D::Memo,
423    /// `None` represents the `ovk = ⊥` case.
424    ovk: Option<D::OutgoingViewingKey>,
425}
426
427impl<D: Domain> NoteEncryption<D> {
428    /// Construct a new note encryption context for the specified note,
429    /// recipient, and memo.
430    pub fn new(ovk: Option<D::OutgoingViewingKey>, note: D::Note, memo: D::Memo) -> Self {
431        let esk = D::derive_esk(&note).expect("ZIP 212 is active.");
432        NoteEncryption {
433            epk: D::ka_derive_public(&note, &esk),
434            esk,
435            note,
436            memo,
437            ovk,
438        }
439    }
440
441    /// For use only with Sapling. This method is preserved in order that test
442    /// code
443    /// be able to generate pre-ZIP-212 ciphertexts so that tests can continue
444    /// to
445    /// cover pre-ZIP-212 transaction decryption.
446    #[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(&note, &esk),
456            esk,
457            note,
458            memo,
459            ovk,
460        }
461    }
462
463    /// Exposes the ephemeral secret key being used to encrypt this note.
464    pub fn esk(&self) -> &D::EphemeralSecretKey {
465        &self.esk
466    }
467
468    /// Exposes the encoding of the ephemeral public key being used to encrypt
469    /// this note.
470    pub fn epk(&self) -> &D::EphemeralPublicKey {
471        &self.epk
472    }
473
474    /// Generates `encCiphertext` for this note.
475    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    /// Generates `outCiphertext` for this note.
497    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            // ovk = ⊥
510            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
535/// Trial decryption of the full note plaintext by the recipient.
536///
537/// Attempts to decrypt and validate the given shielded output using the given
538/// `ivk`.
539/// If successful, the corresponding note and memo are returned, along with the
540/// address to
541/// which the note was sent.
542///
543/// Implements section 4.19.2 of the
544/// [Zcash Protocol
545/// Specification](https://zips.z.cash/protocol/nu5.pdf#decryptivk).
546pub 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>(&note, 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        // In the case corresponding to specification section 4.19.3, we check that `esk` is equal
619        // to `D::derive_esk(note)` prior to calling this method.
620        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            // Before ZIP 212
631            NoteValidity::Valid
632        }
633    } else {
634        // Published commitment doesn't match calculated commitment
635        NoteValidity::Invalid
636    }
637}
638
639/// Trial decryption of the compact note plaintext by the recipient for light
640/// clients.
641///
642/// Attempts to decrypt and validate the given compact shielded output using the
643/// given `ivk`. If successful, the corresponding note is returned, along with
644/// the address
645/// to which the note was sent.
646///
647/// Implements the procedure specified in [`ZIP 307`].
648///
649/// [`ZIP 307`]: https://zips.z.cash/zip-0307
650pub 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    // Start from block 1 to skip over Poly1305 keying output
672    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
688/// Recovery of the full note plaintext by the sender.
689///
690/// Attempts to decrypt and validate the given shielded output using the given
691/// `ovk`.
692/// If successful, the corresponding note and memo are returned, along with the
693/// address to
694/// which the note was sent.
695///
696/// Implements [Zcash Protocol Specification section 4.19.3][decryptovk].
697///
698/// [decryptovk]: https://zips.z.cash/protocol/nu5.pdf#decryptovk
699pub 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
710/// Recovery of the full note plaintext by the sender.
711///
712/// Attempts to decrypt and validate the given shielded output using the given
713/// `ock`.
714/// If successful, the corresponding note and memo are returned, along with the
715/// address to
716/// which the note was sent.
717///
718/// Implements part of section 4.19.3 of the
719/// [Zcash Protocol
720/// Specification](https://zips.z.cash/protocol/nu5.pdf#decryptovk).
721/// For decryption using a Full Viewing Key see
722/// [`try_output_recovery_with_ovk`].
723pub 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
750/// Recovery of the full note plaintext by the sender.
751///
752/// Attempts to decrypt and validate the given shielded output using the given
753/// `pk_d` and `esk`. If
754/// successful, the corresponding note and memo are returned, along with the
755/// address to which the
756/// note was sent.
757///
758/// Implements part of section 4.19.3 of the
759/// [Zcash Protocol
760/// Specification](https://zips.z.cash/protocol/nu5.pdf#decryptovk).
761/// For decryption using a Full Viewing Key see
762/// [`try_output_recovery_with_ovk`].
763pub 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    // The small-order point check at the point of output parsing rejects
775    // non-canonical encodings, so reencoding here for the KDF should
776    // be okay.
777    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    // ZIP 212: Check that the esk provided to this function is consistent with the esk we can
801    // derive from the note. This check corresponds to `ToScalar(PRF^{expand}_{rseed}([4]) = esk`
802    // in https://zips.z.cash/protocol/protocol.pdf#decryptovk. (`ρ^opt = []` for Sapling.)
803    // Preserve the upstream validation code during the ownership migration.
804    #[allow(clippy::collapsible_if)]
805    if let Some(derived_esk) = D::derive_esk(&note) {
806        if (!derived_esk.ct_eq(&esk)).into() {
807            return None;
808        }
809    }
810
811    if let NoteValidity::Valid =
812        check_note_validity::<D>(&note, &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}