diff --git a/components/zcash_note_encryption/src/lib.rs b/components/zcash_note_encryption/src/lib.rs index 3bece34f2..5c4ccbb79 100644 --- a/components/zcash_note_encryption/src/lib.rs +++ b/components/zcash_note_encryption/src/lib.rs @@ -5,7 +5,6 @@ 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 @@ -64,7 +63,7 @@ pub enum NoteValidity { } pub trait Domain { - type EphemeralSecretKey; + type EphemeralSecretKey: ConstantTimeEq; type EphemeralPublicKey; type SharedSecret; type SymmetricKey: AsRef<[u8]>; @@ -75,7 +74,7 @@ pub trait Domain { type OutgoingViewingKey; type ValueCommitment; type ExtractedCommitment; - type ExtractedCommitmentBytes: Eq + TryFrom; + type ExtractedCommitmentBytes: Eq + for<'a> From<&'a Self::ExtractedCommitment>; type Memo; fn derive_esk(note: &Self::Note) -> Option; @@ -111,7 +110,7 @@ pub trait Domain { fn derive_ock( ovk: &Self::OutgoingViewingKey, cv: &Self::ValueCommitment, - cmstar: &Self::ExtractedCommitment, + cmstar_bytes: &Self::ExtractedCommitmentBytes, ephemeral_key: &EphemeralKeyBytes, ) -> OutgoingCipherKey; @@ -149,10 +148,10 @@ pub trait Domain { fn extract_memo(&self, plaintext: &[u8]) -> Self::Memo; fn extract_pk_d( - out_plaintext: &[u8; OUT_CIPHERTEXT_SIZE], + out_plaintext: &[u8; OUT_PLAINTEXT_SIZE], ) -> Option; - fn extract_esk(out_plaintext: &[u8; OUT_CIPHERTEXT_SIZE]) -> Option; + fn extract_esk(out_plaintext: &[u8; OUT_PLAINTEXT_SIZE]) -> Option; } pub trait ShieldedOutput { @@ -292,7 +291,7 @@ impl NoteEncryption { rng: &mut R, ) -> [u8; OUT_CIPHERTEXT_SIZE] { let (ock, input) = if let Some(ovk) = &self.ovk { - let ock = D::derive_ock(ovk, &cv, &cmstar, &D::epk_bytes(&self.epk)); + let ock = D::derive_ock(ovk, &cv, &cmstar.into(), &D::epk_bytes(&self.epk)); let input = D::outgoing_plaintext_bytes(&self.note, &self.esk); (ock, input) @@ -322,11 +321,11 @@ impl NoteEncryption { /// 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. +/// If successful, the corresponding note and memo are returned, along with the address to +/// which the note was sent. /// /// Implements section 4.19.2 of the -/// [Zcash Protocol Specification](https://zips.z.cash/protocol/nu5.pdf#decryptivk) +/// [Zcash Protocol Specification](https://zips.z.cash/protocol/nu5.pdf#decryptivk). pub fn try_note_decryption>( domain: &D, ivk: &D::IncomingViewingKey, @@ -384,9 +383,7 @@ fn check_note_validity( epk: &D::EphemeralPublicKey, cmstar_bytes: &D::ExtractedCommitmentBytes, ) -> NoteValidity { - if D::ExtractedCommitmentBytes::try_from(D::cmstar(¬e)) - .map_or(false, |cs| &cs == cmstar_bytes) - { + if &D::ExtractedCommitmentBytes::from(&D::cmstar(¬e)) == cmstar_bytes { 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)) @@ -407,8 +404,8 @@ fn check_note_validity( /// 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. +/// given `ivk`. If successful, the corresponding note is returned, along with the address +/// to which the note was sent. /// /// Implements the procedure specified in [`ZIP 307`]. /// @@ -437,14 +434,40 @@ pub fn try_compact_note_decryption>( ) } +/// Recovery of the full note plaintext by the sender. +/// +/// Attempts to decrypt and validate the given `enc_ciphertext` using the given `ovk`. +/// If successful, the corresponding note and memo are returned, along with the address to +/// which the note was sent. +/// +/// Implements [Zcash Protocol Specification section 4.19.3][decryptovk]. +/// +/// [decryptovk]: https://zips.z.cash/protocol/nu5.pdf#decryptovk +pub fn try_output_recovery_with_ovk>( + domain: &D, + ovk: &D::OutgoingViewingKey, + output: &Output, + cv: &D::ValueCommitment, + out_ciphertext: &[u8], +) -> Option<(D::Note, D::Recipient, D::Memo)> { + let ock = D::derive_ock( + ovk, + &cv, + &output.cmstar_bytes(), + &D::epk_bytes(&output.epk()), + ); + try_output_recovery_with_ock(domain, &ock, output, out_ciphertext) +} + /// 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. +/// If successful, the corresponding note and memo are returned, along with the address to +/// which the note was sent. /// /// Implements part of section 4.19.3 of the -/// [Zcash Protocol Specification](https://zips.z.cash/protocol/nu5.pdf#decryptovk) +/// [Zcash Protocol Specification](https://zips.z.cash/protocol/nu5.pdf#decryptovk). +/// For decryption using a Full Viewing Key see [`try_output_recovery_with_ovk`]. pub fn try_output_recovery_with_ock>( domain: &D, ock: &OutgoingCipherKey, @@ -454,7 +477,7 @@ pub fn try_output_recovery_with_ock>( assert_eq!(output.enc_ciphertext().len(), ENC_CIPHERTEXT_SIZE); assert_eq!(out_ciphertext.len(), OUT_CIPHERTEXT_SIZE); - let mut op = [0; OUT_CIPHERTEXT_SIZE]; + let mut op = [0; OUT_PLAINTEXT_SIZE]; assert_eq!( ChachaPolyIetf::aead_cipher() .open_to(&mut op, &out_ciphertext, &[], ock.as_ref(), &[0u8; 12]) @@ -489,6 +512,14 @@ 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); + // ZIP 212: Check that the esk provided to this function is consistent with the esk we + // can derive from the note. + if let Some(derived_esk) = D::derive_esk(¬e) { + if (!derived_esk.ct_eq(&esk)).into() { + return None; + } + } + if let NoteValidity::Valid = check_note_validity::(¬e, output.epk(), &output.cmstar_bytes()) { diff --git a/zcash_primitives/src/sapling/note_encryption.rs b/zcash_primitives/src/sapling/note_encryption.rs index f50e2416d..c2d9f85d8 100644 --- a/zcash_primitives/src/sapling/note_encryption.rs +++ b/zcash_primitives/src/sapling/note_encryption.rs @@ -7,10 +7,10 @@ use rand_core::RngCore; use std::convert::TryInto; use zcash_note_encryption::{ - try_compact_note_decryption, try_note_decryption, try_output_recovery_with_ock, Domain, - EphemeralKeyBytes, NoteEncryption, NotePlaintextBytes, NoteValidity, OutPlaintextBytes, - OutgoingCipherKey, ShieldedOutput, COMPACT_NOTE_SIZE, NOTE_PLAINTEXT_SIZE, OUT_CIPHERTEXT_SIZE, - OUT_PLAINTEXT_SIZE, + try_compact_note_decryption, try_note_decryption, try_output_recovery_with_ock, + try_output_recovery_with_ovk, Domain, EphemeralKeyBytes, NoteEncryption, NotePlaintextBytes, + NoteValidity, OutPlaintextBytes, OutgoingCipherKey, ShieldedOutput, COMPACT_NOTE_SIZE, + NOTE_PLAINTEXT_SIZE, OUT_PLAINTEXT_SIZE, }; use crate::{ @@ -54,7 +54,7 @@ fn kdf_sapling(dhsecret: jubjub::SubgroupPoint, ephemeral_key: &EphemeralKeyByte pub fn prf_ock( ovk: &OutgoingViewingKey, cv: &jubjub::ExtendedPoint, - cmu: &bls12_381::Scalar, + cmu_bytes: &[u8; 32], ephemeral_key: &EphemeralKeyBytes, ) -> OutgoingCipherKey { OutgoingCipherKey( @@ -64,7 +64,7 @@ pub fn prf_ock( .to_state() .update(&ovk.0) .update(&cv.to_bytes()) - .update(&cmu.to_repr()) + .update(cmu_bytes) .update(ephemeral_key.as_ref()) .finalize() .as_bytes() @@ -209,10 +209,10 @@ impl Domain for SaplingDomain

{ fn derive_ock( ovk: &Self::OutgoingViewingKey, cv: &Self::ValueCommitment, - cmu: &Self::ExtractedCommitment, + cmu_bytes: &Self::ExtractedCommitmentBytes, epk: &EphemeralKeyBytes, ) -> OutgoingCipherKey { - prf_ock(ovk, cv, cmu, epk) + prf_ock(ovk, cv, cmu_bytes, epk) } fn outgoing_plaintext_bytes( @@ -272,7 +272,7 @@ impl Domain for SaplingDomain

{ note.cmu() } - fn extract_pk_d(op: &[u8; OUT_CIPHERTEXT_SIZE]) -> Option { + fn extract_pk_d(op: &[u8; OUT_PLAINTEXT_SIZE]) -> Option { let pk_d = jubjub::SubgroupPoint::from_bytes( op[0..32].try_into().expect("slice is the correct length"), ); @@ -284,7 +284,7 @@ impl Domain for SaplingDomain

{ } } - fn extract_esk(op: &[u8; OUT_CIPHERTEXT_SIZE]) -> Option { + fn extract_esk(op: &[u8; OUT_PLAINTEXT_SIZE]) -> Option { jubjub::Fr::from_repr( op[32..OUT_PLAINTEXT_SIZE] .try_into() @@ -407,17 +407,12 @@ pub fn try_sapling_output_recovery( ovk: &OutgoingViewingKey, output: &OutputDescription, ) -> Option<(Note, PaymentAddress, MemoBytes)> { - try_sapling_output_recovery_with_ock( - params, + let domain = SaplingDomain { + params: params.clone(), height, - &prf_ock( - &ovk, - &output.cv, - &output.cmu, - &epk_bytes(&output.ephemeral_key), - ), - output, - ) + }; + + try_output_recovery_with_ovk(&domain, ovk, output, &output.cv, &output.out_ciphertext) } #[cfg(test)] @@ -524,7 +519,7 @@ mod tests { &mut rng, ); let epk = *ne.epk(); - let ock = prf_ock(&ovk, &cv, &cmu, &epk_bytes(&epk)); + let ock = prf_ock(&ovk, &cv, &cmu.to_repr(), &epk_bytes(&epk)); let output = OutputDescription { cv, @@ -547,7 +542,7 @@ mod tests { out_ciphertext: &[u8; OUT_CIPHERTEXT_SIZE], modify_plaintext: impl Fn(&mut [u8; NOTE_PLAINTEXT_SIZE]), ) { - let ock = prf_ock(&ovk, &cv, &cmu, &epk_bytes(epk)); + let ock = prf_ock(&ovk, &cv, &cmu.to_repr(), &epk_bytes(epk)); let mut op = [0; OUT_CIPHERTEXT_SIZE]; assert_eq!( @@ -1279,7 +1274,7 @@ mod tests { assert_eq!(k_enc.as_bytes(), tv.k_enc); let ovk = OutgoingViewingKey(tv.ovk); - let ock = prf_ock(&ovk, &cv, &cmu, &epk_bytes(&epk)); + let ock = prf_ock(&ovk, &cv, &cmu.to_repr(), &epk_bytes(&epk)); assert_eq!(ock.as_ref(), tv.ock); let to = PaymentAddress::from_parts(Diversifier(tv.default_d), pk_d).unwrap();