Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
4 changes: 2 additions & 2 deletions rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[toolchain]
channel = "1.56.1"
components = ["clippy", "rustfmt"]
channel = "1.61.0"
components = [ "clippy", "rustfmt" ]
8 changes: 4 additions & 4 deletions src/batch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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<D: BatchDomain, Output: ShieldedOutput<D, ENC_CIPHERTEXT_SIZE>>(
pub fn try_note_decryption<D: BatchDomain, Output: ShieldedOutput<D>>(
ivks: &[D::IncomingViewingKey],
outputs: &[(D, Output)],
) -> Vec<Option<((D::Note, D::Recipient, D::Memo), usize)>> {
Expand All @@ -32,14 +32,14 @@ pub fn try_note_decryption<D: BatchDomain, Output: ShieldedOutput<D, ENC_CIPHERT
/// 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_compact_note_decryption<D: BatchDomain, Output: ShieldedOutput<D, COMPACT_NOTE_SIZE>>(
pub fn try_compact_note_decryption<D: BatchDomain, Output: ShieldedOutput<D>>(
ivks: &[D::IncomingViewingKey],
outputs: &[(D, Output)],
) -> Vec<Option<((D::Note, D::Recipient), usize)>> {
batch_note_decryption(ivks, outputs, try_compact_note_decryption_inner)
}

fn batch_note_decryption<D: BatchDomain, Output: ShieldedOutput<D, CS>, F, FR, const CS: usize>(
fn batch_note_decryption<D: BatchDomain, Output: ShieldedOutput<D>, F, FR>(
ivks: &[D::IncomingViewingKey],
outputs: &[(D, Output)],
decrypt_inner: F,
Expand Down
140 changes: 64 additions & 76 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -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;

Expand Down Expand Up @@ -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]);

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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`.
Expand Down Expand Up @@ -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.
Expand All @@ -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.
///
Expand Down Expand Up @@ -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<D: Domain, const CIPHERTEXT_SIZE: usize> {
pub trait ShieldedOutput<D: Domain> {
/// 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>;

/// 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.
Expand Down Expand Up @@ -403,24 +399,18 @@ impl<D: Domain> NoteEncryption<D> {
}

/// 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.
Expand Down Expand Up @@ -465,7 +455,7 @@ impl<D: Domain> NoteEncryption<D> {
///
/// Implements section 4.19.2 of the
/// [Zcash Protocol Specification](https://zips.z.cash/protocol/nu5.pdf#decryptivk).
pub fn try_note_decryption<D: Domain, Output: ShieldedOutput<D, ENC_CIPHERTEXT_SIZE>>(
pub fn try_note_decryption<D: Domain, Output: ShieldedOutput<D>>(
domain: &D,
ivk: &D::IncomingViewingKey,
output: &Output,
Expand All @@ -479,35 +469,29 @@ pub fn try_note_decryption<D: Domain, Output: ShieldedOutput<D, ENC_CIPHERTEXT_S
try_note_decryption_inner(domain, ivk, &ephemeral_key, output, &key)
}

fn try_note_decryption_inner<D: Domain, Output: ShieldedOutput<D, ENC_CIPHERTEXT_SIZE>>(
fn try_note_decryption_inner<D: Domain, Output: ShieldedOutput<D>>(
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))
}
Expand All @@ -517,7 +501,7 @@ fn parse_note_plaintext_without_memo_ivk<D: Domain>(
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)?;

Expand Down Expand Up @@ -564,7 +548,7 @@ fn check_note_validity<D: Domain>(
/// Implements the procedure specified in [`ZIP 307`].
///
/// [`ZIP 307`]: https://zips.z.cash/zip-0307
pub fn try_compact_note_decryption<D: Domain, Output: ShieldedOutput<D, COMPACT_NOTE_SIZE>>(
pub fn try_compact_note_decryption<D: Domain, Output: ShieldedOutput<D>>(
domain: &D,
ivk: &D::IncomingViewingKey,
output: &Output,
Expand All @@ -578,19 +562,20 @@ pub fn try_compact_note_decryption<D: Domain, Output: ShieldedOutput<D, COMPACT_
try_compact_note_decryption_inner(domain, ivk, &ephemeral_key, output, &key)
}

fn try_compact_note_decryption_inner<D: Domain, Output: ShieldedOutput<D, COMPACT_NOTE_SIZE>>(
fn try_compact_note_decryption_inner<D: Domain, Output: ShieldedOutput<D>>(
domain: &D,
ivk: &D::IncomingViewingKey,
ephemeral_key: &EphemeralKeyBytes,
output: &Output,
key: &D::SymmetricKey,
) -> 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,
Expand All @@ -610,7 +595,7 @@ fn try_compact_note_decryption_inner<D: Domain, Output: ShieldedOutput<D, COMPAC
/// Implements [Zcash Protocol Specification section 4.19.3][decryptovk].
///
/// [decryptovk]: https://zips.z.cash/protocol/nu5.pdf#decryptovk
pub fn try_output_recovery_with_ovk<D: Domain, Output: ShieldedOutput<D, ENC_CIPHERTEXT_SIZE>>(
pub fn try_output_recovery_with_ovk<D: Domain, Output: ShieldedOutput<D>>(
domain: &D,
ovk: &D::OutgoingViewingKey,
output: &Output,
Expand All @@ -630,14 +615,12 @@ pub fn try_output_recovery_with_ovk<D: Domain, Output: ShieldedOutput<D, ENC_CIP
/// 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_output_recovery_with_ovk`].
pub fn try_output_recovery_with_ock<D: Domain, Output: ShieldedOutput<D, ENC_CIPHERTEXT_SIZE>>(
pub fn try_output_recovery_with_ock<D: Domain, Output: ShieldedOutput<D>>(
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]);

Expand All @@ -660,22 +643,17 @@ pub fn try_output_recovery_with_ock<D: Domain, Output: ShieldedOutput<D, ENC_CIP
// be okay.
let key = D::kdf(shared_secret, &ephemeral_key);

let mut plaintext = NotePlaintextBytes([0; NOTE_PLAINTEXT_SIZE]);
plaintext
.0
.copy_from_slice(&enc_ciphertext[..NOTE_PLAINTEXT_SIZE]);
let mut enc_ciphertext = output.enc_ciphertext()?.as_ref().to_owned();

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 (note, to) = domain.parse_note_plaintext_without_memo_ovk(&pk_d, &plaintext)?;
let memo = domain.extract_memo(&plaintext);
let (compact, memo) = domain.extract_memo(&plaintext.as_ref().into());

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`
Expand All @@ -694,3 +672,13 @@ pub fn try_output_recovery_with_ock<D: Domain, Output: ShieldedOutput<D, ENC_CIP
None
}
}

// Splits the AEAD tag from the ciphertext.
fn extract_tag(enc_ciphertext: &mut Vec<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.try_into().unwrap();
(plaintext, tag)
}