V3 encryption (#38)

Added `OrchardDomainV3` on top of the encryption generalization (QED-it/librustzcash#18).

not for review: note_encryption.rs, note_encryptionv2v3.rs and src/test_vectors/note_encryption.rs. These files represent two possible approaches for backward compatibility and will be finalized down the road. (the files were excluded from the build).
This commit is contained in:
Paul 2023-01-31 15:51:29 +02:00 committed by GitHub
parent 4681cc212a
commit cec48d72a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 6025 additions and 2348 deletions

View File

@ -84,3 +84,6 @@ debug = true
[profile.bench]
debug = true
[patch.crates-io]
zcash_note_encryption = { git = "https://github.com/QED-it/librustzcash.git", rev = "07c377ddedf71ab7c7a266d284b054a2dafc2ed4" }

View File

@ -5,7 +5,7 @@ use orchard::{
circuit::ProvingKey,
keys::{FullViewingKey, PreparedIncomingViewingKey, Scope, SpendingKey},
note::AssetId,
note_encryption::{CompactAction, OrchardDomain},
note_encryption_v3::{CompactAction, OrchardDomainV3},
value::NoteValue,
Anchor, Bundle,
};
@ -79,7 +79,7 @@ fn bench_note_decryption(c: &mut Criterion) {
};
let action = bundle.actions().first();
let domain = OrchardDomain::for_action(action);
let domain = OrchardDomainV3::for_action(action);
let compact = {
let mut group = c.benchmark_group("note-decryption");
@ -120,12 +120,12 @@ fn bench_note_decryption(c: &mut Criterion) {
let ivks = 2;
let valid_ivks = vec![valid_ivk; ivks];
let actions: Vec<_> = (0..100)
.map(|_| (OrchardDomain::for_action(action), action.clone()))
.map(|_| (OrchardDomainV3::for_action(action), action.clone()))
.collect();
let compact: Vec<_> = (0..100)
.map(|_| {
(
OrchardDomain::for_action(action),
OrchardDomainV3::for_action(action),
CompactAction::from(action),
)
})

View File

@ -158,7 +158,7 @@ pub(crate) mod testing {
// FIXME: make a real one from the note.
let encrypted_note = TransmittedNoteCiphertext {
epk_bytes: [0u8; 32],
enc_ciphertext: [0u8; 580],
enc_ciphertext: [0u8; 612],
out_ciphertext: [0u8; 80]
};
Action {
@ -192,7 +192,7 @@ pub(crate) mod testing {
// FIXME: make a real one from the note.
let encrypted_note = TransmittedNoteCiphertext {
epk_bytes: [0u8; 32],
enc_ciphertext: [0u8; 580],
enc_ciphertext: [0u8; 612],
out_ciphertext: [0u8; 80]
};

View File

@ -20,7 +20,7 @@ use crate::{
SpendingKey,
},
note::{Note, TransmittedNoteCiphertext},
note_encryption::OrchardNoteEncryption,
note_encryption_v3::OrchardNoteEncryption,
primitives::redpallas::{self, Binding, SpendAuth},
tree::{Anchor, MerklePath},
value::{self, NoteValue, OverflowError, ValueCommitTrapdoor, ValueCommitment, ValueSum},
@ -79,7 +79,12 @@ impl SpendInfo {
/// Returns `None` if the `fvk` does not own the `note`.
///
/// [`Builder::add_spend`]: Builder::add_spend
pub fn new(fvk: FullViewingKey, note: Note, merkle_path: MerklePath) -> Option<Self> {
pub fn new(
fvk: FullViewingKey,
note: Note,
merkle_path: MerklePath,
split_flag: bool,
) -> Option<Self> {
let scope = fvk.scope_for_address(&note.recipient())?;
Some(SpendInfo {
dummy_sk: None,
@ -87,7 +92,7 @@ impl SpendInfo {
scope,
note,
merkle_path,
split_flag: false,
split_flag,
})
}
@ -112,10 +117,7 @@ impl SpendInfo {
/// Return a copy of this note with the split flag set to `true`.
fn create_split_spend(&self) -> Self {
let mut split_spend = SpendInfo::new(self.fvk.clone(), self.note, self.merkle_path.clone())
.expect("The spend info is valid");
split_spend.split_flag = true;
split_spend
SpendInfo::new(self.fvk.clone(), self.note, self.merkle_path.clone(), true).unwrap()
}
}
@ -224,7 +226,7 @@ impl ActionInfo {
let encrypted_note = TransmittedNoteCiphertext {
epk_bytes: encryptor.epk().to_bytes().0,
enc_ciphertext: encryptor.encrypt_note_plaintext(),
enc_ciphertext: encryptor.encrypt_note_plaintext().0,
out_ciphertext: encryptor.encrypt_outgoing_plaintext(&cv_net, &cmx, &mut rng),
};
@ -278,7 +280,7 @@ impl Builder {
/// Returns an error if the given Merkle path does not have the required anchor for
/// the given note.
///
/// [`OrchardDomain`]: crate::note_encryption::OrchardDomain
/// [`OrchardDomain`]: crate::note_encryption_v3::OrchardDomainV3
/// [`MerkleHashOrchard`]: crate::tree::MerkleHashOrchard
pub fn add_spend(
&mut self,

View File

@ -20,7 +20,7 @@ use crate::{
circuit::{Instance, Proof, VerifyingKey},
keys::{IncomingViewingKey, OutgoingViewingKey, PreparedIncomingViewingKey},
note::Note,
note_encryption::OrchardDomain,
note_encryption_v3::OrchardDomainV3,
primitives::redpallas::{self, Binding, SpendAuth},
tree::Anchor,
value::{ValueCommitTrapdoor, ValueCommitment, ValueSum},
@ -305,7 +305,7 @@ impl<T: Authorization, V> Bundle<T, V> {
.iter()
.enumerate()
.filter_map(|(idx, action)| {
let domain = OrchardDomain::for_action(action);
let domain = OrchardDomainV3::for_action(action);
prepared_keys.iter().find_map(|(ivk, prepared_ivk)| {
try_note_decryption(&domain, prepared_ivk, action)
.map(|(n, a, m)| (idx, (*ivk).clone(), n, a, m))
@ -324,7 +324,7 @@ impl<T: Authorization, V> Bundle<T, V> {
) -> Option<(Note, Address, [u8; 512])> {
let prepared_ivk = PreparedIncomingViewingKey::new(key);
self.actions.get(action_idx).and_then(move |action| {
let domain = OrchardDomain::for_action(action);
let domain = OrchardDomainV3::for_action(action);
try_note_decryption(&domain, &prepared_ivk, action)
})
}
@ -341,7 +341,7 @@ impl<T: Authorization, V> Bundle<T, V> {
.iter()
.enumerate()
.filter_map(|(idx, action)| {
let domain = OrchardDomain::for_action(action);
let domain = OrchardDomainV3::for_action(action);
keys.iter().find_map(move |key| {
try_output_recovery_with_ovk(
&domain,
@ -365,7 +365,7 @@ impl<T: Authorization, V> Bundle<T, V> {
key: &OutgoingViewingKey,
) -> Option<(Note, Address, [u8; 512])> {
self.actions.get(action_idx).and_then(move |action| {
let domain = OrchardDomain::for_action(action);
let domain = OrchardDomainV3::for_action(action);
try_output_recovery_with_ovk(
&domain,
key,
@ -527,7 +527,7 @@ pub mod testing {
use super::{Action, Authorization, Authorized, Bundle, Flags};
pub use crate::action::testing::{arb_action, arb_unauthorized_action};
use crate::note::asset_id::testing::zsa_asset_id;
use crate::note::asset_id::testing::arb_zsa_asset_id;
use crate::note::AssetId;
use crate::value::testing::arb_value_sum;
@ -591,7 +591,11 @@ pub mod testing {
prop_compose! {
/// Create an arbitrary vector of assets to burn.
pub fn arb_asset_to_burn()(asset_id in zsa_asset_id(), value in arb_value_sum()) -> (AssetId, ValueSum) {
pub fn arb_asset_to_burn()
(
asset_id in arb_zsa_asset_id(),
value in arb_value_sum()
) -> (AssetId, ValueSum) {
(asset_id, value)
}
}

View File

@ -42,13 +42,13 @@ pub(crate) fn hash_bundle_txid_data<A: Authorization, V: Copy + Into<i64>>(
ch.update(&action.nullifier().to_bytes());
ch.update(&action.cmx().to_bytes());
ch.update(&action.encrypted_note().epk_bytes);
ch.update(&action.encrypted_note().enc_ciphertext[..52]);
ch.update(&action.encrypted_note().enc_ciphertext[..84]); // TODO: make sure it is backward compatible with Orchard [..52]
mh.update(&action.encrypted_note().enc_ciphertext[52..564]);
mh.update(&action.encrypted_note().enc_ciphertext[84..596]);
nh.update(&action.cv_net().to_bytes());
nh.update(&<[u8; 32]>::from(action.rk()));
nh.update(&action.encrypted_note().enc_ciphertext[564..]);
nh.update(&action.encrypted_note().enc_ciphertext[596..]);
nh.update(&action.encrypted_note().out_ciphertext);
}

View File

@ -344,7 +344,7 @@ impl IssueBundle<Signed> {
/// * Asset description size is collect.
/// * `AssetId` for the `IssueAction` has not been previously finalized.
/// * For each `Note` inside an `IssueAction`:
/// * All notes have the same, correct `NoteType`.
/// * All notes have the same, correct `AssetId`.
pub fn verify_issue_bundle(
bundle: &IssueBundle<Signed>,
sighash: [u8; 32],
@ -356,7 +356,6 @@ pub fn verify_issue_bundle(
let s = &mut HashSet::<AssetId>::new();
// An IssueAction could have just one properly derived AssetId.
let newly_finalized = bundle
.actions()
.iter()
@ -373,7 +372,7 @@ pub fn verify_issue_bundle(
return Err(IssueActionPreviouslyFinalizedNoteType(asset));
}
// Add to finalization set, if needed.
// Add to the finalization set, if needed.
if action.is_finalized() {
newly_finalized.insert(asset);
}
@ -1025,30 +1024,31 @@ mod tests {
pub mod testing {
use crate::issuance::{IssueAction, IssueBundle, Prepared, Signed, Unauthorized};
use crate::keys::testing::{arb_issuance_authorizing_key, arb_issuance_validating_key};
use crate::note::asset_id::testing::zsa_asset_id;
use crate::note::testing::arb_zsa_note;
use proptest::collection::vec;
use proptest::prelude::*;
use proptest::prop_compose;
use proptest::string::string_regex;
use rand::{rngs::StdRng, SeedableRng};
prop_compose! {
/// Generate an issue action given note value
pub fn arb_issue_action()(
note in arb_zsa_note(),
asset_descr in string_regex(".{1,512}").unwrap()
) -> IssueAction {
IssueAction::new(asset_descr, &note)
/// Generate an issue action
pub fn arb_issue_action(asset_desc: String)
(
asset in zsa_asset_id(asset_desc.clone()),
)
(
note in arb_zsa_note(asset),
)-> IssueAction {
IssueAction::new(asset_desc.clone(), &note)
}
}
prop_compose! {
/// Generate an arbitrary issue bundle with fake authorization data. This bundle does not
/// necessarily respect consensus rules; for that use
/// [`crate::builder::testing::arb_issue_bundle`]
/// Generate an arbitrary issue bundle with fake authorization data.
pub fn arb_unathorized_issue_bundle(n_actions: usize)
(
actions in vec(arb_issue_action(), n_actions),
actions in vec(arb_issue_action("asset_desc".to_string()), n_actions),
ik in arb_issuance_validating_key()
) -> IssueBundle<Unauthorized> {
IssueBundle {
@ -1061,11 +1061,10 @@ pub mod testing {
prop_compose! {
/// Generate an arbitrary issue bundle with fake authorization data. This bundle does not
/// necessarily respect consensus rules; for that use
/// [`crate::builder::testing::arb_issue_bundle`]
/// necessarily respect consensus rules
pub fn arb_prepared_issue_bundle(n_actions: usize)
(
actions in vec(arb_issue_action(), n_actions),
actions in vec(arb_issue_action("asset_desc".to_string()), n_actions),
ik in arb_issuance_validating_key(),
fake_sighash in prop::array::uniform32(prop::num::u8::ANY)
) -> IssueBundle<Prepared> {
@ -1079,11 +1078,10 @@ pub mod testing {
prop_compose! {
/// Generate an arbitrary issue bundle with fake authorization data. This bundle does not
/// necessarily respect consensus rules; for that use
/// [`crate::builder::testing::arb_issue_bundle`]
/// necessarily respect consensus rules
pub fn arb_signed_issue_bundle(n_actions: usize)
(
actions in vec(arb_issue_action(), n_actions),
actions in vec(arb_issue_action("asset_desc".to_string()), n_actions),
ik in arb_issuance_validating_key(),
isk in arb_issuance_authorizing_key(),
rng_seed in prop::array::uniform32(prop::num::u8::ANY),

View File

@ -25,7 +25,8 @@ mod constants;
pub mod issuance;
pub mod keys;
pub mod note;
pub mod note_encryption;
// pub mod note_encryption; // disabled until backward compatability is implemented.
pub mod note_encryption_v3;
pub mod primitives;
mod spec;
pub mod tree;

View File

@ -279,7 +279,7 @@ pub struct TransmittedNoteCiphertext {
/// The serialization of the ephemeral public key
pub epk_bytes: [u8; 32],
/// The encrypted note ciphertext
pub enc_ciphertext: [u8; 580],
pub enc_ciphertext: [u8; 612],
/// An encrypted value that allows the holder of the outgoing cipher
/// key for the note to recover the note plaintext.
pub out_ciphertext: [u8; 80],
@ -302,7 +302,7 @@ pub mod testing {
use proptest::prelude::*;
use crate::note::asset_id::testing::arb_asset_id;
use crate::note::asset_id::testing::zsa_asset_id;
use crate::note::AssetId;
use crate::value::testing::arb_note_value;
use crate::{
address::testing::arb_address, note::nullifier::testing::arb_nullifier, value::NoteValue,
@ -336,13 +336,30 @@ pub mod testing {
}
prop_compose! {
/// Generate an arbitrary ZSA note
pub fn arb_zsa_note()(
/// Generate an arbitrary native note
pub fn arb_native_note()(
recipient in arb_address(),
value in arb_note_value(),
rho in arb_nullifier(),
rseed in arb_rseed(),
) -> Note {
Note {
recipient,
value,
asset: AssetId::native(),
rho,
rseed,
}
}
}
prop_compose! {
/// Generate an arbitrary zsa note
pub fn arb_zsa_note(asset: AssetId)(
recipient in arb_address(),
value in arb_note_value(),
rho in arb_nullifier(),
rseed in arb_rseed(),
asset in zsa_asset_id(),
) -> Note {
Note {
recipient,

View File

@ -120,8 +120,8 @@ pub mod testing {
}
prop_compose! {
/// Generate the ZSA note type
pub fn zsa_asset_id()(
/// Generate an asset ID
pub fn arb_zsa_asset_id()(
sk in arb_spending_key(),
str in "[A-Za-z]{255}"
) -> AssetId {
@ -130,6 +130,17 @@ pub mod testing {
}
}
prop_compose! {
/// Generate an asset ID using a specific description
pub fn zsa_asset_id(asset_desc: String)(
sk in arb_spending_key(),
) -> AssetId {
assert!(super::is_asset_desc_of_valid_size(&asset_desc));
let isk = IssuanceAuthorizingKey::from(&sk);
AssetId::derive(&IssuanceValidatingKey::from(&isk), &asset_desc)
}
}
#[test]
fn test_vectors() {
let test_vectors = crate::test_vectors::asset_id::test_vectors();

View File

@ -1,12 +1,11 @@
//! In-band secret distribution for Orchard bundles.
use blake2b_simd::{Hash, Params};
use core::fmt;
use group::ff::PrimeField;
use std::fmt;
use zcash_note_encryption::{
BatchDomain, Domain, EphemeralKeyBytes, NotePlaintextBytes, OutPlaintextBytes,
OutgoingCipherKey, ShieldedOutput, COMPACT_NOTE_SIZE, ENC_CIPHERTEXT_SIZE, NOTE_PLAINTEXT_SIZE,
OUT_PLAINTEXT_SIZE,
BatchDomain, Domain, EphemeralKeyBytes, OutPlaintextBytes, OutgoingCipherKey, ShieldedOutput,
AEAD_TAG_SIZE, MEMO_SIZE, OUT_PLAINTEXT_SIZE,
};
use crate::note::AssetId;
@ -24,14 +23,82 @@ use crate::{
const PRF_OCK_ORCHARD_PERSONALIZATION: &[u8; 16] = b"Zcash_Orchardock";
/// The size of the encoding of a ZSA asset type.
const ZSA_TYPE_SIZE: usize = 32;
/// The size of the ZSA variant of COMPACT_NOTE_SIZE.
const COMPACT_ZSA_NOTE_SIZE: usize = COMPACT_NOTE_SIZE + ZSA_TYPE_SIZE;
/// The size of the memo.
const MEMO_SIZE: usize = NOTE_PLAINTEXT_SIZE - COMPACT_NOTE_SIZE;
/// The size of the ZSA variant of the memo.
const ZSA_MEMO_SIZE: usize = NOTE_PLAINTEXT_SIZE - COMPACT_ZSA_NOTE_SIZE;
/// The size of a v2 compact note.
pub const COMPACT_NOTE_SIZE_V2: usize = 1 + // version
11 + // diversifier
8 + // value
32; // rseed (or rcm prior to ZIP 212)
/// The size of [`NotePlaintextBytes`] for V2.
pub const NOTE_PLAINTEXT_SIZE_V2: usize = COMPACT_NOTE_SIZE_V2 + MEMO_SIZE;
/// The size of an encrypted note plaintext.
pub const ENC_CIPHERTEXT_SIZE_V2: usize = NOTE_PLAINTEXT_SIZE_V2 + AEAD_TAG_SIZE;
/// a type to represent the raw bytes of a note plaintext.
#[derive(Clone, Debug)]
pub struct NotePlaintextBytes(pub [u8; NOTE_PLAINTEXT_SIZE_V2]);
/// a type to represent the raw bytes of an encrypted note plaintext.
#[derive(Clone, Debug)]
pub struct NoteCiphertextBytes(pub [u8; ENC_CIPHERTEXT_SIZE_V2]);
/// a type to represent the raw bytes of a compact note.
#[derive(Clone, Debug)]
pub struct CompactNotePlaintextBytes(pub [u8; COMPACT_NOTE_SIZE_V2]);
/// a type to represent the raw bytes of an encrypted compact note.
#[derive(Clone, Debug)]
pub struct CompactNoteCiphertextBytes(pub [u8; COMPACT_NOTE_SIZE_V2]);
impl AsMut<[u8]> for NotePlaintextBytes {
fn as_mut(&mut self) -> &mut [u8] {
self.0.as_mut()
}
}
impl From<&[u8]> for NotePlaintextBytes {
fn from(s: &[u8]) -> Self
where
Self: Sized,
{
NotePlaintextBytes(s.try_into().unwrap())
}
}
impl AsRef<[u8]> for NoteCiphertextBytes {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}
impl From<&[u8]> for NoteCiphertextBytes {
fn from(s: &[u8]) -> Self
where
Self: Sized,
{
NoteCiphertextBytes(s.try_into().unwrap())
}
}
impl AsMut<[u8]> for CompactNotePlaintextBytes {
fn as_mut(&mut self) -> &mut [u8] {
self.0.as_mut()
}
}
impl From<&[u8]> for CompactNotePlaintextBytes {
fn from(s: &[u8]) -> Self
where
Self: Sized,
{
CompactNotePlaintextBytes(s.try_into().unwrap())
}
}
impl AsRef<[u8]> for CompactNoteCiphertextBytes {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}
/// Defined in [Zcash Protocol Spec § 5.4.2: Pseudo Random Functions][concreteprfs].
///
@ -58,59 +125,58 @@ pub(crate) fn prf_ock_orchard(
)
}
/// Domain-specific requirements:
/// - If the note version is 3, the `plaintext` must contain a valid encoding of a ZSA asset type.
/// note_version will return the version of the note plaintext.
pub fn note_version(plaintext: &[u8]) -> Option<u8> {
match plaintext[0] {
0x02 => Some(0x02),
0x03 => Some(0x03),
_ => None,
}
}
fn orchard_parse_note_plaintext_without_memo<F>(
domain: &OrchardDomain,
plaintext: &[u8],
domain: &OrchardDomainV2,
plaintext: &[u8], // TODO: replace with CompactNotePlaintextBytes
get_validated_pk_d: F,
) -> Option<(Note, Address)>
where
F: FnOnce(&Diversifier) -> Option<DiversifiedTransmissionKey>,
{
assert!(plaintext.len() >= COMPACT_NOTE_SIZE);
assert!(plaintext.len() >= COMPACT_NOTE_SIZE_V2);
// Check note plaintext version
// and parse the asset type accordingly.
let asset = parse_version_and_asset_type(plaintext)?;
if note_version(plaintext).unwrap() != 0x02 {
return None;
}
// The unwraps below are guaranteed to succeed by the assertion above
let diversifier = Diversifier::from_bytes(plaintext[1..12].try_into().unwrap());
let value = NoteValue::from_bytes(plaintext[12..20].try_into().unwrap());
let rseed = Option::from(RandomSeed::from_bytes(
plaintext[20..COMPACT_NOTE_SIZE].try_into().unwrap(),
plaintext[20..COMPACT_NOTE_SIZE_V2].try_into().unwrap(),
&domain.rho,
))?;
let pk_d = get_validated_pk_d(&diversifier)?;
let recipient = Address::from_parts(diversifier, pk_d);
let note = Option::from(Note::from_parts(recipient, value, asset, domain.rho, rseed))?;
let note = Option::from(Note::from_parts(
recipient,
value,
AssetId::native(), //V2 notes are always native.
domain.rho,
rseed,
))?;
Some((note, recipient))
}
fn parse_version_and_asset_type(plaintext: &[u8]) -> Option<AssetId> {
// TODO: make this constant-time?
match plaintext[0] {
0x02 => Some(AssetId::native()),
0x03 if plaintext.len() >= COMPACT_ZSA_NOTE_SIZE => {
let bytes = &plaintext[COMPACT_NOTE_SIZE..COMPACT_ZSA_NOTE_SIZE]
.try_into()
.unwrap();
AssetId::from_bytes(bytes).into()
}
_ => None,
}
}
/// Orchard-specific note encryption logic.
#[derive(Debug)]
pub struct OrchardDomain {
pub struct OrchardDomainV2 {
rho: Nullifier,
}
impl memuse::DynamicUsage for OrchardDomain {
impl memuse::DynamicUsage for OrchardDomainV2 {
fn dynamic_usage(&self) -> usize {
self.rho.dynamic_usage()
}
@ -120,21 +186,21 @@ impl memuse::DynamicUsage for OrchardDomain {
}
}
impl OrchardDomain {
impl OrchardDomainV2 {
/// Constructs a domain that can be used to trial-decrypt this action's output note.
pub fn for_action<T>(act: &Action<T>) -> Self {
OrchardDomain {
OrchardDomainV2 {
rho: *act.nullifier(),
}
}
/// Constructs a domain from a nullifier.
pub fn for_nullifier(nullifier: Nullifier) -> Self {
OrchardDomain { rho: nullifier }
OrchardDomainV2 { rho: nullifier }
}
}
impl Domain for OrchardDomain {
impl Domain for OrchardDomainV2 {
type EphemeralSecretKey = EphemeralSecretKey;
type EphemeralPublicKey = EphemeralPublicKey;
type PreparedEphemeralPublicKey = PreparedEphemeralPublicKey;
@ -150,6 +216,11 @@ impl Domain for OrchardDomain {
type ExtractedCommitmentBytes = [u8; 32];
type Memo = [u8; MEMO_SIZE]; // TODO use a more interesting type
type NotePlaintextBytes = NotePlaintextBytes;
type NoteCiphertextBytes = NoteCiphertextBytes;
type CompactNotePlaintextBytes = CompactNotePlaintextBytes;
type CompactNoteCiphertextBytes = CompactNoteCiphertextBytes;
fn derive_esk(note: &Self::Note) -> Option<Self::EphemeralSecretKey> {
Some(note.esk())
}
@ -192,23 +263,12 @@ impl Domain for OrchardDomain {
_: &Self::Recipient,
memo: &Self::Memo,
) -> NotePlaintextBytes {
let is_native: bool = note.asset().is_native().into();
let mut np = [0; NOTE_PLAINTEXT_SIZE];
np[0] = if is_native { 0x02 } else { 0x03 };
let mut np = [0; NOTE_PLAINTEXT_SIZE_V2];
np[0] = 0x02;
np[1..12].copy_from_slice(note.recipient().diversifier().as_array());
np[12..20].copy_from_slice(&note.value().to_bytes());
// todo: add asset_id
np[20..52].copy_from_slice(note.rseed().as_bytes());
if is_native {
np[52..].copy_from_slice(memo);
} else {
let zsa_type = note.asset().to_bytes();
np[52..84].copy_from_slice(&zsa_type);
let short_memo = &memo[0..memo.len() - ZSA_TYPE_SIZE];
np[84..].copy_from_slice(short_memo);
// TODO: handle full-size memo or make short_memo explicit.
};
np[52..].copy_from_slice(memo);
NotePlaintextBytes(np)
}
@ -246,9 +306,9 @@ impl Domain for OrchardDomain {
fn parse_note_plaintext_without_memo_ivk(
&self,
ivk: &Self::IncomingViewingKey,
plaintext: &[u8],
plaintext: &CompactNotePlaintextBytes,
) -> Option<(Self::Note, Self::Recipient)> {
orchard_parse_note_plaintext_without_memo(self, plaintext, |diversifier| {
orchard_parse_note_plaintext_without_memo(self, &plaintext.0, |diversifier| {
Some(DiversifiedTransmissionKey::derive(ivk, diversifier))
})
}
@ -258,7 +318,7 @@ impl Domain for OrchardDomain {
pk_d: &Self::DiversifiedTransmissionKey,
esk: &Self::EphemeralSecretKey,
ephemeral_key: &EphemeralKeyBytes,
plaintext: &NotePlaintextBytes,
plaintext: &CompactNotePlaintextBytes,
) -> Option<(Self::Note, Self::Recipient)> {
orchard_parse_note_plaintext_without_memo(self, &plaintext.0, |diversifier| {
if esk
@ -274,21 +334,12 @@ impl Domain for OrchardDomain {
})
}
fn extract_memo(&self, plaintext: &NotePlaintextBytes) -> Self::Memo {
let mut memo = [0; MEMO_SIZE];
match get_note_plaintext_version(plaintext) {
0x02 => {
let full_memo = &plaintext.0[COMPACT_NOTE_SIZE..NOTE_PLAINTEXT_SIZE];
memo.copy_from_slice(full_memo);
}
0x03 => {
// ZSA note plaintexts have a shorter memo.
let short_memo = &plaintext.0[COMPACT_ZSA_NOTE_SIZE..NOTE_PLAINTEXT_SIZE];
memo[..ZSA_MEMO_SIZE].copy_from_slice(short_memo);
}
_ => {}
};
memo
fn extract_memo(
&self,
plaintext: &NotePlaintextBytes,
) -> (Self::CompactNotePlaintextBytes, Self::Memo) {
let (compact, memo) = plaintext.0.split_at(COMPACT_NOTE_SIZE_V2);
(compact.try_into().unwrap(), memo.try_into().unwrap())
}
fn extract_pk_d(out_plaintext: &OutPlaintextBytes) -> Option<Self::DiversifiedTransmissionKey> {
@ -301,7 +352,7 @@ impl Domain for OrchardDomain {
}
}
impl BatchDomain for OrchardDomain {
impl BatchDomain for OrchardDomainV2 {
fn batch_kdf<'a>(
items: impl Iterator<Item = (Option<Self::SharedSecret>, &'a EphemeralKeyBytes)>,
) -> Vec<Option<Self::SymmetricKey>> {
@ -316,14 +367,10 @@ impl BatchDomain for OrchardDomain {
}
}
fn get_note_plaintext_version(plaintext: &NotePlaintextBytes) -> u8 {
plaintext.0[0]
}
/// Implementation of in-band secret distribution for Orchard bundles.
pub type OrchardNoteEncryption = zcash_note_encryption::NoteEncryption<OrchardDomain>;
pub type OrchardNoteEncryption = zcash_note_encryption::NoteEncryption<OrchardDomainV2>;
impl<T> ShieldedOutput<OrchardDomain, ENC_CIPHERTEXT_SIZE> for Action<T> {
impl<T> ShieldedOutput<OrchardDomainV2> for Action<T> {
fn ephemeral_key(&self) -> EphemeralKeyBytes {
EphemeralKeyBytes(self.encrypted_note().epk_bytes)
}
@ -332,8 +379,16 @@ impl<T> ShieldedOutput<OrchardDomain, ENC_CIPHERTEXT_SIZE> for Action<T> {
self.cmx().to_bytes()
}
fn enc_ciphertext(&self) -> &[u8; ENC_CIPHERTEXT_SIZE] {
&self.encrypted_note().enc_ciphertext
fn enc_ciphertext(&self) -> Option<NoteCiphertextBytes> {
Some(NoteCiphertextBytes(self.encrypted_note().enc_ciphertext))
}
fn enc_ciphertext_compact(&self) -> CompactNoteCiphertextBytes {
CompactNoteCiphertextBytes(
self.encrypted_note().enc_ciphertext[..COMPACT_NOTE_SIZE_V2]
.try_into()
.unwrap(),
)
}
}
@ -342,7 +397,7 @@ pub struct CompactAction {
nullifier: Nullifier,
cmx: ExtractedNoteCommitment,
ephemeral_key: EphemeralKeyBytes,
enc_ciphertext: [u8; COMPACT_NOTE_SIZE],
enc_ciphertext: CompactNoteCiphertextBytes,
}
impl fmt::Debug for CompactAction {
@ -351,20 +406,25 @@ impl fmt::Debug for CompactAction {
}
}
impl<T> From<&Action<T>> for CompactAction {
impl<T> From<&Action<T>> for CompactAction
where
Action<T>: ShieldedOutput<OrchardDomainV2>,
{
fn from(action: &Action<T>) -> Self {
CompactAction {
nullifier: *action.nullifier(),
cmx: *action.cmx(),
ephemeral_key: action.ephemeral_key(),
enc_ciphertext: action.encrypted_note().enc_ciphertext[..COMPACT_NOTE_SIZE]
.try_into()
.unwrap(),
enc_ciphertext: CompactNoteCiphertextBytes(
action.encrypted_note().enc_ciphertext[..COMPACT_NOTE_SIZE_V2]
.try_into()
.unwrap(),
),
}
}
}
impl ShieldedOutput<OrchardDomain, COMPACT_NOTE_SIZE> for CompactAction {
impl ShieldedOutput<OrchardDomainV2> for CompactAction {
fn ephemeral_key(&self) -> EphemeralKeyBytes {
EphemeralKeyBytes(self.ephemeral_key.0)
}
@ -373,8 +433,12 @@ impl ShieldedOutput<OrchardDomain, COMPACT_NOTE_SIZE> for CompactAction {
self.cmx.to_bytes()
}
fn enc_ciphertext(&self) -> &[u8; COMPACT_NOTE_SIZE] {
&self.enc_ciphertext
fn enc_ciphertext(&self) -> Option<NoteCiphertextBytes> {
None
}
fn enc_ciphertext_compact(&self) -> CompactNoteCiphertextBytes {
self.enc_ciphertext.clone()
}
}
@ -384,7 +448,7 @@ impl CompactAction {
nullifier: Nullifier,
cmx: ExtractedNoteCommitment,
ephemeral_key: EphemeralKeyBytes,
enc_ciphertext: [u8; 52],
enc_ciphertext: CompactNoteCiphertextBytes,
) -> Self {
Self {
nullifier,
@ -402,14 +466,14 @@ impl CompactAction {
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use proptest::proptest;
use rand::rngs::OsRng;
use zcash_note_encryption::{
try_compact_note_decryption, try_note_decryption, try_output_recovery_with_ovk, Domain,
EphemeralKeyBytes,
};
use super::{prf_ock_orchard, CompactAction, OrchardDomain, OrchardNoteEncryption};
use super::{prf_ock_orchard, CompactAction, OrchardDomainV2, OrchardNoteEncryption};
use crate::note::AssetId;
use crate::{
action::Action,
@ -418,53 +482,46 @@ mod tests {
OutgoingViewingKey, PreparedIncomingViewingKey,
},
note::{
testing::arb_note, ExtractedNoteCommitment, Nullifier, RandomSeed,
testing::arb_native_note, ExtractedNoteCommitment, Nullifier, RandomSeed,
TransmittedNoteCiphertext,
},
note_encryption::{
note_version, orchard_parse_note_plaintext_without_memo, NoteCiphertextBytes,
},
primitives::redpallas,
value::{NoteValue, ValueCommitment},
Address, Note,
};
use super::{get_note_plaintext_version, orchard_parse_note_plaintext_without_memo};
proptest! {
#[test]
fn test_encoding_roundtrip(
note in arb_note(NoteValue::from_raw(10)),
) {
let memo = &crate::test_vectors::note_encryption::test_vectors()[0].memo;
#[test]
fn test_encoding_roundtrip(
note in arb_native_note(),
) {
let memo = &crate::test_vectors::note_encryption::test_vectors()[0].memo;
// Encode.
let plaintext = OrchardDomain::note_plaintext_bytes(&note, &note.recipient(), memo);
// Encode.
let mut plaintext = OrchardDomainV2::note_plaintext_bytes(&note, &note.recipient(), memo);
// Decode.
let domain = OrchardDomain { rho: note.rho() };
let parsed_version = get_note_plaintext_version(&plaintext);
let parsed_memo = domain.extract_memo(&plaintext);
// Decode.
let domain = OrchardDomainV2 { rho: note.rho() };
let parsed_version = note_version(plaintext.as_mut()).unwrap();
let (compact,parsed_memo) = domain.extract_memo(&plaintext);
let (parsed_note, parsed_recipient) = orchard_parse_note_plaintext_without_memo(&domain, &plaintext.0,
|diversifier| {
assert_eq!(diversifier, &note.recipient().diversifier());
Some(*note.recipient().pk_d())
}
).expect("Plaintext parsing failed");
let (parsed_note, parsed_recipient) = orchard_parse_note_plaintext_without_memo(&domain, &compact.0,
|diversifier| {
assert_eq!(diversifier, &note.recipient().diversifier());
Some(*note.recipient().pk_d())
}
).expect("Plaintext parsing failed");
// Check.
assert_eq!(parsed_note, note);
assert_eq!(parsed_recipient, note.recipient());
if parsed_note.asset().is_native().into() {
assert_eq!(parsed_version, 0x02);
// Check.
assert_eq!(parsed_note, note);
assert_eq!(parsed_recipient, note.recipient());
assert_eq!(&parsed_memo, memo);
} else {
assert_eq!(parsed_version, 0x03);
let mut short_memo = *memo;
short_memo[512 - 32..].copy_from_slice(&[0; 32]);
assert_eq!(parsed_memo, short_memo);
assert_eq!(parsed_version, 0x02);
}
}
}
#[test]
fn test_vectors() {
@ -509,13 +566,7 @@ mod tests {
assert_eq!(ock.as_ref(), tv.ock);
let recipient = Address::from_parts(d, pk_d);
let asset = match tv.asset {
None => AssetId::native(),
Some(type_bytes) => AssetId::from_bytes(&type_bytes).unwrap(),
};
let note = Note::from_parts(recipient, value, asset, rho, rseed).unwrap();
let note = Note::from_parts(recipient, value, AssetId::native(), rho, rseed).unwrap();
assert_eq!(ExtractedNoteCommitment::from(note.commitment()), cmx);
let action = Action::from_parts(
@ -538,7 +589,7 @@ mod tests {
// (Tested first because it only requires immutable references.)
//
let domain = OrchardDomain { rho };
let domain = OrchardDomainV2 { rho };
match try_note_decryption(&domain, &ivk, &action) {
Some((decrypted_note, decrypted_to, decrypted_memo)) => {
@ -554,8 +605,7 @@ mod tests {
assert_eq!(decrypted_note, note);
assert_eq!(decrypted_to, recipient);
}
None => assert!(tv.asset.is_some(), "Compact note decryption failed"),
// Ignore that ZSA notes are not detected in compact decryption.
None => panic!("Compact note decryption failed"),
}
match try_output_recovery_with_ovk(&domain, &ovk, &action, &cv_net, &tv.c_out) {

717
src/note_encryption_v2v3.rs Normal file
View File

@ -0,0 +1,717 @@
//! In-band secret distribution for Orchard bundles.
use blake2b_simd::{Hash, Params};
use core::fmt;
use group::ff::PrimeField;
use zcash_note_encryption::{
BatchDomain, Domain, EphemeralKeyBytes, OutPlaintextBytes, OutgoingCipherKey, ShieldedOutput,
AEAD_TAG_SIZE, MEMO_SIZE, OUT_PLAINTEXT_SIZE,
};
use crate::note::AssetId;
use crate::{
action::Action,
keys::{
DiversifiedTransmissionKey, Diversifier, EphemeralPublicKey, EphemeralSecretKey,
IncomingViewingKey, OutgoingViewingKey, SharedSecret,
},
note::{ExtractedNoteCommitment, Nullifier, RandomSeed},
spec::diversify_hash,
value::{NoteValue, ValueCommitment},
Address, Note,
};
const PRF_OCK_ORCHARD_PERSONALIZATION: &[u8; 16] = b"Zcash_Orchardock";
/// The size of a v2 compact note.
pub const COMPACT_NOTE_SIZE_V2: usize = 1 + // version
11 + // diversifier
8 + // value
32; // rseed (or rcm prior to ZIP 212)
/// The size of [`NotePlaintextBytes`] for V2.
pub const NOTE_PLAINTEXT_SIZE_V2: usize = COMPACT_NOTE_SIZE_V2 + MEMO_SIZE;
/// The size of an encrypted note plaintext.
pub const ENC_CIPHERTEXT_SIZE_V2: usize = NOTE_PLAINTEXT_SIZE_V2 + AEAD_TAG_SIZE;
/// The size of the encoding of a ZSA asset id.
const ZSA_ASSET_SIZE: usize = 32;
/// The size of a v3 compact note.
const COMPACT_NOTE_SIZE_V3: usize = COMPACT_NOTE_SIZE_V2 + ZSA_ASSET_SIZE;
/// The size of [`NotePlaintextBytes`] for V3.
const NOTE_PLAINTEXT_SIZE_V3: usize = COMPACT_NOTE_SIZE_V3 + MEMO_SIZE;
/// The size of the encrypted ciphertext of the ZSA variant of a note.
const ENC_CIPHERTEXT_SIZE_V3: usize = NOTE_PLAINTEXT_SIZE_V3 + AEAD_TAG_SIZE;
/// Defined in [Zcash Protocol Spec § 5.4.2: Pseudo Random Functions][concreteprfs].
///
/// [concreteprfs]: https://zips.z.cash/protocol/nu5.pdf#concreteprfs
pub(crate) fn prf_ock_orchard(
ovk: &OutgoingViewingKey,
cv: &ValueCommitment,
cmx_bytes: &[u8; 32],
ephemeral_key: &EphemeralKeyBytes,
) -> OutgoingCipherKey {
OutgoingCipherKey(
Params::new()
.hash_length(32)
.personal(PRF_OCK_ORCHARD_PERSONALIZATION)
.to_state()
.update(ovk.as_ref())
.update(&cv.to_bytes())
.update(cmx_bytes)
.update(ephemeral_key.as_ref())
.finalize()
.as_bytes()
.try_into()
.unwrap(),
)
}
// TODO: VA: Needs updating
/// Domain-specific requirements:
/// - If the note version is 3, the `plaintext` must contain a valid encoding of a ZSA asset type.
fn orchard_parse_note_plaintext_without_memo<F>(
domain: &OrchardDomain,
plaintext: &CompactNotePlaintextBytes,
get_validated_pk_d: F,
) -> Option<(Note, Address)>
where
F: FnOnce(&Diversifier) -> Option<DiversifiedTransmissionKey>,
{
// Check note plaintext version
// and parse the asset type accordingly.
let asset = parse_version_and_asset_type(plaintext)?;
let mut plaintext_inner = [0u8; COMPACT_NOTE_SIZE_V2];
match plaintext {
CompactNotePlaintextBytes::V2(x) => {
plaintext_inner.copy_from_slice(&x[..COMPACT_NOTE_SIZE_V2])
}
CompactNotePlaintextBytes::V3(x) => {
plaintext_inner.copy_from_slice(&x[..COMPACT_NOTE_SIZE_V2])
}
}
// The unwraps below are guaranteed to succeed by the assertion above
let diversifier = Diversifier::from_bytes(plaintext_inner[1..12].try_into().unwrap());
let value = NoteValue::from_bytes(plaintext_inner[12..20].try_into().unwrap());
let rseed = Option::from(RandomSeed::from_bytes(
plaintext_inner[20..COMPACT_NOTE_SIZE_V2]
.try_into()
.unwrap(),
&domain.rho,
))?;
let pk_d = get_validated_pk_d(&diversifier)?;
let recipient = Address::from_parts(diversifier, pk_d);
let note = Option::from(Note::from_parts(recipient, value, asset, domain.rho, rseed))?;
Some((note, recipient))
}
fn parse_version_and_asset_type(plaintext: &CompactNotePlaintextBytes) -> Option<AssetId> {
match plaintext {
CompactNotePlaintextBytes::V2(x) if x[0] == 0x02 => Some(AssetId::native()),
CompactNotePlaintextBytes::V3(x) if x[0] == 0x03 => {
let bytes = x[COMPACT_NOTE_SIZE_V2..COMPACT_NOTE_SIZE_V3]
.try_into()
.unwrap();
AssetId::from_bytes(bytes).into()
}
_ => None,
}
}
/// Orchard-specific note encryption logic.
#[derive(Debug)]
pub struct OrchardDomain {
rho: Nullifier,
}
/// Newtype for encoding the note plaintext post ZSA.
// pub struct NotePlaintextZSA (pub [u8; ZSA_NOTE_PLAINTEXT_SIZE]);
#[derive(Clone, Debug)]
pub enum NotePlaintextBytes {
/// Variant for old note plaintexts.
V2([u8; NOTE_PLAINTEXT_SIZE_V2]),
/// Variant for the new note plaintexts post ZSA.
V3([u8; NOTE_PLAINTEXT_SIZE_V3]),
}
impl AsMut<[u8]> for NotePlaintextBytes {
fn as_mut(&mut self) -> &mut [u8] {
match self {
NotePlaintextBytes::V2(x) => x.as_mut(),
NotePlaintextBytes::V3(x) => x.as_mut(),
}
}
}
impl From<&[u8]> for NotePlaintextBytes {
fn from(s: &[u8]) -> Self
where
Self: Sized,
{
match s.len() {
NOTE_PLAINTEXT_SIZE_V2 => NotePlaintextBytes::V2(s.try_into().unwrap()),
NOTE_PLAINTEXT_SIZE_V3 => NotePlaintextBytes::V3(s.try_into().unwrap()),
_ => panic!("Invalid note plaintext size"),
}
}
}
/// Newtype for encoding the encrypted note ciphertext post ZSA.
// pub struct EncNoteCiphertextZSA (pub [u8; ZSA_ENC_CIPHERTEXT_SIZE]);
#[derive(Clone, Debug)]
pub enum NoteCiphertextBytes {
/// Variant for old encrypted note ciphertexts.
V2([u8; ENC_CIPHERTEXT_SIZE_V2]),
/// Variant for new encrypted note ciphertexts post ZSA.
V3([u8; ENC_CIPHERTEXT_SIZE_V3]),
}
impl AsRef<[u8]> for NoteCiphertextBytes {
fn as_ref(&self) -> &[u8] {
match self {
NoteCiphertextBytes::V2(x) => x,
NoteCiphertextBytes::V3(x) => x,
}
}
}
/// Panics if the given slice is not `ENC_CIPHERTEXT_SIZE_V2` or `ENC_CIPHERTEXT_SIZE_V3` bytes long.
impl From<&[u8]> for NoteCiphertextBytes {
fn from(s: &[u8]) -> Self
where
Self: Sized,
{
match s.len() {
ENC_CIPHERTEXT_SIZE_V2 => NoteCiphertextBytes::V2(s.try_into().unwrap()),
ENC_CIPHERTEXT_SIZE_V3 => NoteCiphertextBytes::V3(s.try_into().unwrap()),
_ => panic!("Invalid length for compact note plaintext"),
}
}
}
/// Newtype for encoding a compact note
#[derive(Clone, Debug)]
pub enum CompactNotePlaintextBytes {
/// Variant for old compact notes.
V2([u8; COMPACT_NOTE_SIZE_V2]),
/// Variant for new compact notes post ZSA.
V3([u8; COMPACT_NOTE_SIZE_V3]),
}
impl AsMut<[u8]> for CompactNotePlaintextBytes {
fn as_mut(&mut self) -> &mut [u8] {
match self {
CompactNotePlaintextBytes::V2(x) => x,
CompactNotePlaintextBytes::V3(x) => x,
}
}
}
/// Panics if the given slice is not `COMPACT_NOTE_SIZE_V2` or `COMPACT_NOTE_SIZE_V3` bytes long.
impl From<&[u8]> for CompactNotePlaintextBytes {
fn from(s: &[u8]) -> Self
where
Self: Sized,
{
match s.len() {
COMPACT_NOTE_SIZE_V2 => CompactNotePlaintextBytes::V2(s.try_into().unwrap()),
COMPACT_NOTE_SIZE_V3 => CompactNotePlaintextBytes::V3(s.try_into().unwrap()),
_ => panic!("Invalid length for compact note plaintext"),
}
}
}
/// Newtype for encoding a compact note
#[derive(Clone, Debug)]
pub enum CompactNoteCiphertextBytes {
/// Variant for old compact notes.
V2([u8; COMPACT_NOTE_SIZE_V2]),
/// Variant for new compact notes post ZSA.
V3([u8; COMPACT_NOTE_SIZE_V3]),
}
impl AsRef<[u8]> for CompactNoteCiphertextBytes {
fn as_ref(&self) -> &[u8] {
match self {
CompactNoteCiphertextBytes::V2(x) => x,
CompactNoteCiphertextBytes::V3(x) => x,
}
}
}
impl OrchardDomain {
/// Constructs a domain that can be used to trial-decrypt this action's output note.
pub fn for_action<T>(act: &Action<T>) -> Self {
OrchardDomain {
rho: *act.nullifier(),
}
}
/// Constructs a domain from a nullifier.
pub fn for_nullifier(nullifier: Nullifier) -> Self {
OrchardDomain { rho: nullifier }
}
}
impl Domain for OrchardDomain {
type EphemeralSecretKey = EphemeralSecretKey;
type EphemeralPublicKey = EphemeralPublicKey;
type PreparedEphemeralPublicKey = EphemeralPublicKey;
type SharedSecret = SharedSecret;
type SymmetricKey = Hash;
type Note = Note;
type Recipient = Address;
type DiversifiedTransmissionKey = DiversifiedTransmissionKey;
type IncomingViewingKey = IncomingViewingKey;
type OutgoingViewingKey = OutgoingViewingKey;
type ValueCommitment = ValueCommitment;
type ExtractedCommitment = ExtractedNoteCommitment;
type ExtractedCommitmentBytes = [u8; 32];
type Memo = [u8; MEMO_SIZE]; // TODO use a more interesting type
type NotePlaintextBytes = NotePlaintextBytes;
type NoteCiphertextBytes = NoteCiphertextBytes;
type CompactNotePlaintextBytes = CompactNotePlaintextBytes;
type CompactNoteCiphertextBytes = CompactNoteCiphertextBytes;
fn derive_esk(note: &Self::Note) -> Option<Self::EphemeralSecretKey> {
Some(note.esk())
}
fn get_pk_d(note: &Self::Note) -> Self::DiversifiedTransmissionKey {
*note.recipient().pk_d()
}
fn prepare_epk(epk: Self::EphemeralPublicKey) -> Self::PreparedEphemeralPublicKey {
epk
}
fn ka_derive_public(
note: &Self::Note,
esk: &Self::EphemeralSecretKey,
) -> Self::EphemeralPublicKey {
esk.derive_public(note.recipient().g_d())
}
fn ka_agree_enc(
esk: &Self::EphemeralSecretKey,
pk_d: &Self::DiversifiedTransmissionKey,
) -> Self::SharedSecret {
esk.agree(pk_d)
}
fn ka_agree_dec(
ivk: &Self::IncomingViewingKey,
epk: &Self::PreparedEphemeralPublicKey,
) -> Self::SharedSecret {
epk.agree(ivk)
}
fn kdf(secret: Self::SharedSecret, ephemeral_key: &EphemeralKeyBytes) -> Self::SymmetricKey {
secret.kdf_orchard(ephemeral_key)
}
fn note_plaintext_bytes(
note: &Self::Note,
_: &Self::Recipient,
memo: &Self::Memo,
) -> NotePlaintextBytes {
let mut np = [0u8; NOTE_PLAINTEXT_SIZE_V3];
np[0] = 0x03;
np[1..12].copy_from_slice(note.recipient().diversifier().as_array());
np[12..20].copy_from_slice(&note.value().to_bytes());
np[20..52].copy_from_slice(note.rseed().as_bytes());
let zsa_type = note.asset().to_bytes();
np[52..84].copy_from_slice(&zsa_type);
np[84..].copy_from_slice(memo);
NotePlaintextBytes::V3(np)
}
fn derive_ock(
ovk: &Self::OutgoingViewingKey,
cv: &Self::ValueCommitment,
cmstar_bytes: &Self::ExtractedCommitmentBytes,
ephemeral_key: &EphemeralKeyBytes,
) -> OutgoingCipherKey {
prf_ock_orchard(ovk, cv, cmstar_bytes, ephemeral_key)
}
fn outgoing_plaintext_bytes(
note: &Self::Note,
esk: &Self::EphemeralSecretKey,
) -> OutPlaintextBytes {
let mut op = [0; OUT_PLAINTEXT_SIZE];
op[..32].copy_from_slice(&note.recipient().pk_d().to_bytes());
op[32..].copy_from_slice(&esk.0.to_repr());
OutPlaintextBytes(op)
}
fn epk_bytes(epk: &Self::EphemeralPublicKey) -> EphemeralKeyBytes {
epk.to_bytes()
}
fn epk(ephemeral_key: &EphemeralKeyBytes) -> Option<Self::EphemeralPublicKey> {
EphemeralPublicKey::from_bytes(&ephemeral_key.0).into()
}
fn cmstar(note: &Self::Note) -> Self::ExtractedCommitment {
note.commitment().into()
}
fn parse_note_plaintext_without_memo_ivk(
&self,
ivk: &Self::IncomingViewingKey,
plaintext: &CompactNotePlaintextBytes,
) -> Option<(Self::Note, Self::Recipient)> {
orchard_parse_note_plaintext_without_memo(self, plaintext, |diversifier| {
Some(DiversifiedTransmissionKey::derive(ivk, diversifier))
})
}
fn parse_note_plaintext_without_memo_ovk(
&self,
pk_d: &Self::DiversifiedTransmissionKey,
esk: &Self::EphemeralSecretKey,
ephemeral_key: &EphemeralKeyBytes,
plaintext: &CompactNotePlaintextBytes,
) -> Option<(Self::Note, Self::Recipient)> {
orchard_parse_note_plaintext_without_memo(self, plaintext, |diversifier| {
if esk
.derive_public(diversify_hash(diversifier.as_array()))
.to_bytes()
.0
== ephemeral_key.0
{
Some(*pk_d)
} else {
None
}
})
}
fn extract_memo(
&self,
plaintext: &NotePlaintextBytes,
) -> (Self::CompactNotePlaintextBytes, Self::Memo) {
match plaintext {
NotePlaintextBytes::V2(np) => {
let (compact, memo) = np.split_at(COMPACT_NOTE_SIZE_V2);
(
CompactNotePlaintextBytes::V2(compact.try_into().unwrap()),
memo.try_into().unwrap(),
)
}
NotePlaintextBytes::V3(np) => {
let (compact, memo) = np.split_at(COMPACT_NOTE_SIZE_V3);
(
CompactNotePlaintextBytes::V3(compact.try_into().unwrap()),
memo.try_into().unwrap(),
)
}
}
}
fn extract_pk_d(out_plaintext: &OutPlaintextBytes) -> Option<Self::DiversifiedTransmissionKey> {
DiversifiedTransmissionKey::from_bytes(out_plaintext.0[0..32].try_into().unwrap()).into()
}
fn extract_esk(out_plaintext: &OutPlaintextBytes) -> Option<Self::EphemeralSecretKey> {
EphemeralSecretKey::from_bytes(out_plaintext.0[32..OUT_PLAINTEXT_SIZE].try_into().unwrap())
.into()
}
}
impl BatchDomain for OrchardDomain {
fn batch_kdf<'a>(
items: impl Iterator<Item = (Option<Self::SharedSecret>, &'a EphemeralKeyBytes)>,
) -> Vec<Option<Self::SymmetricKey>> {
let (shared_secrets, ephemeral_keys): (Vec<_>, Vec<_>) = items.unzip();
SharedSecret::batch_to_affine(shared_secrets)
.zip(ephemeral_keys.into_iter())
.map(|(secret, ephemeral_key)| {
secret.map(|dhsecret| SharedSecret::kdf_orchard_inner(dhsecret, ephemeral_key))
})
.collect()
}
}
/// Implementation of in-band secret distribution for Orchard bundles.
pub type OrchardNoteEncryption = zcash_note_encryption::NoteEncryption<OrchardDomain>;
impl<T> ShieldedOutput<OrchardDomain> for Action<T> {
fn ephemeral_key(&self) -> EphemeralKeyBytes {
EphemeralKeyBytes(self.encrypted_note().epk_bytes)
}
fn cmstar_bytes(&self) -> [u8; 32] {
self.cmx().to_bytes()
}
fn enc_ciphertext(&self) -> Option<NoteCiphertextBytes> {
let result = self.encrypted_note().enc_ciphertext.clone();
Some(result)
}
fn enc_ciphertext_compact(&self) -> CompactNoteCiphertextBytes {
match self.encrypted_note().enc_ciphertext {
NoteCiphertextBytes::V2(ncx) => {
CompactNoteCiphertextBytes::V2(ncx[..COMPACT_NOTE_SIZE_V2].try_into().unwrap())
}
NoteCiphertextBytes::V3(ncx) => {
CompactNoteCiphertextBytes::V3(ncx[..COMPACT_NOTE_SIZE_V3].try_into().unwrap())
}
}
}
}
/// A compact Action for light clients.
pub struct CompactAction {
nullifier: Nullifier,
cmx: ExtractedNoteCommitment,
ephemeral_key: EphemeralKeyBytes,
enc_ciphertext: CompactNoteCiphertextBytes,
}
impl fmt::Debug for CompactAction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "CompactAction")
}
}
impl<T> From<&Action<T>> for CompactAction {
fn from(action: &Action<T>) -> Self {
let comp_ciphertext: CompactNoteCiphertextBytes =
match action.encrypted_note().enc_ciphertext {
NoteCiphertextBytes::V2(ncx) => {
CompactNoteCiphertextBytes::V2(ncx[..COMPACT_NOTE_SIZE_V2].try_into().unwrap())
}
NoteCiphertextBytes::V3(ncx) => {
CompactNoteCiphertextBytes::V3(ncx[..COMPACT_NOTE_SIZE_V3].try_into().unwrap())
}
};
CompactAction {
nullifier: *action.nullifier(),
cmx: *action.cmx(),
ephemeral_key: action.ephemeral_key(),
enc_ciphertext: comp_ciphertext,
}
}
}
impl ShieldedOutput<OrchardDomain> for CompactAction {
fn ephemeral_key(&self) -> EphemeralKeyBytes {
EphemeralKeyBytes(self.ephemeral_key.0)
}
fn cmstar_bytes(&self) -> [u8; 32] {
self.cmx.to_bytes()
}
fn enc_ciphertext(&self) -> Option<NoteCiphertextBytes> {
None
}
fn enc_ciphertext_compact(&self) -> CompactNoteCiphertextBytes {
&self.enc_ciphertext
}
}
impl CompactAction {
/// Create a CompactAction from its constituent parts
pub fn from_parts(
nullifier: Nullifier,
cmx: ExtractedNoteCommitment,
ephemeral_key: EphemeralKeyBytes,
enc_ciphertext: CompactNoteCiphertextBytes,
) -> Self {
Self {
nullifier,
cmx,
ephemeral_key,
enc_ciphertext,
}
}
///Returns the nullifier of the note being spent.
pub fn nullifier(&self) -> Nullifier {
self.nullifier
}
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use rand::rngs::OsRng;
use zcash_note_encryption::{
try_compact_note_decryption, try_note_decryption, try_output_recovery_with_ovk, Domain,
EphemeralKeyBytes,
};
use super::{prf_ock_orchard, CompactAction, OrchardDomain, OrchardNoteEncryption};
use crate::note::AssetId;
use crate::note_encryption::NoteCiphertextBytes;
use crate::{
action::Action,
keys::{
DiversifiedTransmissionKey, Diversifier, EphemeralSecretKey, IncomingViewingKey,
OutgoingViewingKey,
},
note::{
testing::arb_note, ExtractedNoteCommitment, Nullifier, RandomSeed,
TransmittedNoteCiphertext,
},
primitives::redpallas,
value::{NoteValue, ValueCommitment},
Address, Note,
};
use super::{version, orchard_parse_note_plaintext_without_memo};
proptest! {
#[test]
fn test_encoding_roundtrip(
note in arb_note(NoteValue::from_raw(100)),
) {
let memo = &crate::test_vectors::note_encryption::test_vectors()[0].memo;
// Encode.
let mut plaintext = OrchardDomain::note_plaintext_bytes(&note, &note.recipient(), memo);
// Decode.
let domain = OrchardDomain { rho: note.rho() };
let parsed_version = version(plaintext.as_mut()).unwrap();
let (mut compact,parsed_memo) = domain.extract_memo(&plaintext);
let (parsed_note, parsed_recipient) = orchard_parse_note_plaintext_without_memo(&domain, &compact.as_mut(),
|diversifier| {
assert_eq!(diversifier, &note.recipient().diversifier());
Some(*note.recipient().pk_d())
}
).expect("Plaintext parsing failed");
// Check.
assert_eq!(parsed_note, note);
assert_eq!(parsed_recipient, note.recipient());
assert_eq!(&parsed_memo, memo);
assert_eq!(parsed_version, 0x03);
}
}
#[test]
fn test_vectors() {
let test_vectors = crate::test_vectors::note_encryption::test_vectors();
for tv in test_vectors {
//
// Load the test vector components
//
// Recipient key material
let ivk = IncomingViewingKey::from_bytes(&tv.incoming_viewing_key).unwrap();
let ovk = OutgoingViewingKey::from(tv.ovk);
let d = Diversifier::from_bytes(tv.default_d);
let pk_d = DiversifiedTransmissionKey::from_bytes(&tv.default_pk_d).unwrap();
// Received Action
let cv_net = ValueCommitment::from_bytes(&tv.cv_net).unwrap();
let rho = Nullifier::from_bytes(&tv.rho).unwrap();
let cmx = ExtractedNoteCommitment::from_bytes(&tv.cmx).unwrap();
let esk = EphemeralSecretKey::from_bytes(&tv.esk).unwrap();
let ephemeral_key = EphemeralKeyBytes(tv.ephemeral_key);
// Details about the expected note
let value = NoteValue::from_raw(tv.v);
let rseed = RandomSeed::from_bytes(tv.rseed, &rho).unwrap();
//
// Test the individual components
//
let shared_secret = esk.agree(&pk_d);
assert_eq!(shared_secret.to_bytes(), tv.shared_secret);
let k_enc = shared_secret.kdf_orchard(&ephemeral_key);
assert_eq!(k_enc.as_bytes(), tv.k_enc);
let ock = prf_ock_orchard(&ovk, &cv_net, &cmx.to_bytes(), &ephemeral_key);
assert_eq!(ock.as_ref(), tv.ock);
let recipient = Address::from_parts(d, pk_d);
let asset = match tv.asset {
None => AssetId::native(),
Some(type_bytes) => AssetId::from_bytes(&type_bytes).unwrap(),
};
let note = Note::from_parts(recipient, value, asset, rho, rseed).unwrap();
assert_eq!(ExtractedNoteCommitment::from(note.commitment()), cmx);
let action = Action::from_parts(
// rho is the nullifier in the receiving Action.
rho,
// We don't need a valid rk for this test.
redpallas::VerificationKey::dummy(),
cmx,
TransmittedNoteCiphertext {
epk_bytes: ephemeral_key.0,
enc_ciphertext: NoteCiphertextBytes::V3(tv.c_enc), // TODO: VA: Would need a mix of V2 and V3 eventually
out_ciphertext: tv.c_out,
},
cv_net.clone(),
(),
);
//
// Test decryption
// (Tested first because it only requires immutable references.)
//
let domain = OrchardDomain { rho };
match try_note_decryption(&domain, &ivk, &action) {
Some((decrypted_note, decrypted_to, decrypted_memo)) => {
assert_eq!(decrypted_note, note);
assert_eq!(decrypted_to, recipient);
assert_eq!(&decrypted_memo[..], &tv.memo[..]);
}
None => panic!("Note decryption failed"),
}
match try_compact_note_decryption(&domain, &ivk, &CompactAction::from(&action)) {
Some((decrypted_note, decrypted_to)) => {
assert_eq!(decrypted_note, note);
assert_eq!(decrypted_to, recipient);
}
None => panic!("Compact note decryption failed"),
}
match try_output_recovery_with_ovk(&domain, &ovk, &action, &cv_net, &tv.c_out) {
Some((decrypted_note, decrypted_to, decrypted_memo)) => {
assert_eq!(decrypted_note, note);
assert_eq!(decrypted_to, recipient);
assert_eq!(&decrypted_memo[..], &tv.memo[..]);
}
None => panic!("Output recovery failed"),
}
//
// Test encryption
//
let ne = OrchardNoteEncryption::new_with_esk(esk, Some(ovk), note, recipient, tv.memo);
// assert_eq!(ne.encrypt_note_plaintext().as_ref(), &tv.c_enc[..]);
assert_eq!(
&ne.encrypt_outgoing_plaintext(&cv_net, &cmx, &mut OsRng)[..],
&tv.c_out[..]
);
}
}
}

634
src/note_encryption_v3.rs Normal file
View File

@ -0,0 +1,634 @@
//! In-band secret distribution for Orchard bundles.
use blake2b_simd::{Hash, Params};
use core::fmt;
use group::ff::PrimeField;
use zcash_note_encryption::{
BatchDomain, Domain, EphemeralKeyBytes, OutPlaintextBytes, OutgoingCipherKey, ShieldedOutput,
AEAD_TAG_SIZE, MEMO_SIZE, OUT_PLAINTEXT_SIZE,
};
use crate::note::AssetId;
use crate::{
action::Action,
keys::{
DiversifiedTransmissionKey, Diversifier, EphemeralPublicKey, EphemeralSecretKey,
OutgoingViewingKey, PreparedEphemeralPublicKey, PreparedIncomingViewingKey, SharedSecret,
},
note::{ExtractedNoteCommitment, Nullifier, RandomSeed},
spec::diversify_hash,
value::{NoteValue, ValueCommitment},
Address, Note,
};
const PRF_OCK_ORCHARD_PERSONALIZATION: &[u8; 16] = b"Zcash_Orchardock";
/// The size of a v2 compact note.
pub const COMPACT_NOTE_SIZE_V2: usize = 1 + // version
11 + // diversifier
8 + // value
32; // rseed (or rcm prior to ZIP 212)
/// The size of [`NotePlaintextBytes`] for V2.
/// The size of the encoding of a ZSA asset id.
const ZSA_ASSET_SIZE: usize = 32;
/// The size of a v3 compact note.
pub const COMPACT_NOTE_SIZE_V3: usize = COMPACT_NOTE_SIZE_V2 + ZSA_ASSET_SIZE;
/// The size of [`NotePlaintextBytes`] for V3.
pub const NOTE_PLAINTEXT_SIZE_V3: usize = COMPACT_NOTE_SIZE_V3 + MEMO_SIZE;
/// The size of the encrypted ciphertext of the ZSA variant of a note.
pub const ENC_CIPHERTEXT_SIZE_V3: usize = NOTE_PLAINTEXT_SIZE_V3 + AEAD_TAG_SIZE;
/// a type to represent the raw bytes of a note plaintext.
#[derive(Clone, Debug)]
pub struct NotePlaintextBytes(pub [u8; NOTE_PLAINTEXT_SIZE_V3]);
/// a type to represent the raw bytes of an encrypted note plaintext.
#[derive(Clone, Debug)]
pub struct NoteCiphertextBytes(pub [u8; ENC_CIPHERTEXT_SIZE_V3]);
/// a type to represent the raw bytes of a compact note.
#[derive(Clone, Debug)]
pub struct CompactNotePlaintextBytes(pub [u8; COMPACT_NOTE_SIZE_V3]);
/// a type to represent the raw bytes of an encrypted compact note.
#[derive(Clone, Debug)]
pub struct CompactNoteCiphertextBytes(pub [u8; COMPACT_NOTE_SIZE_V3]);
impl AsMut<[u8]> for NotePlaintextBytes {
fn as_mut(&mut self) -> &mut [u8] {
self.0.as_mut()
}
}
impl From<&[u8]> for NotePlaintextBytes {
fn from(s: &[u8]) -> Self
where
Self: Sized,
{
NotePlaintextBytes(s.try_into().unwrap())
}
}
impl AsRef<[u8]> for NoteCiphertextBytes {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}
impl From<&[u8]> for NoteCiphertextBytes {
fn from(s: &[u8]) -> Self
where
Self: Sized,
{
NoteCiphertextBytes(s.try_into().unwrap())
}
}
impl AsMut<[u8]> for CompactNotePlaintextBytes {
fn as_mut(&mut self) -> &mut [u8] {
self.0.as_mut()
}
}
impl From<&[u8]> for CompactNotePlaintextBytes {
fn from(s: &[u8]) -> Self
where
Self: Sized,
{
CompactNotePlaintextBytes(s.try_into().unwrap())
}
}
impl AsRef<[u8]> for CompactNoteCiphertextBytes {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}
/// Defined in [Zcash Protocol Spec § 5.4.2: Pseudo Random Functions][concreteprfs].
///
/// [concreteprfs]: https://zips.z.cash/protocol/nu5.pdf#concreteprfs
pub(crate) fn prf_ock_orchard(
ovk: &OutgoingViewingKey,
cv: &ValueCommitment,
cmx_bytes: &[u8; 32],
ephemeral_key: &EphemeralKeyBytes,
) -> OutgoingCipherKey {
OutgoingCipherKey(
Params::new()
.hash_length(32)
.personal(PRF_OCK_ORCHARD_PERSONALIZATION)
.to_state()
.update(ovk.as_ref())
.update(&cv.to_bytes())
.update(cmx_bytes)
.update(ephemeral_key.as_ref())
.finalize()
.as_bytes()
.try_into()
.unwrap(),
)
}
/// note_version will return the version of the note plaintext.
pub fn note_version(plaintext: &[u8]) -> Option<u8> {
match plaintext[0] {
0x02 => Some(0x02),
0x03 => Some(0x03),
_ => None,
}
}
/// Domain-specific requirements:
/// - If the note version is 3, the `plaintext` must contain a valid encoding of a ZSA asset type.
fn orchard_parse_note_plaintext_without_memo<F>(
domain: &OrchardDomainV3,
plaintext: &CompactNotePlaintextBytes,
get_validated_pk_d: F,
) -> Option<(Note, Address)>
where
F: FnOnce(&Diversifier) -> Option<DiversifiedTransmissionKey>,
{
// The unwraps below are guaranteed to succeed by the assertion above
let diversifier = Diversifier::from_bytes(plaintext.0[1..12].try_into().unwrap());
let value = NoteValue::from_bytes(plaintext.0[12..20].try_into().unwrap());
let rseed = Option::from(RandomSeed::from_bytes(
plaintext.0[20..COMPACT_NOTE_SIZE_V2].try_into().unwrap(),
&domain.rho,
))?;
let pk_d = get_validated_pk_d(&diversifier)?;
let recipient = Address::from_parts(diversifier, pk_d);
let asset = match note_version(plaintext.0.as_ref())? {
0x02 => AssetId::native(),
0x03 => {
let bytes = plaintext.0[COMPACT_NOTE_SIZE_V2..COMPACT_NOTE_SIZE_V3]
.try_into()
.unwrap();
AssetId::from_bytes(bytes).unwrap()
}
_ => panic!("invalid note version"),
};
let note = Option::from(Note::from_parts(recipient, value, asset, domain.rho, rseed))?;
Some((note, recipient))
}
/// Orchard-specific note encryption logic.
#[derive(Debug)]
pub struct OrchardDomainV3 {
rho: Nullifier,
}
impl OrchardDomainV3 {
/// Constructs a domain that can be used to trial-decrypt this action's output note.
pub fn for_action<T>(act: &Action<T>) -> Self {
OrchardDomainV3 {
rho: *act.nullifier(),
}
}
/// Constructs a domain from a nullifier.
pub fn for_nullifier(nullifier: Nullifier) -> Self {
OrchardDomainV3 { rho: nullifier }
}
}
impl Domain for OrchardDomainV3 {
type EphemeralSecretKey = EphemeralSecretKey;
type EphemeralPublicKey = EphemeralPublicKey;
type PreparedEphemeralPublicKey = PreparedEphemeralPublicKey;
type SharedSecret = SharedSecret;
type SymmetricKey = Hash;
type Note = Note;
type Recipient = Address;
type DiversifiedTransmissionKey = DiversifiedTransmissionKey;
type IncomingViewingKey = PreparedIncomingViewingKey;
type OutgoingViewingKey = OutgoingViewingKey;
type ValueCommitment = ValueCommitment;
type ExtractedCommitment = ExtractedNoteCommitment;
type ExtractedCommitmentBytes = [u8; 32];
type Memo = [u8; MEMO_SIZE];
type NotePlaintextBytes = NotePlaintextBytes;
type NoteCiphertextBytes = NoteCiphertextBytes;
type CompactNotePlaintextBytes = CompactNotePlaintextBytes;
type CompactNoteCiphertextBytes = CompactNoteCiphertextBytes;
fn derive_esk(note: &Self::Note) -> Option<Self::EphemeralSecretKey> {
Some(note.esk())
}
fn get_pk_d(note: &Self::Note) -> Self::DiversifiedTransmissionKey {
*note.recipient().pk_d()
}
fn prepare_epk(epk: Self::EphemeralPublicKey) -> Self::PreparedEphemeralPublicKey {
PreparedEphemeralPublicKey::new(epk)
}
fn ka_derive_public(
note: &Self::Note,
esk: &Self::EphemeralSecretKey,
) -> Self::EphemeralPublicKey {
esk.derive_public(note.recipient().g_d())
}
fn ka_agree_enc(
esk: &Self::EphemeralSecretKey,
pk_d: &Self::DiversifiedTransmissionKey,
) -> Self::SharedSecret {
esk.agree(pk_d)
}
fn ka_agree_dec(
ivk: &Self::IncomingViewingKey,
epk: &Self::PreparedEphemeralPublicKey,
) -> Self::SharedSecret {
epk.agree(ivk)
}
fn kdf(secret: Self::SharedSecret, ephemeral_key: &EphemeralKeyBytes) -> Self::SymmetricKey {
secret.kdf_orchard(ephemeral_key)
}
fn note_plaintext_bytes(
note: &Self::Note,
_: &Self::Recipient,
memo: &Self::Memo,
) -> NotePlaintextBytes {
let mut np = [0u8; NOTE_PLAINTEXT_SIZE_V3];
np[0] = 0x03;
np[1..12].copy_from_slice(note.recipient().diversifier().as_array());
np[12..20].copy_from_slice(&note.value().to_bytes());
np[20..52].copy_from_slice(note.rseed().as_bytes());
np[52..84].copy_from_slice(&note.asset().to_bytes());
np[84..].copy_from_slice(memo);
NotePlaintextBytes(np)
}
fn derive_ock(
ovk: &Self::OutgoingViewingKey,
cv: &Self::ValueCommitment,
cmstar_bytes: &Self::ExtractedCommitmentBytes,
ephemeral_key: &EphemeralKeyBytes,
) -> OutgoingCipherKey {
prf_ock_orchard(ovk, cv, cmstar_bytes, ephemeral_key)
}
fn outgoing_plaintext_bytes(
note: &Self::Note,
esk: &Self::EphemeralSecretKey,
) -> OutPlaintextBytes {
let mut op = [0; OUT_PLAINTEXT_SIZE];
op[..32].copy_from_slice(&note.recipient().pk_d().to_bytes());
op[32..].copy_from_slice(&esk.0.to_repr());
OutPlaintextBytes(op)
}
fn epk_bytes(epk: &Self::EphemeralPublicKey) -> EphemeralKeyBytes {
epk.to_bytes()
}
fn epk(ephemeral_key: &EphemeralKeyBytes) -> Option<Self::EphemeralPublicKey> {
EphemeralPublicKey::from_bytes(&ephemeral_key.0).into()
}
fn cmstar(note: &Self::Note) -> Self::ExtractedCommitment {
note.commitment().into()
}
fn parse_note_plaintext_without_memo_ivk(
&self,
ivk: &Self::IncomingViewingKey,
plaintext: &CompactNotePlaintextBytes,
) -> Option<(Self::Note, Self::Recipient)> {
orchard_parse_note_plaintext_without_memo(self, plaintext, |diversifier| {
Some(DiversifiedTransmissionKey::derive(ivk, diversifier))
})
}
fn parse_note_plaintext_without_memo_ovk(
&self,
pk_d: &Self::DiversifiedTransmissionKey,
esk: &Self::EphemeralSecretKey,
ephemeral_key: &EphemeralKeyBytes,
plaintext: &CompactNotePlaintextBytes,
) -> Option<(Self::Note, Self::Recipient)> {
orchard_parse_note_plaintext_without_memo(self, plaintext, |diversifier| {
if esk
.derive_public(diversify_hash(diversifier.as_array()))
.to_bytes()
.0
== ephemeral_key.0
{
Some(*pk_d)
} else {
None
}
})
}
fn extract_memo(
&self,
plaintext: &NotePlaintextBytes,
) -> (Self::CompactNotePlaintextBytes, Self::Memo) {
let (compact, memo) = plaintext.0.split_at(COMPACT_NOTE_SIZE_V3);
(
CompactNotePlaintextBytes(compact.try_into().unwrap()),
memo.try_into().unwrap(),
)
}
fn extract_pk_d(out_plaintext: &OutPlaintextBytes) -> Option<Self::DiversifiedTransmissionKey> {
DiversifiedTransmissionKey::from_bytes(out_plaintext.0[0..32].try_into().unwrap()).into()
}
fn extract_esk(out_plaintext: &OutPlaintextBytes) -> Option<Self::EphemeralSecretKey> {
EphemeralSecretKey::from_bytes(out_plaintext.0[32..OUT_PLAINTEXT_SIZE].try_into().unwrap())
.into()
}
}
impl BatchDomain for OrchardDomainV3 {
fn batch_kdf<'a>(
items: impl Iterator<Item = (Option<Self::SharedSecret>, &'a EphemeralKeyBytes)>,
) -> Vec<Option<Self::SymmetricKey>> {
let (shared_secrets, ephemeral_keys): (Vec<_>, Vec<_>) = items.unzip();
SharedSecret::batch_to_affine(shared_secrets)
.zip(ephemeral_keys.into_iter())
.map(|(secret, ephemeral_key)| {
secret.map(|dhsecret| SharedSecret::kdf_orchard_inner(dhsecret, ephemeral_key))
})
.collect()
}
}
/// Implementation of in-band secret distribution for Orchard bundles.
pub type OrchardNoteEncryption = zcash_note_encryption::NoteEncryption<OrchardDomainV3>;
impl<T> ShieldedOutput<OrchardDomainV3> for Action<T> {
fn ephemeral_key(&self) -> EphemeralKeyBytes {
EphemeralKeyBytes(self.encrypted_note().epk_bytes)
}
fn cmstar_bytes(&self) -> [u8; 32] {
self.cmx().to_bytes()
}
fn enc_ciphertext(&self) -> Option<NoteCiphertextBytes> {
Some(NoteCiphertextBytes(self.encrypted_note().enc_ciphertext))
}
fn enc_ciphertext_compact(&self) -> CompactNoteCiphertextBytes {
CompactNoteCiphertextBytes(
self.encrypted_note().enc_ciphertext[..COMPACT_NOTE_SIZE_V3]
.try_into()
.unwrap(),
)
}
}
/// A compact Action for light clients.
pub struct CompactAction {
nullifier: Nullifier,
cmx: ExtractedNoteCommitment,
ephemeral_key: EphemeralKeyBytes,
enc_ciphertext: CompactNoteCiphertextBytes,
}
impl fmt::Debug for CompactAction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "CompactAction")
}
}
impl<T> From<&Action<T>> for CompactAction
where
Action<T>: ShieldedOutput<OrchardDomainV3>,
{
fn from(action: &Action<T>) -> Self {
CompactAction {
nullifier: *action.nullifier(),
cmx: *action.cmx(),
ephemeral_key: action.ephemeral_key(),
enc_ciphertext: CompactNoteCiphertextBytes(
action.encrypted_note().enc_ciphertext[..COMPACT_NOTE_SIZE_V3]
.try_into()
.unwrap(),
),
}
}
}
impl ShieldedOutput<OrchardDomainV3> for CompactAction {
fn ephemeral_key(&self) -> EphemeralKeyBytes {
EphemeralKeyBytes(self.ephemeral_key.0)
}
fn cmstar_bytes(&self) -> [u8; 32] {
self.cmx.to_bytes()
}
fn enc_ciphertext(&self) -> Option<NoteCiphertextBytes> {
None
}
fn enc_ciphertext_compact(&self) -> CompactNoteCiphertextBytes {
self.enc_ciphertext.clone()
}
}
impl CompactAction {
/// Create a CompactAction from its constituent parts
pub fn from_parts(
nullifier: Nullifier,
cmx: ExtractedNoteCommitment,
ephemeral_key: EphemeralKeyBytes,
enc_ciphertext: CompactNoteCiphertextBytes,
) -> Self {
Self {
nullifier,
cmx,
ephemeral_key,
enc_ciphertext,
}
}
///Returns the nullifier of the note being spent.
pub fn nullifier(&self) -> Nullifier {
self.nullifier
}
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use rand::rngs::OsRng;
use zcash_note_encryption::{
try_compact_note_decryption, try_note_decryption, try_output_recovery_with_ovk, Domain,
EphemeralKeyBytes,
};
use super::{
note_version, orchard_parse_note_plaintext_without_memo, prf_ock_orchard, CompactAction,
OrchardDomainV3, OrchardNoteEncryption,
};
use crate::{
action::Action,
keys::{
DiversifiedTransmissionKey, Diversifier, EphemeralSecretKey, IncomingViewingKey,
OutgoingViewingKey, PreparedIncomingViewingKey,
},
note::{
testing::arb_note, AssetId, ExtractedNoteCommitment, Nullifier, RandomSeed,
TransmittedNoteCiphertext,
},
primitives::redpallas,
value::{NoteValue, ValueCommitment},
Address, Note,
};
proptest! {
#[test]
fn test_encoding_roundtrip(
note in arb_note(NoteValue::from_raw(100)),
) {
let memo = &crate::test_vectors::note_encryption::test_vectors()[0].memo;
// Encode.
let mut plaintext = OrchardDomainV3::note_plaintext_bytes(&note, &note.recipient(), memo);
// Decode.
let domain = OrchardDomainV3 { rho: note.rho() };
let parsed_version = note_version(plaintext.as_mut()).unwrap();
let (compact,parsed_memo) = domain.extract_memo(&plaintext);
let (parsed_note, parsed_recipient) = orchard_parse_note_plaintext_without_memo(&domain, &compact,
|diversifier| {
assert_eq!(diversifier, &note.recipient().diversifier());
Some(*note.recipient().pk_d())
}
).expect("Plaintext parsing failed");
// Check.
assert_eq!(parsed_note, note);
assert_eq!(parsed_recipient, note.recipient());
assert_eq!(&parsed_memo, memo);
assert_eq!(parsed_version, 0x03);
}
}
#[test]
fn test_vectors() {
let test_vectors = crate::test_vectors::note_encryption_v3::test_vectors();
for tv in test_vectors {
//
// Load the test vector components
//
// Recipient key material
let ivk = PreparedIncomingViewingKey::new(
&IncomingViewingKey::from_bytes(&tv.incoming_viewing_key).unwrap(),
);
let ovk = OutgoingViewingKey::from(tv.ovk);
let d = Diversifier::from_bytes(tv.default_d);
let pk_d = DiversifiedTransmissionKey::from_bytes(&tv.default_pk_d).unwrap();
// Received Action
let cv_net = ValueCommitment::from_bytes(&tv.cv_net).unwrap();
let rho = Nullifier::from_bytes(&tv.rho).unwrap();
let cmx = ExtractedNoteCommitment::from_bytes(&tv.cmx).unwrap();
let esk = EphemeralSecretKey::from_bytes(&tv.esk).unwrap();
let ephemeral_key = EphemeralKeyBytes(tv.ephemeral_key);
// Details about the expected note
let value = NoteValue::from_raw(tv.v);
let rseed = RandomSeed::from_bytes(tv.rseed, &rho).unwrap();
//
// Test the individual components
//
let shared_secret = esk.agree(&pk_d);
assert_eq!(shared_secret.to_bytes(), tv.shared_secret);
let k_enc = shared_secret.kdf_orchard(&ephemeral_key);
assert_eq!(k_enc.as_bytes(), tv.k_enc);
let ock = prf_ock_orchard(&ovk, &cv_net, &cmx.to_bytes(), &ephemeral_key);
assert_eq!(ock.as_ref(), tv.ock);
let recipient = Address::from_parts(d, pk_d);
let asset = AssetId::from_bytes(&tv.asset).unwrap();
let note = Note::from_parts(recipient, value, asset, rho, rseed).unwrap();
assert_eq!(ExtractedNoteCommitment::from(note.commitment()), cmx);
let action = Action::from_parts(
// rho is the nullifier in the receiving Action.
rho,
// We don't need a valid rk for this test.
redpallas::VerificationKey::dummy(),
cmx,
TransmittedNoteCiphertext {
epk_bytes: ephemeral_key.0,
enc_ciphertext: tv.c_enc,
out_ciphertext: tv.c_out,
},
cv_net.clone(),
(),
);
//
// Test decryption
// (Tested first because it only requires immutable references.)
//
let domain = OrchardDomainV3 { rho };
match try_note_decryption(&domain, &ivk, &action) {
Some((decrypted_note, decrypted_to, decrypted_memo)) => {
assert_eq!(decrypted_note, note);
assert_eq!(decrypted_to, recipient);
assert_eq!(&decrypted_memo[..], &tv.memo[..]);
}
None => panic!("Note decryption failed"),
}
match try_compact_note_decryption(&domain, &ivk, &CompactAction::from(&action)) {
Some((decrypted_note, decrypted_to)) => {
assert_eq!(decrypted_note, note);
assert_eq!(decrypted_to, recipient);
}
None => panic!("Compact note decryption failed"),
}
match try_output_recovery_with_ovk(&domain, &ovk, &action, &cv_net, &tv.c_out) {
Some((decrypted_note, decrypted_to, decrypted_memo)) => {
assert_eq!(decrypted_note, note);
assert_eq!(decrypted_to, recipient);
assert_eq!(&decrypted_memo[..], &tv.memo[..]);
}
None => panic!("Output recovery failed"),
}
//
// Test encryption
//
let ne = OrchardNoteEncryption::new_with_esk(esk, Some(ovk), note, recipient, tv.memo);
assert_eq!(ne.encrypt_note_plaintext().as_ref(), &tv.c_enc[..]);
assert_eq!(
&ne.encrypt_outgoing_plaintext(&cv_net, &cmx, &mut OsRng)[..],
&tv.c_out[..]
);
}
}
}

View File

@ -3,3 +3,4 @@ pub(crate) mod commitment_tree;
pub(crate) mod keys;
pub(crate) mod merkle_path;
pub(crate) mod note_encryption;
pub(crate) mod note_encryption_v3;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -53,13 +53,14 @@ use pasta_curves::{
use rand::RngCore;
use subtle::CtOption;
use crate::builder::Error;
use crate::note::AssetId;
use crate::{
constants::fixed_bases::{VALUE_COMMITMENT_PERSONALIZATION, VALUE_COMMITMENT_R_BYTES},
primitives::redpallas::{self, Binding},
};
use crate::builder::Error;
use crate::note::AssetId;
/// Maximum note value.
pub const MAX_NOTE_VALUE: u64 = u64::MAX;

View File

@ -6,7 +6,7 @@ use orchard::{
circuit::{ProvingKey, VerifyingKey},
keys::{FullViewingKey, PreparedIncomingViewingKey, Scope, SpendAuthorizingKey, SpendingKey},
note::ExtractedNoteCommitment,
note_encryption::OrchardDomain,
note_encryption_v3::OrchardDomainV3,
tree::{MerkleHashOrchard, MerklePath},
value::NoteValue,
Anchor, Bundle, Note,
@ -89,7 +89,7 @@ fn bundle_chain() {
.actions()
.iter()
.find_map(|action| {
let domain = OrchardDomain::for_action(action);
let domain = OrchardDomainV3::for_action(action);
try_note_decryption(&domain, &ivk, action)
})
.unwrap();

View File

@ -7,7 +7,7 @@ use orchard::bundle::Authorized;
use orchard::issuance::{verify_issue_bundle, IssueBundle, Signed, Unauthorized};
use orchard::keys::{IssuanceAuthorizingKey, IssuanceValidatingKey};
use orchard::note::{AssetId, ExtractedNoteCommitment};
use orchard::note_encryption::OrchardDomain;
use orchard::note_encryption_v3::OrchardDomainV3;
use orchard::tree::{MerkleHashOrchard, MerklePath};
use orchard::{
builder::Builder,
@ -204,7 +204,7 @@ fn create_native_note(keys: &Keychain) -> Note {
.actions()
.iter()
.find_map(|action| {
let domain = OrchardDomain::for_action(action);
let domain = OrchardDomainV3::for_action(action);
try_note_decryption(&domain, &PreparedIncomingViewingKey::new(&ivk), action)
})
.unwrap();