diff --git a/CHANGELOG.md b/CHANGELOG.md index 9af97c0..e329e12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,27 @@ and this library adheres to Rust's notion of [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Changed +- **Breaking change:** removed the constants `COMPACT_NOTE_SIZE`, + `NOTE_PLAINTEXT_SIZE`, and `ENC_CIPHERTEXT_SIZE` as they are now + implementation-specific (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. +- 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.1] - 2024-12-06 ### Added diff --git a/src/batch.rs b/src/batch.rs index 59577b5..08f3ea2 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 d5b1274..2b27681 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; +pub mod note_bytes; + +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; -/// The size of an encrypted note plaintext. -pub const ENC_CIPHERTEXT_SIZE: usize = NOTE_PLAINTEXT_SIZE + AEAD_TAG_SIZE; +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; @@ -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: NoteBytes; + type NoteCiphertextBytes: NoteBytes; + type CompactNotePlaintextBytes: NoteBytes; + type CompactNoteCiphertextBytes: NoteBytes; + /// 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,20 @@ 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 given note plaintext into the compact part (containing the note) and + /// the memo field. /// /// # 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 split_plaintext_at_memo( + &self, + plaintext: &Self::NotePlaintextBytes, + ) -> Option<(Self::CompactNotePlaintextBytes, Self::Memo)>; /// Parses the `DiversifiedTransmissionKey` field of the outgoing plaintext. /// @@ -280,6 +278,34 @@ 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 has the wrong length for a note plaintext. + fn parse_note_plaintext_bytes(plaintext: &[u8]) -> Option { + Self::NotePlaintextBytes::from_slice(plaintext) + } + + /// Parses the given note ciphertext bytes. + /// + /// `output` is the ciphertext bytes, and `tag` is the authentication tag. + /// + /// Returns `None` if the `output` byte slice has the wrong length for a note ciphertext. + fn parse_note_ciphertext_bytes( + output: &[u8], + tag: [u8; AEAD_TAG_SIZE], + ) -> Option { + Self::NoteCiphertextBytes::from_slice_with_tag(output, tag) + } + + /// Parses the given compact note plaintext bytes. + /// + /// Returns `None` if the byte slice has the wrong length for a compact note plaintext. + fn parse_compact_note_plaintext_bytes( + plaintext: &[u8], + ) -> Option { + Self::CompactNotePlaintextBytes::from_slice(plaintext) + } } /// Trait that encapsulates protocol-specific batch trial decryption logic. @@ -326,19 +352,41 @@ 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<&D::NoteCiphertextBytes>; + + // 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. + /// + /// Returns `None` if the output is compact. + fn split_ciphertext_at_tag(&self) -> Option<(D::NotePlaintextBytes, [u8; AEAD_TAG_SIZE])> { + let enc_ciphertext_bytes = self.enc_ciphertext()?.as_ref(); + + let tag_loc = enc_ciphertext_bytes + .len() + .checked_sub(AEAD_TAG_SIZE) + .expect("D::CompactNoteCiphertextBytes should be at least AEAD_TAG_SIZE bytes"); + let (plaintext, tail) = enc_ciphertext_bytes.split_at(tag_loc); + + let tag: [u8; AEAD_TAG_SIZE] = tail.try_into().expect("the length of the tag is correct"); + + Some(( + D::parse_note_plaintext_bytes(plaintext) + .expect("D::NoteCiphertextBytes and D::NotePlaintextBytes should be consistent"), + tag, + )) + } } /// A struct containing context required for encrypting Sapling and Orchard notes. @@ -403,24 +451,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::parse_note_ciphertext_bytes(output, tag.into()).expect("the output length is correct") } /// Generates `outCiphertext` for this note. @@ -465,7 +507,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 +521,27 @@ 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 plaintext = - NotePlaintextBytes(enc_ciphertext[..NOTE_PLAINTEXT_SIZE].try_into().unwrap()); + let (mut plaintext, tag) = output.split_ciphertext_at_tag()?; 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.as_mut(), &tag.into()) .ok()?; + let (compact, memo) = domain.split_plaintext_at_memo(&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 +551,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 +598,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 +612,7 @@ pub fn try_compact_note_decryption>( +fn try_compact_note_decryption_inner>( domain: &D, ivk: &D::IncomingViewingKey, ephemeral_key: &EphemeralKeyBytes, @@ -586,11 +620,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 = + 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); - keystream.apply_keystream(&mut plaintext); + keystream.apply_keystream(plaintext.as_mut()); parse_note_plaintext_without_memo_ivk( domain, @@ -610,7 +645,7 @@ fn try_compact_note_decryption_inner>( +pub fn try_output_recovery_with_ovk>( domain: &D, ovk: &D::OutgoingViewingKey, output: &Output, @@ -630,7 +665,7 @@ pub fn try_output_recovery_with_ovk>( +pub fn try_output_recovery_with_ock>( domain: &D, ock: &OutgoingCipherKey, output: &Output, @@ -663,10 +698,7 @@ pub fn try_output_recovery_with_ock, ->( +pub fn try_output_recovery_with_pkd_esk>( domain: &D, pk_d: D::DiversifiedTransmissionKey, esk: D::EphemeralSecretKey, @@ -679,23 +711,15 @@ pub fn try_output_recovery_with_pkd_esk< // be okay. let key = D::kdf(shared_secret, &ephemeral_key); - let enc_ciphertext = output.enc_ciphertext(); - let mut plaintext = NotePlaintextBytes([0; NOTE_PLAINTEXT_SIZE]); - plaintext - .0 - .copy_from_slice(&enc_ciphertext[..NOTE_PLAINTEXT_SIZE]); + let (mut plaintext, tag) = output.split_ciphertext_at_tag()?; 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.as_mut(), &tag.into()) .ok()?; - let (note, to) = domain.parse_note_plaintext_without_memo_ovk(&pk_d, &plaintext)?; - let memo = domain.extract_memo(&plaintext); + let (compact, memo) = domain.split_plaintext_at_memo(&plaintext)?; + + let (note, to) = domain.parse_note_plaintext_without_memo_ovk(&pk_d, &compact)?; // ZIP 212: Check that the esk provided to this function is consistent with the esk we can // derive from the note. This check corresponds to `ToScalar(PRF^{expand}_{rseed}([4]) = esk` diff --git a/src/note_bytes.rs b/src/note_bytes.rs new file mode 100644 index 0000000..c041fa6 --- /dev/null +++ b/src/note_bytes.rs @@ -0,0 +1,49 @@ +/// 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> { + bytes.try_into().ok().map(NoteBytesData) + } + + 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)) + } +}