Skip to main content

sapling_crypto/
note_encryption.rs

1//! Implementation of in-band secret distribution for Zcash transactions.
2//!
3//! NB: the example code is only covering the post-Canopy case.
4
5use alloc::vec::Vec;
6use blake2b_simd::{Hash as Blake2bHash, Params as Blake2bParams};
7use ff::PrimeField;
8use rand_core::Rng;
9
10use zcash_note_encryption::{
11    BatchDomain, COMPACT_NOTE_SIZE, Domain, ENC_CIPHERTEXT_SIZE, EphemeralKeyBytes,
12    NOTE_PLAINTEXT_SIZE, NoteEncryption, NotePlaintextBytes, OUT_PLAINTEXT_SIZE, OutPlaintextBytes,
13    OutgoingCipherKey, ShieldedOutput, try_compact_note_decryption, try_note_decryption,
14    try_output_recovery_with_ock, try_output_recovery_with_ovk,
15};
16
17use crate::{
18    Diversifier, Note, PaymentAddress, Rseed,
19    bundle::{GrothProofBytes, OutputDescription},
20    keys::{
21        DiversifiedTransmissionKey, EphemeralPublicKey, EphemeralSecretKey, OutgoingViewingKey,
22        SharedSecret,
23    },
24    value::{NoteValue, ValueCommitment},
25};
26
27use super::note::ExtractedNoteCommitment;
28
29pub use crate::keys::{PreparedEphemeralPublicKey, PreparedIncomingViewingKey};
30
31pub const KDF_SAPLING_PERSONALIZATION: &[u8; 16] = b"Zcash_SaplingKDF";
32pub const PRF_OCK_PERSONALIZATION: &[u8; 16] = b"Zcash_Derive_ock";
33
34/// Sapling PRF^ock.
35///
36/// Implemented per section 5.4.2 of the Zcash Protocol Specification.
37pub fn prf_ock(
38    ovk: &OutgoingViewingKey,
39    cv: &ValueCommitment,
40    cmu_bytes: &[u8; 32],
41    ephemeral_key: &EphemeralKeyBytes,
42) -> OutgoingCipherKey {
43    OutgoingCipherKey(
44        Blake2bParams::new()
45            .hash_length(32)
46            .personal(PRF_OCK_PERSONALIZATION)
47            .to_state()
48            .update(&ovk.0)
49            .update(&cv.to_bytes())
50            .update(cmu_bytes)
51            .update(ephemeral_key.as_ref())
52            .finalize()
53            .as_bytes()
54            .try_into()
55            .unwrap(),
56    )
57}
58
59/// The enforcement policy for ZIP 212 that should be applied when creating or decrypting
60/// Sapling outputs.
61#[derive(Clone, Copy, Debug, PartialEq, Eq)]
62pub enum Zip212Enforcement {
63    Off,
64    GracePeriod,
65    On,
66}
67
68/// `get_pk_d` must check that the diversifier contained within the note plaintext is a
69/// valid Sapling diversifier.
70fn sapling_parse_note_plaintext_without_memo<F>(
71    domain: &SaplingDomain,
72    plaintext: &[u8],
73    get_pk_d: F,
74) -> Option<(Note, PaymentAddress)>
75where
76    F: FnOnce(&Diversifier) -> Option<DiversifiedTransmissionKey>,
77{
78    assert!(plaintext.len() >= COMPACT_NOTE_SIZE);
79
80    // Check note plaintext version
81    if !plaintext_version_is_valid(domain.zip212_enforcement, plaintext[0]) {
82        return None;
83    }
84
85    // The unwraps below are guaranteed to succeed by the assertion above
86    let diversifier = Diversifier(
87        plaintext[1..12]
88            .try_into()
89            .expect("Note plaintext is checked to have length >= COMPACT_NOTE_SIZE."),
90    );
91    let value = NoteValue::from_bytes(
92        plaintext[12..20]
93            .try_into()
94            .expect("Note plaintext is checked to have length >= COMPACT_NOTE_SIZE."),
95    );
96    let r: [u8; 32] = plaintext[20..COMPACT_NOTE_SIZE]
97        .try_into()
98        .expect("Note plaintext is checked to have length >= COMPACT_NOTE_SIZE.");
99
100    let rseed = if plaintext[0] == 0x01 {
101        let rcm = Option::from(jubjub::Fr::from_repr(r))?;
102        Rseed::BeforeZip212(rcm)
103    } else {
104        Rseed::AfterZip212(r)
105    };
106
107    let pk_d = get_pk_d(&diversifier)?;
108
109    // `diversifier` was checked by `get_pk_d`.
110    let to = PaymentAddress::from_parts_unchecked(diversifier, pk_d)?;
111    let note = to.create_note(value, rseed);
112    Some((note, to))
113}
114
115pub struct SaplingDomain {
116    zip212_enforcement: Zip212Enforcement,
117}
118
119impl SaplingDomain {
120    pub fn new(zip212_enforcement: Zip212Enforcement) -> Self {
121        Self { zip212_enforcement }
122    }
123}
124
125impl Domain for SaplingDomain {
126    type EphemeralSecretKey = EphemeralSecretKey;
127    // It is acceptable for this to be a point rather than a byte array, because we
128    // enforce by consensus that points must not be small-order, and all points with
129    // non-canonical serialization are small-order.
130    type EphemeralPublicKey = EphemeralPublicKey;
131    type PreparedEphemeralPublicKey = PreparedEphemeralPublicKey;
132    type SharedSecret = SharedSecret;
133    type SymmetricKey = Blake2bHash;
134    type Note = Note;
135    type Recipient = PaymentAddress;
136    type DiversifiedTransmissionKey = DiversifiedTransmissionKey;
137    type IncomingViewingKey = PreparedIncomingViewingKey;
138    type OutgoingViewingKey = OutgoingViewingKey;
139    type ValueCommitment = ValueCommitment;
140    type ExtractedCommitment = ExtractedNoteCommitment;
141    type ExtractedCommitmentBytes = [u8; 32];
142    type Memo = [u8; 512];
143
144    fn derive_esk(note: &Self::Note) -> Option<Self::EphemeralSecretKey> {
145        note.derive_esk()
146    }
147
148    fn get_pk_d(note: &Self::Note) -> Self::DiversifiedTransmissionKey {
149        *note.recipient().pk_d()
150    }
151
152    fn prepare_epk(epk: Self::EphemeralPublicKey) -> Self::PreparedEphemeralPublicKey {
153        PreparedEphemeralPublicKey::new(epk)
154    }
155
156    fn ka_derive_public(
157        note: &Self::Note,
158        esk: &Self::EphemeralSecretKey,
159    ) -> Self::EphemeralPublicKey {
160        esk.derive_public(note.recipient().g_d().into())
161    }
162
163    fn ka_agree_enc(
164        esk: &Self::EphemeralSecretKey,
165        pk_d: &Self::DiversifiedTransmissionKey,
166    ) -> Self::SharedSecret {
167        esk.agree(pk_d)
168    }
169
170    fn ka_agree_dec(
171        ivk: &Self::IncomingViewingKey,
172        epk: &Self::PreparedEphemeralPublicKey,
173    ) -> Self::SharedSecret {
174        epk.agree(ivk)
175    }
176
177    /// Sapling KDF for note encryption.
178    ///
179    /// Implements section 5.4.4.4 of the Zcash Protocol Specification.
180    fn kdf(dhsecret: SharedSecret, epk: &EphemeralKeyBytes) -> Blake2bHash {
181        dhsecret.kdf_sapling(epk)
182    }
183
184    fn note_plaintext_bytes(note: &Self::Note, memo: &Self::Memo) -> NotePlaintextBytes {
185        // Note plaintext encoding is defined in section 5.5 of the Zcash Protocol
186        // Specification.
187        let mut input = [0; NOTE_PLAINTEXT_SIZE];
188        input[0] = match note.rseed() {
189            Rseed::BeforeZip212(_) => 1,
190            Rseed::AfterZip212(_) => 2,
191        };
192        input[1..12].copy_from_slice(&note.recipient().diversifier().0);
193        input[12..20].copy_from_slice(&note.value().inner().to_le_bytes());
194
195        match note.rseed() {
196            Rseed::BeforeZip212(rcm) => {
197                input[20..COMPACT_NOTE_SIZE].copy_from_slice(rcm.to_repr().as_ref());
198            }
199            Rseed::AfterZip212(rseed) => {
200                input[20..COMPACT_NOTE_SIZE].copy_from_slice(rseed);
201            }
202        }
203
204        input[COMPACT_NOTE_SIZE..NOTE_PLAINTEXT_SIZE].copy_from_slice(&memo[..]);
205
206        NotePlaintextBytes(input)
207    }
208
209    fn derive_ock(
210        ovk: &Self::OutgoingViewingKey,
211        cv: &Self::ValueCommitment,
212        cmu_bytes: &Self::ExtractedCommitmentBytes,
213        epk: &EphemeralKeyBytes,
214    ) -> OutgoingCipherKey {
215        prf_ock(ovk, cv, cmu_bytes, epk)
216    }
217
218    fn outgoing_plaintext_bytes(
219        note: &Self::Note,
220        esk: &Self::EphemeralSecretKey,
221    ) -> OutPlaintextBytes {
222        let mut input = [0u8; OUT_PLAINTEXT_SIZE];
223        input[0..32].copy_from_slice(&note.recipient().pk_d().to_bytes());
224        input[32..OUT_PLAINTEXT_SIZE].copy_from_slice(esk.0.to_repr().as_ref());
225
226        OutPlaintextBytes(input)
227    }
228
229    fn epk_bytes(epk: &Self::EphemeralPublicKey) -> EphemeralKeyBytes {
230        epk.to_bytes()
231    }
232
233    fn epk(ephemeral_key: &EphemeralKeyBytes) -> Option<Self::EphemeralPublicKey> {
234        // ZIP 216: We unconditionally reject non-canonical encodings, because these have
235        // always been rejected by consensus (due to small-order checks).
236        // https://zips.z.cash/zip-0216#specification
237        EphemeralPublicKey::from_bytes(&ephemeral_key.0).into()
238    }
239
240    fn parse_note_plaintext_without_memo_ivk(
241        &self,
242        ivk: &Self::IncomingViewingKey,
243        plaintext: &[u8],
244    ) -> Option<(Self::Note, Self::Recipient)> {
245        sapling_parse_note_plaintext_without_memo(self, plaintext, |diversifier| {
246            DiversifiedTransmissionKey::derive(ivk, diversifier)
247        })
248    }
249
250    fn parse_note_plaintext_without_memo_ovk(
251        &self,
252        pk_d: &Self::DiversifiedTransmissionKey,
253        plaintext: &NotePlaintextBytes,
254    ) -> Option<(Self::Note, Self::Recipient)> {
255        sapling_parse_note_plaintext_without_memo(self, &plaintext.0, |diversifier| {
256            diversifier.g_d().map(|_| *pk_d)
257        })
258    }
259
260    fn cmstar(note: &Self::Note) -> Self::ExtractedCommitment {
261        note.cmu()
262    }
263
264    fn extract_pk_d(op: &OutPlaintextBytes) -> Option<Self::DiversifiedTransmissionKey> {
265        DiversifiedTransmissionKey::from_bytes(
266            op.0[0..32].try_into().expect("slice is the correct length"),
267        )
268        .into()
269    }
270
271    fn extract_esk(op: &OutPlaintextBytes) -> Option<Self::EphemeralSecretKey> {
272        EphemeralSecretKey::from_bytes(
273            op.0[32..OUT_PLAINTEXT_SIZE]
274                .try_into()
275                .expect("slice is the correct length"),
276        )
277        .into()
278    }
279
280    fn extract_memo(&self, plaintext: &NotePlaintextBytes) -> Self::Memo {
281        plaintext.0[COMPACT_NOTE_SIZE..NOTE_PLAINTEXT_SIZE]
282            .try_into()
283            .expect("correct length")
284    }
285}
286
287impl BatchDomain for SaplingDomain {
288    fn batch_kdf<'a>(
289        items: impl Iterator<Item = (Option<Self::SharedSecret>, &'a EphemeralKeyBytes)>,
290    ) -> Vec<Option<Self::SymmetricKey>> {
291        let (shared_secrets, ephemeral_keys): (Vec<_>, Vec<_>) = items.unzip();
292
293        SharedSecret::batch_to_affine(shared_secrets)
294            .zip(ephemeral_keys)
295            .map(|(secret, ephemeral_key)| {
296                secret.map(|dhsecret| SharedSecret::kdf_sapling_inner(dhsecret, ephemeral_key))
297            })
298            .collect()
299    }
300
301    fn batch_epk(
302        ephemeral_keys: impl Iterator<Item = EphemeralKeyBytes>,
303    ) -> Vec<(Option<Self::PreparedEphemeralPublicKey>, EphemeralKeyBytes)> {
304        let ephemeral_keys: Vec<_> = ephemeral_keys.collect();
305        let epks = jubjub::AffinePoint::batch_from_bytes(ephemeral_keys.iter().map(|b| b.0));
306        epks.into_iter()
307            .zip(ephemeral_keys)
308            .map(|(epk, ephemeral_key)| {
309                (
310                    Option::from(epk)
311                        .map(EphemeralPublicKey::from_affine)
312                        .map(Self::prepare_epk),
313                    ephemeral_key,
314                )
315            })
316            .collect()
317    }
318}
319
320#[derive(Clone)]
321pub struct CompactOutputDescription {
322    pub ephemeral_key: EphemeralKeyBytes,
323    pub cmu: ExtractedNoteCommitment,
324    pub enc_ciphertext: [u8; COMPACT_NOTE_SIZE],
325}
326
327impl ShieldedOutput<SaplingDomain, COMPACT_NOTE_SIZE> for CompactOutputDescription {
328    fn ephemeral_key(&self) -> EphemeralKeyBytes {
329        self.ephemeral_key.clone()
330    }
331
332    fn cmstar_bytes(&self) -> [u8; 32] {
333        self.cmu.to_bytes()
334    }
335
336    fn enc_ciphertext(&self) -> &[u8; COMPACT_NOTE_SIZE] {
337        &self.enc_ciphertext
338    }
339}
340
341/// Creates a new encryption context for the given note.
342///
343/// Setting `ovk` to `None` represents the `ovk = ⊥` case, where the note cannot be
344/// recovered by the sender.
345///
346/// NB: the example code here only covers the post-Canopy case.
347///
348/// # Examples
349///
350/// ```
351/// use ff::Field;
352/// use rand::rng;
353/// use sapling_crypto::{
354///     keys::OutgoingViewingKey,
355///     note_encryption::{sapling_note_encryption, Zip212Enforcement},
356///     util::generate_random_rseed,
357///     value::{NoteValue, ValueCommitTrapdoor, ValueCommitment},
358///     Diversifier, PaymentAddress, Rseed, SaplingIvk,
359/// };
360///
361/// let mut rng = rng();
362///
363/// let ivk = SaplingIvk(jubjub::Scalar::random(&mut rng));
364/// let diversifier = Diversifier([0; 11]);
365/// let to = ivk.to_payment_address(diversifier).unwrap();
366/// let ovk = Some(OutgoingViewingKey([0; 32]));
367///
368/// let value = NoteValue::from_raw(1000);
369/// let rcv = ValueCommitTrapdoor::random(&mut rng);
370/// let cv = ValueCommitment::derive(value, rcv);
371/// let rseed = generate_random_rseed(
372///     Zip212Enforcement::GracePeriod,
373///     &mut rng,
374/// );
375/// let note = to.create_note(value, rseed);
376/// let cmu = note.cmu();
377///
378/// let mut enc = sapling_note_encryption(ovk, note, [0x37; 512], &mut rng);
379/// let encCiphertext = enc.encrypt_note_plaintext();
380/// let outCiphertext = enc.encrypt_outgoing_plaintext(&cv, &cmu, &mut rng);
381/// ```
382pub fn sapling_note_encryption<R: Rng>(
383    ovk: Option<OutgoingViewingKey>,
384    note: Note,
385    memo: [u8; 512],
386    rng: &mut R,
387) -> NoteEncryption<SaplingDomain> {
388    let esk = note.generate_or_derive_esk_internal(rng);
389    NoteEncryption::new_with_esk(esk, ovk, note, memo)
390}
391
392#[allow(clippy::if_same_then_else)]
393#[allow(clippy::needless_bool)]
394pub fn plaintext_version_is_valid(zip212_enforcement: Zip212Enforcement, leadbyte: u8) -> bool {
395    match zip212_enforcement {
396        Zip212Enforcement::Off => leadbyte == 0x01,
397        Zip212Enforcement::GracePeriod => leadbyte == 0x01 || leadbyte == 0x02,
398        Zip212Enforcement::On => leadbyte == 0x02,
399    }
400}
401
402pub fn try_sapling_note_decryption<Output: ShieldedOutput<SaplingDomain, ENC_CIPHERTEXT_SIZE>>(
403    ivk: &PreparedIncomingViewingKey,
404    output: &Output,
405    zip212_enforcement: Zip212Enforcement,
406) -> Option<(Note, PaymentAddress, [u8; 512])> {
407    let domain = SaplingDomain::new(zip212_enforcement);
408    try_note_decryption(&domain, ivk, output)
409}
410
411pub fn try_sapling_compact_note_decryption<
412    Output: ShieldedOutput<SaplingDomain, COMPACT_NOTE_SIZE>,
413>(
414    ivk: &PreparedIncomingViewingKey,
415    output: &Output,
416    zip212_enforcement: Zip212Enforcement,
417) -> Option<(Note, PaymentAddress)> {
418    let domain = SaplingDomain::new(zip212_enforcement);
419    try_compact_note_decryption(&domain, ivk, output)
420}
421
422/// Recovery of the full note plaintext by the sender.
423///
424/// Attempts to decrypt and validate the given `enc_ciphertext` using the given `ock`.
425/// If successful, the corresponding Sapling note and memo are returned, along with the
426/// `PaymentAddress` to which the note was sent.
427///
428/// Implements part of section 4.19.3 of the Zcash Protocol Specification.
429/// For decryption using a Full Viewing Key see [`try_sapling_output_recovery`].
430pub fn try_sapling_output_recovery_with_ock(
431    ock: &OutgoingCipherKey,
432    output: &OutputDescription<GrothProofBytes>,
433    zip212_enforcement: Zip212Enforcement,
434) -> Option<(Note, PaymentAddress, [u8; 512])> {
435    let domain = SaplingDomain::new(zip212_enforcement);
436    try_output_recovery_with_ock(&domain, ock, output, output.out_ciphertext())
437}
438
439/// Recovery of the full note plaintext by the sender.
440///
441/// Attempts to decrypt and validate the given `enc_ciphertext` using the given `ovk`.
442/// If successful, the corresponding Sapling note and memo are returned, along with the
443/// `PaymentAddress` to which the note was sent.
444///
445/// Implements section 4.19.3 of the Zcash Protocol Specification.
446#[allow(clippy::too_many_arguments)]
447pub fn try_sapling_output_recovery(
448    ovk: &OutgoingViewingKey,
449    output: &OutputDescription<GrothProofBytes>,
450    zip212_enforcement: Zip212Enforcement,
451) -> Option<(Note, PaymentAddress, [u8; 512])> {
452    let domain = SaplingDomain::new(zip212_enforcement);
453    try_output_recovery_with_ovk(&domain, ovk, output, output.cv(), output.out_ciphertext())
454}
455
456#[cfg(test)]
457mod tests {
458    use alloc::vec::Vec;
459    use chacha20poly1305::{
460        ChaCha20Poly1305,
461        aead::{AeadInOut, KeyInit},
462    };
463    use ff::{Field, PrimeField};
464    use group::Group;
465    use group::GroupEncoding;
466    use rand::{SeedableRng, rngs::StdRng};
467    use rand_core::{CryptoRng, Rng};
468
469    use zcash_note_encryption::{
470        ENC_CIPHERTEXT_SIZE, EphemeralKeyBytes, NOTE_PLAINTEXT_SIZE, NoteEncryption,
471        OUT_CIPHERTEXT_SIZE, OUT_PLAINTEXT_SIZE, OutgoingCipherKey, batch,
472    };
473
474    use super::{
475        CompactOutputDescription, SaplingDomain, Zip212Enforcement, prf_ock,
476        sapling_note_encryption, try_sapling_compact_note_decryption, try_sapling_note_decryption,
477        try_sapling_output_recovery, try_sapling_output_recovery_with_ock,
478    };
479
480    use crate::{
481        Diversifier, PaymentAddress, Rseed, SaplingIvk,
482        bundle::{GrothProofBytes, OutputDescription},
483        constants::GROTH_PROOF_SIZE,
484        keys::{DiversifiedTransmissionKey, EphemeralSecretKey, OutgoingViewingKey},
485        note::ExtractedNoteCommitment,
486        note_encryption::PreparedIncomingViewingKey,
487        rng_compat::OsRng,
488        util::generate_random_rseed,
489        value::{NoteValue, ValueCommitTrapdoor, ValueCommitment},
490    };
491
492    fn random_enc_ciphertext<R: Rng + CryptoRng>(
493        zip212_enforcement: Zip212Enforcement,
494        mut rng: &mut R,
495    ) -> (
496        OutgoingViewingKey,
497        OutgoingCipherKey,
498        PreparedIncomingViewingKey,
499        OutputDescription<GrothProofBytes>,
500    ) {
501        let ivk = SaplingIvk(jubjub::Fr::random(&mut rng));
502        let prepared_ivk = PreparedIncomingViewingKey::new(&ivk);
503
504        let (ovk, ock, output) = random_enc_ciphertext_with(&ivk, zip212_enforcement, rng);
505
506        assert!(try_sapling_note_decryption(&prepared_ivk, &output, zip212_enforcement).is_some());
507        assert!(
508            try_sapling_compact_note_decryption(
509                &prepared_ivk,
510                &CompactOutputDescription::from(output.clone()),
511                zip212_enforcement,
512            )
513            .is_some()
514        );
515
516        let ovk_output_recovery = try_sapling_output_recovery(&ovk, &output, zip212_enforcement);
517
518        let ock_output_recovery =
519            try_sapling_output_recovery_with_ock(&ock, &output, zip212_enforcement);
520        assert!(ovk_output_recovery.is_some());
521        assert!(ock_output_recovery.is_some());
522        assert_eq!(ovk_output_recovery, ock_output_recovery);
523
524        (ovk, ock, prepared_ivk, output)
525    }
526
527    fn random_enc_ciphertext_with<R: Rng + CryptoRng>(
528        ivk: &SaplingIvk,
529        zip212_enforcement: Zip212Enforcement,
530        mut rng: &mut R,
531    ) -> (
532        OutgoingViewingKey,
533        OutgoingCipherKey,
534        OutputDescription<GrothProofBytes>,
535    ) {
536        let diversifier = Diversifier([0; 11]);
537        let pa = ivk.to_payment_address(diversifier).unwrap();
538
539        // Construct the value commitment for the proof instance
540        let value = NoteValue::from_raw(100);
541        let rcv = ValueCommitTrapdoor::random(&mut rng);
542        let cv = ValueCommitment::derive(value, rcv);
543
544        let rseed = generate_random_rseed(zip212_enforcement, &mut rng);
545
546        let note = pa.create_note(value, rseed);
547        let cmu = note.cmu();
548
549        let ovk = OutgoingViewingKey([0; 32]);
550        let ne = sapling_note_encryption(Some(ovk), note, [0x37; 512], &mut rng);
551        let epk = ne.epk();
552        let ock = prf_ock(&ovk, &cv, &cmu.to_bytes(), &epk.to_bytes());
553
554        let out_ciphertext = ne.encrypt_outgoing_plaintext(&cv, &cmu, rng);
555        let output = OutputDescription::from_parts(
556            cv,
557            cmu,
558            epk.to_bytes(),
559            ne.encrypt_note_plaintext(),
560            out_ciphertext,
561            [0u8; GROTH_PROOF_SIZE],
562        );
563
564        (ovk, ock, output)
565    }
566
567    fn reencrypt_out_ciphertext(
568        ovk: &OutgoingViewingKey,
569        cv: &ValueCommitment,
570        cmu: &ExtractedNoteCommitment,
571        ephemeral_key: &EphemeralKeyBytes,
572        out_ciphertext: &[u8; OUT_CIPHERTEXT_SIZE],
573        modify_plaintext: impl Fn(&mut [u8; OUT_PLAINTEXT_SIZE]),
574    ) -> [u8; OUT_CIPHERTEXT_SIZE] {
575        let ock = prf_ock(ovk, cv, &cmu.to_bytes(), ephemeral_key);
576
577        let mut op = [0; OUT_PLAINTEXT_SIZE];
578        op.copy_from_slice(&out_ciphertext[..OUT_PLAINTEXT_SIZE]);
579
580        ChaCha20Poly1305::new_from_slice(ock.as_ref())
581            .unwrap()
582            .decrypt_inout_detached(
583                (&[0u8; 12]).into(),
584                &[],
585                (&mut op[..]).into(),
586                out_ciphertext[OUT_PLAINTEXT_SIZE..].try_into().unwrap(),
587            )
588            .unwrap();
589
590        modify_plaintext(&mut op);
591
592        let tag = ChaCha20Poly1305::new_from_slice(ock.as_ref())
593            .unwrap()
594            .encrypt_inout_detached((&[0u8; 12]).into(), &[], (&mut op[..]).into())
595            .unwrap();
596
597        let mut out_ciphertext = [0u8; OUT_CIPHERTEXT_SIZE];
598        out_ciphertext[..OUT_PLAINTEXT_SIZE].copy_from_slice(&op);
599        out_ciphertext[OUT_PLAINTEXT_SIZE..].copy_from_slice(&tag);
600        out_ciphertext
601    }
602
603    fn reencrypt_enc_ciphertext(
604        ovk: &OutgoingViewingKey,
605        cv: &ValueCommitment,
606        cmu: &ExtractedNoteCommitment,
607        ephemeral_key: &EphemeralKeyBytes,
608        enc_ciphertext: &[u8; ENC_CIPHERTEXT_SIZE],
609        out_ciphertext: &[u8; OUT_CIPHERTEXT_SIZE],
610        modify_plaintext: impl Fn(&mut [u8; NOTE_PLAINTEXT_SIZE]),
611    ) -> [u8; ENC_CIPHERTEXT_SIZE] {
612        let ock = prf_ock(ovk, cv, &cmu.to_bytes(), ephemeral_key);
613
614        let mut op = [0; OUT_PLAINTEXT_SIZE];
615        op.copy_from_slice(&out_ciphertext[..OUT_PLAINTEXT_SIZE]);
616
617        ChaCha20Poly1305::new_from_slice(ock.as_ref())
618            .unwrap()
619            .decrypt_inout_detached(
620                (&[0u8; 12]).into(),
621                &[],
622                (&mut op[..]).into(),
623                out_ciphertext[OUT_PLAINTEXT_SIZE..].try_into().unwrap(),
624            )
625            .unwrap();
626
627        let pk_d = DiversifiedTransmissionKey::from_bytes(&op[0..32].try_into().unwrap()).unwrap();
628
629        let esk = jubjub::Fr::from_repr(op[32..OUT_PLAINTEXT_SIZE].try_into().unwrap()).unwrap();
630
631        let shared_secret = EphemeralSecretKey(esk).agree(&pk_d);
632        let key = shared_secret.kdf_sapling(ephemeral_key);
633
634        let mut plaintext = [0; NOTE_PLAINTEXT_SIZE];
635        plaintext.copy_from_slice(&enc_ciphertext[..NOTE_PLAINTEXT_SIZE]);
636
637        ChaCha20Poly1305::new_from_slice(key.as_bytes())
638            .unwrap()
639            .decrypt_inout_detached(
640                (&[0u8; 12]).into(),
641                &[],
642                (&mut plaintext[..]).into(),
643                enc_ciphertext[NOTE_PLAINTEXT_SIZE..].try_into().unwrap(),
644            )
645            .unwrap();
646
647        modify_plaintext(&mut plaintext);
648
649        let tag = ChaCha20Poly1305::new_from_slice(key.as_ref())
650            .unwrap()
651            .encrypt_inout_detached((&[0u8; 12]).into(), &[], (&mut plaintext[..]).into())
652            .unwrap();
653
654        let mut enc_ciphertext = [0u8; ENC_CIPHERTEXT_SIZE];
655        enc_ciphertext[..NOTE_PLAINTEXT_SIZE].copy_from_slice(&plaintext);
656        enc_ciphertext[NOTE_PLAINTEXT_SIZE..].copy_from_slice(&tag);
657        enc_ciphertext
658    }
659
660    fn find_invalid_diversifier() -> Diversifier {
661        // Find an invalid diversifier
662        let mut d = Diversifier([0; 11]);
663        loop {
664            for k in 0..11 {
665                d.0[k] = d.0[k].wrapping_add(1);
666                if d.0[k] != 0 {
667                    break;
668                }
669            }
670            if d.g_d().is_none() {
671                break;
672            }
673        }
674        d
675    }
676
677    fn find_valid_diversifier() -> Diversifier {
678        // Find a different valid diversifier
679        let mut d = Diversifier([0; 11]);
680        loop {
681            for k in 0..11 {
682                d.0[k] = d.0[k].wrapping_add(1);
683                if d.0[k] != 0 {
684                    break;
685                }
686            }
687            if d.g_d().is_some() {
688                break;
689            }
690        }
691        d
692    }
693
694    #[test]
695    fn decryption_with_invalid_ivk() {
696        let mut rng = OsRng;
697        let zip212_states = [
698            Zip212Enforcement::Off,
699            Zip212Enforcement::GracePeriod,
700            Zip212Enforcement::On,
701        ];
702
703        for zip212_enforcement in zip212_states {
704            let (_, _, _, output) = random_enc_ciphertext(zip212_enforcement, &mut rng);
705
706            assert_eq!(
707                try_sapling_note_decryption(
708                    &PreparedIncomingViewingKey::new(&SaplingIvk(jubjub::Fr::random(&mut rng))),
709                    &output,
710                    zip212_enforcement,
711                ),
712                None
713            );
714        }
715    }
716
717    #[test]
718    fn decryption_with_invalid_epk() {
719        let mut rng = OsRng;
720        let zip212_states = [
721            Zip212Enforcement::Off,
722            Zip212Enforcement::GracePeriod,
723            Zip212Enforcement::On,
724        ];
725
726        for zip212_enforcement in zip212_states {
727            let (_, _, ivk, mut output) = random_enc_ciphertext(zip212_enforcement, &mut rng);
728
729            *output.ephemeral_key_mut() = jubjub::ExtendedPoint::random(&mut rng).to_bytes().into();
730
731            assert_eq!(
732                try_sapling_note_decryption(&ivk, &output, zip212_enforcement,),
733                None
734            );
735        }
736    }
737
738    #[test]
739    fn decryption_with_invalid_cmu() {
740        let mut rng = OsRng;
741        let zip212_states = [
742            Zip212Enforcement::Off,
743            Zip212Enforcement::GracePeriod,
744            Zip212Enforcement::On,
745        ];
746
747        for zip212_enforcement in zip212_states {
748            let (_, _, ivk, mut output) = random_enc_ciphertext(zip212_enforcement, &mut rng);
749            *output.cmu_mut() =
750                ExtractedNoteCommitment::from_bytes(&bls12_381::Scalar::random(&mut rng).to_repr())
751                    .unwrap();
752
753            assert_eq!(
754                try_sapling_note_decryption(&ivk, &output, zip212_enforcement,),
755                None
756            );
757        }
758    }
759
760    #[test]
761    fn decryption_with_invalid_tag() {
762        let mut rng = OsRng;
763        let zip212_states = [
764            Zip212Enforcement::Off,
765            Zip212Enforcement::GracePeriod,
766            Zip212Enforcement::On,
767        ];
768
769        for zip212_enforcement in zip212_states {
770            let (_, _, ivk, mut output) = random_enc_ciphertext(zip212_enforcement, &mut rng);
771            output.enc_ciphertext_mut()[ENC_CIPHERTEXT_SIZE - 1] ^= 0xff;
772
773            assert_eq!(
774                try_sapling_note_decryption(&ivk, &output, zip212_enforcement,),
775                None
776            );
777        }
778    }
779
780    #[test]
781    fn decryption_with_invalid_version_byte() {
782        let mut rng = OsRng;
783        let zip212_states = [
784            Zip212Enforcement::Off,
785            Zip212Enforcement::GracePeriod,
786            Zip212Enforcement::On,
787        ];
788        let leadbytes = [0x02, 0x03, 0x01];
789
790        for (zip212_enforcement, leadbyte) in zip212_states.into_iter().zip(leadbytes) {
791            let (ovk, _, ivk, mut output) = random_enc_ciphertext(zip212_enforcement, &mut rng);
792
793            *output.enc_ciphertext_mut() = reencrypt_enc_ciphertext(
794                &ovk,
795                output.cv(),
796                output.cmu(),
797                output.ephemeral_key(),
798                output.enc_ciphertext(),
799                output.out_ciphertext(),
800                |pt| pt[0] = leadbyte,
801            );
802            assert_eq!(
803                try_sapling_note_decryption(&ivk, &output, zip212_enforcement),
804                None
805            );
806        }
807    }
808
809    #[test]
810    fn decryption_with_invalid_diversifier() {
811        let mut rng = OsRng;
812        let zip212_states = [
813            Zip212Enforcement::Off,
814            Zip212Enforcement::GracePeriod,
815            Zip212Enforcement::On,
816        ];
817
818        for zip212_enforcement in zip212_states {
819            let (ovk, _, ivk, mut output) = random_enc_ciphertext(zip212_enforcement, &mut rng);
820
821            *output.enc_ciphertext_mut() = reencrypt_enc_ciphertext(
822                &ovk,
823                output.cv(),
824                output.cmu(),
825                output.ephemeral_key(),
826                output.enc_ciphertext(),
827                output.out_ciphertext(),
828                |pt| pt[1..12].copy_from_slice(&find_invalid_diversifier().0),
829            );
830            assert_eq!(
831                try_sapling_note_decryption(&ivk, &output, zip212_enforcement),
832                None
833            );
834        }
835    }
836
837    #[test]
838    fn decryption_with_incorrect_diversifier() {
839        let mut rng = OsRng;
840        let zip212_states = [
841            Zip212Enforcement::Off,
842            Zip212Enforcement::GracePeriod,
843            Zip212Enforcement::On,
844        ];
845
846        for zip212_enforcement in zip212_states {
847            let (ovk, _, ivk, mut output) = random_enc_ciphertext(zip212_enforcement, &mut rng);
848
849            *output.enc_ciphertext_mut() = reencrypt_enc_ciphertext(
850                &ovk,
851                output.cv(),
852                output.cmu(),
853                output.ephemeral_key(),
854                output.enc_ciphertext(),
855                output.out_ciphertext(),
856                |pt| pt[1..12].copy_from_slice(&find_valid_diversifier().0),
857            );
858
859            assert_eq!(
860                try_sapling_note_decryption(&ivk, &output, zip212_enforcement),
861                None
862            );
863        }
864    }
865
866    #[test]
867    fn compact_decryption_with_invalid_ivk() {
868        let mut rng = OsRng;
869        let zip212_states = [
870            Zip212Enforcement::Off,
871            Zip212Enforcement::GracePeriod,
872            Zip212Enforcement::On,
873        ];
874
875        for zip212_enforcement in zip212_states {
876            let (_, _, _, output) = random_enc_ciphertext(zip212_enforcement, &mut rng);
877
878            assert_eq!(
879                try_sapling_compact_note_decryption(
880                    &PreparedIncomingViewingKey::new(&SaplingIvk(jubjub::Fr::random(&mut rng))),
881                    &CompactOutputDescription::from(output),
882                    zip212_enforcement,
883                ),
884                None
885            );
886        }
887    }
888
889    #[test]
890    fn compact_decryption_with_invalid_epk() {
891        let mut rng = OsRng;
892        let zip212_states = [
893            Zip212Enforcement::Off,
894            Zip212Enforcement::GracePeriod,
895            Zip212Enforcement::On,
896        ];
897
898        for zip212_enforcement in zip212_states {
899            let (_, _, ivk, mut output) = random_enc_ciphertext(zip212_enforcement, &mut rng);
900            *output.ephemeral_key_mut() = jubjub::ExtendedPoint::random(&mut rng).to_bytes().into();
901
902            assert_eq!(
903                try_sapling_compact_note_decryption(
904                    &ivk,
905                    &CompactOutputDescription::from(output),
906                    zip212_enforcement,
907                ),
908                None
909            );
910        }
911    }
912
913    #[test]
914    fn compact_decryption_with_invalid_cmu() {
915        let mut rng = OsRng;
916        let zip212_states = [
917            Zip212Enforcement::Off,
918            Zip212Enforcement::GracePeriod,
919            Zip212Enforcement::On,
920        ];
921
922        for zip212_enforcement in zip212_states {
923            let (_, _, ivk, mut output) = random_enc_ciphertext(zip212_enforcement, &mut rng);
924            *output.cmu_mut() =
925                ExtractedNoteCommitment::from_bytes(&bls12_381::Scalar::random(&mut rng).to_repr())
926                    .unwrap();
927
928            assert_eq!(
929                try_sapling_compact_note_decryption(
930                    &ivk,
931                    &CompactOutputDescription::from(output),
932                    zip212_enforcement,
933                ),
934                None
935            );
936        }
937    }
938
939    #[test]
940    fn compact_decryption_with_invalid_version_byte() {
941        let mut rng = OsRng;
942        let zip212_states = [
943            Zip212Enforcement::Off,
944            Zip212Enforcement::GracePeriod,
945            Zip212Enforcement::On,
946        ];
947        let leadbytes = [0x02, 0x03, 0x01];
948
949        for (zip212_enforcement, leadbyte) in zip212_states.into_iter().zip(leadbytes) {
950            let (ovk, _, ivk, mut output) = random_enc_ciphertext(zip212_enforcement, &mut rng);
951
952            *output.enc_ciphertext_mut() = reencrypt_enc_ciphertext(
953                &ovk,
954                output.cv(),
955                output.cmu(),
956                output.ephemeral_key(),
957                output.enc_ciphertext(),
958                output.out_ciphertext(),
959                |pt| pt[0] = leadbyte,
960            );
961            assert_eq!(
962                try_sapling_compact_note_decryption(
963                    &ivk,
964                    &CompactOutputDescription::from(output),
965                    zip212_enforcement,
966                ),
967                None
968            );
969        }
970    }
971
972    #[test]
973    fn compact_decryption_with_invalid_diversifier() {
974        let mut rng = OsRng;
975        let zip212_states = [
976            Zip212Enforcement::Off,
977            Zip212Enforcement::GracePeriod,
978            Zip212Enforcement::On,
979        ];
980
981        for zip212_enforcement in zip212_states {
982            let (ovk, _, ivk, mut output) = random_enc_ciphertext(zip212_enforcement, &mut rng);
983
984            *output.enc_ciphertext_mut() = reencrypt_enc_ciphertext(
985                &ovk,
986                output.cv(),
987                output.cmu(),
988                output.ephemeral_key(),
989                output.enc_ciphertext(),
990                output.out_ciphertext(),
991                |pt| pt[1..12].copy_from_slice(&find_invalid_diversifier().0),
992            );
993            assert_eq!(
994                try_sapling_compact_note_decryption(
995                    &ivk,
996                    &CompactOutputDescription::from(output),
997                    zip212_enforcement,
998                ),
999                None
1000            );
1001        }
1002    }
1003
1004    #[test]
1005    fn compact_decryption_with_incorrect_diversifier() {
1006        let mut rng = OsRng;
1007        let zip212_states = [
1008            Zip212Enforcement::Off,
1009            Zip212Enforcement::GracePeriod,
1010            Zip212Enforcement::On,
1011        ];
1012
1013        for zip212_enforcement in zip212_states {
1014            let (ovk, _, ivk, mut output) = random_enc_ciphertext(zip212_enforcement, &mut rng);
1015
1016            *output.enc_ciphertext_mut() = reencrypt_enc_ciphertext(
1017                &ovk,
1018                output.cv(),
1019                output.cmu(),
1020                output.ephemeral_key(),
1021                output.enc_ciphertext(),
1022                output.out_ciphertext(),
1023                |pt| pt[1..12].copy_from_slice(&find_valid_diversifier().0),
1024            );
1025            assert_eq!(
1026                try_sapling_compact_note_decryption(
1027                    &ivk,
1028                    &CompactOutputDescription::from(output),
1029                    zip212_enforcement,
1030                ),
1031                None
1032            );
1033        }
1034    }
1035
1036    #[test]
1037    fn recovery_with_invalid_ovk() {
1038        let mut rng = OsRng;
1039        let zip212_states = [
1040            Zip212Enforcement::Off,
1041            Zip212Enforcement::GracePeriod,
1042            Zip212Enforcement::On,
1043        ];
1044
1045        for zip212_enforcement in zip212_states {
1046            let (mut ovk, _, _, output) = random_enc_ciphertext(zip212_enforcement, &mut rng);
1047
1048            ovk.0[0] ^= 0xff;
1049            assert_eq!(
1050                try_sapling_output_recovery(&ovk, &output, zip212_enforcement),
1051                None
1052            );
1053        }
1054    }
1055
1056    #[test]
1057    fn recovery_with_invalid_ock() {
1058        let mut rng = OsRng;
1059        let zip212_states = [
1060            Zip212Enforcement::Off,
1061            Zip212Enforcement::GracePeriod,
1062            Zip212Enforcement::On,
1063        ];
1064
1065        for zip212_enforcement in zip212_states {
1066            let (_, _, _, output) = random_enc_ciphertext(zip212_enforcement, &mut rng);
1067
1068            assert_eq!(
1069                try_sapling_output_recovery_with_ock(
1070                    &OutgoingCipherKey([0u8; 32]),
1071                    &output,
1072                    zip212_enforcement,
1073                ),
1074                None
1075            );
1076        }
1077    }
1078
1079    #[test]
1080    fn recovery_with_invalid_cv() {
1081        let mut rng = OsRng;
1082        let zip212_states = [
1083            Zip212Enforcement::Off,
1084            Zip212Enforcement::GracePeriod,
1085            Zip212Enforcement::On,
1086        ];
1087
1088        for zip212_enforcement in zip212_states {
1089            let (ovk, _, _, mut output) = random_enc_ciphertext(zip212_enforcement, &mut rng);
1090            *output.cv_mut() = ValueCommitment::derive(
1091                NoteValue::from_raw(7),
1092                ValueCommitTrapdoor::random(&mut rng),
1093            );
1094
1095            assert_eq!(
1096                try_sapling_output_recovery(&ovk, &output, zip212_enforcement,),
1097                None
1098            );
1099        }
1100    }
1101
1102    #[test]
1103    fn recovery_with_invalid_cmu() {
1104        let mut rng = OsRng;
1105        let zip212_states = [
1106            Zip212Enforcement::Off,
1107            Zip212Enforcement::GracePeriod,
1108            Zip212Enforcement::On,
1109        ];
1110
1111        for zip212_enforcement in zip212_states {
1112            let (ovk, ock, _, mut output) = random_enc_ciphertext(zip212_enforcement, &mut rng);
1113            *output.cmu_mut() =
1114                ExtractedNoteCommitment::from_bytes(&bls12_381::Scalar::random(&mut rng).to_repr())
1115                    .unwrap();
1116
1117            assert_eq!(
1118                try_sapling_output_recovery(&ovk, &output, zip212_enforcement),
1119                None
1120            );
1121
1122            assert_eq!(
1123                try_sapling_output_recovery_with_ock(&ock, &output, zip212_enforcement),
1124                None
1125            );
1126        }
1127    }
1128
1129    #[test]
1130    fn recovery_with_invalid_epk() {
1131        let mut rng = OsRng;
1132        let zip212_states = [
1133            Zip212Enforcement::Off,
1134            Zip212Enforcement::GracePeriod,
1135            Zip212Enforcement::On,
1136        ];
1137
1138        for zip212_enforcement in zip212_states {
1139            let (ovk, ock, _, mut output) = random_enc_ciphertext(zip212_enforcement, &mut rng);
1140            *output.ephemeral_key_mut() = jubjub::ExtendedPoint::random(&mut rng).to_bytes().into();
1141
1142            assert_eq!(
1143                try_sapling_output_recovery(&ovk, &output, zip212_enforcement),
1144                None
1145            );
1146
1147            assert_eq!(
1148                try_sapling_output_recovery_with_ock(&ock, &output, zip212_enforcement),
1149                None
1150            );
1151        }
1152    }
1153
1154    #[test]
1155    fn recovery_with_invalid_enc_tag() {
1156        let mut rng = OsRng;
1157        let zip212_states = [
1158            Zip212Enforcement::Off,
1159            Zip212Enforcement::GracePeriod,
1160            Zip212Enforcement::On,
1161        ];
1162
1163        for zip212_enforcement in zip212_states {
1164            let (ovk, ock, _, mut output) = random_enc_ciphertext(zip212_enforcement, &mut rng);
1165
1166            output.enc_ciphertext_mut()[ENC_CIPHERTEXT_SIZE - 1] ^= 0xff;
1167            assert_eq!(
1168                try_sapling_output_recovery(&ovk, &output, zip212_enforcement),
1169                None
1170            );
1171            assert_eq!(
1172                try_sapling_output_recovery_with_ock(&ock, &output, zip212_enforcement),
1173                None
1174            );
1175        }
1176    }
1177
1178    #[test]
1179    fn recovery_with_invalid_out_tag() {
1180        let mut rng = OsRng;
1181        let zip212_states = [
1182            Zip212Enforcement::Off,
1183            Zip212Enforcement::GracePeriod,
1184            Zip212Enforcement::On,
1185        ];
1186
1187        for zip212_enforcement in zip212_states {
1188            let (ovk, ock, _, mut output) = random_enc_ciphertext(zip212_enforcement, &mut rng);
1189
1190            output.out_ciphertext_mut()[OUT_CIPHERTEXT_SIZE - 1] ^= 0xff;
1191            assert_eq!(
1192                try_sapling_output_recovery(&ovk, &output, zip212_enforcement),
1193                None
1194            );
1195            assert_eq!(
1196                try_sapling_output_recovery_with_ock(&ock, &output, zip212_enforcement),
1197                None
1198            );
1199        }
1200    }
1201
1202    #[test]
1203    fn recovery_with_invalid_version_byte() {
1204        let mut rng = OsRng;
1205        let zip212_states = [
1206            Zip212Enforcement::Off,
1207            Zip212Enforcement::GracePeriod,
1208            Zip212Enforcement::On,
1209        ];
1210        let leadbytes = [0x02, 0x03, 0x01];
1211
1212        for (zip212_enforcement, leadbyte) in zip212_states.into_iter().zip(leadbytes) {
1213            let (ovk, ock, _, mut output) = random_enc_ciphertext(zip212_enforcement, &mut rng);
1214
1215            *output.enc_ciphertext_mut() = reencrypt_enc_ciphertext(
1216                &ovk,
1217                output.cv(),
1218                output.cmu(),
1219                output.ephemeral_key(),
1220                output.enc_ciphertext(),
1221                output.out_ciphertext(),
1222                |pt| pt[0] = leadbyte,
1223            );
1224            assert_eq!(
1225                try_sapling_output_recovery(&ovk, &output, zip212_enforcement),
1226                None
1227            );
1228            assert_eq!(
1229                try_sapling_output_recovery_with_ock(&ock, &output, zip212_enforcement),
1230                None
1231            );
1232        }
1233    }
1234
1235    #[test]
1236    fn recovery_with_invalid_diversifier() {
1237        let mut rng = OsRng;
1238        let zip212_states = [
1239            Zip212Enforcement::Off,
1240            Zip212Enforcement::GracePeriod,
1241            Zip212Enforcement::On,
1242        ];
1243
1244        for zip212_enforcement in zip212_states {
1245            let (ovk, ock, _, mut output) = random_enc_ciphertext(zip212_enforcement, &mut rng);
1246
1247            *output.enc_ciphertext_mut() = reencrypt_enc_ciphertext(
1248                &ovk,
1249                output.cv(),
1250                output.cmu(),
1251                output.ephemeral_key(),
1252                output.enc_ciphertext(),
1253                output.out_ciphertext(),
1254                |pt| pt[1..12].copy_from_slice(&find_invalid_diversifier().0),
1255            );
1256            assert_eq!(
1257                try_sapling_output_recovery(&ovk, &output, zip212_enforcement),
1258                None
1259            );
1260            assert_eq!(
1261                try_sapling_output_recovery_with_ock(&ock, &output, zip212_enforcement),
1262                None
1263            );
1264        }
1265    }
1266
1267    #[test]
1268    fn recovery_with_incorrect_diversifier() {
1269        let mut rng = OsRng;
1270        let zip212_states = [
1271            Zip212Enforcement::Off,
1272            Zip212Enforcement::GracePeriod,
1273            Zip212Enforcement::On,
1274        ];
1275
1276        for zip212_enforcement in zip212_states {
1277            let (ovk, ock, _, mut output) = random_enc_ciphertext(zip212_enforcement, &mut rng);
1278
1279            *output.enc_ciphertext_mut() = reencrypt_enc_ciphertext(
1280                &ovk,
1281                output.cv(),
1282                output.cmu(),
1283                output.ephemeral_key(),
1284                output.enc_ciphertext(),
1285                output.out_ciphertext(),
1286                |pt| pt[1..12].copy_from_slice(&find_valid_diversifier().0),
1287            );
1288            assert_eq!(
1289                try_sapling_output_recovery(&ovk, &output, zip212_enforcement),
1290                None
1291            );
1292            assert_eq!(
1293                try_sapling_output_recovery_with_ock(&ock, &output, zip212_enforcement),
1294                None
1295            );
1296        }
1297    }
1298
1299    #[test]
1300    fn recovery_with_invalid_pk_d() {
1301        let mut rng = OsRng;
1302        let zip212_states = [
1303            Zip212Enforcement::Off,
1304            Zip212Enforcement::GracePeriod,
1305            Zip212Enforcement::On,
1306        ];
1307
1308        for zip212_enforcement in zip212_states {
1309            let (ovk, ock, _, mut output) = random_enc_ciphertext(zip212_enforcement, &mut rng);
1310            let invalid_pk_d = jubjub::ExtendedPoint::random(&mut rng).to_bytes();
1311
1312            *output.out_ciphertext_mut() = reencrypt_out_ciphertext(
1313                &ovk,
1314                output.cv(),
1315                output.cmu(),
1316                output.ephemeral_key(),
1317                output.out_ciphertext(),
1318                |pt| pt[0..32].copy_from_slice(&invalid_pk_d),
1319            );
1320            assert_eq!(
1321                try_sapling_output_recovery(&ovk, &output, zip212_enforcement),
1322                None
1323            );
1324            assert_eq!(
1325                try_sapling_output_recovery_with_ock(&ock, &output, zip212_enforcement),
1326                None
1327            );
1328        }
1329    }
1330
1331    #[test]
1332    fn test_vectors() {
1333        let test_vectors = crate::test_vectors::note_encryption::make_test_vectors();
1334
1335        macro_rules! read_cmu {
1336            ($field:expr_2021) => {{ ExtractedNoteCommitment::from_bytes($field[..].try_into().unwrap()).unwrap() }};
1337        }
1338
1339        macro_rules! read_jubjub_scalar {
1340            ($field:expr_2021) => {{ jubjub::Fr::from_repr($field[..].try_into().unwrap()).unwrap() }};
1341        }
1342
1343        macro_rules! read_pk_d {
1344            ($field:expr_2021) => {
1345                DiversifiedTransmissionKey::from_bytes(&$field).unwrap()
1346            };
1347        }
1348
1349        macro_rules! read_cv {
1350            ($field:expr_2021) => {
1351                ValueCommitment::from_bytes_not_small_order(&$field).unwrap()
1352            };
1353        }
1354
1355        let zip212_enforcement = Zip212Enforcement::Off;
1356
1357        for tv in test_vectors {
1358            //
1359            // Load the test vector components
1360            //
1361
1362            let ivk = PreparedIncomingViewingKey::new(&SaplingIvk(read_jubjub_scalar!(tv.ivk)));
1363            let pk_d = read_pk_d!(tv.default_pk_d);
1364            let rcm = read_jubjub_scalar!(tv.rcm);
1365            let cv = read_cv!(tv.cv);
1366            let cmu = read_cmu!(tv.cmu);
1367            let esk = EphemeralSecretKey(read_jubjub_scalar!(tv.esk));
1368            let ephemeral_key = EphemeralKeyBytes(tv.epk);
1369
1370            //
1371            // Test the individual components
1372            //
1373
1374            let shared_secret = esk.agree(&pk_d);
1375            assert_eq!(shared_secret.to_bytes(), tv.shared_secret);
1376
1377            let k_enc = shared_secret.kdf_sapling(&ephemeral_key);
1378            assert_eq!(k_enc.as_bytes(), tv.k_enc);
1379
1380            let ovk = OutgoingViewingKey(tv.ovk);
1381            let ock = prf_ock(&ovk, &cv, &cmu.to_bytes(), &ephemeral_key);
1382            assert_eq!(ock.as_ref(), tv.ock);
1383
1384            let to = PaymentAddress::from_parts(Diversifier(tv.default_d), pk_d).unwrap();
1385            let note = to.create_note(NoteValue::from_raw(tv.v), Rseed::BeforeZip212(rcm));
1386            assert_eq!(note.cmu(), cmu);
1387
1388            let output = OutputDescription::from_parts(
1389                cv.clone(),
1390                cmu,
1391                ephemeral_key,
1392                tv.c_enc,
1393                tv.c_out,
1394                [0u8; GROTH_PROOF_SIZE],
1395            );
1396
1397            //
1398            // Test decryption
1399            // (Tested first because it only requires immutable references.)
1400            //
1401
1402            match try_sapling_note_decryption(&ivk, &output, zip212_enforcement) {
1403                Some((decrypted_note, decrypted_to, decrypted_memo)) => {
1404                    assert_eq!(decrypted_note, note);
1405                    assert_eq!(decrypted_to, to);
1406                    assert_eq!(&decrypted_memo[..], &tv.memo[..]);
1407                }
1408                None => panic!("Note decryption failed"),
1409            }
1410
1411            match try_sapling_compact_note_decryption(
1412                &ivk,
1413                &CompactOutputDescription::from(output.clone()),
1414                zip212_enforcement,
1415            ) {
1416                Some((decrypted_note, decrypted_to)) => {
1417                    assert_eq!(decrypted_note, note);
1418                    assert_eq!(decrypted_to, to);
1419                }
1420                None => panic!("Compact note decryption failed"),
1421            }
1422
1423            match try_sapling_output_recovery(&ovk, &output, zip212_enforcement) {
1424                Some((decrypted_note, decrypted_to, decrypted_memo)) => {
1425                    assert_eq!(decrypted_note, note);
1426                    assert_eq!(decrypted_to, to);
1427                    assert_eq!(&decrypted_memo[..], &tv.memo[..]);
1428                }
1429                None => panic!("Output recovery failed"),
1430            }
1431
1432            match &batch::try_note_decryption(
1433                core::slice::from_ref(&ivk),
1434                &[(SaplingDomain::new(zip212_enforcement), output.clone())],
1435            )[..]
1436            {
1437                [Some(((decrypted_note, decrypted_to, decrypted_memo), i))] => {
1438                    assert_eq!(decrypted_note, &note);
1439                    assert_eq!(decrypted_to, &to);
1440                    assert_eq!(&decrypted_memo[..], &tv.memo[..]);
1441                    assert_eq!(*i, 0);
1442                }
1443                _ => panic!("Note decryption failed"),
1444            }
1445
1446            match &batch::try_compact_note_decryption(
1447                core::slice::from_ref(&ivk),
1448                &[(
1449                    SaplingDomain::new(zip212_enforcement),
1450                    CompactOutputDescription::from(output.clone()),
1451                )],
1452            )[..]
1453            {
1454                [Some(((decrypted_note, decrypted_to), i))] => {
1455                    assert_eq!(decrypted_note, &note);
1456                    assert_eq!(decrypted_to, &to);
1457                    assert_eq!(*i, 0);
1458                }
1459                _ => panic!("Note decryption failed"),
1460            }
1461
1462            //
1463            // Test encryption
1464            //
1465
1466            let ne = NoteEncryption::<SaplingDomain>::new_with_esk(esk, Some(ovk), note, tv.memo);
1467
1468            assert_eq!(ne.encrypt_note_plaintext().as_ref(), &tv.c_enc[..]);
1469            assert_eq!(
1470                &ne.encrypt_outgoing_plaintext(&cv, &cmu, &mut OsRng)[..],
1471                &tv.c_out[..]
1472            );
1473        }
1474    }
1475
1476    /// Without an outgoing viewing key, `outCiphertext` encrypts random bytes
1477    /// under a random key. The caller's RNG supplies the key first and then the
1478    /// outgoing plaintext, and nothing else is drawn from it.
1479    #[test]
1480    fn outgoing_ciphertext_without_ovk_draws_key_then_plaintext_from_rng() {
1481        /// Any fixed seed works; the test compares two generators seeded alike.
1482        const OUTGOING_RNG_SEED: u64 = 0x0a1c_e5ee_d000_0001;
1483
1484        let mut rng = OsRng;
1485        let zip212_enforcement = Zip212Enforcement::On;
1486        let ivk = SaplingIvk(jubjub::Fr::random(&mut rng));
1487        let pa = ivk.to_payment_address(Diversifier([0; 11])).unwrap();
1488        let value = NoteValue::from_raw(100);
1489        let cv = ValueCommitment::derive(value, ValueCommitTrapdoor::random(&mut rng));
1490        let note = pa.create_note(value, generate_random_rseed(zip212_enforcement, &mut rng));
1491        let cmu = note.cmu();
1492        let ne = sapling_note_encryption(None, note, [0x37; 512], &mut rng);
1493
1494        let mut outgoing_rng = StdRng::seed_from_u64(OUTGOING_RNG_SEED);
1495        let out_ciphertext = ne.encrypt_outgoing_plaintext(&cv, &cmu, &mut outgoing_rng);
1496
1497        let mut expected_rng = StdRng::seed_from_u64(OUTGOING_RNG_SEED);
1498        let mut ock = OutgoingCipherKey([0; 32]);
1499        expected_rng.fill_bytes(&mut ock.0);
1500        let mut expected = [0u8; OUT_CIPHERTEXT_SIZE];
1501        expected_rng.fill_bytes(&mut expected[..OUT_PLAINTEXT_SIZE]);
1502        let tag = ChaCha20Poly1305::new_from_slice(ock.as_ref())
1503            .unwrap()
1504            .encrypt_inout_detached(
1505                (&[0u8; 12]).into(),
1506                &[],
1507                (&mut expected[..OUT_PLAINTEXT_SIZE]).into(),
1508            )
1509            .unwrap();
1510        expected[OUT_PLAINTEXT_SIZE..].copy_from_slice(&tag);
1511
1512        assert_eq!(out_ciphertext, expected);
1513        assert_eq!(
1514            outgoing_rng.next_u64(),
1515            expected_rng.next_u64(),
1516            "encryption must draw exactly the key and the outgoing plaintext"
1517        );
1518    }
1519
1520    #[test]
1521    fn batching() {
1522        let mut rng = OsRng;
1523        let zip212_enforcement = Zip212Enforcement::On;
1524
1525        // Test batch trial-decryption with multiple IVKs and outputs.
1526        let invalid_ivk =
1527            PreparedIncomingViewingKey::new(&SaplingIvk(jubjub::Fr::random(&mut rng)));
1528        let valid_ivk = SaplingIvk(jubjub::Fr::random(&mut rng));
1529        let outputs: Vec<_> = (0..10)
1530            .map(|_| {
1531                (
1532                    SaplingDomain::new(zip212_enforcement),
1533                    random_enc_ciphertext_with(&valid_ivk, zip212_enforcement, &mut rng).2,
1534                )
1535            })
1536            .collect();
1537        let valid_ivk = PreparedIncomingViewingKey::new(&valid_ivk);
1538
1539        // Check that batched trial decryptions with invalid_ivk fails.
1540        let res = batch::try_note_decryption(core::slice::from_ref(&invalid_ivk), &outputs);
1541        assert_eq!(res.len(), 10);
1542        assert_eq!(&res[..], &vec![None; 10][..]);
1543
1544        // Check that batched trial decryptions with valid_ivk succeeds.
1545        let res = batch::try_note_decryption(&[invalid_ivk, valid_ivk.clone()], &outputs);
1546        assert_eq!(res.len(), 10);
1547        for (result, (_, output)) in res.iter().zip(outputs.iter()) {
1548            // Confirm the successful batched trial decryptions gave the same result.
1549            // In all cases, the index of the valid ivk is returned.
1550            assert!(result.is_some());
1551            assert_eq!(
1552                result,
1553                &try_sapling_note_decryption(&valid_ivk, output, zip212_enforcement)
1554                    .map(|r| (r, 1))
1555            );
1556        }
1557    }
1558}