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) +}