From e14dab6945177815f2b13fbee9c61e12caa96e5a Mon Sep 17 00:00:00 2001 From: Dmitry Demin Date: Tue, 19 Dec 2023 22:08:46 +0300 Subject: [PATCH 1/8] Add generalization to support OrchardZSA and AssetBase (#1) * Copy updated src folder from librustzcash/zsa1 * Cherry-pick the latest update of zcash_note_encryption from zcash/librustzcash (it was missed in QED-it/librustzcash) * Upgrade Rust version * Upgrade Rust version to 1.65 * Add --features=alloc to command line for build-nodefault jon in ci.yml * Downgrade Rust version back to 1.61.0 --------- Co-authored-by: Dmitry Demin --- .github/workflows/ci.yml | 2 +- Cargo.toml | 2 +- rust-toolchain.toml | 4 +- src/batch.rs | 8 +-- src/lib.rs | 140 ++++++++++++++++++--------------------- 5 files changed, 72 insertions(+), 84 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5d444aa..fd93d95 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,7 +46,7 @@ jobs: - name: Add target run: rustup target add ${{ matrix.target }} - name: Build crate - run: cargo build --no-default-features --verbose --target ${{ matrix.target }} + run: cargo build --features=alloc --no-default-features --verbose --target ${{ matrix.target }} bitrot: name: Bitrot check diff --git a/Cargo.toml b/Cargo.toml index 34d359e..0ae49d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ repository = "https://github.com/zcash/librustzcash" readme = "README.md" license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.56.1" +rust-version = "1.61.0" categories = ["cryptography::cryptocurrencies"] [package.metadata.docs.rs] diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 57237a5..b54a9f4 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.56.1" -components = ["clippy", "rustfmt"] +channel = "1.61.0" +components = [ "clippy", "rustfmt" ] diff --git a/src/batch.rs b/src/batch.rs index ad70416..e06f35e 100644 --- a/src/batch.rs +++ b/src/batch.rs @@ -4,7 +4,7 @@ use alloc::vec::Vec; // module is alloc only use crate::{ try_compact_note_decryption_inner, try_note_decryption_inner, BatchDomain, EphemeralKeyBytes, - ShieldedOutput, COMPACT_NOTE_SIZE, ENC_CIPHERTEXT_SIZE, + ShieldedOutput, }; /// Trial decryption of a batch of notes with a set of recipients. @@ -16,7 +16,7 @@ use crate::{ /// provided, along with the index in the `ivks` slice associated with /// the IVK that successfully decrypted the output. #[allow(clippy::type_complexity)] -pub fn try_note_decryption>( +pub fn try_note_decryption>( ivks: &[D::IncomingViewingKey], outputs: &[(D, Output)], ) -> Vec> { @@ -32,14 +32,14 @@ pub fn try_note_decryption>( +pub fn try_compact_note_decryption>( ivks: &[D::IncomingViewingKey], outputs: &[(D, Output)], ) -> Vec> { batch_note_decryption(ivks, outputs, try_compact_note_decryption_inner) } -fn batch_note_decryption, F, FR, const CS: usize>( +fn batch_note_decryption, F, FR>( ivks: &[D::IncomingViewingKey], outputs: &[(D, Output)], decrypt_inner: F, diff --git a/src/lib.rs b/src/lib.rs index 16c089b..406a030 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,7 +24,7 @@ use core::fmt::{self, Write}; #[cfg(feature = "alloc")] extern crate alloc; #[cfg(feature = "alloc")] -use alloc::vec::Vec; +use alloc::{borrow::ToOwned, vec::Vec}; use chacha20::{ cipher::{StreamCipher, StreamCipherSeek}, @@ -40,19 +40,14 @@ use subtle::{Choice, ConstantTimeEq}; #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] pub mod batch; -/// The size of a compact note. -pub const COMPACT_NOTE_SIZE: usize = 1 + // version - 11 + // diversifier - 8 + // value - 32; // rseed (or rcm prior to ZIP 212) -/// The size of [`NotePlaintextBytes`]. -pub const NOTE_PLAINTEXT_SIZE: usize = COMPACT_NOTE_SIZE + 512; +/// The size of the memo. +pub const MEMO_SIZE: usize = 512; +/// The size of the authentication tag used for note encryption. +pub const AEAD_TAG_SIZE: usize = 16; + /// The size of [`OutPlaintextBytes`]. pub const OUT_PLAINTEXT_SIZE: usize = 32 + // pk_d 32; // esk -const AEAD_TAG_SIZE: usize = 16; -/// The size of an encrypted note plaintext. -pub const ENC_CIPHERTEXT_SIZE: usize = NOTE_PLAINTEXT_SIZE + AEAD_TAG_SIZE; /// The size of an encrypted outgoing plaintext. pub const OUT_CIPHERTEXT_SIZE: usize = OUT_PLAINTEXT_SIZE + AEAD_TAG_SIZE; @@ -114,8 +109,6 @@ impl ConstantTimeEq for EphemeralKeyBytes { } } -/// Newtype representing the byte encoding of a note plaintext. -pub struct NotePlaintextBytes(pub [u8; NOTE_PLAINTEXT_SIZE]); /// Newtype representing the byte encoding of a outgoing plaintext. pub struct OutPlaintextBytes(pub [u8; OUT_PLAINTEXT_SIZE]); @@ -145,6 +138,11 @@ pub trait Domain { type ExtractedCommitmentBytes: Eq + for<'a> From<&'a Self::ExtractedCommitment>; type Memo; + type NotePlaintextBytes: AsMut<[u8]> + for<'a> From<&'a [u8]>; + type NoteCiphertextBytes: AsRef<[u8]> + for<'a> From<&'a [u8]>; + type CompactNotePlaintextBytes: AsMut<[u8]> + for<'a> From<&'a [u8]>; + type CompactNoteCiphertextBytes: AsRef<[u8]>; + /// Derives the `EphemeralSecretKey` corresponding to this note. /// /// Returns `None` if the note was created prior to [ZIP 212], and doesn't have a @@ -192,7 +190,7 @@ pub trait Domain { fn kdf(secret: Self::SharedSecret, ephemeral_key: &EphemeralKeyBytes) -> Self::SymmetricKey; /// Encodes the given `Note` and `Memo` as a note plaintext. - fn note_plaintext_bytes(note: &Self::Note, memo: &Self::Memo) -> NotePlaintextBytes; + fn note_plaintext_bytes(note: &Self::Note, memo: &Self::Memo) -> Self::NotePlaintextBytes; /// Derives the [`OutgoingCipherKey`] for an encrypted note, given the note-specific /// public data and an `OutgoingViewingKey`. @@ -233,14 +231,10 @@ pub trait Domain { /// such as rules like [ZIP 212] that become active at a specific block height. /// /// [ZIP 212]: https://zips.z.cash/zip-0212 - /// - /// # Panics - /// - /// Panics if `plaintext` is shorter than [`COMPACT_NOTE_SIZE`]. fn parse_note_plaintext_without_memo_ivk( &self, ivk: &Self::IncomingViewingKey, - plaintext: &[u8], + plaintext: &Self::CompactNotePlaintextBytes, ) -> Option<(Self::Note, Self::Recipient)>; /// Parses the given note plaintext from the sender's perspective. @@ -258,16 +252,19 @@ pub trait Domain { fn parse_note_plaintext_without_memo_ovk( &self, pk_d: &Self::DiversifiedTransmissionKey, - plaintext: &NotePlaintextBytes, + plaintext: &Self::CompactNotePlaintextBytes, ) -> Option<(Self::Note, Self::Recipient)>; - /// Extracts the memo field from the given note plaintext. + /// Splits the memo field from the given note plaintext. /// /// # Compatibility /// /// `&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: &NotePlaintextBytes) -> Self::Memo; + fn extract_memo( + &self, + plaintext: &Self::NotePlaintextBytes, + ) -> (Self::CompactNotePlaintextBytes, Self::Memo); /// Parses the `DiversifiedTransmissionKey` field of the outgoing plaintext. /// @@ -326,19 +323,18 @@ pub trait BatchDomain: Domain { } /// Trait that provides access to the components of an encrypted transaction output. -/// -/// Implementations of this trait are required to define the length of their ciphertext -/// field. In order to use the trial decryption APIs in this crate, the length must be -/// either [`ENC_CIPHERTEXT_SIZE`] or [`COMPACT_NOTE_SIZE`]. -pub trait ShieldedOutput { +pub trait ShieldedOutput { /// Exposes the `ephemeral_key` field of the output. fn ephemeral_key(&self) -> EphemeralKeyBytes; /// Exposes the `cmu_bytes` or `cmx_bytes` field of the output. fn cmstar_bytes(&self) -> D::ExtractedCommitmentBytes; - /// Exposes the note ciphertext of the output. - fn enc_ciphertext(&self) -> &[u8; CIPHERTEXT_SIZE]; + /// Exposes the note ciphertext of the output. Returns `None` if the output is compact. + fn enc_ciphertext(&self) -> Option; + + /// Exposes the compact note ciphertext of the output. + fn enc_ciphertext_compact(&self) -> D::CompactNoteCiphertextBytes; } /// A struct containing context required for encrypting Sapling and Orchard notes. @@ -403,24 +399,18 @@ impl NoteEncryption { } /// Generates `encCiphertext` for this note. - pub fn encrypt_note_plaintext(&self) -> [u8; ENC_CIPHERTEXT_SIZE] { + pub fn encrypt_note_plaintext(&self) -> D::NoteCiphertextBytes { 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.memo); + let mut input = D::note_plaintext_bytes(&self.note, &self.memo); + + let output = input.as_mut(); - let mut output = [0u8; ENC_CIPHERTEXT_SIZE]; - output[..NOTE_PLAINTEXT_SIZE].copy_from_slice(&input.0); let tag = ChaCha20Poly1305::new(key.as_ref().into()) - .encrypt_in_place_detached( - [0u8; 12][..].into(), - &[], - &mut output[..NOTE_PLAINTEXT_SIZE], - ) + .encrypt_in_place_detached([0u8; 12][..].into(), &[], output) .unwrap(); - output[NOTE_PLAINTEXT_SIZE..].copy_from_slice(&tag); - - output + D::NoteCiphertextBytes::from(&[output, tag.as_ref()].concat()) } /// Generates `outCiphertext` for this note. @@ -465,7 +455,7 @@ impl NoteEncryption { /// /// Implements section 4.19.2 of the /// [Zcash Protocol Specification](https://zips.z.cash/protocol/nu5.pdf#decryptivk). -pub fn try_note_decryption>( +pub fn try_note_decryption>( domain: &D, ivk: &D::IncomingViewingKey, output: &Output, @@ -479,35 +469,29 @@ pub fn try_note_decryption>( +fn try_note_decryption_inner>( domain: &D, ivk: &D::IncomingViewingKey, ephemeral_key: &EphemeralKeyBytes, output: &Output, key: &D::SymmetricKey, ) -> Option<(D::Note, D::Recipient, D::Memo)> { - let enc_ciphertext = output.enc_ciphertext(); + let mut enc_ciphertext = output.enc_ciphertext()?.as_ref().to_owned(); - let mut plaintext = - NotePlaintextBytes(enc_ciphertext[..NOTE_PLAINTEXT_SIZE].try_into().unwrap()); + let (plaintext, tag) = extract_tag(&mut enc_ciphertext); ChaCha20Poly1305::new(key.as_ref().into()) - .decrypt_in_place_detached( - [0u8; 12][..].into(), - &[], - &mut plaintext.0, - enc_ciphertext[NOTE_PLAINTEXT_SIZE..].into(), - ) + .decrypt_in_place_detached([0u8; 12][..].into(), &[], plaintext, &tag.into()) .ok()?; + let (compact, memo) = domain.extract_memo(&D::NotePlaintextBytes::from(plaintext)); let (note, to) = parse_note_plaintext_without_memo_ivk( domain, ivk, ephemeral_key, &output.cmstar_bytes(), - &plaintext.0, + &compact, )?; - let memo = domain.extract_memo(&plaintext); Some((note, to, memo)) } @@ -517,7 +501,7 @@ fn parse_note_plaintext_without_memo_ivk( ivk: &D::IncomingViewingKey, ephemeral_key: &EphemeralKeyBytes, cmstar_bytes: &D::ExtractedCommitmentBytes, - plaintext: &[u8], + plaintext: &D::CompactNotePlaintextBytes, ) -> Option<(D::Note, D::Recipient)> { let (note, to) = domain.parse_note_plaintext_without_memo_ivk(ivk, plaintext)?; @@ -564,7 +548,7 @@ 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, output: &Output, @@ -578,7 +562,7 @@ pub fn try_compact_note_decryption>( +fn try_compact_note_decryption_inner>( domain: &D, ivk: &D::IncomingViewingKey, ephemeral_key: &EphemeralKeyBytes, @@ -586,11 +570,12 @@ fn try_compact_note_decryption_inner Option<(D::Note, D::Recipient)> { // Start from block 1 to skip over Poly1305 keying output - let mut plaintext = [0; COMPACT_NOTE_SIZE]; - plaintext.copy_from_slice(output.enc_ciphertext()); + let mut plaintext: D::CompactNotePlaintextBytes = + output.enc_ciphertext_compact().as_ref().into(); + let mut keystream = ChaCha20::new(key.as_ref().into(), [0u8; 12][..].into()); keystream.seek(64); - keystream.apply_keystream(&mut plaintext); + keystream.apply_keystream(plaintext.as_mut()); parse_note_plaintext_without_memo_ivk( domain, @@ -610,7 +595,7 @@ fn try_compact_note_decryption_inner>( +pub fn try_output_recovery_with_ovk>( domain: &D, ovk: &D::OutgoingViewingKey, output: &Output, @@ -630,14 +615,12 @@ pub fn try_output_recovery_with_ovk>( +pub fn try_output_recovery_with_ock>( domain: &D, ock: &OutgoingCipherKey, output: &Output, out_ciphertext: &[u8; OUT_CIPHERTEXT_SIZE], ) -> Option<(D::Note, D::Recipient, D::Memo)> { - let enc_ciphertext = output.enc_ciphertext(); - let mut op = OutPlaintextBytes([0; OUT_PLAINTEXT_SIZE]); op.0.copy_from_slice(&out_ciphertext[..OUT_PLAINTEXT_SIZE]); @@ -660,22 +643,17 @@ pub fn try_output_recovery_with_ock) -> (&mut [u8], [u8; AEAD_TAG_SIZE]) { + let tag_loc = enc_ciphertext.len() - AEAD_TAG_SIZE; + + let (plaintext, tail) = enc_ciphertext.split_at_mut(tag_loc); + + let tag: [u8; AEAD_TAG_SIZE] = tail.try_into().unwrap(); + (plaintext, tag) +} From 8b6b31dcea4a883a606425c4b644fca213e78b3b Mon Sep 17 00:00:00 2001 From: Dmitry Demin Date: Wed, 17 Apr 2024 12:59:11 +0300 Subject: [PATCH 2/8] Refactor/modify lib.rs to avoid Vec usage and fix CI build failure (#3) Co-authored-by: Dmitry Demin --- .github/workflows/ci.yml | 2 +- src/lib.rs | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd93d95..5d444aa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,7 +46,7 @@ jobs: - name: Add target run: rustup target add ${{ matrix.target }} - name: Build crate - run: cargo build --features=alloc --no-default-features --verbose --target ${{ matrix.target }} + run: cargo build --no-default-features --verbose --target ${{ matrix.target }} bitrot: name: Bitrot check diff --git a/src/lib.rs b/src/lib.rs index 406a030..397a663 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,7 +24,7 @@ use core::fmt::{self, Write}; #[cfg(feature = "alloc")] extern crate alloc; #[cfg(feature = "alloc")] -use alloc::{borrow::ToOwned, vec::Vec}; +use alloc::vec::Vec; use chacha20::{ cipher::{StreamCipher, StreamCipherSeek}, @@ -139,7 +139,7 @@ pub trait Domain { type Memo; type NotePlaintextBytes: AsMut<[u8]> + for<'a> From<&'a [u8]>; - type NoteCiphertextBytes: AsRef<[u8]> + for<'a> From<&'a [u8]>; + type NoteCiphertextBytes: AsMut<[u8]> + for<'a> From<(&'a [u8], &'a [u8])>; type CompactNotePlaintextBytes: AsMut<[u8]> + for<'a> From<&'a [u8]>; type CompactNoteCiphertextBytes: AsRef<[u8]>; @@ -410,7 +410,7 @@ impl NoteEncryption { let tag = ChaCha20Poly1305::new(key.as_ref().into()) .encrypt_in_place_detached([0u8; 12][..].into(), &[], output) .unwrap(); - D::NoteCiphertextBytes::from(&[output, tag.as_ref()].concat()) + D::NoteCiphertextBytes::from((output, tag.as_ref())) } /// Generates `outCiphertext` for this note. @@ -476,9 +476,10 @@ fn try_note_decryption_inner>( output: &Output, key: &D::SymmetricKey, ) -> Option<(D::Note, D::Recipient, D::Memo)> { - let mut enc_ciphertext = output.enc_ciphertext()?.as_ref().to_owned(); + let mut enc_ciphertext = output.enc_ciphertext()?; + let enc_ciphertext_ref = enc_ciphertext.as_mut(); - let (plaintext, tag) = extract_tag(&mut enc_ciphertext); + let (plaintext, tag) = extract_tag(enc_ciphertext_ref); ChaCha20Poly1305::new(key.as_ref().into()) .decrypt_in_place_detached([0u8; 12][..].into(), &[], plaintext, &tag.into()) @@ -643,9 +644,10 @@ pub fn try_output_recovery_with_ock>( // be okay. let key = D::kdf(shared_secret, &ephemeral_key); - let mut enc_ciphertext = output.enc_ciphertext()?.as_ref().to_owned(); + let mut enc_ciphertext = output.enc_ciphertext()?; + let enc_ciphertext_ref = enc_ciphertext.as_mut(); - let (plaintext, tag) = extract_tag(&mut enc_ciphertext); + let (plaintext, tag) = extract_tag(enc_ciphertext_ref); ChaCha20Poly1305::new(key.as_ref().into()) .decrypt_in_place_detached([0u8; 12][..].into(), &[], plaintext, &tag.into()) @@ -674,7 +676,7 @@ pub fn try_output_recovery_with_ock>( } // Splits the AEAD tag from the ciphertext. -fn extract_tag(enc_ciphertext: &mut Vec) -> (&mut [u8], [u8; AEAD_TAG_SIZE]) { +fn extract_tag(enc_ciphertext: &mut [u8]) -> (&mut [u8], [u8; AEAD_TAG_SIZE]) { let tag_loc = enc_ciphertext.len() - AEAD_TAG_SIZE; let (plaintext, tail) = enc_ciphertext.split_at_mut(tag_loc); From 1ca015a4962a801571b4d1abcf080d2d3e972147 Mon Sep 17 00:00:00 2001 From: Dmitry Demin Date: Thu, 16 May 2024 16:55:43 +0200 Subject: [PATCH 3/8] Update CHANGELOG.md with changes for note plaintext size generalization (#6) * Update CHANGELOG.md with changes for note plaintext size generalization * Improve the description in CHANGELOG.md --------- Co-authored-by: Dmitry Demin --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cedc180..b464b50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ and this library adheres to Rust's notion of [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Changed +- Generalized the note plaintext size to support variable sizes by adding the + abstract types `NotePlaintextBytes`, `NoteCiphertextBytes`, + `CompactNotePlaintextBytes`, and `CompactNoteCiphertextBytes` to the `Domain` + trait. +- Moved the specific constants into the `Domain` trait implementations. ## [0.4.0] - 2023-06-06 ### Changed From b8bd2a186fc04ec4f55b2db44df7374f03ab5725 Mon Sep 17 00:00:00 2001 From: Dmitry Demin Date: Mon, 20 May 2024 11:06:12 +0200 Subject: [PATCH 4/8] Revert MSRV to 1.56.1 (#5) Co-authored-by: Dmitry Demin --- Cargo.toml | 2 +- rust-toolchain.toml | 2 +- src/lib.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0ae49d7..34d359e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ repository = "https://github.com/zcash/librustzcash" readme = "README.md" license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.61.0" +rust-version = "1.56.1" categories = ["cryptography::cryptocurrencies"] [package.metadata.docs.rs] diff --git a/rust-toolchain.toml b/rust-toolchain.toml index b54a9f4..bd93f58 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.61.0" +channel = "1.56.1" components = [ "clippy", "rustfmt" ] diff --git a/src/lib.rs b/src/lib.rs index 397a663..89977ef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -681,6 +681,6 @@ fn extract_tag(enc_ciphertext: &mut [u8]) -> (&mut [u8], [u8; AEAD_TAG_SIZE]) { let (plaintext, tail) = enc_ciphertext.split_at_mut(tag_loc); - let tag: [u8; AEAD_TAG_SIZE] = tail.try_into().unwrap(); + let tag: [u8; AEAD_TAG_SIZE] = tail.as_ref().try_into().unwrap(); (plaintext, tag) } From 319c7823ec6a152ed0c68a7d7e40fc1ad9dbb3b3 Mon Sep 17 00:00:00 2001 From: Dmitry Demin Date: Tue, 30 Jul 2024 17:56:04 +0200 Subject: [PATCH 5/8] Resolve Issues from PR #2 in zcash/zcash_note_encryption repository (#10) * Attempt to resolve review issues for zcash/zcash_note_encryption/pull/2 * NoteBytes moved to zcash_note_encryption (copy of #8) (#9) * Add NoteBytes * Implement concat manually for wasm * Fix slice bounds in concat * Fmt * Split interface and implementation of NoteBytes * Add new method to NoteBytes --------- Co-authored-by: alexeykoren <2365507+alexeykoren@users.noreply.github.com> * Resolve review issues for zcash/zcash_note_encryption/pull/2 (except returning references from ShieldedOutput methods) * Fix cargo doc issue * Fix based on feedback from PR #10 review * Make split_ciphertext_at_tag a method of ShieldedOutput with minor refactoring --------- Co-authored-by: Dmitry Demin Co-authored-by: alexeykoren <2365507+alexeykoren@users.noreply.github.com> --- src/lib.rs | 98 ++++++++++++++++++++++++++++++++--------------- src/note_bytes.rs | 50 ++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 30 deletions(-) create mode 100644 src/note_bytes.rs diff --git a/src/lib.rs b/src/lib.rs index 89977ef..e052a76 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,6 +40,18 @@ use subtle::{Choice, ConstantTimeEq}; #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] pub mod batch; +pub mod note_bytes; + +use note_bytes::NoteBytes; + +/// The size of a compact note for Sapling and Orchard Vanilla. +pub const COMPACT_NOTE_SIZE: usize = 1 + // version + 11 + // diversifier + 8 + // value + 32; // rseed (or rcm prior to ZIP 212) +/// The size of `NotePlaintextBytes` for Sapling and Orchard Vanilla. +pub const NOTE_PLAINTEXT_SIZE: usize = COMPACT_NOTE_SIZE + 512; + /// The size of the memo. pub const MEMO_SIZE: usize = 512; /// The size of the authentication tag used for note encryption. @@ -51,6 +63,9 @@ pub const OUT_PLAINTEXT_SIZE: usize = 32 + // pk_d /// The size of an encrypted outgoing plaintext. pub const OUT_CIPHERTEXT_SIZE: usize = OUT_PLAINTEXT_SIZE + AEAD_TAG_SIZE; +/// The size of an encrypted note plaintext for Sapling and Orchard Vanilla. +pub const ENC_CIPHERTEXT_SIZE: usize = NOTE_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]); @@ -138,10 +153,10 @@ pub trait Domain { type ExtractedCommitmentBytes: Eq + for<'a> From<&'a Self::ExtractedCommitment>; type Memo; - type NotePlaintextBytes: AsMut<[u8]> + for<'a> From<&'a [u8]>; - type NoteCiphertextBytes: AsMut<[u8]> + for<'a> From<(&'a [u8], &'a [u8])>; - type CompactNotePlaintextBytes: AsMut<[u8]> + for<'a> From<&'a [u8]>; - type CompactNoteCiphertextBytes: AsRef<[u8]>; + type NotePlaintextBytes: NoteBytes; + type NoteCiphertextBytes: NoteBytes; + type CompactNotePlaintextBytes: NoteBytes; + type CompactNoteCiphertextBytes: NoteBytes; /// Derives the `EphemeralSecretKey` corresponding to this note. /// @@ -261,10 +276,10 @@ pub trait Domain { /// /// `&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( + fn split_plaintext_at_memo( &self, plaintext: &Self::NotePlaintextBytes, - ) -> (Self::CompactNotePlaintextBytes, Self::Memo); + ) -> Option<(Self::CompactNotePlaintextBytes, Self::Memo)>; /// Parses the `DiversifiedTransmissionKey` field of the outgoing plaintext. /// @@ -277,6 +292,28 @@ pub trait Domain { /// Returns `None` if `out_plaintext` does not contain a valid byte encoding of an /// `EphemeralSecretKey`. fn extract_esk(out_plaintext: &OutPlaintextBytes) -> Option; + + /// Parses the given note plaintext bytes. + /// + /// Returns `None` if the byte slice does not represent a valid note plaintext. + fn parse_note_plaintext_bytes(plaintext: &[u8]) -> Option; + + /// Parses the given note ciphertext bytes. + /// + /// `output` is the ciphertext bytes, and `tag` is the authentication tag. + /// + /// Returns `None` if the byte slice does not represent a valid note ciphertext. + fn parse_note_ciphertext_bytes( + output: &[u8], + tag: [u8; AEAD_TAG_SIZE], + ) -> Option; + + /// Parses the given compact note plaintext bytes. + /// + /// Returns `None` if the byte slice does not represent a valid compact note plaintext. + fn parse_compact_note_plaintext_bytes( + plaintext: &[u8], + ) -> Option; } /// Trait that encapsulates protocol-specific batch trial decryption logic. @@ -333,8 +370,25 @@ pub trait ShieldedOutput { /// Exposes the note ciphertext of the output. Returns `None` if the output is compact. fn enc_ciphertext(&self) -> Option; + // FIXME: Should we return `Option` or + // `&D::CompactNoteCiphertextBytes` instead? (complexity)? /// Exposes the compact note ciphertext of the output. fn enc_ciphertext_compact(&self) -> D::CompactNoteCiphertextBytes; + + //// Splits the AEAD tag from the ciphertext. + fn split_ciphertext_at_tag(&self) -> Option<(D::NotePlaintextBytes, [u8; AEAD_TAG_SIZE])> { + let enc_ciphertext = self.enc_ciphertext()?; + let enc_ciphertext_bytes = enc_ciphertext.as_ref(); + + let (plaintext, tail) = enc_ciphertext_bytes + .len() + .checked_sub(AEAD_TAG_SIZE) + .map(|tag_loc| enc_ciphertext_bytes.split_at(tag_loc))?; + + let tag: [u8; AEAD_TAG_SIZE] = tail.try_into().expect("the length of the tag is correct"); + + D::parse_note_plaintext_bytes(plaintext).map(|plaintext| (plaintext, tag)) + } } /// A struct containing context required for encrypting Sapling and Orchard notes. @@ -410,7 +464,7 @@ impl NoteEncryption { let tag = ChaCha20Poly1305::new(key.as_ref().into()) .encrypt_in_place_detached([0u8; 12][..].into(), &[], output) .unwrap(); - D::NoteCiphertextBytes::from((output, tag.as_ref())) + D::parse_note_ciphertext_bytes(output, tag.into()).expect("the output length is correct") } /// Generates `outCiphertext` for this note. @@ -476,16 +530,13 @@ fn try_note_decryption_inner>( output: &Output, key: &D::SymmetricKey, ) -> Option<(D::Note, D::Recipient, D::Memo)> { - let mut enc_ciphertext = output.enc_ciphertext()?; - let enc_ciphertext_ref = enc_ciphertext.as_mut(); - - let (plaintext, tag) = extract_tag(enc_ciphertext_ref); + let (mut plaintext, tag) = output.split_ciphertext_at_tag()?; ChaCha20Poly1305::new(key.as_ref().into()) - .decrypt_in_place_detached([0u8; 12][..].into(), &[], plaintext, &tag.into()) + .decrypt_in_place_detached([0u8; 12][..].into(), &[], plaintext.as_mut(), &tag.into()) .ok()?; - let (compact, memo) = domain.extract_memo(&D::NotePlaintextBytes::from(plaintext)); + let (compact, memo) = domain.split_plaintext_at_memo(&plaintext)?; let (note, to) = parse_note_plaintext_without_memo_ivk( domain, ivk, @@ -572,7 +623,7 @@ fn try_compact_note_decryption_inner>( ) -> Option<(D::Note, D::Recipient)> { // Start from block 1 to skip over Poly1305 keying output let mut plaintext: D::CompactNotePlaintextBytes = - output.enc_ciphertext_compact().as_ref().into(); + D::parse_compact_note_plaintext_bytes(output.enc_ciphertext_compact().as_ref())?; let mut keystream = ChaCha20::new(key.as_ref().into(), [0u8; 12][..].into()); keystream.seek(64); @@ -644,16 +695,13 @@ pub fn try_output_recovery_with_ock>( // be okay. let key = D::kdf(shared_secret, &ephemeral_key); - let mut enc_ciphertext = output.enc_ciphertext()?; - let enc_ciphertext_ref = enc_ciphertext.as_mut(); - - let (plaintext, tag) = extract_tag(enc_ciphertext_ref); + let (mut plaintext, tag) = output.split_ciphertext_at_tag()?; ChaCha20Poly1305::new(key.as_ref().into()) - .decrypt_in_place_detached([0u8; 12][..].into(), &[], plaintext, &tag.into()) + .decrypt_in_place_detached([0u8; 12][..].into(), &[], plaintext.as_mut(), &tag.into()) .ok()?; - let (compact, memo) = domain.extract_memo(&plaintext.as_ref().into()); + let (compact, memo) = domain.split_plaintext_at_memo(&plaintext)?; let (note, to) = domain.parse_note_plaintext_without_memo_ovk(&pk_d, &compact)?; @@ -674,13 +722,3 @@ pub fn try_output_recovery_with_ock>( None } } - -// Splits the AEAD tag from the ciphertext. -fn extract_tag(enc_ciphertext: &mut [u8]) -> (&mut [u8], [u8; AEAD_TAG_SIZE]) { - let tag_loc = enc_ciphertext.len() - AEAD_TAG_SIZE; - - let (plaintext, tail) = enc_ciphertext.split_at_mut(tag_loc); - - let tag: [u8; AEAD_TAG_SIZE] = tail.as_ref().try_into().unwrap(); - (plaintext, tag) -} diff --git a/src/note_bytes.rs b/src/note_bytes.rs new file mode 100644 index 0000000..8350d43 --- /dev/null +++ b/src/note_bytes.rs @@ -0,0 +1,50 @@ +/// Represents a fixed-size array of bytes for note components. +#[derive(Clone, Copy, Debug)] +pub struct NoteBytesData(pub [u8; N]); + +impl AsRef<[u8]> for NoteBytesData { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl AsMut<[u8]> for NoteBytesData { + fn as_mut(&mut self) -> &mut [u8] { + &mut self.0 + } +} + +/// Provides a unified interface for handling fixed-size byte arrays used in note encryption. +pub trait NoteBytes: AsRef<[u8]> + AsMut<[u8]> + Clone + Copy { + fn from_slice(bytes: &[u8]) -> Option; + + fn from_slice_with_tag( + output: &[u8], + tag: [u8; TAG_SIZE], + ) -> Option; +} + +impl NoteBytes for NoteBytesData { + fn from_slice(bytes: &[u8]) -> Option> { + let data = bytes.try_into().ok()?; + Some(NoteBytesData(data)) + } + + fn from_slice_with_tag( + output: &[u8], + tag: [u8; TAG_SIZE], + ) -> Option> { + let expected_output_len = N.checked_sub(TAG_SIZE)?; + + if output.len() != expected_output_len { + return None; + } + + let mut data = [0u8; N]; + + data[..expected_output_len].copy_from_slice(output); + data[expected_output_len..].copy_from_slice(&tag); + + Some(NoteBytesData(data)) + } +} From 58384553aab76b2ee6d6eb328cf2187fa824ec9a Mon Sep 17 00:00:00 2001 From: Dmitry Demin Date: Wed, 31 Jul 2024 11:28:01 +0200 Subject: [PATCH 6/8] Implement new parse_... methods directly in the Domain trait (#12) Co-authored-by: Dmitry Demin --- src/lib.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e052a76..4d18376 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -296,7 +296,9 @@ pub trait Domain { /// Parses the given note plaintext bytes. /// /// Returns `None` if the byte slice does not represent a valid note plaintext. - fn parse_note_plaintext_bytes(plaintext: &[u8]) -> Option; + fn parse_note_plaintext_bytes(plaintext: &[u8]) -> Option { + Self::NotePlaintextBytes::from_slice(plaintext) + } /// Parses the given note ciphertext bytes. /// @@ -306,14 +308,18 @@ pub trait Domain { fn parse_note_ciphertext_bytes( output: &[u8], tag: [u8; AEAD_TAG_SIZE], - ) -> Option; + ) -> Option { + Self::NoteCiphertextBytes::from_slice_with_tag(output, tag) + } /// Parses the given compact note plaintext bytes. /// /// Returns `None` if the byte slice does not represent a valid compact note plaintext. fn parse_compact_note_plaintext_bytes( plaintext: &[u8], - ) -> Option; + ) -> Option { + Self::CompactNotePlaintextBytes::from_slice(plaintext) + } } /// Trait that encapsulates protocol-specific batch trial decryption logic. From 663a2e504f4ac372dfe762617f3fe51795c5b797 Mon Sep 17 00:00:00 2001 From: Dmitry Demin Date: Mon, 12 Aug 2024 21:16:45 +0200 Subject: [PATCH 7/8] Refactor enc_ciphertext to return reference instead of copy (#13) * Refactor enc_ciphertext to return reference instead of copy These changes were discussed and suggested in PR zcash_note_encryption#2 * Remove extra spaces in rust-toolchain.toml * Restore the original order of const definition to reduce PR diff * Fix the comment for split_plaintext_at_memo * Fix docstring for NOTE_PLAINTEXT_SIZE * Update CHANGELOG * Remove unused constants COMPACT_NOTE_SIZE, NOTE_PLAINTEXT_SIZE, ENC_CIPHERTEXT_SIZE, and update CHANGELOG accordingly * Minor improvement in CHANGELOG.md --------- Co-authored-by: Dmitry Demin --- CHANGELOG.md | 19 +++++++++++++++++-- rust-toolchain.toml | 2 +- src/lib.rs | 25 +++++-------------------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b464b50..9ec5ec9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,27 @@ and this library adheres to Rust's notion of ## [Unreleased] ### Changed +- **Breaking change:** removed the constants `COMPACT_NOTE_SIZE`, + `NOTE_PLAINTEXT_SIZE`, and `ENC_CIPHERTEXT_SIZE` as they are now + implementation spesific (located in `orchard` and `sapling-crypto` crates). - Generalized the note plaintext size to support variable sizes by adding the abstract types `NotePlaintextBytes`, `NoteCiphertextBytes`, `CompactNotePlaintextBytes`, and `CompactNoteCiphertextBytes` to the `Domain` trait. -- Moved the specific constants into the `Domain` trait implementations. - +- Removed the separate `NotePlaintextBytes` type definition (as it is now an + associated type). +- Added new `parse_note_plaintext_bytes`, `parse_note_ciphertext_bytes`, and + `parse_compact_note_plaintext_bytes` methods to the `Domain` trait. +- Updated the `note_plaintext_bytes` method of the `Domain` trait to return the + `NotePlaintextBytes` associated type. +- Updated the `encrypt_note_plaintext` method of `NoteEncryption` to return the + `NoteCiphertextBytes` associated type of the `Domain` instead of the explicit + array. +- Updated the `enc_ciphertext` method of the `ShieldedOutput` trait to return an + `Option` of a reference instead of a copy. +- Added a new `note_bytes` module with helper trait and struct to deal with note + bytes data with abstracted underlying array size. + ## [0.4.0] - 2023-06-06 ### Changed - The `esk` and `ephemeral_key` arguments have been removed from diff --git a/rust-toolchain.toml b/rust-toolchain.toml index bd93f58..57237a5 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] channel = "1.56.1" -components = [ "clippy", "rustfmt" ] +components = ["clippy", "rustfmt"] diff --git a/src/lib.rs b/src/lib.rs index 4d18376..3e28501 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,28 +44,13 @@ pub mod note_bytes; use note_bytes::NoteBytes; -/// The size of a compact note for Sapling and Orchard Vanilla. -pub const COMPACT_NOTE_SIZE: usize = 1 + // version - 11 + // diversifier - 8 + // value - 32; // rseed (or rcm prior to ZIP 212) -/// The size of `NotePlaintextBytes` for Sapling and Orchard Vanilla. -pub const NOTE_PLAINTEXT_SIZE: usize = COMPACT_NOTE_SIZE + 512; - -/// The size of the memo. -pub const MEMO_SIZE: usize = 512; -/// The size of the authentication tag used for note encryption. -pub const AEAD_TAG_SIZE: usize = 16; - /// The size of [`OutPlaintextBytes`]. pub const OUT_PLAINTEXT_SIZE: usize = 32 + // pk_d 32; // esk +const AEAD_TAG_SIZE: usize = 16; /// The size of an encrypted outgoing plaintext. pub const OUT_CIPHERTEXT_SIZE: usize = OUT_PLAINTEXT_SIZE + AEAD_TAG_SIZE; -/// The size of an encrypted note plaintext for Sapling and Orchard Vanilla. -pub const ENC_CIPHERTEXT_SIZE: usize = NOTE_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]); @@ -270,7 +255,8 @@ pub trait Domain { plaintext: &Self::CompactNotePlaintextBytes, ) -> Option<(Self::Note, Self::Recipient)>; - /// Splits the memo field from the given note plaintext. + /// Splits the given note plaintext into the compact part (containing the note) and + /// the memo field. /// /// # Compatibility /// @@ -374,7 +360,7 @@ pub trait ShieldedOutput { fn cmstar_bytes(&self) -> D::ExtractedCommitmentBytes; /// Exposes the note ciphertext of the output. Returns `None` if the output is compact. - fn enc_ciphertext(&self) -> Option; + fn enc_ciphertext(&self) -> Option<&D::NoteCiphertextBytes>; // FIXME: Should we return `Option` or // `&D::CompactNoteCiphertextBytes` instead? (complexity)? @@ -383,8 +369,7 @@ pub trait ShieldedOutput { //// Splits the AEAD tag from the ciphertext. fn split_ciphertext_at_tag(&self) -> Option<(D::NotePlaintextBytes, [u8; AEAD_TAG_SIZE])> { - let enc_ciphertext = self.enc_ciphertext()?; - let enc_ciphertext_bytes = enc_ciphertext.as_ref(); + let enc_ciphertext_bytes = self.enc_ciphertext()?.as_ref(); let (plaintext, tail) = enc_ciphertext_bytes .len() From 76745f00551d4442dee11ad64a8400b75132d18f Mon Sep 17 00:00:00 2001 From: Dmitry Demin Date: Tue, 13 Aug 2024 13:14:37 +0200 Subject: [PATCH 8/8] Restore visibility for AEAD_TAG_SIZE const (#15) Co-authored-by: Dmitry Demin --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 3e28501..4bc6705 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -47,7 +47,7 @@ use note_bytes::NoteBytes; /// The size of [`OutPlaintextBytes`]. pub const OUT_PLAINTEXT_SIZE: usize = 32 + // pk_d 32; // esk -const AEAD_TAG_SIZE: usize = 16; +pub const AEAD_TAG_SIZE: usize = 16; /// The size of an encrypted outgoing plaintext. pub const OUT_CIPHERTEXT_SIZE: usize = OUT_PLAINTEXT_SIZE + AEAD_TAG_SIZE;