diff --git a/Cargo.toml b/Cargo.toml index 46e1bec..bed67da 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" @@ -11,3 +12,14 @@ 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" +subtle = "2.2.3" + +[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 0ee39e7..481ee1e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,500 @@ -#[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; +use std::convert::TryFrom; +use subtle::{Choice, ConstantTimeEq}; + +pub const COMPACT_NOTE_SIZE: usize = 1 + // version + 11 + // diversifier + 8 + // value + 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 +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]); + +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 + } +} + +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) + } +} + +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]); + +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum NoteValidity { + 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 ExtractedCommitment; + type ExtractedCommitmentBytes: Eq + TryFrom; + 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, 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 + // to be removed. + fn note_plaintext_bytes( + note: &Self::Note, + recipient: &Self::Recipient, + memo: &Self::Memo, + ) -> NotePlaintextBytes; + + fn derive_ock( + ovk: &Self::OutgoingViewingKey, + cv: &Self::ValueCommitment, + cmstar: &Self::ExtractedCommitment, + ephemeral_key: &EphemeralKeyBytes, + ) -> OutgoingCipherKey; + + fn outgoing_plaintext_bytes( + note: &Self::Note, + esk: &Self::EphemeralSecretKey, + ) -> OutPlaintextBytes; + + fn epk_bytes(epk: &Self::EphemeralPublicKey) -> EphemeralKeyBytes; + + fn check_epk_bytes NoteValidity>( + note: &Self::Note, + check: F, + ) -> NoteValidity; + + fn cmstar(note: &Self::Note) -> Self::ExtractedCommitment; + + 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 { + fn epk(&self) -> &D::EphemeralPublicKey; + fn cmstar_bytes(&self) -> D::ExtractedCommitmentBytes; + fn enc_ciphertext(&self) -> &[u8]; +} + +/// 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.19 of the +/// [Zcash Protocol Specification](https://zips.z.cash/protocol/nu5.pdf#saplingandorchardinband) +/// NB: the example code is only covering the post-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::{TEST_NETWORK, TestNetwork, NetworkUpgrade, Parameters}, +/// memo::MemoBytes, +/// sapling::{ +/// keys::{OutgoingViewingKey, prf_expand}, +/// note_encryption::sapling_note_encryption, +/// util::generate_random_rseed, +/// 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 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); +/// 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. 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, + 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 encoding of 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, &D::epk_bytes(&self.epk)); + let input = D::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( + &self, + cv: &D::ValueCommitment, + 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, &cmstar, &D::epk_bytes(&self.epk)); + let input = D::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.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, + output: &Output, +) -> Option<(D::Note, D::Recipient, D::Memo)> { + 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, &D::epk_bytes(output.epk())); + + let mut plaintext = [0; ENC_CIPHERTEXT_SIZE]; + assert_eq!( + ChachaPolyIetf::aead_cipher() + .open_to( + &mut plaintext, + output.enc_ciphertext(), + &[], + key.as_ref(), + &[0u8; 12] + ) + .ok()?, + NOTE_PLAINTEXT_SIZE + ); + + let (note, to) = parse_note_plaintext_without_memo_ivk( + domain, + ivk, + output.epk(), + &output.cmstar_bytes(), + &plaintext, + )?; + let memo = domain.extract_memo(&plaintext); + + Some((note, to, memo)) +} + +fn parse_note_plaintext_without_memo_ivk( + domain: &D, + ivk: &D::IncomingViewingKey, + epk: &D::EphemeralPublicKey, + 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_bytes) { + Some((note, to)) + } else { + None + } +} + +fn check_note_validity( + note: &D::Note, + epk: &D::EphemeralPublicKey, + cmstar_bytes: &D::ExtractedCommitmentBytes, +) -> NoteValidity { + 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| { + if D::epk_bytes(&D::ka_derive_public(¬e, &derived_esk)) + .ct_eq(&epk_bytes) + .into() + { + NoteValidity::Valid + } else { + NoteValidity::Invalid + } + }) + } else { + // Published commitment doesn't match calculated commitment + NoteValidity::Invalid + } +} + +/// 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, + output: &Output, +) -> Option<(D::Note, D::Recipient)> { + 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, &D::epk_bytes(output.epk())); + + // Start from block 1 to skip over Poly1305 keying output + let mut plaintext = [0; COMPACT_NOTE_SIZE]; + 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, + ) +} + +/// 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.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, + ock: &OutgoingCipherKey, + output: &Output, + out_ciphertext: &[u8], +) -> Option<(D::Note, D::Recipient, D::Memo)> { + assert_eq!(output.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); + // 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]; + assert_eq!( + ChachaPolyIetf::aead_cipher() + .open_to( + &mut plaintext, + output.enc_ciphertext(), + &[], + key.as_ref(), + &[0u8; 12] + ) + .ok()?, + NOTE_PLAINTEXT_SIZE + ); + + 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, output.epk(), &output.cmstar_bytes()) + { + Some((note, to, memo)) + } else { + None } }