diff --git a/zcash_client_backend/src/welding_rig.rs b/zcash_client_backend/src/welding_rig.rs index 340775762..0772a45fd 100644 --- a/zcash_client_backend/src/welding_rig.rs +++ b/zcash_client_backend/src/welding_rig.rs @@ -257,7 +257,7 @@ mod tests { rseed, }; let encryptor = SaplingNoteEncryption::new( - extfvk.fvk.ovk, + Some(extfvk.fvk.ovk), note.clone(), to.clone(), Memo::default(), diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index 87327ee6a..9ecaafeec 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -133,7 +133,7 @@ mod tests { rseed, }; let encryptor = SaplingNoteEncryption::new( - extfvk.fvk.ovk, + Some(extfvk.fvk.ovk), note.clone(), to.clone(), Memo::default(), @@ -193,7 +193,7 @@ mod tests { rseed, }; let encryptor = SaplingNoteEncryption::new( - extfvk.fvk.ovk, + Some(extfvk.fvk.ovk), note.clone(), to, Memo::default(), @@ -221,7 +221,7 @@ mod tests { rseed, }; let encryptor = SaplingNoteEncryption::new( - extfvk.fvk.ovk, + Some(extfvk.fvk.ovk), note.clone(), change_addr, Memo::default(), diff --git a/zcash_client_sqlite/src/transact.rs b/zcash_client_sqlite/src/transact.rs index 6aa7d7be9..9feab59e8 100644 --- a/zcash_client_sqlite/src/transact.rs +++ b/zcash_client_sqlite/src/transact.rs @@ -1,7 +1,7 @@ //! Functions for creating transactions. use ff::PrimeField; -use rand_core::{OsRng, RngCore}; +use rand_core::OsRng; use rusqlite::{types::ToSql, Connection, NO_PARAMS}; use std::convert::TryInto; use std::path::Path; @@ -148,16 +148,9 @@ pub fn create_to_address>( // Apply the outgoing viewing key policy. let ovk = match ovk_policy { - OvkPolicy::Sender => extfvk.fvk.ovk, - OvkPolicy::Custom(ovk) => ovk, - OvkPolicy::Discard => { - // Generate a random outgoing viewing key that the caller does not know. - // The probability of this colliding with a legitimate outgoing viewing - // key is negligible. - let mut ovk = [0; 32]; - OsRng.fill_bytes(&mut ovk); - OutgoingViewingKey(ovk) - } + OvkPolicy::Sender => Some(extfvk.fvk.ovk), + OvkPolicy::Custom(ovk) => Some(ovk), + OvkPolicy::Discard => None, }; // Target the next block, assuming we are up-to-date. diff --git a/zcash_primitives/src/note_encryption.rs b/zcash_primitives/src/note_encryption.rs index 292724d1f..bb25defdf 100644 --- a/zcash_primitives/src/note_encryption.rs +++ b/zcash_primitives/src/note_encryption.rs @@ -152,6 +152,21 @@ fn kdf_sapling(dhsecret: jubjub::SubgroupPoint, epk: &jubjub::SubgroupPoint) -> .finalize() } +/// A symmetric key that can be used to recover a single Sapling output. +pub struct OutgoingCipherKey([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 + } +} + /// Sapling PRF^ock. /// /// Implemented per section 5.4.2 of the Zcash Protocol Specification. @@ -160,16 +175,21 @@ pub fn prf_ock( cv: &jubjub::ExtendedPoint, cmu: &bls12_381::Scalar, epk: &jubjub::SubgroupPoint, -) -> Blake2bHash { - Blake2bParams::new() - .hash_length(32) - .personal(PRF_OCK_PERSONALIZATION) - .to_state() - .update(&ovk.0) - .update(&cv.to_bytes()) - .update(&cmu.to_repr()) - .update(&epk.to_bytes()) - .finalize() +) -> OutgoingCipherKey { + OutgoingCipherKey( + Blake2bParams::new() + .hash_length(32) + .personal(PRF_OCK_PERSONALIZATION) + .to_state() + .update(&ovk.0) + .update(&cv.to_bytes()) + .update(&cmu.to_repr()) + .update(&epk.to_bytes()) + .finalize() + .as_bytes() + .try_into() + .unwrap(), + ) } /// An API for encrypting Sapling notes. @@ -201,7 +221,7 @@ pub fn prf_ock( /// let diversifier = Diversifier([0; 11]); /// let pk_d = diversifier.g_d().unwrap(); /// let to = PaymentAddress::from_parts(diversifier, pk_d).unwrap(); -/// let ovk = OutgoingViewingKey([0; 32]); +/// let ovk = Some(OutgoingViewingKey([0; 32])); /// /// let value = 1000; /// let rcv = jubjub::Fr::random(&mut rng); @@ -213,29 +233,34 @@ pub fn prf_ock( /// let note = to.create_note(value, Rseed::BeforeZip212(rcm)).unwrap(); /// let cmu = note.cmu(); /// -/// let enc = SaplingNoteEncryption::new(ovk, note, to, Memo::default(), &mut rng); +/// let mut enc = SaplingNoteEncryption::new(ovk, note, to, Memo::default(), &mut rng); /// let encCiphertext = enc.encrypt_note_plaintext(); /// let outCiphertext = enc.encrypt_outgoing_plaintext(&cv.commitment().into(), &cmu); /// ``` -pub struct SaplingNoteEncryption { +pub struct SaplingNoteEncryption { epk: jubjub::SubgroupPoint, esk: jubjub::Fr, note: Note, to: PaymentAddress, memo: Memo, - ovk: OutgoingViewingKey, + /// `None` represents the `ovk = ⊥` case. + ovk: Option, + rng: R, } -impl SaplingNoteEncryption { +impl SaplingNoteEncryption { /// Creates a new encryption context for the given note. - pub fn new( - ovk: OutgoingViewingKey, + /// + /// Setting `ovk` to `None` represents the `ovk = ⊥` case, where the note cannot be + /// recovered by the sender. + pub fn new( + ovk: Option, note: Note, to: PaymentAddress, memo: Memo, - rng: &mut R, - ) -> SaplingNoteEncryption { - let esk = note.generate_or_derive_esk(rng); + mut rng: R, + ) -> Self { + let esk = note.generate_or_derive_esk(&mut rng); let epk = note.g_d * esk; SaplingNoteEncryption { @@ -245,6 +270,7 @@ impl SaplingNoteEncryption { to, memo, ovk, + rng, } } @@ -297,20 +323,33 @@ impl SaplingNoteEncryption { /// Generates `outCiphertext` for this note. pub fn encrypt_outgoing_plaintext( - &self, + &mut self, cv: &jubjub::ExtendedPoint, cmu: &bls12_381::Scalar, ) -> [u8; OUT_CIPHERTEXT_SIZE] { - let key = prf_ock(&self.ovk, &cv, &cmu, &self.epk); + let (ock, input) = if let Some(ovk) = &self.ovk { + let ock = prf_ock(ovk, &cv, &cmu, &self.epk); - let mut input = [0u8; OUT_PLAINTEXT_SIZE]; - input[0..32].copy_from_slice(&self.note.pk_d.to_bytes()); - input[32..OUT_PLAINTEXT_SIZE].copy_from_slice(self.esk.to_repr().as_ref()); + let mut input = [0u8; OUT_PLAINTEXT_SIZE]; + input[0..32].copy_from_slice(&self.note.pk_d.to_bytes()); + input[32..OUT_PLAINTEXT_SIZE].copy_from_slice(self.esk.to_repr().as_ref()); + + (ock, input) + } else { + // ovk = ⊥ + let mut ock = OutgoingCipherKey([0; 32]); + let mut input = [0u8; OUT_PLAINTEXT_SIZE]; + + self.rng.fill_bytes(&mut ock.0); + self.rng.fill_bytes(&mut input); + + (ock, input) + }; let mut output = [0u8; OUT_CIPHERTEXT_SIZE]; assert_eq!( ChachaPolyIetf::aead_cipher() - .seal_to(&mut output, &input, &[], key.as_bytes(), &[0u8; 12]) + .seal_to(&mut output, &input, &[], ock.as_ref(), &[0u8; 12]) .unwrap(), OUT_CIPHERTEXT_SIZE ); @@ -468,7 +507,7 @@ pub fn try_sapling_compact_note_decryption( /// For decryption using a Full Viewing Key see [`try_sapling_output_recovery`]. pub fn try_sapling_output_recovery_with_ock( height: u32, - ock: &[u8], + ock: &OutgoingCipherKey, cmu: &bls12_381::Scalar, epk: &jubjub::SubgroupPoint, enc_ciphertext: &[u8], @@ -480,7 +519,7 @@ pub fn try_sapling_output_recovery_with_ock( let mut op = [0; OUT_CIPHERTEXT_SIZE]; assert_eq!( ChachaPolyIetf::aead_cipher() - .open_to(&mut op, &out_ciphertext, &[], &ock, &[0u8; 12]) + .open_to(&mut op, &out_ciphertext, &[], ock.as_ref(), &[0u8; 12]) .ok()?, OUT_PLAINTEXT_SIZE ); @@ -583,7 +622,7 @@ pub fn try_sapling_output_recovery( ) -> Option<(Note, PaymentAddress, Memo)> { try_sapling_output_recovery_with_ock::

( height, - prf_ock(&ovk, &cv, &cmu, &epk).as_bytes(), + &prf_ock(&ovk, &cv, &cmu, &epk), cmu, epk, enc_ciphertext, @@ -593,7 +632,6 @@ pub fn try_sapling_output_recovery( #[cfg(test)] mod tests { - use blake2b_simd::Hash as Blake2bHash; use crypto_api_chachapoly::ChachaPolyIetf; use ff::{Field, PrimeField}; use group::Group; @@ -606,8 +644,9 @@ mod tests { use super::{ kdf_sapling, prf_ock, sapling_ka_agree, try_sapling_compact_note_decryption, try_sapling_note_decryption, try_sapling_output_recovery, - try_sapling_output_recovery_with_ock, Memo, SaplingNoteEncryption, COMPACT_NOTE_SIZE, - ENC_CIPHERTEXT_SIZE, NOTE_PLAINTEXT_SIZE, OUT_CIPHERTEXT_SIZE, OUT_PLAINTEXT_SIZE, + try_sapling_output_recovery_with_ock, Memo, OutgoingCipherKey, SaplingNoteEncryption, + COMPACT_NOTE_SIZE, ENC_CIPHERTEXT_SIZE, NOTE_PLAINTEXT_SIZE, OUT_CIPHERTEXT_SIZE, + OUT_PLAINTEXT_SIZE, }; use crate::{ consensus::{ @@ -741,7 +780,7 @@ mod tests { mut rng: &mut R, ) -> ( OutgoingViewingKey, - Blake2bHash, + OutgoingCipherKey, jubjub::Fr, jubjub::ExtendedPoint, bls12_381::Scalar, @@ -782,7 +821,7 @@ mod tests { ); let ock_output_recovery = try_sapling_output_recovery_with_ock::( height, - ock.as_bytes(), + &ock, &cmu, &epk, &enc_ciphertext, @@ -801,7 +840,7 @@ mod tests { mut rng: &mut R, ) -> ( OutgoingViewingKey, - Blake2bHash, + OutgoingCipherKey, jubjub::Fr, jubjub::ExtendedPoint, bls12_381::Scalar, @@ -827,22 +866,13 @@ mod tests { let cmu = note.cmu(); let ovk = OutgoingViewingKey([0; 32]); - let ne = SaplingNoteEncryption::new(ovk, note, pa, Memo([0; 512]), &mut rng); - let epk = ne.epk(); + let mut ne = SaplingNoteEncryption::new(Some(ovk), note, pa, Memo([0; 512]), &mut rng); + let epk = ne.epk().clone(); let enc_ciphertext = ne.encrypt_note_plaintext(); let out_ciphertext = ne.encrypt_outgoing_plaintext(&cv, &cmu); let ock = prf_ock(&ovk, &cv, &cmu, &epk); - ( - ovk, - ock, - ivk, - cv, - cmu, - epk.clone(), - enc_ciphertext, - out_ciphertext, - ) + (ovk, ock, ivk, cv, cmu, epk, enc_ciphertext, out_ciphertext) } fn reencrypt_enc_ciphertext( @@ -859,7 +889,7 @@ mod tests { let mut op = [0; OUT_CIPHERTEXT_SIZE]; assert_eq!( ChachaPolyIetf::aead_cipher() - .open_to(&mut op, out_ciphertext, &[], ock.as_bytes(), &[0u8; 12]) + .open_to(&mut op, out_ciphertext, &[], ock.as_ref(), &[0u8; 12]) .unwrap(), OUT_PLAINTEXT_SIZE ); @@ -1351,7 +1381,7 @@ mod tests { assert_eq!( try_sapling_output_recovery_with_ock::( height, - &[0u8; 32], + &OutgoingCipherKey([0u8; 32]), &cmu, &epk, &enc_ciphertext, @@ -1416,7 +1446,7 @@ mod tests { assert_eq!( try_sapling_output_recovery_with_ock::( height, - &ock.as_bytes(), + &ock, &bls12_381::Scalar::random(&mut rng), &epk, &enc_ctext, @@ -1454,7 +1484,7 @@ mod tests { assert_eq!( try_sapling_output_recovery_with_ock::( height, - &ock.as_bytes(), + &ock, &cmu, &jubjub::SubgroupPoint::random(&mut rng), &enc_ciphertext, @@ -1493,7 +1523,7 @@ mod tests { assert_eq!( try_sapling_output_recovery_with_ock::( height, - &ock.as_bytes(), + &ock, &cmu, &epk, &enc_ciphertext, @@ -1532,7 +1562,7 @@ mod tests { assert_eq!( try_sapling_output_recovery_with_ock::( height, - &ock.as_bytes(), + &ock, &cmu, &epk, &enc_ciphertext, @@ -1582,7 +1612,7 @@ mod tests { assert_eq!( try_sapling_output_recovery_with_ock::( height, - &ock.as_bytes(), + &ock, &cmu, &epk, &enc_ciphertext, @@ -1629,7 +1659,7 @@ mod tests { assert_eq!( try_sapling_output_recovery_with_ock::( height, - &ock.as_bytes(), + &ock, &cmu, &epk, &enc_ciphertext, @@ -1676,7 +1706,7 @@ mod tests { assert_eq!( try_sapling_output_recovery_with_ock::( height, - &ock.as_bytes(), + &ock, &cmu, &epk, &enc_ciphertext, @@ -1715,7 +1745,7 @@ mod tests { assert_eq!( try_sapling_output_recovery_with_ock::( height, - &ock.as_bytes(), + &ock, &cmu, &epk, &enc_ciphertext, @@ -1776,7 +1806,7 @@ mod tests { let ovk = OutgoingViewingKey(tv.ovk); let ock = prf_ock(&ovk, &cv, &cmu, &epk); - assert_eq!(ock.as_bytes(), tv.ock); + assert_eq!(ock.as_ref(), tv.ock); let to = PaymentAddress::from_parts(Diversifier(tv.default_d), pk_d).unwrap(); let note = to.create_note(tv.v, Rseed::BeforeZip212(rcm)).unwrap(); @@ -1825,7 +1855,7 @@ mod tests { // Test encryption // - let mut ne = SaplingNoteEncryption::new(ovk, note, to, Memo(tv.memo), &mut OsRng); + let mut ne = SaplingNoteEncryption::new(Some(ovk), note, to, Memo(tv.memo), OsRng); // Swap in the ephemeral keypair from the test vectors ne.esk = esk; ne.epk = epk; diff --git a/zcash_primitives/src/transaction/builder.rs b/zcash_primitives/src/transaction/builder.rs index 302656edf..616179245 100644 --- a/zcash_primitives/src/transaction/builder.rs +++ b/zcash_primitives/src/transaction/builder.rs @@ -76,7 +76,8 @@ struct SpendDescriptionInfo { } pub struct SaplingOutput { - ovk: OutgoingViewingKey, + /// `None` represents the `ovk = ⊥` case. + ovk: Option, to: PaymentAddress, note: Note, memo: Memo, @@ -86,7 +87,7 @@ impl SaplingOutput { pub fn new( height: u32, rng: &mut R, - ovk: OutgoingViewingKey, + ovk: Option, to: PaymentAddress, value: Amount, memo: Option, @@ -122,7 +123,7 @@ impl SaplingOutput { ctx: &mut P::SaplingProvingContext, rng: &mut R, ) -> OutputDescription { - let encryptor = SaplingNoteEncryption::new( + let mut encryptor = SaplingNoteEncryption::new( self.ovk, self.note.clone(), self.to.clone(), @@ -396,7 +397,7 @@ impl Builder { /// Adds a Sapling address to send funds to. pub fn add_sapling_output( &mut self, - ovk: OutgoingViewingKey, + ovk: Option, to: PaymentAddress, value: Amount, memo: Option, @@ -502,7 +503,7 @@ impl Builder { return Err(Error::NoChangeAddress); }; - self.add_sapling_output(change_address.0, change_address.1, change, None)?; + self.add_sapling_output(Some(change_address.0), change_address.1, change, None)?; } // @@ -728,7 +729,7 @@ mod tests { let mut builder = Builder::::new(0); assert_eq!( - builder.add_sapling_output(ovk, to, Amount::from_i64(-1).unwrap(), None), + builder.add_sapling_output(Some(ovk), to, Amount::from_i64(-1).unwrap(), None), Err(Error::InvalidAmount) ); } @@ -840,7 +841,7 @@ mod tests { } let extfvk = ExtendedFullViewingKey::from(&extsk); - let ovk = extfvk.fvk.ovk; + let ovk = Some(extfvk.fvk.ovk); let to = extfvk.default_address().unwrap().1; // Fail if there is only a Sapling output