Skip to content

Commit aae4d02

Browse files
author
joe bebel
committed
update note encryption
1 parent d130e70 commit aae4d02

File tree

7 files changed

+174
-160
lines changed

7 files changed

+174
-160
lines changed

masp_note_encryption/src/lib.rs

Lines changed: 10 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,13 @@ extern crate alloc;
2323
#[cfg(feature = "alloc")]
2424
use alloc::vec::Vec;
2525

26-
use core::convert::TryInto;
27-
2826
use chacha20::{
2927
cipher::{StreamCipher, StreamCipherSeek},
3028
ChaCha20,
3129
};
3230
use chacha20poly1305::{aead::AeadInPlace, ChaCha20Poly1305, KeyInit};
3331
use cipher::KeyIvInit;
32+
use core::convert::TryInto;
3433

3534
//use crate::constants::ASSET_IDENTIFIER_LENGTH;
3635
pub const ASSET_IDENTIFIER_LENGTH: usize = 32;
@@ -176,20 +175,7 @@ pub trait Domain {
176175
fn kdf(secret: Self::SharedSecret, ephemeral_key: &EphemeralKeyBytes) -> Self::SymmetricKey;
177176

178177
/// Encodes the given `Note` and `Memo` as a note plaintext.
179-
///
180-
/// # Future breaking changes
181-
///
182-
/// The `recipient` argument is present as a secondary way to obtain the diversifier;
183-
/// this is due to a historical quirk of how the Sapling `Note` struct was implemented
184-
/// in the `zcash_primitives` crate. `recipient` will be removed from this method in a
185-
/// future crate release, once [`zcash_primitives` has been refactored].
186-
///
187-
/// [`zcash_primitives` has been refactored]: https://github.com/zcash/librustzcash/issues/454
188-
fn note_plaintext_bytes(
189-
note: &Self::Note,
190-
recipient: &Self::Recipient,
191-
memo: &Self::Memo,
192-
) -> NotePlaintextBytes;
178+
fn note_plaintext_bytes(note: &Self::Note, memo: &Self::Memo) -> NotePlaintextBytes;
193179

194180
/// Derives the [`OutgoingCipherKey`] for an encrypted note, given the note-specific
195181
/// public data and an `OutgoingViewingKey`.
@@ -247,8 +233,6 @@ pub trait Domain {
247233
/// which may be passed via `self`).
248234
/// - The note plaintext contains valid encodings of its various fields.
249235
/// - Any domain-specific requirements are satisfied.
250-
/// - `ephemeral_key` can be derived from `esk` and the diversifier within the note
251-
/// plaintext.
252236
///
253237
/// `&self` is passed here to enable the implementation to enforce contextual checks,
254238
/// such as rules like [ZIP 212] that become active at a specific block height.
@@ -257,8 +241,6 @@ pub trait Domain {
257241
fn parse_note_plaintext_without_memo_ovk(
258242
&self,
259243
pk_d: &Self::DiversifiedTransmissionKey,
260-
esk: &Self::EphemeralSecretKey,
261-
ephemeral_key: &EphemeralKeyBytes,
262244
plaintext: &NotePlaintextBytes,
263245
) -> Option<(Self::Note, Self::Recipient)>;
264246

@@ -350,12 +332,10 @@ pub trait ShieldedOutput<D: Domain, const CIPHERTEXT_SIZE: usize> {
350332
///
351333
/// Implements section 4.19 of the
352334
/// [Zcash Protocol Specification](https://zips.z.cash/protocol/nu5.pdf#saplingandorchardinband)
353-
354335
pub struct NoteEncryption<D: Domain> {
355336
epk: D::EphemeralPublicKey,
356337
esk: D::EphemeralSecretKey,
357338
note: D::Note,
358-
to: D::Recipient,
359339
memo: D::Memo,
360340
/// `None` represents the `ovk = ⊥` case.
361341
ovk: Option<D::OutgoingViewingKey>,
@@ -364,18 +344,12 @@ pub struct NoteEncryption<D: Domain> {
364344
impl<D: Domain> NoteEncryption<D> {
365345
/// Construct a new note encryption context for the specified note,
366346
/// recipient, and memo.
367-
pub fn new(
368-
ovk: Option<D::OutgoingViewingKey>,
369-
note: D::Note,
370-
to: D::Recipient,
371-
memo: D::Memo,
372-
) -> Self {
347+
pub fn new(ovk: Option<D::OutgoingViewingKey>, note: D::Note, memo: D::Memo) -> Self {
373348
let esk = D::derive_esk(&note).expect("ZIP 212 is active.");
374349
NoteEncryption {
375350
epk: D::ka_derive_public(&note, &esk),
376351
esk,
377352
note,
378-
to,
379353
memo,
380354
ovk,
381355
}
@@ -390,14 +364,12 @@ impl<D: Domain> NoteEncryption<D> {
390364
esk: D::EphemeralSecretKey,
391365
ovk: Option<D::OutgoingViewingKey>,
392366
note: D::Note,
393-
to: D::Recipient,
394367
memo: D::Memo,
395368
) -> Self {
396369
NoteEncryption {
397370
epk: D::ka_derive_public(&note, &esk),
398371
esk,
399372
note,
400-
to,
401373
memo,
402374
ovk,
403375
}
@@ -418,7 +390,7 @@ impl<D: Domain> NoteEncryption<D> {
418390
let pk_d = D::get_pk_d(&self.note);
419391
let shared_secret = D::ka_agree_enc(&self.esk, &pk_d);
420392
let key = D::kdf(shared_secret, &D::epk_bytes(&self.epk));
421-
let input = D::note_plaintext_bytes(&self.note, &self.to, &self.memo);
393+
let input = D::note_plaintext_bytes(&self.note, &self.memo);
422394

423395
let mut output = [0u8; ENC_CIPHERTEXT_SIZE];
424396
output[..NOTE_PLAINTEXT_SIZE].copy_from_slice(&input.0);
@@ -545,6 +517,8 @@ fn check_note_validity<D: Domain>(
545517
cmstar_bytes: &D::ExtractedCommitmentBytes,
546518
) -> NoteValidity {
547519
if &D::ExtractedCommitmentBytes::from(&D::cmstar(note)) == cmstar_bytes {
520+
// In the case corresponding to specification section 4.19.3, we check that `esk` is equal
521+
// to `D::derive_esk(note)` prior to calling this method.
548522
if let Some(derived_esk) = D::derive_esk(note) {
549523
if D::epk_bytes(&D::ka_derive_public(note, &derived_esk))
550524
.ct_eq(ephemeral_key)
@@ -683,12 +657,12 @@ pub fn try_output_recovery_with_ock<D: Domain, Output: ShieldedOutput<D, ENC_CIP
683657
)
684658
.ok()?;
685659

686-
let (note, to) =
687-
domain.parse_note_plaintext_without_memo_ovk(&pk_d, &esk, &ephemeral_key, &plaintext)?;
660+
let (note, to) = domain.parse_note_plaintext_without_memo_ovk(&pk_d, &plaintext)?;
688661
let memo = domain.extract_memo(&plaintext);
689662

690-
// ZIP 212: Check that the esk provided to this function is consistent with the esk we
691-
// can derive from the note.
663+
// ZIP 212: Check that the esk provided to this function is consistent with the esk we can
664+
// derive from the note. This check corresponds to `ToScalar(PRF^{expand}_{rseed}([4]) = esk`
665+
// in https://zips.z.cash/protocol/protocol.pdf#decryptovk. (`ρ^opt = []` for Sapling.)
692666
if let Some(derived_esk) = D::derive_esk(&note) {
693667
if (!derived_esk.ct_eq(&esk)).into() {
694668
return None;

masp_primitives/src/sapling.rs

Lines changed: 101 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ pub mod prover;
88
pub mod redjubjub;
99
pub mod util;
1010

11-
use bitvec::{order::Lsb0, view::AsBits};
11+
use bitvec::{array::BitArray, order::Lsb0, view::AsBits};
1212
use blake2s_simd::Params as Blake2sParams;
1313
use borsh::{BorshDeserialize, BorshSerialize};
1414
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
@@ -434,14 +434,8 @@ impl PaymentAddress {
434434
self.diversifier.g_d()
435435
}
436436

437-
pub fn create_note(&self, asset_type: AssetType, value: u64, rseed: Rseed) -> Option<Note> {
438-
self.g_d().map(|g_d| Note {
439-
asset_type,
440-
value,
441-
rseed,
442-
g_d,
443-
pk_d: self.pk_d,
444-
})
437+
pub fn create_note(&self, asset_type: AssetType, value: u64, rseed: Rseed) -> Note {
438+
Note::from_parts(asset_type, *self, NoteValue::from_raw(value), rseed)
445439
}
446440
}
447441

@@ -531,9 +525,29 @@ impl ConstantTimeEq for Nullifier {
531525
}
532526
}
533527

534-
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
528+
/// The non-negative value of an individual Sapling note.
529+
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
535530
pub struct NoteValue(u64);
536531

532+
impl NoteValue {
533+
/// Returns the raw underlying value.
534+
pub fn inner(&self) -> u64 {
535+
self.0
536+
}
537+
538+
/// Creates a note value from its raw numeric value.
539+
///
540+
/// This only enforces that the value is an unsigned 64-bit integer. Callers should
541+
/// enforce any additional constraints on the value's valid range themselves.
542+
pub fn from_raw(value: u64) -> Self {
543+
NoteValue(value)
544+
}
545+
546+
pub(crate) fn to_le_bits(self) -> BitArray<[u8; 8], Lsb0> {
547+
BitArray::<_, Lsb0>::new(self.0.to_le_bytes())
548+
}
549+
}
550+
537551
impl TryFrom<u64> for NoteValue {
538552
type Error = ();
539553

@@ -552,36 +566,80 @@ impl From<NoteValue> for u64 {
552566
}
553567
}
554568

569+
impl From<NoteValue> for i128 {
570+
fn from(value: NoteValue) -> i128 {
571+
value.0.into()
572+
}
573+
}
574+
575+
/// A discrete amount of funds received by an address.
555576
#[derive(Clone, Debug, Copy)]
556577
pub struct Note {
557578
/// The asset type that the note represents
558579
pub asset_type: AssetType,
580+
/// The recipient of the funds.
581+
recipient: PaymentAddress,
559582
/// The value of the note
560-
pub value: u64,
561-
/// The diversified base of the address, GH(d)
562-
pub g_d: jubjub::SubgroupPoint,
563-
/// The public key of the address, g_d^ivk
564-
pub pk_d: jubjub::SubgroupPoint,
565-
/// rseed
583+
pub value: NoteValue,
584+
/// The seed randomness for various note components.
566585
pub rseed: Rseed,
567586
}
568587

569588
impl PartialEq for Note {
570589
fn eq(&self, other: &Self) -> bool {
571-
self.value == other.value
572-
&& self.asset_type == other.asset_type
573-
&& self.g_d == other.g_d
574-
&& self.pk_d == other.pk_d
575-
&& self.rcm() == other.rcm()
590+
// Notes are canonically defined by their commitments.
591+
self.cmu().eq(&other.cmu())
576592
}
577593
}
578594

595+
impl Eq for Note {}
596+
579597
impl Note {
580598
pub fn uncommitted() -> bls12_381::Scalar {
581599
// The smallest u-coordinate that is not on the curve
582600
// is one.
583601
bls12_381::Scalar::one()
584602
}
603+
/// Creates a note from its component parts.
604+
///
605+
/// # Caveats
606+
///
607+
/// This low-level constructor enforces that the provided arguments produce an
608+
/// internally valid `Note`. However, it allows notes to be constructed in a way that
609+
/// violates required security checks for note decryption, as specified in
610+
/// [Section 4.19] of the Zcash Protocol Specification. Users of this constructor
611+
/// should only call it with note components that have been fully validated by
612+
/// decrypting a received note according to [Section 4.19].
613+
///
614+
/// [Section 4.19]: https://zips.z.cash/protocol/protocol.pdf#saplingandorchardinband
615+
pub fn from_parts(
616+
asset_type: AssetType,
617+
recipient: PaymentAddress,
618+
value: NoteValue,
619+
rseed: Rseed,
620+
) -> Self {
621+
Note {
622+
asset_type,
623+
recipient,
624+
value,
625+
rseed,
626+
}
627+
}
628+
629+
/// Returns the recipient of this note.
630+
pub fn recipient(&self) -> PaymentAddress {
631+
self.recipient
632+
}
633+
634+
/// Returns the value of this note.
635+
pub fn value(&self) -> NoteValue {
636+
self.value
637+
}
638+
639+
/// Returns the rseed value of this note.
640+
pub fn rseed(&self) -> &Rseed {
641+
&self.rseed
642+
}
585643

586644
/// Computes the note commitment, returning the full point.
587645
fn cm_full_point(&self) -> jubjub::SubgroupPoint {
@@ -592,13 +650,15 @@ impl Note {
592650
note_contents.extend_from_slice(&self.asset_type.asset_generator().to_bytes());
593651

594652
// Writing the value in little endian
595-
note_contents.write_u64::<LittleEndian>(self.value).unwrap();
653+
note_contents
654+
.write_u64::<LittleEndian>(self.value.into())
655+
.unwrap();
596656

597657
// Write g_d
598-
note_contents.extend_from_slice(&self.g_d.to_bytes());
658+
note_contents.extend_from_slice(&self.recipient.g_d().unwrap().to_bytes());
599659

600660
// Write pk_d
601-
note_contents.extend_from_slice(&self.pk_d.to_bytes());
661+
note_contents.extend_from_slice(&self.recipient.pk_d().to_bytes());
602662

603663
assert_eq!(note_contents.len(), 32 + 32 + 32 + 8);
604664

@@ -644,6 +704,9 @@ impl Note {
644704
.get_u()
645705
}
646706

707+
/// Defined in [Zcash Protocol Spec § 4.7.2: Sending Notes (Sapling)][saplingsend].
708+
///
709+
/// [saplingsend]: https://zips.z.cash/protocol/protocol.pdf#saplingsend
647710
pub fn rcm(&self) -> jubjub::Fr {
648711
match self.rseed {
649712
Rseed::BeforeZip212(rcm) => rcm,
@@ -653,6 +716,8 @@ impl Note {
653716
}
654717
}
655718

719+
/// Derives `esk` from the internal `Rseed` value, or generates a random value if this
720+
/// note was created with a v1 (i.e. pre-ZIP 212) note plaintext.
656721
pub fn generate_or_derive_esk<R: RngCore + CryptoRng>(&self, rng: &mut R) -> jubjub::Fr {
657722
self.generate_or_derive_esk_internal(rng)
658723
}
@@ -688,11 +753,11 @@ impl BorshSerialize for Note {
688753
// Write asset type
689754
self.asset_type.serialize(writer)?;
690755
// Write note value
691-
writer.write_u64::<LittleEndian>(self.value)?;
692-
// Write diversified base
693-
writer.write_all(&self.g_d.to_bytes())?;
756+
writer.write_u64::<LittleEndian>(self.value().inner())?;
757+
// Write diversifier
758+
writer.write_all(&self.recipient().diversifier().0)?;
694759
// Write diversified transmission key
695-
writer.write_all(&self.pk_d.to_bytes())?;
760+
writer.write_all(&self.recipient().pk_d().to_bytes())?;
696761
match self.rseed {
697762
Rseed::BeforeZip212(rcm) => {
698763
// Write note plaintext lead byte
@@ -717,9 +782,10 @@ impl BorshDeserialize for Note {
717782
let asset_type = AssetType::deserialize(buf)?;
718783
// Read note value
719784
let value = buf.read_u64::<LittleEndian>()?;
720-
// Read diversified base
721-
let g_d_bytes = <[u8; 32]>::deserialize(buf)?;
722-
let g_d = Option::from(jubjub::SubgroupPoint::from_bytes(&g_d_bytes))
785+
// Read diversifier
786+
let diversifier = Diversifier(<[u8; 11]>::deserialize(buf)?);
787+
diversifier
788+
.g_d()
723789
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "g_d not in field"))?;
724790
// Read diversified transmission key
725791
let pk_d_bytes = <[u8; 32]>::deserialize(buf)?;
@@ -739,9 +805,8 @@ impl BorshDeserialize for Note {
739805
// Finally construct note object
740806
Ok(Note {
741807
asset_type,
742-
value,
743-
g_d,
744-
pk_d,
808+
value: NoteValue::from_raw(value),
809+
recipient: PaymentAddress::from_parts(diversifier, pk_d).unwrap(),
745810
rseed,
746811
})
747812
}
@@ -799,13 +864,12 @@ pub mod testing {
799864
prop_compose! {
800865
pub fn arb_note(value: NoteValue)(
801866
asset_type in crate::asset_type::testing::arb_asset_type(),
802-
addr in arb_payment_address(),
867+
recipient in arb_payment_address(),
803868
rseed in prop::array::uniform32(prop::num::u8::ANY).prop_map(Rseed::AfterZip212)
804869
) -> Note {
805870
Note {
806871
value: value.into(),
807-
g_d: addr.g_d().unwrap(), // this unwrap is safe because arb_payment_address always generates an address with a valid g_d
808-
pk_d: *addr.pk_d(),
872+
recipient,
809873
rseed,
810874
asset_type
811875
}

0 commit comments

Comments
 (0)