Orchard note encryption

This commit is contained in:
Jack Grigg 2021-06-02 23:22:22 +01:00
parent 11350339f5
commit 99665572a2
7 changed files with 311 additions and 5 deletions

View File

@ -28,6 +28,10 @@ impl Address {
Address { d, pk_d } Address { d, pk_d }
} }
pub(crate) fn diversifer(&self) -> Diversifier {
self.d
}
pub(crate) fn g_d(&self) -> NonIdentityPallasPoint { pub(crate) fn g_d(&self) -> NonIdentityPallasPoint {
diversify_hash(self.d.as_array()) diversify_hash(self.d.as_array())
} }

View File

@ -295,6 +295,10 @@ impl DiversifierKey {
pub struct Diversifier([u8; 11]); pub struct Diversifier([u8; 11]);
impl Diversifier { impl Diversifier {
pub(crate) fn from_bytes(d: [u8; 11]) -> Self {
Diversifier(d)
}
/// Returns the byte array corresponding to this diversifier. /// Returns the byte array corresponding to this diversifier.
pub fn as_array(&self) -> &[u8; 11] { pub fn as_array(&self) -> &[u8; 11] {
&self.0 &self.0
@ -339,7 +343,7 @@ impl KeyAgreementPrivateKey {
/// Returns the payment address for this key corresponding to the given diversifier. /// Returns the payment address for this key corresponding to the given diversifier.
fn address(&self, d: Diversifier) -> Address { fn address(&self, d: Diversifier) -> Address {
let pk_d = DiversifiedTransmissionKey::derive(self, &d); let pk_d = DiversifiedTransmissionKey::derive_inner(self, &d);
Address::from_parts(d, pk_d) Address::from_parts(d, pk_d)
} }
} }
@ -406,23 +410,38 @@ impl From<&FullViewingKey> for OutgoingViewingKey {
} }
} }
impl AsRef<[u8; 32]> for OutgoingViewingKey {
fn as_ref(&self) -> &[u8; 32] {
&self.0
}
}
/// The diversified transmission key for a given payment address. /// The diversified transmission key for a given payment address.
/// ///
/// Defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents]. /// Defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents].
/// ///
/// [orchardkeycomponents]: https://zips.z.cash/protocol/nu5.pdf#orchardkeycomponents /// [orchardkeycomponents]: https://zips.z.cash/protocol/nu5.pdf#orchardkeycomponents
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub(crate) struct DiversifiedTransmissionKey(NonIdentityPallasPoint); pub struct DiversifiedTransmissionKey(NonIdentityPallasPoint);
impl DiversifiedTransmissionKey { impl DiversifiedTransmissionKey {
/// Defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents]. /// Defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents].
/// ///
/// [orchardkeycomponents]: https://zips.z.cash/protocol/nu5.pdf#orchardkeycomponents /// [orchardkeycomponents]: https://zips.z.cash/protocol/nu5.pdf#orchardkeycomponents
fn derive(ivk: &KeyAgreementPrivateKey, d: &Diversifier) -> Self { pub(crate) fn derive(ivk: &IncomingViewingKey, d: &Diversifier) -> Self {
Self::derive_inner(&ivk.ivk, d)
}
fn derive_inner(ivk: &KeyAgreementPrivateKey, d: &Diversifier) -> Self {
let g_d = diversify_hash(&d.as_array()); let g_d = diversify_hash(&d.as_array());
DiversifiedTransmissionKey(ka_orchard(&ivk.0, &g_d)) DiversifiedTransmissionKey(ka_orchard(&ivk.0, &g_d))
} }
/// $abst_P(bytes)$
pub(crate) fn from_bytes(bytes: &[u8; 32]) -> CtOption<Self> {
NonIdentityPallasPoint::from_bytes(bytes).map(DiversifiedTransmissionKey)
}
/// $repr_P(self)$ /// $repr_P(self)$
pub(crate) fn to_bytes(self) -> [u8; 32] { pub(crate) fn to_bytes(self) -> [u8; 32] {
self.0.to_bytes() self.0.to_bytes()

View File

@ -23,6 +23,7 @@ mod circuit;
mod constants; mod constants;
pub mod keys; pub mod keys;
pub mod note; pub mod note;
mod note_encryption;
pub mod primitives; pub mod primitives;
mod spec; mod spec;
mod tree; mod tree;

View File

@ -5,7 +5,7 @@ use rand::RngCore;
use subtle::CtOption; use subtle::CtOption;
use crate::{ use crate::{
keys::{FullViewingKey, SpendingKey}, keys::{EphemeralSecretKey, FullViewingKey, SpendingKey},
spec::{to_base, to_scalar, NonZeroPallasScalar, PrfExpand}, spec::{to_base, to_scalar, NonZeroPallasScalar, PrfExpand},
value::NoteValue, value::NoteValue,
Address, Address,
@ -39,6 +39,10 @@ impl RandomSeed {
CtOption::new(rseed, esk.is_some()) CtOption::new(rseed, esk.is_some())
} }
pub(crate) fn to_bytes(&self) -> &[u8; 32] {
&self.0
}
/// Defined in [Zcash Protocol Spec § 4.7.3: Sending Notes (Orchard)][orchardsend]. /// Defined in [Zcash Protocol Spec § 4.7.3: Sending Notes (Orchard)][orchardsend].
/// ///
/// [orchardsend]: https://zips.z.cash/protocol/nu5.pdf#orchardsend /// [orchardsend]: https://zips.z.cash/protocol/nu5.pdf#orchardsend
@ -92,7 +96,6 @@ pub struct Note {
} }
impl Note { impl Note {
#[cfg(test)]
pub(crate) fn from_parts( pub(crate) fn from_parts(
recipient: Address, recipient: Address,
value: NoteValue, value: NoteValue,
@ -154,11 +157,26 @@ impl Note {
(sk, fvk, note) (sk, fvk, note)
} }
/// Returns the recipient of this note.
pub fn recipient(&self) -> Address {
self.recipient
}
/// Returns the value of this note. /// Returns the value of this note.
pub fn value(&self) -> NoteValue { pub fn value(&self) -> NoteValue {
self.value self.value
} }
/// Derives the ephemeral secret key for this note.
pub(crate) fn rseed(&self) -> &RandomSeed {
&self.rseed
}
/// Derives the ephemeral secret key for this note.
pub(crate) fn esk(&self) -> EphemeralSecretKey {
EphemeralSecretKey(self.rseed.esk(&self.rho))
}
/// Derives the commitment to this note. /// Derives the commitment to this note.
/// ///
/// Defined in [Zcash Protocol Spec § 3.2: Notes][notes]. /// Defined in [Zcash Protocol Spec § 3.2: Notes][notes].

View File

@ -71,3 +71,9 @@ impl std::ops::Deref for ExtractedNoteCommitment {
&self.0 &self.0
} }
} }
impl From<&ExtractedNoteCommitment> for [u8; 32] {
fn from(cmx: &ExtractedNoteCommitment) -> Self {
cmx.to_bytes()
}
}

250
src/note_encryption.rs Normal file
View File

@ -0,0 +1,250 @@
//! In-band secret distribution for Orchard bundles.
use std::convert::TryInto;
use blake2b_simd::{Hash, Params};
use halo2::arithmetic::FieldExt;
use zcash_note_encryption::{
Domain, EphemeralKeyBytes, NotePlaintextBytes, NoteValidity, OutPlaintextBytes,
OutgoingCipherKey, ShieldedOutput, COMPACT_NOTE_SIZE, NOTE_PLAINTEXT_SIZE, OUT_PLAINTEXT_SIZE,
};
use crate::{
bundle::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";
/// 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(),
)
}
fn orchard_parse_note_plaintext_without_memo<F>(
domain: &OrchardDomain,
plaintext: &[u8],
get_validated_pk_d: F,
) -> Option<(Note, Address)>
where
F: FnOnce(&Diversifier) -> Option<DiversifiedTransmissionKey>,
{
assert!(plaintext.len() >= COMPACT_NOTE_SIZE);
// Check note plaintext version
if plaintext[0] != 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(),
&domain.rho,
))?;
let pk_d = get_validated_pk_d(&diversifier)?;
let recipient = Address::from_parts(diversifier, pk_d);
let note = Note::from_parts(recipient, value, domain.rho, rseed);
Some((note, recipient))
}
/// Orchard-specific note encryption logic.
#[derive(Debug)]
pub struct OrchardDomain {
rho: Nullifier,
}
impl Domain for OrchardDomain {
type EphemeralSecretKey = EphemeralSecretKey;
type EphemeralPublicKey = 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; 512]; // TODO use a more interesting type
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 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::EphemeralPublicKey,
) -> 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 = [0; NOTE_PLAINTEXT_SIZE];
np[0] = 0x02;
np[1..12].copy_from_slice(note.recipient().diversifer().as_array());
np[12..20].copy_from_slice(&note.value().to_bytes());
np[20..52].copy_from_slice(note.rseed().to_bytes());
np[52..].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_bytes());
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 check_epk_bytes<F: Fn(&Self::EphemeralSecretKey) -> NoteValidity>(
note: &Self::Note,
check: F,
) -> NoteValidity {
check(&note.esk())
}
fn cmstar(note: &Self::Note) -> Self::ExtractedCommitment {
note.commitment().into()
}
fn parse_note_plaintext_without_memo_ivk(
&self,
ivk: &Self::IncomingViewingKey,
plaintext: &[u8],
) -> 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: &[u8],
) -> 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: &[u8]) -> Self::Memo {
plaintext[COMPACT_NOTE_SIZE..NOTE_PLAINTEXT_SIZE]
.try_into()
.unwrap()
}
fn extract_pk_d(
out_plaintext: &[u8; OUT_PLAINTEXT_SIZE],
) -> Option<Self::DiversifiedTransmissionKey> {
DiversifiedTransmissionKey::from_bytes(out_plaintext[0..32].try_into().unwrap()).into()
}
fn extract_esk(out_plaintext: &[u8; OUT_PLAINTEXT_SIZE]) -> Option<Self::EphemeralSecretKey> {
EphemeralSecretKey::from_bytes(out_plaintext[32..OUT_PLAINTEXT_SIZE].try_into().unwrap())
.into()
}
}
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) -> &[u8] {
&self.encrypted_note().enc_ciphertext
}
}

View File

@ -74,6 +74,14 @@ impl NoteValue {
NoteValue(value) NoteValue(value)
} }
pub(crate) fn from_bytes(bytes: [u8; 8]) -> Self {
NoteValue(u64::from_le_bytes(bytes))
}
pub(crate) fn to_bytes(self) -> [u8; 8] {
self.0.to_le_bytes()
}
pub(crate) fn to_le_bits(self) -> BitArray<Lsb0, [u8; 8]> { pub(crate) fn to_le_bits(self) -> BitArray<Lsb0, [u8; 8]> {
BitArray::<Lsb0, _>::new(self.0.to_le_bytes()) BitArray::<Lsb0, _>::new(self.0.to_le_bytes())
} }