diff --git a/CHANGELOG.md b/CHANGELOG.md index cedc180..9ec5ec9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,28 @@ 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 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. +- 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/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..4bc6705 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 does not represent a valid 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 byte slice does not represent a valid 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 does not represent a valid 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,34 @@ 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. + fn split_ciphertext_at_tag(&self) -> Option<(D::NotePlaintextBytes, [u8; AEAD_TAG_SIZE])> { + let enc_ciphertext_bytes = self.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. @@ -403,24 +444,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 +500,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 +514,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 +544,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 +591,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 +605,7 @@ pub fn try_compact_note_decryption>( +fn try_compact_note_decryption_inner>( domain: &D, ivk: &D::IncomingViewingKey, ephemeral_key: &EphemeralKeyBytes, @@ -586,11 +613,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 +638,7 @@ fn try_compact_note_decryption_inner>( +pub fn try_output_recovery_with_ovk>( domain: &D, ovk: &D::OutgoingViewingKey, output: &Output, @@ -630,14 +658,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 +686,15 @@ pub fn try_output_recovery_with_ock(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)) + } +}