Skip to main content

orchard/
note_encryption.rs

1//! In-band secret distribution for Orchard bundles.
2
3use alloc::vec::Vec;
4use core::fmt;
5
6use blake2b_simd::{Hash, Params};
7use group::ff::PrimeField;
8use zcash_note_encryption::{
9    BatchDomain, Domain, EphemeralKeyBytes, NotePlaintextBytes, OutPlaintextBytes,
10    OutgoingCipherKey, ShieldedOutput, COMPACT_NOTE_SIZE, ENC_CIPHERTEXT_SIZE, NOTE_PLAINTEXT_SIZE,
11    OUT_PLAINTEXT_SIZE,
12};
13
14use crate::{
15    action::Action,
16    keys::{
17        DiversifiedTransmissionKey, Diversifier, EphemeralPublicKey, EphemeralSecretKey,
18        OutgoingViewingKey, PreparedEphemeralPublicKey, PreparedIncomingViewingKey, SharedSecret,
19    },
20    note::{ExtractedNoteCommitment, Nullifier, RandomSeed, Rho},
21    value::{NoteValue, ValueCommitment},
22    Address, Note,
23};
24
25const PRF_OCK_ORCHARD_PERSONALIZATION: &[u8; 16] = b"Zcash_Orchardock";
26
27/// Defined in [Zcash Protocol Spec ยง 5.4.2: Pseudo Random Functions][concreteprfs].
28///
29/// [concreteprfs]: https://zips.z.cash/protocol/nu5.pdf#concreteprfs
30pub(crate) fn prf_ock_orchard(
31    ovk: &OutgoingViewingKey,
32    cv: &ValueCommitment,
33    cmx_bytes: &[u8; 32],
34    ephemeral_key: &EphemeralKeyBytes,
35) -> OutgoingCipherKey {
36    OutgoingCipherKey(
37        Params::new()
38            .hash_length(32)
39            .personal(PRF_OCK_ORCHARD_PERSONALIZATION)
40            .to_state()
41            .update(ovk.as_ref())
42            .update(&cv.to_bytes())
43            .update(cmx_bytes)
44            .update(ephemeral_key.as_ref())
45            .finalize()
46            .as_bytes()
47            .try_into()
48            .unwrap(),
49    )
50}
51
52fn orchard_parse_note_plaintext_without_memo<F>(
53    domain: &OrchardDomain,
54    plaintext: &[u8],
55    get_pk_d: F,
56) -> Option<(Note, Address)>
57where
58    F: FnOnce(&Diversifier) -> DiversifiedTransmissionKey,
59{
60    assert!(plaintext.len() >= COMPACT_NOTE_SIZE);
61
62    // Check note plaintext version
63    if plaintext[0] != 0x02 {
64        return None;
65    }
66
67    // The unwraps below are guaranteed to succeed by the assertion above
68    let diversifier = Diversifier::from_bytes(plaintext[1..12].try_into().unwrap());
69    let value = NoteValue::from_bytes(plaintext[12..20].try_into().unwrap());
70    let rseed = Option::from(RandomSeed::from_bytes(
71        plaintext[20..COMPACT_NOTE_SIZE].try_into().unwrap(),
72        &domain.rho,
73    ))?;
74
75    let pk_d = get_pk_d(&diversifier);
76
77    let recipient = Address::from_parts(diversifier, pk_d);
78    let note = Option::from(Note::from_parts(recipient, value, domain.rho, rseed))?;
79    Some((note, recipient))
80}
81
82/// Orchard-specific note encryption logic.
83#[derive(Debug)]
84pub struct OrchardDomain {
85    rho: Rho,
86}
87
88impl memuse::DynamicUsage for OrchardDomain {
89    fn dynamic_usage(&self) -> usize {
90        self.rho.dynamic_usage()
91    }
92
93    fn dynamic_usage_bounds(&self) -> (usize, Option<usize>) {
94        self.rho.dynamic_usage_bounds()
95    }
96}
97
98impl OrchardDomain {
99    /// Constructs a domain that can be used to trial-decrypt this action's output note.
100    pub fn for_action<T>(act: &Action<T>) -> Self {
101        Self { rho: act.rho() }
102    }
103
104    /// Constructs a domain that can be used to trial-decrypt a PCZT action's output note.
105    pub fn for_pczt_action(act: &crate::pczt::Action) -> Self {
106        Self {
107            rho: Rho::from_nf_old(act.spend().nullifier),
108        }
109    }
110
111    /// Constructs a domain that can be used to trial-decrypt this action's output note.
112    pub fn for_compact_action(act: &CompactAction) -> Self {
113        Self { rho: act.rho() }
114    }
115}
116
117impl Domain for OrchardDomain {
118    type EphemeralSecretKey = EphemeralSecretKey;
119    type EphemeralPublicKey = EphemeralPublicKey;
120    type PreparedEphemeralPublicKey = PreparedEphemeralPublicKey;
121    type SharedSecret = SharedSecret;
122    type SymmetricKey = Hash;
123    type Note = Note;
124    type Recipient = Address;
125    type DiversifiedTransmissionKey = DiversifiedTransmissionKey;
126    type IncomingViewingKey = PreparedIncomingViewingKey;
127    type OutgoingViewingKey = OutgoingViewingKey;
128    type ValueCommitment = ValueCommitment;
129    type ExtractedCommitment = ExtractedNoteCommitment;
130    type ExtractedCommitmentBytes = [u8; 32];
131    type Memo = [u8; 512]; // TODO use a more interesting type
132
133    fn derive_esk(note: &Self::Note) -> Option<Self::EphemeralSecretKey> {
134        Some(note.esk())
135    }
136
137    fn get_pk_d(note: &Self::Note) -> Self::DiversifiedTransmissionKey {
138        *note.recipient().pk_d()
139    }
140
141    fn prepare_epk(epk: Self::EphemeralPublicKey) -> Self::PreparedEphemeralPublicKey {
142        PreparedEphemeralPublicKey::new(epk)
143    }
144
145    fn ka_derive_public(
146        note: &Self::Note,
147        esk: &Self::EphemeralSecretKey,
148    ) -> Self::EphemeralPublicKey {
149        esk.derive_public(note.recipient().g_d())
150    }
151
152    fn ka_agree_enc(
153        esk: &Self::EphemeralSecretKey,
154        pk_d: &Self::DiversifiedTransmissionKey,
155    ) -> Self::SharedSecret {
156        esk.agree(pk_d)
157    }
158
159    fn ka_agree_dec(
160        ivk: &Self::IncomingViewingKey,
161        epk: &Self::PreparedEphemeralPublicKey,
162    ) -> Self::SharedSecret {
163        epk.agree(ivk)
164    }
165
166    fn kdf(secret: Self::SharedSecret, ephemeral_key: &EphemeralKeyBytes) -> Self::SymmetricKey {
167        secret.kdf_orchard(ephemeral_key)
168    }
169
170    fn note_plaintext_bytes(note: &Self::Note, memo: &Self::Memo) -> NotePlaintextBytes {
171        let mut np = [0; NOTE_PLAINTEXT_SIZE];
172        np[0] = 0x02;
173        np[1..12].copy_from_slice(note.recipient().diversifier().as_array());
174        np[12..20].copy_from_slice(&note.value().to_bytes());
175        np[20..52].copy_from_slice(note.rseed().as_bytes());
176        np[52..].copy_from_slice(memo);
177        NotePlaintextBytes(np)
178    }
179
180    fn derive_ock(
181        ovk: &Self::OutgoingViewingKey,
182        cv: &Self::ValueCommitment,
183        cmstar_bytes: &Self::ExtractedCommitmentBytes,
184        ephemeral_key: &EphemeralKeyBytes,
185    ) -> OutgoingCipherKey {
186        prf_ock_orchard(ovk, cv, cmstar_bytes, ephemeral_key)
187    }
188
189    fn outgoing_plaintext_bytes(
190        note: &Self::Note,
191        esk: &Self::EphemeralSecretKey,
192    ) -> OutPlaintextBytes {
193        let mut op = [0; OUT_PLAINTEXT_SIZE];
194        op[..32].copy_from_slice(&note.recipient().pk_d().to_bytes());
195        op[32..].copy_from_slice(&esk.0.to_repr());
196        OutPlaintextBytes(op)
197    }
198
199    fn epk_bytes(epk: &Self::EphemeralPublicKey) -> EphemeralKeyBytes {
200        epk.to_bytes()
201    }
202
203    fn epk(ephemeral_key: &EphemeralKeyBytes) -> Option<Self::EphemeralPublicKey> {
204        EphemeralPublicKey::from_bytes(&ephemeral_key.0).into()
205    }
206
207    fn cmstar(note: &Self::Note) -> Self::ExtractedCommitment {
208        note.commitment().into()
209    }
210
211    fn parse_note_plaintext_without_memo_ivk(
212        &self,
213        ivk: &Self::IncomingViewingKey,
214        plaintext: &[u8],
215    ) -> Option<(Self::Note, Self::Recipient)> {
216        orchard_parse_note_plaintext_without_memo(self, plaintext, |diversifier| {
217            DiversifiedTransmissionKey::derive(ivk, diversifier)
218        })
219    }
220
221    fn parse_note_plaintext_without_memo_ovk(
222        &self,
223        pk_d: &Self::DiversifiedTransmissionKey,
224        plaintext: &NotePlaintextBytes,
225    ) -> Option<(Self::Note, Self::Recipient)> {
226        orchard_parse_note_plaintext_without_memo(self, &plaintext.0, |_| *pk_d)
227    }
228
229    fn extract_memo(&self, plaintext: &NotePlaintextBytes) -> Self::Memo {
230        plaintext.0[COMPACT_NOTE_SIZE..NOTE_PLAINTEXT_SIZE]
231            .try_into()
232            .unwrap()
233    }
234
235    fn extract_pk_d(out_plaintext: &OutPlaintextBytes) -> Option<Self::DiversifiedTransmissionKey> {
236        DiversifiedTransmissionKey::from_bytes(out_plaintext.0[0..32].try_into().unwrap()).into()
237    }
238
239    fn extract_esk(out_plaintext: &OutPlaintextBytes) -> Option<Self::EphemeralSecretKey> {
240        EphemeralSecretKey::from_bytes(out_plaintext.0[32..OUT_PLAINTEXT_SIZE].try_into().unwrap())
241            .into()
242    }
243}
244
245impl BatchDomain for OrchardDomain {
246    fn batch_kdf<'a>(
247        items: impl Iterator<Item = (Option<Self::SharedSecret>, &'a EphemeralKeyBytes)>,
248    ) -> Vec<Option<Self::SymmetricKey>> {
249        let (shared_secrets, ephemeral_keys): (Vec<_>, Vec<_>) = items.unzip();
250
251        SharedSecret::batch_to_affine(shared_secrets)
252            .zip(ephemeral_keys)
253            .map(|(secret, ephemeral_key)| {
254                secret.map(|dhsecret| SharedSecret::kdf_orchard_inner(dhsecret, ephemeral_key))
255            })
256            .collect()
257    }
258}
259
260/// Implementation of in-band secret distribution for Orchard bundles.
261pub type OrchardNoteEncryption = zcash_note_encryption::NoteEncryption<OrchardDomain>;
262
263impl<T> ShieldedOutput<OrchardDomain, ENC_CIPHERTEXT_SIZE> for Action<T> {
264    fn ephemeral_key(&self) -> EphemeralKeyBytes {
265        EphemeralKeyBytes(self.encrypted_note().epk_bytes)
266    }
267
268    fn cmstar_bytes(&self) -> [u8; 32] {
269        self.cmx().to_bytes()
270    }
271
272    fn enc_ciphertext(&self) -> &[u8; ENC_CIPHERTEXT_SIZE] {
273        &self.encrypted_note().enc_ciphertext
274    }
275}
276
277impl ShieldedOutput<OrchardDomain, ENC_CIPHERTEXT_SIZE> for crate::pczt::Action {
278    fn ephemeral_key(&self) -> EphemeralKeyBytes {
279        EphemeralKeyBytes(self.output().encrypted_note().epk_bytes)
280    }
281
282    fn cmstar_bytes(&self) -> [u8; 32] {
283        self.output().cmx().to_bytes()
284    }
285
286    fn enc_ciphertext(&self) -> &[u8; ENC_CIPHERTEXT_SIZE] {
287        &self.output().encrypted_note().enc_ciphertext
288    }
289}
290
291/// A compact Action for light clients.
292#[derive(Clone)]
293pub struct CompactAction {
294    nullifier: Nullifier,
295    cmx: ExtractedNoteCommitment,
296    ephemeral_key: EphemeralKeyBytes,
297    enc_ciphertext: [u8; 52],
298}
299
300impl fmt::Debug for CompactAction {
301    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
302        write!(f, "CompactAction")
303    }
304}
305
306impl<T> From<&Action<T>> for CompactAction {
307    fn from(action: &Action<T>) -> Self {
308        CompactAction {
309            nullifier: *action.nullifier(),
310            cmx: *action.cmx(),
311            ephemeral_key: action.ephemeral_key(),
312            enc_ciphertext: action.encrypted_note().enc_ciphertext[..52]
313                .try_into()
314                .unwrap(),
315        }
316    }
317}
318
319impl ShieldedOutput<OrchardDomain, COMPACT_NOTE_SIZE> for CompactAction {
320    fn ephemeral_key(&self) -> EphemeralKeyBytes {
321        EphemeralKeyBytes(self.ephemeral_key.0)
322    }
323
324    fn cmstar_bytes(&self) -> [u8; 32] {
325        self.cmx.to_bytes()
326    }
327
328    fn enc_ciphertext(&self) -> &[u8; COMPACT_NOTE_SIZE] {
329        &self.enc_ciphertext
330    }
331}
332
333impl CompactAction {
334    /// Create a CompactAction from its constituent parts
335    pub fn from_parts(
336        nullifier: Nullifier,
337        cmx: ExtractedNoteCommitment,
338        ephemeral_key: EphemeralKeyBytes,
339        enc_ciphertext: [u8; 52],
340    ) -> Self {
341        Self {
342            nullifier,
343            cmx,
344            ephemeral_key,
345            enc_ciphertext,
346        }
347    }
348
349    /// Returns the nullifier of the note being spent.
350    pub fn nullifier(&self) -> Nullifier {
351        self.nullifier
352    }
353
354    /// Returns the commitment to the new note being created.
355    pub fn cmx(&self) -> ExtractedNoteCommitment {
356        self.cmx
357    }
358
359    /// Obtains the [`Rho`] value that was used to construct the new note being created.
360    pub fn rho(&self) -> Rho {
361        Rho::from_nf_old(self.nullifier)
362    }
363}
364
365/// Utilities for constructing test data.
366#[cfg(feature = "test-dependencies")]
367pub mod testing {
368    use rand::RngCore;
369    use zcash_note_encryption::Domain;
370
371    use crate::{
372        keys::OutgoingViewingKey,
373        note::{ExtractedNoteCommitment, Nullifier, RandomSeed, Rho},
374        value::NoteValue,
375        Address, Note,
376    };
377
378    use super::{CompactAction, OrchardDomain, OrchardNoteEncryption};
379
380    /// Creates a fake `CompactAction` paying the given recipient the specified value.
381    ///
382    /// Returns the `CompactAction` and the new note.
383    pub fn fake_compact_action<R: RngCore>(
384        rng: &mut R,
385        nf_old: Nullifier,
386        recipient: Address,
387        value: NoteValue,
388        ovk: Option<OutgoingViewingKey>,
389    ) -> (CompactAction, Note) {
390        let rho = Rho::from_nf_old(nf_old);
391        let rseed = {
392            loop {
393                let mut bytes = [0; 32];
394                rng.fill_bytes(&mut bytes);
395                let rseed = RandomSeed::from_bytes(bytes, &rho);
396                if rseed.is_some().into() {
397                    break rseed.unwrap();
398                }
399            }
400        };
401        let note = Note::from_parts(recipient, value, rho, rseed).unwrap();
402        let encryptor = OrchardNoteEncryption::new(ovk, note, [0u8; 512]);
403        let cmx = ExtractedNoteCommitment::from(note.commitment());
404        let ephemeral_key = OrchardDomain::epk_bytes(encryptor.epk());
405        let enc_ciphertext = encryptor.encrypt_note_plaintext();
406
407        (
408            CompactAction {
409                nullifier: nf_old,
410                cmx,
411                ephemeral_key,
412                enc_ciphertext: enc_ciphertext.as_ref()[..52].try_into().unwrap(),
413            },
414            note,
415        )
416    }
417}
418
419#[cfg(test)]
420mod tests {
421    use rand::rngs::OsRng;
422    use zcash_note_encryption::{
423        try_compact_note_decryption, try_note_decryption, try_output_recovery_with_ovk,
424        EphemeralKeyBytes,
425    };
426
427    use super::{prf_ock_orchard, CompactAction, OrchardDomain, OrchardNoteEncryption};
428    use crate::{
429        action::Action,
430        keys::{
431            DiversifiedTransmissionKey, Diversifier, EphemeralSecretKey, IncomingViewingKey,
432            OutgoingViewingKey, PreparedIncomingViewingKey,
433        },
434        note::{ExtractedNoteCommitment, Nullifier, RandomSeed, Rho, TransmittedNoteCiphertext},
435        primitives::redpallas,
436        value::{NoteValue, ValueCommitment},
437        Address, Note,
438    };
439
440    #[test]
441    fn test_vectors() {
442        let test_vectors = crate::test_vectors::note_encryption::test_vectors();
443
444        for tv in test_vectors {
445            //
446            // Load the test vector components
447            //
448
449            // Recipient key material
450            let ivk = PreparedIncomingViewingKey::new(
451                &IncomingViewingKey::from_bytes(&tv.incoming_viewing_key).unwrap(),
452            );
453            let ovk = OutgoingViewingKey::from(tv.ovk);
454            let d = Diversifier::from_bytes(tv.default_d);
455            let pk_d = DiversifiedTransmissionKey::from_bytes(&tv.default_pk_d).unwrap();
456
457            // Received Action
458            let cv_net = ValueCommitment::from_bytes(&tv.cv_net).unwrap();
459            let nf_old = Nullifier::from_bytes(&tv.nf_old).unwrap();
460            let rho = Rho::from_nf_old(nf_old);
461            let cmx = ExtractedNoteCommitment::from_bytes(&tv.cmx).unwrap();
462
463            let esk = EphemeralSecretKey::from_bytes(&tv.esk).unwrap();
464            let ephemeral_key = EphemeralKeyBytes(tv.ephemeral_key);
465
466            // Details about the expected note
467            let value = NoteValue::from_raw(tv.v);
468            let rseed = RandomSeed::from_bytes(tv.rseed, &rho).unwrap();
469
470            //
471            // Test the individual components
472            //
473
474            let shared_secret = esk.agree(&pk_d);
475            assert_eq!(shared_secret.to_bytes(), tv.shared_secret);
476
477            let k_enc = shared_secret.kdf_orchard(&ephemeral_key);
478            assert_eq!(k_enc.as_bytes(), tv.k_enc);
479
480            let ock = prf_ock_orchard(&ovk, &cv_net, &cmx.to_bytes(), &ephemeral_key);
481            assert_eq!(ock.as_ref(), tv.ock);
482
483            let recipient = Address::from_parts(d, pk_d);
484            let note = Note::from_parts(recipient, value, rho, rseed).unwrap();
485            assert_eq!(ExtractedNoteCommitment::from(note.commitment()), cmx);
486
487            let action = Action::from_parts(
488                // nf_old is the nullifier revealed by the receiving Action.
489                nf_old,
490                // We don't need a valid rk for this test.
491                redpallas::VerificationKey::dummy(),
492                cmx,
493                TransmittedNoteCiphertext {
494                    epk_bytes: ephemeral_key.0,
495                    enc_ciphertext: tv.c_enc,
496                    out_ciphertext: tv.c_out,
497                },
498                cv_net.clone(),
499                (),
500            );
501
502            //
503            // Test decryption
504            // (Tested first because it only requires immutable references.)
505            //
506
507            let domain = OrchardDomain { rho };
508
509            match try_note_decryption(&domain, &ivk, &action) {
510                Some((decrypted_note, decrypted_to, decrypted_memo)) => {
511                    assert_eq!(decrypted_note, note);
512                    assert_eq!(decrypted_to, recipient);
513                    assert_eq!(&decrypted_memo[..], &tv.memo[..]);
514                }
515                None => panic!("Note decryption failed"),
516            }
517
518            match try_compact_note_decryption(&domain, &ivk, &CompactAction::from(&action)) {
519                Some((decrypted_note, decrypted_to)) => {
520                    assert_eq!(decrypted_note, note);
521                    assert_eq!(decrypted_to, recipient);
522                }
523                None => panic!("Compact note decryption failed"),
524            }
525
526            match try_output_recovery_with_ovk(&domain, &ovk, &action, &cv_net, &tv.c_out) {
527                Some((decrypted_note, decrypted_to, decrypted_memo)) => {
528                    assert_eq!(decrypted_note, note);
529                    assert_eq!(decrypted_to, recipient);
530                    assert_eq!(&decrypted_memo[..], &tv.memo[..]);
531                }
532                None => panic!("Output recovery failed"),
533            }
534
535            //
536            // Test encryption
537            //
538
539            let ne = OrchardNoteEncryption::new_with_esk(esk, Some(ovk), note, tv.memo);
540
541            assert_eq!(ne.encrypt_note_plaintext().as_ref(), &tv.c_enc[..]);
542            assert_eq!(
543                &ne.encrypt_outgoing_plaintext(&cv_net, &cmx, &mut OsRng)[..],
544                &tv.c_out[..]
545            );
546        }
547    }
548}