From 44bb542f8d36d9538de003415872ff6304c022cd Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Mon, 22 Mar 2021 14:59:25 -0600 Subject: [PATCH 01/19] Move generalized note encryption functionality to zcash_note_encryption crate. Extracted from: https://github.com/zcash/librustzcash/commit/266285b536a07a0385985f0bec4b285b2c09db50 --- Cargo.toml | 9 ++ src/lib.rs | 390 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 393 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 46e1bec..b749ed0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,12 @@ license = "MIT OR Apache-2.0" edition = "2018" [dependencies] +blake2b_simd = "0.5" +byteorder = "1" +crypto_api_chachapoly = "0.4" +ff = "0.8" +group = "0.8" +rand_core = "0.5.1" + +[dev-dependencies] +zcash_primitives = { version = "0.4", path = "../../zcash_primitives" } diff --git a/src/lib.rs b/src/lib.rs index 0ee39e7..a008edb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,386 @@ -#[cfg(test)] -mod tests { - #[allow(clippy::eq_op)] - #[test] - fn it_works() { - assert_eq!(2 + 2, 4); +//! Implementation of in-band secret distribution abstractions +//! for Zcash transactions. The implementations here provide +//! functionality that is shared between the Sapling and Orchard +//! protocols. + +use crypto_api_chachapoly::{ChaCha20Ietf, ChachaPolyIetf}; +use rand_core::RngCore; + +pub const COMPACT_NOTE_SIZE: usize = 1 + // version + 11 + // diversifier + 8 + // value + 32; // rcv +pub const NOTE_PLAINTEXT_SIZE: usize = COMPACT_NOTE_SIZE + 512; +pub const OUT_PLAINTEXT_SIZE: usize = 32 + // pk_d + 32; // esk +pub const ENC_CIPHERTEXT_SIZE: usize = NOTE_PLAINTEXT_SIZE + 16; +pub const OUT_CIPHERTEXT_SIZE: usize = OUT_PLAINTEXT_SIZE + 16; + +/// A symmetric key that can be used to recover a single Sapling or Orchard output. +pub struct OutgoingCipherKey(pub [u8; 32]); + +impl From<[u8; 32]> for OutgoingCipherKey { + fn from(ock: [u8; 32]) -> Self { + OutgoingCipherKey(ock) } } + +impl AsRef<[u8]> for OutgoingCipherKey { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +//FIXME: use constant-time checks for equality +#[derive(Eq, PartialEq)] +pub struct EphemeralKeyBytes(pub [u8; 32]); + +impl From<[u8; 32]> for EphemeralKeyBytes { + fn from(value: [u8; 32]) -> EphemeralKeyBytes { + EphemeralKeyBytes(value) + } +} + +pub struct NotePlaintextBytes(pub [u8; NOTE_PLAINTEXT_SIZE]); +pub struct OutPlaintextBytes(pub [u8; OUT_PLAINTEXT_SIZE]); + +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum EpkValidity { + Valid, + Invalid, +} + +pub trait Domain { + type EphemeralSecretKey; + type EphemeralPublicKey; + type SharedSecret; + type SymmetricKey: AsRef<[u8]>; + type Note; + type Recipient; + type DiversifiedTransmissionKey; + type IncomingViewingKey; + type OutgoingViewingKey; + type ValueCommitment; + type NoteCommitment; + type ExtractedCommitment: Eq; + type Memo; + + fn derive_esk(note: &Self::Note) -> Option; + + fn get_pk_d(note: &Self::Note) -> Self::DiversifiedTransmissionKey; + + fn ka_derive_public( + note: &Self::Note, + esk: &Self::EphemeralSecretKey, + ) -> Self::EphemeralPublicKey; + + fn ka_agree_enc( + esk: &Self::EphemeralSecretKey, + pk_d: &Self::DiversifiedTransmissionKey, + ) -> Self::SharedSecret; + + fn ka_agree_dec( + ivk: &Self::IncomingViewingKey, + epk: &Self::EphemeralPublicKey, + ) -> Self::SharedSecret; + + fn kdf(secret: Self::SharedSecret, epk: &Self::EphemeralPublicKey) -> Self::SymmetricKey; + + // for right now, we just need `recipient` to get `d`; in the future when we + // can get that from a Sapling note, the recipient parameter will be able + // to be removed. + fn to_note_plaintext_bytes( + note: &Self::Note, + recipient: &Self::Recipient, + memo: &Self::Memo, + ) -> NotePlaintextBytes; + + fn get_ock( + ovk: &Self::OutgoingViewingKey, + cv: &Self::ValueCommitment, + cm: &Self::NoteCommitment, + epk: &Self::EphemeralPublicKey, + ) -> OutgoingCipherKey; + + fn to_outgoing_plaintext_bytes( + note: &Self::Note, + esk: &Self::EphemeralSecretKey, + ) -> OutPlaintextBytes; + + fn to_epk_bytes(epk: &Self::EphemeralPublicKey) -> EphemeralKeyBytes; + + fn check_epk_bytes EpkValidity>( + note: &Self::Note, + check: F, + ) -> EpkValidity; + + fn extract_note_commitment(note: &Self::Note) -> Self::ExtractedCommitment; + + fn parse_note_plaintext_without_memo( + &self, + ivk: &Self::IncomingViewingKey, + plaintext: &[u8], + ) -> Option<(Self::Note, Self::Recipient)>; + + // &self is passed here in anticipation of future changes + // to memo handling where the memos may no longer be + // part of the note plaintext. + fn extract_memo(&self, plaintext: &[u8]) -> Self::Memo; +} + +pub trait ShieldedOutput<'a, D: Domain> { + fn ivk(&'a self) -> &'a D::IncomingViewingKey; + fn epk(&'a self) -> &'a D::EphemeralPublicKey; + fn cmstar(&'a self) -> &'a D::ExtractedCommitment; +} + +/// A struct containing context required for encrypting Sapling and Orchard notes. +/// +/// This struct provides a safe API for encrypting Sapling and Orchard notes. In particular, it +/// enforces that fresh ephemeral keys are used for every note, and that the ciphertexts are +/// consistent with each other. +/// +/// Implements section 4.17.1 of the Zcash Protocol Specification. +/// NB: the example code is only covering the pre-Canopy case. +/// +/// # Examples +/// +/// ``` +/// extern crate ff; +/// extern crate rand_core; +/// extern crate zcash_primitives; +/// +/// use ff::Field; +/// use rand_core::OsRng; +/// use zcash_primitives::{ +/// consensus::TestNetwork, +/// sapling::{ +/// keys::{OutgoingViewingKey, prf_expand}, +/// note_encryption::{Memo, sapling_note_encryption}, +/// Diversifier, PaymentAddress, Rseed, ValueCommitment +/// }, +/// }; +/// +/// let mut rng = OsRng; +/// +/// let diversifier = Diversifier([0; 11]); +/// let pk_d = diversifier.g_d().unwrap(); +/// let to = PaymentAddress::from_parts(diversifier, pk_d).unwrap(); +/// let ovk = Some(OutgoingViewingKey([0; 32])); +/// +/// let value = 1000; +/// let rcv = jubjub::Fr::random(&mut rng); +/// let cv = ValueCommitment { +/// value, +/// randomness: rcv.clone(), +/// }; +/// let rcm = jubjub::Fr::random(&mut rng); +/// let note = to.create_note(value, Rseed::BeforeZip212(rcm)).unwrap(); +/// let cmu = note.cmu(); +/// +/// let mut enc = sapling_note_encryption::<_, TestNetwork>(ovk, note, to, Memo::default(), &mut rng); +/// let encCiphertext = enc.encrypt_note_plaintext(); +/// let outCiphertext = enc.encrypt_outgoing_plaintext(&cv.commitment().into(), &cmu, &mut rng); +/// ``` +pub struct NoteEncryption { + epk: D::EphemeralPublicKey, + esk: D::EphemeralSecretKey, + note: D::Note, + to: D::Recipient, + memo: D::Memo, + /// `None` represents the `ovk = ⊥` case. + ovk: Option, +} + +impl NoteEncryption { + /// Construct a new note encryption context for the specified note, + /// recipient, and memo. + pub fn new( + ovk: Option, + note: D::Note, + to: D::Recipient, + memo: D::Memo, + ) -> Self { + let esk = D::derive_esk(¬e).expect("ZIP 212 is active."); + Self::new_with_esk(esk, ovk, note, to, memo) + } + + /// For use only with Sapling. + pub fn new_with_esk( + esk: D::EphemeralSecretKey, + ovk: Option, + note: D::Note, + to: D::Recipient, + memo: D::Memo, + ) -> Self { + NoteEncryption { + epk: D::ka_derive_public(¬e, &esk), + esk, + note, + to, + memo, + ovk, + } + } + + /// Exposes the ephemeral secret key being used to encrypt this note. + pub fn esk(&self) -> &D::EphemeralSecretKey { + &self.esk + } + + /// Exposes the ephemeral public key being used to encrypt this note. + pub fn epk(&self) -> &D::EphemeralPublicKey { + &self.epk + } + + /// Generates `encCiphertext` for this note. + pub fn encrypt_note_plaintext(&self) -> [u8; ENC_CIPHERTEXT_SIZE] { + let pk_d = D::get_pk_d(&self.note); + let shared_secret = D::ka_agree_enc(&self.esk, &pk_d); + let key = D::kdf(shared_secret, &self.epk); + let input = D::to_note_plaintext_bytes(&self.note, &self.to, &self.memo); + + let mut output = [0u8; ENC_CIPHERTEXT_SIZE]; + assert_eq!( + ChachaPolyIetf::aead_cipher() + .seal_to(&mut output, &input.0, &[], key.as_ref(), &[0u8; 12]) + .unwrap(), + ENC_CIPHERTEXT_SIZE + ); + + output + } + + /// Generates `outCiphertext` for this note. + pub fn encrypt_outgoing_plaintext( + &mut self, + cv: &D::ValueCommitment, + cm: &D::NoteCommitment, + rng: &mut R, + ) -> [u8; OUT_CIPHERTEXT_SIZE] { + let (ock, input) = if let Some(ovk) = &self.ovk { + let ock = D::get_ock(ovk, &cv, &cm, &self.epk); + let input = D::to_outgoing_plaintext_bytes(&self.note, &self.esk); + + (ock, input) + } else { + // ovk = ⊥ + let mut ock = OutgoingCipherKey([0; 32]); + let mut input = [0u8; OUT_PLAINTEXT_SIZE]; + + rng.fill_bytes(&mut ock.0); + rng.fill_bytes(&mut input); + + (ock, OutPlaintextBytes(input)) + }; + + let mut output = [0u8; OUT_CIPHERTEXT_SIZE]; + assert_eq!( + ChachaPolyIetf::aead_cipher() + .seal_to(&mut output, &input.0, &[], ock.as_ref(), &[0u8; 12]) + .unwrap(), + OUT_CIPHERTEXT_SIZE + ); + + output + } +} + +/// Trial decryption of the full note plaintext by the recipient. +/// +/// Attempts to decrypt and validate the given `enc_ciphertext` using the given `ivk`. +/// If successful, the corresponding Sapling note and memo are returned, along with the +/// `PaymentAddress` to which the note was sent. +/// +/// Implements section 4.17.2 of the Zcash Protocol Specification. +pub fn try_note_decryption( + domain: &D, + //output: &ShieldedOutput, + ivk: &D::IncomingViewingKey, + epk: &D::EphemeralPublicKey, + cmstar: &D::ExtractedCommitment, + enc_ciphertext: &[u8], +) -> Option<(D::Note, D::Recipient, D::Memo)> { + assert_eq!(enc_ciphertext.len(), ENC_CIPHERTEXT_SIZE); + + let shared_secret = D::ka_agree_dec(ivk, epk); + let key = D::kdf(shared_secret, epk); + + let mut plaintext = [0; ENC_CIPHERTEXT_SIZE]; + assert_eq!( + ChachaPolyIetf::aead_cipher() + .open_to( + &mut plaintext, + &enc_ciphertext, + &[], + key.as_ref(), + &[0u8; 12] + ) + .ok()?, + NOTE_PLAINTEXT_SIZE + ); + + let (note, to) = parse_note_plaintext_without_memo(domain, ivk, epk, cmstar, &plaintext)?; + let memo = domain.extract_memo(&plaintext); + + Some((note, to, memo)) +} + +fn parse_note_plaintext_without_memo( + domain: &D, + ivk: &D::IncomingViewingKey, + epk: &D::EphemeralPublicKey, + cmstar: &D::ExtractedCommitment, + plaintext: &[u8], +) -> Option<(D::Note, D::Recipient)> { + let (note, to) = domain.parse_note_plaintext_without_memo(ivk, &plaintext)?; + + if &D::extract_note_commitment(¬e) != cmstar { + // Published commitment doesn't match calculated commitment + return None; + } else { + let epk_bytes = D::to_epk_bytes(epk); + let validity = D::check_epk_bytes(¬e, |derived_esk| { + if D::to_epk_bytes(&D::ka_derive_public(¬e, &derived_esk)) == epk_bytes { + EpkValidity::Valid + } else { + EpkValidity::Invalid + } + }); + + if validity != EpkValidity::Valid { + return None; + } + } + + Some((note, to)) +} + +/// Trial decryption of the compact note plaintext by the recipient for light clients. +/// +/// Attempts to decrypt and validate the first 52 bytes of `enc_ciphertext` using the +/// given `ivk`. If successful, the corresponding Sapling note is returned, along with the +/// `PaymentAddress` to which the note was sent. +/// +/// Implements the procedure specified in [`ZIP 307`]. +/// +/// [`ZIP 307`]: https://zips.z.cash/zip-0307 +pub fn try_compact_note_decryption( + domain: &D, + ivk: &D::IncomingViewingKey, + epk: &D::EphemeralPublicKey, + cmstar: &D::ExtractedCommitment, + enc_ciphertext: &[u8], +) -> Option<(D::Note, D::Recipient)> { + assert_eq!(enc_ciphertext.len(), COMPACT_NOTE_SIZE); + + let shared_secret = D::ka_agree_dec(&ivk, epk); + let key = D::kdf(shared_secret, &epk); + + // Start from block 1 to skip over Poly1305 keying output + let mut plaintext = [0; COMPACT_NOTE_SIZE]; + plaintext.copy_from_slice(&enc_ciphertext); + ChaCha20Ietf::xor(key.as_ref(), &[0u8; 12], 1, &mut plaintext); + + parse_note_plaintext_without_memo(domain, ivk, epk, cmstar, &plaintext) +} From fd018d64f9f2f720f2b8a815307c9d6e5bfe850c Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Tue, 23 Mar 2021 11:29:16 -0600 Subject: [PATCH 02/19] Add try_output_recovery_with_ovk to shared note encryption code. Extracted from: https://github.com/zcash/librustzcash/commit/5b13bb3a1e98099a2ed849b47cf1b31cf82af47a --- src/lib.rs | 117 ++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 98 insertions(+), 19 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index a008edb..e402717 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,7 +45,7 @@ pub struct NotePlaintextBytes(pub [u8; NOTE_PLAINTEXT_SIZE]); pub struct OutPlaintextBytes(pub [u8; OUT_PLAINTEXT_SIZE]); #[derive(Copy, Clone, PartialEq, Eq)] -pub enum EpkValidity { +pub enum NoteValidity { Valid, Invalid, } @@ -109,23 +109,37 @@ pub trait Domain { fn to_epk_bytes(epk: &Self::EphemeralPublicKey) -> EphemeralKeyBytes; - fn check_epk_bytes EpkValidity>( + fn check_epk_bytes NoteValidity>( note: &Self::Note, check: F, - ) -> EpkValidity; + ) -> NoteValidity; fn extract_note_commitment(note: &Self::Note) -> Self::ExtractedCommitment; - fn parse_note_plaintext_without_memo( + fn parse_note_plaintext_without_memo_ivk( &self, ivk: &Self::IncomingViewingKey, plaintext: &[u8], ) -> Option<(Self::Note, Self::Recipient)>; + fn parse_note_plaintext_without_memo_ovk( + &self, + pk_d: &Self::DiversifiedTransmissionKey, + esk: &Self::EphemeralSecretKey, + epk: &Self::EphemeralPublicKey, + plaintext: &[u8], + ) -> Option<(Self::Note, Self::Recipient)>; + // &self is passed here in anticipation of future changes // to memo handling where the memos may no longer be // part of the note plaintext. fn extract_memo(&self, plaintext: &[u8]) -> Self::Memo; + + fn extract_pk_d( + out_plaintext: &[u8; OUT_CIPHERTEXT_SIZE], + ) -> Option; + + fn extract_esk(out_plaintext: &[u8; OUT_CIPHERTEXT_SIZE]) -> Option; } pub trait ShieldedOutput<'a, D: Domain> { @@ -320,40 +334,47 @@ pub fn try_note_decryption( NOTE_PLAINTEXT_SIZE ); - let (note, to) = parse_note_plaintext_without_memo(domain, ivk, epk, cmstar, &plaintext)?; + let (note, to) = parse_note_plaintext_without_memo_ivk(domain, ivk, epk, cmstar, &plaintext)?; let memo = domain.extract_memo(&plaintext); Some((note, to, memo)) } -fn parse_note_plaintext_without_memo( +fn parse_note_plaintext_without_memo_ivk( domain: &D, ivk: &D::IncomingViewingKey, epk: &D::EphemeralPublicKey, cmstar: &D::ExtractedCommitment, plaintext: &[u8], ) -> Option<(D::Note, D::Recipient)> { - let (note, to) = domain.parse_note_plaintext_without_memo(ivk, &plaintext)?; + let (note, to) = domain.parse_note_plaintext_without_memo_ivk(ivk, &plaintext)?; + let validity = check_note_validity::(¬e, epk, cmstar); + if validity == NoteValidity::Valid { + Some((note, to)) + } else { + None + } +} + +fn check_note_validity( + note: &D::Note, + epk: &D::EphemeralPublicKey, + cmstar: &D::ExtractedCommitment, +) -> NoteValidity { if &D::extract_note_commitment(¬e) != cmstar { // Published commitment doesn't match calculated commitment - return None; + NoteValidity::Invalid } else { let epk_bytes = D::to_epk_bytes(epk); - let validity = D::check_epk_bytes(¬e, |derived_esk| { + D::check_epk_bytes(¬e, |derived_esk| { if D::to_epk_bytes(&D::ka_derive_public(¬e, &derived_esk)) == epk_bytes { - EpkValidity::Valid + NoteValidity::Valid } else { - EpkValidity::Invalid + NoteValidity::Invalid } - }); - - if validity != EpkValidity::Valid { - return None; - } + }) } - - Some((note, to)) } /// Trial decryption of the compact note plaintext by the recipient for light clients. @@ -382,5 +403,63 @@ pub fn try_compact_note_decryption( plaintext.copy_from_slice(&enc_ciphertext); ChaCha20Ietf::xor(key.as_ref(), &[0u8; 12], 1, &mut plaintext); - parse_note_plaintext_without_memo(domain, ivk, epk, cmstar, &plaintext) + parse_note_plaintext_without_memo_ivk(domain, ivk, epk, cmstar, &plaintext) +} + +/// Recovery of the full note plaintext by the sender. +/// +/// Attempts to decrypt and validate the given `enc_ciphertext` using the given `ock`. +/// If successful, the corresponding Sapling note and memo are returned, along with the +/// `PaymentAddress` to which the note was sent. +/// +/// Implements part of section 4.17.3 of the Zcash Protocol Specification. +/// For decryption using a Full Viewing Key see [`try_sapling_output_recovery`]. +pub fn try_output_recovery_with_ock( + domain: &D, + ock: &OutgoingCipherKey, + cmstar: &D::ExtractedCommitment, + epk: &D::EphemeralPublicKey, + enc_ciphertext: &[u8], + out_ciphertext: &[u8], +) -> Option<(D::Note, D::Recipient, D::Memo)> { + assert_eq!(enc_ciphertext.len(), ENC_CIPHERTEXT_SIZE); + assert_eq!(out_ciphertext.len(), OUT_CIPHERTEXT_SIZE); + + let mut op = [0; OUT_CIPHERTEXT_SIZE]; + assert_eq!( + ChachaPolyIetf::aead_cipher() + .open_to(&mut op, &out_ciphertext, &[], ock.as_ref(), &[0u8; 12]) + .ok()?, + OUT_PLAINTEXT_SIZE + ); + + let pk_d = D::extract_pk_d(&op)?; + let esk = D::extract_esk(&op)?; + + let shared_secret = D::ka_agree_enc(&esk, &pk_d); + let key = D::kdf(shared_secret, &epk); + + let mut plaintext = [0; ENC_CIPHERTEXT_SIZE]; + assert_eq!( + ChachaPolyIetf::aead_cipher() + .open_to( + &mut plaintext, + &enc_ciphertext, + &[], + key.as_ref(), + &[0u8; 12] + ) + .ok()?, + NOTE_PLAINTEXT_SIZE + ); + + let (note, to) = domain.parse_note_plaintext_without_memo_ovk(&pk_d, &esk, &epk, &plaintext)?; + let memo = domain.extract_memo(&plaintext); + + let validity = check_note_validity::(¬e, epk, cmstar); + if validity == NoteValidity::Valid { + Some((note, to, memo)) + } else { + None + } } From 41c71910b1fdbf895c1455ad0c030f9466c6e447 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Sat, 27 Mar 2021 08:43:40 -0600 Subject: [PATCH 03/19] Update zcash_primitives dev dependency for zcash_note_encryption Extracted from: https://github.com/zcash/librustzcash/commit/be225daabfe612cc7f1c87442a848efba4c7d6ad --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index b749ed0..caf2624 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,4 +19,4 @@ group = "0.8" rand_core = "0.5.1" [dev-dependencies] -zcash_primitives = { version = "0.4", path = "../../zcash_primitives" } +zcash_primitives = { version = "0.5", path = "../../zcash_primitives" } From 8838bf5ef567dc010f965dad6c3de162e5510193 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Sat, 27 Mar 2021 08:51:44 -0600 Subject: [PATCH 04/19] Fix Clippy complaints. Extracted from: https://github.com/zcash/librustzcash/commit/213cd6cce91cbbc6f97fbebcc7200e41975c689c --- src/lib.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e402717..2b965cc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -89,7 +89,7 @@ pub trait Domain { // for right now, we just need `recipient` to get `d`; in the future when we // can get that from a Sapling note, the recipient parameter will be able // to be removed. - fn to_note_plaintext_bytes( + fn note_plaintext_bytes( note: &Self::Note, recipient: &Self::Recipient, memo: &Self::Memo, @@ -102,12 +102,12 @@ pub trait Domain { epk: &Self::EphemeralPublicKey, ) -> OutgoingCipherKey; - fn to_outgoing_plaintext_bytes( + fn outgoing_plaintext_bytes( note: &Self::Note, esk: &Self::EphemeralSecretKey, ) -> OutPlaintextBytes; - fn to_epk_bytes(epk: &Self::EphemeralPublicKey) -> EphemeralKeyBytes; + fn epk_bytes(epk: &Self::EphemeralPublicKey) -> EphemeralKeyBytes; fn check_epk_bytes NoteValidity>( note: &Self::Note, @@ -252,7 +252,7 @@ impl NoteEncryption { let pk_d = D::get_pk_d(&self.note); let shared_secret = D::ka_agree_enc(&self.esk, &pk_d); let key = D::kdf(shared_secret, &self.epk); - let input = D::to_note_plaintext_bytes(&self.note, &self.to, &self.memo); + let input = D::note_plaintext_bytes(&self.note, &self.to, &self.memo); let mut output = [0u8; ENC_CIPHERTEXT_SIZE]; assert_eq!( @@ -274,7 +274,7 @@ impl NoteEncryption { ) -> [u8; OUT_CIPHERTEXT_SIZE] { let (ock, input) = if let Some(ovk) = &self.ovk { let ock = D::get_ock(ovk, &cv, &cm, &self.epk); - let input = D::to_outgoing_plaintext_bytes(&self.note, &self.esk); + let input = D::outgoing_plaintext_bytes(&self.note, &self.esk); (ock, input) } else { @@ -366,9 +366,9 @@ fn check_note_validity( // Published commitment doesn't match calculated commitment NoteValidity::Invalid } else { - let epk_bytes = D::to_epk_bytes(epk); + let epk_bytes = D::epk_bytes(epk); D::check_epk_bytes(¬e, |derived_esk| { - if D::to_epk_bytes(&D::ka_derive_public(¬e, &derived_esk)) == epk_bytes { + if D::epk_bytes(&D::ka_derive_public(¬e, &derived_esk)) == epk_bytes { NoteValidity::Valid } else { NoteValidity::Invalid From cb60fd2092e3835c746d19fe200e94457be816af Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Sat, 27 Mar 2021 19:29:42 -0600 Subject: [PATCH 05/19] Fix zcash_note_encryption doctests. Extracted from: https://github.com/zcash/librustzcash/commit/dad8663c55ffb739da9758b9971d00ccd5ccfb73 --- Cargo.toml | 1 + src/lib.rs | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index caf2624..044e913 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,3 +20,4 @@ rand_core = "0.5.1" [dev-dependencies] zcash_primitives = { version = "0.5", path = "../../zcash_primitives" } +jubjub = "0.5.1" diff --git a/src/lib.rs b/src/lib.rs index 2b965cc..4f34719 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -168,9 +168,10 @@ pub trait ShieldedOutput<'a, D: Domain> { /// use rand_core::OsRng; /// use zcash_primitives::{ /// consensus::TestNetwork, +/// memo::MemoBytes, /// sapling::{ /// keys::{OutgoingViewingKey, prf_expand}, -/// note_encryption::{Memo, sapling_note_encryption}, +/// note_encryption::{sapling_note_encryption}, /// Diversifier, PaymentAddress, Rseed, ValueCommitment /// }, /// }; @@ -192,7 +193,7 @@ pub trait ShieldedOutput<'a, D: Domain> { /// let note = to.create_note(value, Rseed::BeforeZip212(rcm)).unwrap(); /// let cmu = note.cmu(); /// -/// let mut enc = sapling_note_encryption::<_, TestNetwork>(ovk, note, to, Memo::default(), &mut rng); +/// let mut enc = sapling_note_encryption::<_, TestNetwork>(ovk, note, to, MemoBytes::empty(), &mut rng); /// let encCiphertext = enc.encrypt_note_plaintext(); /// let outCiphertext = enc.encrypt_outgoing_plaintext(&cv.commitment().into(), &cmu, &mut rng); /// ``` From 5e355ffc249a77398e7e8907b408240b00207de2 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Mon, 5 Apr 2021 09:49:32 -0600 Subject: [PATCH 06/19] Apply suggestions from code review Co-authored-by: str4d Extracted from: https://github.com/zcash/librustzcash/commit/879eea863adf4dc1c598536362ceb796c07aebc7 --- src/lib.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 4f34719..5201680 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -350,8 +350,7 @@ fn parse_note_plaintext_without_memo_ivk( ) -> Option<(D::Note, D::Recipient)> { let (note, to) = domain.parse_note_plaintext_without_memo_ivk(ivk, &plaintext)?; - let validity = check_note_validity::(¬e, epk, cmstar); - if validity == NoteValidity::Valid { + if let NoteValidity::Valid = check_note_validity::(¬e, epk, cmstar) { Some((note, to)) } else { None @@ -457,8 +456,7 @@ pub fn try_output_recovery_with_ock( let (note, to) = domain.parse_note_plaintext_without_memo_ovk(&pk_d, &esk, &epk, &plaintext)?; let memo = domain.extract_memo(&plaintext); - let validity = check_note_validity::(¬e, epk, cmstar); - if validity == NoteValidity::Valid { + if let NoteValidity::Valid = check_note_validity::(¬e, epk, cmstar) { Some((note, to, memo)) } else { None From 78bb0fd6bd157465ab8782f7b74d4e345d445c84 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Thu, 8 Apr 2021 10:08:00 -0600 Subject: [PATCH 07/19] Remove spurious mut references. Extracted from: https://github.com/zcash/librustzcash/commit/a560101bb2f2c9635aaa8a77256fef0c49a113eb --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 5201680..c4e2d87 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -268,7 +268,7 @@ impl NoteEncryption { /// Generates `outCiphertext` for this note. pub fn encrypt_outgoing_plaintext( - &mut self, + &self, cv: &D::ValueCommitment, cm: &D::NoteCommitment, rng: &mut R, From c6f37302794999ec8f68dc23859613276babec96 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Thu, 8 Apr 2021 10:08:58 -0600 Subject: [PATCH 08/19] Update comments describing COMPACT_NOTE_SIZE components. Co-authored-by: ebfull Extracted from: https://github.com/zcash/librustzcash/commit/24e62d3a7b33fe5a7a7681afabb36a63299161c7 --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index c4e2d87..be93903 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,7 @@ use rand_core::RngCore; pub const COMPACT_NOTE_SIZE: usize = 1 + // version 11 + // diversifier 8 + // value - 32; // rcv + 32; // rseed (or rcm prior to ZIP 212) pub const NOTE_PLAINTEXT_SIZE: usize = COMPACT_NOTE_SIZE + 512; pub const OUT_PLAINTEXT_SIZE: usize = 32 + // pk_d 32; // esk From a8fd731e261580e2fee7371a44923f987e172075 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Thu, 8 Apr 2021 10:12:47 -0600 Subject: [PATCH 09/19] Add myself to crate contributors. Extracted from: https://github.com/zcash/librustzcash/commit/cfdbafe2e39cf4270103b8810e613cbb2cb8e55a --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 044e913..2785e27 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ description = "TBD" version = "0.0.0" authors = [ "Jack Grigg ", + "Kris Nuttycombe " ] homepage = "https://github.com/zcash/librustzcash" repository = "https://github.com/zcash/librustzcash" From e06b628f19c57eded78554565f32f667ae787e5a Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Mon, 12 Apr 2021 09:13:04 -0600 Subject: [PATCH 10/19] Use constant-time equality for EphemeralKeyBytes. Fixes #370 Extracted from: https://github.com/zcash/librustzcash/commit/e654cc4ce67472106c59e416d384b2081597becb --- Cargo.toml | 1 + src/lib.rs | 11 ++++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2785e27..bed67da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ crypto_api_chachapoly = "0.4" ff = "0.8" group = "0.8" rand_core = "0.5.1" +subtle = "2.2.3" [dev-dependencies] zcash_primitives = { version = "0.5", path = "../../zcash_primitives" } diff --git a/src/lib.rs b/src/lib.rs index be93903..5d0456f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,7 @@ use crypto_api_chachapoly::{ChaCha20Ietf, ChachaPolyIetf}; use rand_core::RngCore; +use subtle::{ConstantTimeEq, Choice}; pub const COMPACT_NOTE_SIZE: usize = 1 + // version 11 + // diversifier @@ -31,8 +32,6 @@ impl AsRef<[u8]> for OutgoingCipherKey { } } -//FIXME: use constant-time checks for equality -#[derive(Eq, PartialEq)] pub struct EphemeralKeyBytes(pub [u8; 32]); impl From<[u8; 32]> for EphemeralKeyBytes { @@ -41,6 +40,12 @@ impl From<[u8; 32]> for EphemeralKeyBytes { } } +impl ConstantTimeEq for EphemeralKeyBytes { + fn ct_eq(&self, other: &Self) -> Choice { + self.0.ct_eq(&other.0) + } +} + pub struct NotePlaintextBytes(pub [u8; NOTE_PLAINTEXT_SIZE]); pub struct OutPlaintextBytes(pub [u8; OUT_PLAINTEXT_SIZE]); @@ -368,7 +373,7 @@ fn check_note_validity( } else { let epk_bytes = D::epk_bytes(epk); D::check_epk_bytes(¬e, |derived_esk| { - if D::epk_bytes(&D::ka_derive_public(¬e, &derived_esk)) == epk_bytes { + if D::epk_bytes(&D::ka_derive_public(¬e, &derived_esk)).ct_eq(&epk_bytes).into() { NoteValidity::Valid } else { NoteValidity::Invalid From 4903214bfe3c1e8ab5c1167880641f72711aefbd Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Mon, 12 Apr 2021 09:18:51 -0600 Subject: [PATCH 11/19] Apply suggestions from code review Co-authored-by: Daira Hopwood Extracted from: https://github.com/zcash/librustzcash/commit/4f22f1d578668187ce14716bb01021199cd5f01f --- src/lib.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 5d0456f..887c1f1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,8 +14,9 @@ pub const COMPACT_NOTE_SIZE: usize = 1 + // version pub const NOTE_PLAINTEXT_SIZE: usize = COMPACT_NOTE_SIZE + 512; pub const OUT_PLAINTEXT_SIZE: usize = 32 + // pk_d 32; // esk -pub const ENC_CIPHERTEXT_SIZE: usize = NOTE_PLAINTEXT_SIZE + 16; -pub const OUT_CIPHERTEXT_SIZE: usize = OUT_PLAINTEXT_SIZE + 16; +pub const AEAD_TAG_SIZE: usize = 16; +pub const ENC_CIPHERTEXT_SIZE: usize = NOTE_PLAINTEXT_SIZE + AEAD_TAG_SIZE; +pub const OUT_CIPHERTEXT_SIZE: usize = OUT_PLAINTEXT_SIZE + AEAD_TAG_SIZE; /// A symmetric key that can be used to recover a single Sapling or Orchard output. pub struct OutgoingCipherKey(pub [u8; 32]); @@ -159,7 +160,8 @@ pub trait ShieldedOutput<'a, D: Domain> { /// enforces that fresh ephemeral keys are used for every note, and that the ciphertexts are /// consistent with each other. /// -/// Implements section 4.17.1 of the Zcash Protocol Specification. +/// Implements section 4.19 of the Zcash Protocol Specification. +/// /// NB: the example code is only covering the pre-Canopy case. /// /// # Examples @@ -248,7 +250,7 @@ impl NoteEncryption { &self.esk } - /// Exposes the ephemeral public key being used to encrypt this note. + /// Exposes the encoding of the ephemeral public key being used to encrypt this note. pub fn epk(&self) -> &D::EphemeralPublicKey { &self.epk } From 3843f4ba2b4bf64e84b03fac667d1e290529ff32 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Mon, 5 Apr 2021 11:51:07 -0600 Subject: [PATCH 12/19] Use ShieldedOutput trait for note encryption/decryption. This change modifies note encryption and decryption functions to treat a shielded output as a single value instead of handling the parts of an output as independent arguments. Extracted from: https://github.com/zcash/librustzcash/commit/324fc36521ef474fde25faa2cb976104533c1bd2 --- src/lib.rs | 71 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 887c1f1..36ccc76 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,7 +5,7 @@ use crypto_api_chachapoly::{ChaCha20Ietf, ChachaPolyIetf}; use rand_core::RngCore; -use subtle::{ConstantTimeEq, Choice}; +use subtle::{Choice, ConstantTimeEq}; pub const COMPACT_NOTE_SIZE: usize = 1 + // version 11 + // diversifier @@ -148,10 +148,10 @@ pub trait Domain { fn extract_esk(out_plaintext: &[u8; OUT_CIPHERTEXT_SIZE]) -> Option; } -pub trait ShieldedOutput<'a, D: Domain> { - fn ivk(&'a self) -> &'a D::IncomingViewingKey; - fn epk(&'a self) -> &'a D::EphemeralPublicKey; - fn cmstar(&'a self) -> &'a D::ExtractedCommitment; +pub trait ShieldedOutput { + fn epk(&self) -> &D::EphemeralPublicKey; + fn cmstar(&self) -> D::ExtractedCommitment; + fn enc_ciphertext(&self) -> &[u8]; } /// A struct containing context required for encrypting Sapling and Orchard notes. @@ -315,25 +315,22 @@ impl NoteEncryption { /// `PaymentAddress` to which the note was sent. /// /// Implements section 4.17.2 of the Zcash Protocol Specification. -pub fn try_note_decryption( +pub fn try_note_decryption>( domain: &D, - //output: &ShieldedOutput, ivk: &D::IncomingViewingKey, - epk: &D::EphemeralPublicKey, - cmstar: &D::ExtractedCommitment, - enc_ciphertext: &[u8], + output: &Output, ) -> Option<(D::Note, D::Recipient, D::Memo)> { - assert_eq!(enc_ciphertext.len(), ENC_CIPHERTEXT_SIZE); + assert_eq!(output.enc_ciphertext().len(), ENC_CIPHERTEXT_SIZE); - let shared_secret = D::ka_agree_dec(ivk, epk); - let key = D::kdf(shared_secret, epk); + let shared_secret = D::ka_agree_dec(ivk, output.epk()); + let key = D::kdf(shared_secret, output.epk()); let mut plaintext = [0; ENC_CIPHERTEXT_SIZE]; assert_eq!( ChachaPolyIetf::aead_cipher() .open_to( &mut plaintext, - &enc_ciphertext, + output.enc_ciphertext(), &[], key.as_ref(), &[0u8; 12] @@ -342,7 +339,13 @@ pub fn try_note_decryption( NOTE_PLAINTEXT_SIZE ); - let (note, to) = parse_note_plaintext_without_memo_ivk(domain, ivk, epk, cmstar, &plaintext)?; + let (note, to) = parse_note_plaintext_without_memo_ivk( + domain, + ivk, + output.epk(), + &output.cmstar(), + &plaintext, + )?; let memo = domain.extract_memo(&plaintext); Some((note, to, memo)) @@ -375,7 +378,10 @@ fn check_note_validity( } else { let epk_bytes = D::epk_bytes(epk); D::check_epk_bytes(¬e, |derived_esk| { - if D::epk_bytes(&D::ka_derive_public(¬e, &derived_esk)).ct_eq(&epk_bytes).into() { + if D::epk_bytes(&D::ka_derive_public(¬e, &derived_esk)) + .ct_eq(&epk_bytes) + .into() + { NoteValidity::Valid } else { NoteValidity::Invalid @@ -393,24 +399,22 @@ fn check_note_validity( /// Implements the procedure specified in [`ZIP 307`]. /// /// [`ZIP 307`]: https://zips.z.cash/zip-0307 -pub fn try_compact_note_decryption( +pub fn try_compact_note_decryption>( domain: &D, ivk: &D::IncomingViewingKey, - epk: &D::EphemeralPublicKey, - cmstar: &D::ExtractedCommitment, - enc_ciphertext: &[u8], + output: &Output, ) -> Option<(D::Note, D::Recipient)> { - assert_eq!(enc_ciphertext.len(), COMPACT_NOTE_SIZE); + assert_eq!(output.enc_ciphertext().len(), COMPACT_NOTE_SIZE); - let shared_secret = D::ka_agree_dec(&ivk, epk); - let key = D::kdf(shared_secret, &epk); + let shared_secret = D::ka_agree_dec(&ivk, output.epk()); + let key = D::kdf(shared_secret, output.epk()); // Start from block 1 to skip over Poly1305 keying output let mut plaintext = [0; COMPACT_NOTE_SIZE]; - plaintext.copy_from_slice(&enc_ciphertext); + plaintext.copy_from_slice(output.enc_ciphertext()); ChaCha20Ietf::xor(key.as_ref(), &[0u8; 12], 1, &mut plaintext); - parse_note_plaintext_without_memo_ivk(domain, ivk, epk, cmstar, &plaintext) + parse_note_plaintext_without_memo_ivk(domain, ivk, output.epk(), &output.cmstar(), &plaintext) } /// Recovery of the full note plaintext by the sender. @@ -421,15 +425,13 @@ pub fn try_compact_note_decryption( /// /// Implements part of section 4.17.3 of the Zcash Protocol Specification. /// For decryption using a Full Viewing Key see [`try_sapling_output_recovery`]. -pub fn try_output_recovery_with_ock( +pub fn try_output_recovery_with_ock>( domain: &D, ock: &OutgoingCipherKey, - cmstar: &D::ExtractedCommitment, - epk: &D::EphemeralPublicKey, - enc_ciphertext: &[u8], + output: &Output, out_ciphertext: &[u8], ) -> Option<(D::Note, D::Recipient, D::Memo)> { - assert_eq!(enc_ciphertext.len(), ENC_CIPHERTEXT_SIZE); + assert_eq!(output.enc_ciphertext().len(), ENC_CIPHERTEXT_SIZE); assert_eq!(out_ciphertext.len(), OUT_CIPHERTEXT_SIZE); let mut op = [0; OUT_CIPHERTEXT_SIZE]; @@ -444,14 +446,14 @@ pub fn try_output_recovery_with_ock( let esk = D::extract_esk(&op)?; let shared_secret = D::ka_agree_enc(&esk, &pk_d); - let key = D::kdf(shared_secret, &epk); + let key = D::kdf(shared_secret, output.epk()); let mut plaintext = [0; ENC_CIPHERTEXT_SIZE]; assert_eq!( ChachaPolyIetf::aead_cipher() .open_to( &mut plaintext, - &enc_ciphertext, + output.enc_ciphertext(), &[], key.as_ref(), &[0u8; 12] @@ -460,10 +462,11 @@ pub fn try_output_recovery_with_ock( NOTE_PLAINTEXT_SIZE ); - let (note, to) = domain.parse_note_plaintext_without_memo_ovk(&pk_d, &esk, &epk, &plaintext)?; + let (note, to) = + domain.parse_note_plaintext_without_memo_ovk(&pk_d, &esk, output.epk(), &plaintext)?; let memo = domain.extract_memo(&plaintext); - if let NoteValidity::Valid = check_note_validity::(¬e, epk, cmstar) { + if let NoteValidity::Valid = check_note_validity::(¬e, output.epk(), &output.cmstar()) { Some((note, to, memo)) } else { None From 2884d70e29346eb666fb544338f2ec360f6888ac Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Mon, 12 Apr 2021 16:19:50 -0600 Subject: [PATCH 13/19] Use ephemeral_key bytes instead of the epk abstract point where specified. Extracted from: https://github.com/zcash/librustzcash/commit/6fc1d1d1c0be68ec69f10392c1d1a0004bf17951 --- src/lib.rs | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 36ccc76..5063a6c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,6 +35,12 @@ impl AsRef<[u8]> for OutgoingCipherKey { pub struct EphemeralKeyBytes(pub [u8; 32]); +impl AsRef<[u8]> for EphemeralKeyBytes { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + impl From<[u8; 32]> for EphemeralKeyBytes { fn from(value: [u8; 32]) -> EphemeralKeyBytes { EphemeralKeyBytes(value) @@ -90,7 +96,7 @@ pub trait Domain { epk: &Self::EphemeralPublicKey, ) -> Self::SharedSecret; - fn kdf(secret: Self::SharedSecret, epk: &Self::EphemeralPublicKey) -> Self::SymmetricKey; + fn kdf(secret: Self::SharedSecret, ephemeral_key: &EphemeralKeyBytes) -> Self::SymmetricKey; // for right now, we just need `recipient` to get `d`; in the future when we // can get that from a Sapling note, the recipient parameter will be able @@ -101,11 +107,11 @@ pub trait Domain { memo: &Self::Memo, ) -> NotePlaintextBytes; - fn get_ock( + fn derive_ock( ovk: &Self::OutgoingViewingKey, cv: &Self::ValueCommitment, cm: &Self::NoteCommitment, - epk: &Self::EphemeralPublicKey, + ephemeral_key: &EphemeralKeyBytes, ) -> OutgoingCipherKey; fn outgoing_plaintext_bytes( @@ -227,7 +233,9 @@ impl NoteEncryption { Self::new_with_esk(esk, ovk, note, to, memo) } - /// For use only with Sapling. + /// For use only with Sapling. This method is preserved in order that test code + /// be able to generate pre-ZIP-212 ciphertexts so that tests can continue to + /// cover pre-ZIP-212 transaction decryption. pub fn new_with_esk( esk: D::EphemeralSecretKey, ovk: Option, @@ -259,7 +267,7 @@ impl NoteEncryption { pub fn encrypt_note_plaintext(&self) -> [u8; ENC_CIPHERTEXT_SIZE] { let pk_d = D::get_pk_d(&self.note); let shared_secret = D::ka_agree_enc(&self.esk, &pk_d); - let key = D::kdf(shared_secret, &self.epk); + let key = D::kdf(shared_secret, &D::epk_bytes(&self.epk)); let input = D::note_plaintext_bytes(&self.note, &self.to, &self.memo); let mut output = [0u8; ENC_CIPHERTEXT_SIZE]; @@ -281,7 +289,7 @@ impl NoteEncryption { rng: &mut R, ) -> [u8; OUT_CIPHERTEXT_SIZE] { let (ock, input) = if let Some(ovk) = &self.ovk { - let ock = D::get_ock(ovk, &cv, &cm, &self.epk); + let ock = D::derive_ock(ovk, &cv, &cm, &D::epk_bytes(&self.epk)); let input = D::outgoing_plaintext_bytes(&self.note, &self.esk); (ock, input) @@ -323,7 +331,7 @@ pub fn try_note_decryption>( assert_eq!(output.enc_ciphertext().len(), ENC_CIPHERTEXT_SIZE); let shared_secret = D::ka_agree_dec(ivk, output.epk()); - let key = D::kdf(shared_secret, output.epk()); + let key = D::kdf(shared_secret, &D::epk_bytes(output.epk())); let mut plaintext = [0; ENC_CIPHERTEXT_SIZE]; assert_eq!( @@ -407,7 +415,7 @@ pub fn try_compact_note_decryption>( assert_eq!(output.enc_ciphertext().len(), COMPACT_NOTE_SIZE); let shared_secret = D::ka_agree_dec(&ivk, output.epk()); - let key = D::kdf(shared_secret, output.epk()); + let key = D::kdf(shared_secret, &D::epk_bytes(output.epk())); // Start from block 1 to skip over Poly1305 keying output let mut plaintext = [0; COMPACT_NOTE_SIZE]; @@ -446,7 +454,7 @@ pub fn try_output_recovery_with_ock>( let esk = D::extract_esk(&op)?; let shared_secret = D::ka_agree_enc(&esk, &pk_d); - let key = D::kdf(shared_secret, output.epk()); + let key = D::kdf(shared_secret, &D::epk_bytes(output.epk())); let mut plaintext = [0; ENC_CIPHERTEXT_SIZE]; assert_eq!( From c3d0a64faca9685a67b20cf06592b8d1a7c83258 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Mon, 12 Apr 2021 16:42:04 -0600 Subject: [PATCH 14/19] Update documentation for note encryption traits. Extracted from: https://github.com/zcash/librustzcash/commit/f34e87884a73b676ebad2d4697e26dcf21c272d5 --- src/lib.rs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 5063a6c..b4df91e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -166,8 +166,8 @@ pub trait ShieldedOutput { /// enforces that fresh ephemeral keys are used for every note, and that the ciphertexts are /// consistent with each other. /// -/// Implements section 4.19 of the Zcash Protocol Specification. -/// +/// Implements section 4.19 of the +/// [Zcash Protocol Specification](https://zips.z.cash/protocol/nu5.pdf#saplingandorchardinband) /// NB: the example code is only covering the pre-Canopy case. /// /// # Examples @@ -180,11 +180,12 @@ pub trait ShieldedOutput { /// use ff::Field; /// use rand_core::OsRng; /// use zcash_primitives::{ -/// consensus::TestNetwork, +/// consensus::{TEST_NETWORK, TestNetwork, NetworkUpgrade, Parameters}, /// memo::MemoBytes, /// sapling::{ /// keys::{OutgoingViewingKey, prf_expand}, -/// note_encryption::{sapling_note_encryption}, +/// note_encryption::sapling_note_encryption, +/// util::generate_random_rseed, /// Diversifier, PaymentAddress, Rseed, ValueCommitment /// }, /// }; @@ -202,8 +203,9 @@ pub trait ShieldedOutput { /// value, /// randomness: rcv.clone(), /// }; -/// let rcm = jubjub::Fr::random(&mut rng); -/// let note = to.create_note(value, Rseed::BeforeZip212(rcm)).unwrap(); +/// let height = TEST_NETWORK.activation_height(NetworkUpgrade::Canopy).unwrap(); +/// let rseed = generate_random_rseed(&TEST_NETWORK, height, &mut rng); +/// let note = to.create_note(value, rseed).unwrap(); /// let cmu = note.cmu(); /// /// let mut enc = sapling_note_encryption::<_, TestNetwork>(ovk, note, to, MemoBytes::empty(), &mut rng); @@ -322,7 +324,8 @@ impl NoteEncryption { /// If successful, the corresponding Sapling note and memo are returned, along with the /// `PaymentAddress` to which the note was sent. /// -/// Implements section 4.17.2 of the Zcash Protocol Specification. +/// Implements section 4.19.2 of the +/// [Zcash Protocol Specification](https://zips.z.cash/protocol/nu5.pdf#decryptivk) pub fn try_note_decryption>( domain: &D, ivk: &D::IncomingViewingKey, @@ -431,7 +434,8 @@ pub fn try_compact_note_decryption>( /// If successful, the corresponding Sapling note and memo are returned, along with the /// `PaymentAddress` to which the note was sent. /// -/// Implements part of section 4.17.3 of the Zcash Protocol Specification. +/// Implements part of section 4.19.3 of the +/// [Zcash Protocol Specification](https://zips.z.cash/protocol/nu5.pdf#decryptovk) /// For decryption using a Full Viewing Key see [`try_sapling_output_recovery`]. pub fn try_output_recovery_with_ock>( domain: &D, From a14db84fea018c3750d1701f47a1884509f422bf Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Mon, 12 Apr 2021 18:43:21 -0600 Subject: [PATCH 15/19] Minor comment on epk canonicity. Extracted from: https://github.com/zcash/librustzcash/commit/389e6ca6a38c4b7418d49bd13639a9511c981885 --- src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index b4df91e..d4ab8df 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -458,6 +458,9 @@ pub fn try_output_recovery_with_ock>( let esk = D::extract_esk(&op)?; let shared_secret = D::ka_agree_enc(&esk, &pk_d); + // The small-order point check at the point of output parsing rejects + // non-canonical encodings, so reencoding here for the KDF should + // be okay. let key = D::kdf(shared_secret, &D::epk_bytes(output.epk())); let mut plaintext = [0; ENC_CIPHERTEXT_SIZE]; From 5358e678b218ff71bd6ed3a5a7677baaccf44ee8 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Wed, 14 Apr 2021 09:24:42 -0600 Subject: [PATCH 16/19] Make cmstar check follow the spec more closely. Extracted from: https://github.com/zcash/librustzcash/commit/00d04de54711be2be2fce237c9a2f42a0e0101a9 --- src/lib.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d4ab8df..7302a51 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,7 @@ use crypto_api_chachapoly::{ChaCha20Ietf, ChachaPolyIetf}; use rand_core::RngCore; +use std::convert::TryFrom; use subtle::{Choice, ConstantTimeEq}; pub const COMPACT_NOTE_SIZE: usize = 1 + // version @@ -74,7 +75,7 @@ pub trait Domain { type OutgoingViewingKey; type ValueCommitment; type NoteCommitment; - type ExtractedCommitment: Eq; + type ExtractedCommitment: Eq + TryFrom; type Memo; fn derive_esk(note: &Self::Note) -> Option; @@ -126,7 +127,7 @@ pub trait Domain { check: F, ) -> NoteValidity; - fn extract_note_commitment(note: &Self::Note) -> Self::ExtractedCommitment; + fn note_commitment(note: &Self::Note) -> Self::NoteCommitment; fn parse_note_plaintext_without_memo_ivk( &self, @@ -383,10 +384,9 @@ fn check_note_validity( epk: &D::EphemeralPublicKey, cmstar: &D::ExtractedCommitment, ) -> NoteValidity { - if &D::extract_note_commitment(¬e) != cmstar { - // Published commitment doesn't match calculated commitment - NoteValidity::Invalid - } else { + if D::ExtractedCommitment::try_from(D::note_commitment(¬e)) + .map_or(false, |cs| &cs == cmstar) + { let epk_bytes = D::epk_bytes(epk); D::check_epk_bytes(¬e, |derived_esk| { if D::epk_bytes(&D::ka_derive_public(¬e, &derived_esk)) @@ -398,6 +398,9 @@ fn check_note_validity( NoteValidity::Invalid } }) + } else { + // Published commitment doesn't match calculated commitment + NoteValidity::Invalid } } From 9e499c08ca56c49b5be434693ab658ff75c9be0c Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Thu, 15 Apr 2021 15:15:54 -0600 Subject: [PATCH 17/19] Fix naming cmstar -> cmstar_bytes and cm -> cmstar Extracted from: https://github.com/zcash/librustzcash/commit/b2b3efd4c2e15a1debf75145cfe9f61937121627 --- src/lib.rs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 7302a51..ddb8447 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -74,8 +74,8 @@ pub trait Domain { type IncomingViewingKey; type OutgoingViewingKey; type ValueCommitment; - type NoteCommitment; - type ExtractedCommitment: Eq + TryFrom; + type ExtractedCommitment; + type ExtractedCommitmentBytes: Eq + TryFrom; type Memo; fn derive_esk(note: &Self::Note) -> Option; @@ -111,7 +111,7 @@ pub trait Domain { fn derive_ock( ovk: &Self::OutgoingViewingKey, cv: &Self::ValueCommitment, - cm: &Self::NoteCommitment, + cmstar: &Self::ExtractedCommitment, ephemeral_key: &EphemeralKeyBytes, ) -> OutgoingCipherKey; @@ -127,7 +127,7 @@ pub trait Domain { check: F, ) -> NoteValidity; - fn note_commitment(note: &Self::Note) -> Self::NoteCommitment; + fn cmstar(note: &Self::Note) -> Self::ExtractedCommitment; fn parse_note_plaintext_without_memo_ivk( &self, @@ -157,7 +157,7 @@ pub trait Domain { pub trait ShieldedOutput { fn epk(&self) -> &D::EphemeralPublicKey; - fn cmstar(&self) -> D::ExtractedCommitment; + fn cmstar_bytes(&self) -> D::ExtractedCommitmentBytes; fn enc_ciphertext(&self) -> &[u8]; } @@ -288,11 +288,11 @@ impl NoteEncryption { pub fn encrypt_outgoing_plaintext( &self, cv: &D::ValueCommitment, - cm: &D::NoteCommitment, + cmstar: &D::ExtractedCommitment, rng: &mut R, ) -> [u8; OUT_CIPHERTEXT_SIZE] { let (ock, input) = if let Some(ovk) = &self.ovk { - let ock = D::derive_ock(ovk, &cv, &cm, &D::epk_bytes(&self.epk)); + let ock = D::derive_ock(ovk, &cv, &cmstar, &D::epk_bytes(&self.epk)); let input = D::outgoing_plaintext_bytes(&self.note, &self.esk); (ock, input) @@ -355,7 +355,7 @@ pub fn try_note_decryption>( domain, ivk, output.epk(), - &output.cmstar(), + &output.cmstar_bytes(), &plaintext, )?; let memo = domain.extract_memo(&plaintext); @@ -367,12 +367,12 @@ fn parse_note_plaintext_without_memo_ivk( domain: &D, ivk: &D::IncomingViewingKey, epk: &D::EphemeralPublicKey, - cmstar: &D::ExtractedCommitment, + cmstar_bytes: &D::ExtractedCommitmentBytes, plaintext: &[u8], ) -> Option<(D::Note, D::Recipient)> { let (note, to) = domain.parse_note_plaintext_without_memo_ivk(ivk, &plaintext)?; - if let NoteValidity::Valid = check_note_validity::(¬e, epk, cmstar) { + if let NoteValidity::Valid = check_note_validity::(¬e, epk, cmstar_bytes) { Some((note, to)) } else { None @@ -382,10 +382,10 @@ fn parse_note_plaintext_without_memo_ivk( fn check_note_validity( note: &D::Note, epk: &D::EphemeralPublicKey, - cmstar: &D::ExtractedCommitment, + cmstar_bytes: &D::ExtractedCommitmentBytes, ) -> NoteValidity { - if D::ExtractedCommitment::try_from(D::note_commitment(¬e)) - .map_or(false, |cs| &cs == cmstar) + if D::ExtractedCommitmentBytes::try_from(D::cmstar(¬e)) + .map_or(false, |cs| &cs == cmstar_bytes) { let epk_bytes = D::epk_bytes(epk); D::check_epk_bytes(¬e, |derived_esk| { @@ -428,7 +428,7 @@ pub fn try_compact_note_decryption>( plaintext.copy_from_slice(output.enc_ciphertext()); ChaCha20Ietf::xor(key.as_ref(), &[0u8; 12], 1, &mut plaintext); - parse_note_plaintext_without_memo_ivk(domain, ivk, output.epk(), &output.cmstar(), &plaintext) + parse_note_plaintext_without_memo_ivk(domain, ivk, output.epk(), &output.cmstar_bytes(), &plaintext) } /// Recovery of the full note plaintext by the sender. @@ -484,7 +484,7 @@ pub fn try_output_recovery_with_ock>( domain.parse_note_plaintext_without_memo_ovk(&pk_d, &esk, output.epk(), &plaintext)?; let memo = domain.extract_memo(&plaintext); - if let NoteValidity::Valid = check_note_validity::(¬e, output.epk(), &output.cmstar()) { + if let NoteValidity::Valid = check_note_validity::(¬e, output.epk(), &output.cmstar_bytes()) { Some((note, to, memo)) } else { None From 0a4fb8ff340afe47785f55f933669a4be50a018c Mon Sep 17 00:00:00 2001 From: Daira Hopwood Date: Fri, 16 Apr 2021 00:10:05 +0100 Subject: [PATCH 18/19] Update comment about which case is covered by example code Extracted from: https://github.com/zcash/librustzcash/commit/dc0f6e71153b93467fa0a34b3b9b059243d47a72 --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index ddb8447..27796b4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -169,7 +169,7 @@ pub trait ShieldedOutput { /// /// Implements section 4.19 of the /// [Zcash Protocol Specification](https://zips.z.cash/protocol/nu5.pdf#saplingandorchardinband) -/// NB: the example code is only covering the pre-Canopy case. +/// NB: the example code is only covering the post-Canopy case. /// /// # Examples /// From dc22102d41e40f0a4b4a1828b8895b0e06898f38 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Fri, 16 Apr 2021 14:03:55 +1200 Subject: [PATCH 19/19] cargo fmt Extracted from: https://github.com/zcash/librustzcash/commit/28a45028ab1e9490e7f1c052ac4ae75e9bfe29e7 --- src/lib.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 27796b4..481ee1e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -428,7 +428,13 @@ pub fn try_compact_note_decryption>( plaintext.copy_from_slice(output.enc_ciphertext()); ChaCha20Ietf::xor(key.as_ref(), &[0u8; 12], 1, &mut plaintext); - parse_note_plaintext_without_memo_ivk(domain, ivk, output.epk(), &output.cmstar_bytes(), &plaintext) + parse_note_plaintext_without_memo_ivk( + domain, + ivk, + output.epk(), + &output.cmstar_bytes(), + &plaintext, + ) } /// Recovery of the full note plaintext by the sender. @@ -484,7 +490,9 @@ pub fn try_output_recovery_with_ock>( domain.parse_note_plaintext_without_memo_ovk(&pk_d, &esk, output.epk(), &plaintext)?; let memo = domain.extract_memo(&plaintext); - if let NoteValidity::Valid = check_note_validity::(¬e, output.epk(), &output.cmstar_bytes()) { + if let NoteValidity::Valid = + check_note_validity::(¬e, output.epk(), &output.cmstar_bytes()) + { Some((note, to, memo)) } else { None