Merge pull request #99 from zcash/note-encryption

Note encryption
This commit is contained in:
str4d 2021-06-14 17:16:54 +01:00 committed by GitHub
commit 1182d8d5a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 2904 additions and 38 deletions

View File

@ -44,6 +44,10 @@ rev = "d04b532368d05b505e622f8cac4c0693574fbd93"
git = "https://github.com/str4d/redjubjub.git" git = "https://github.com/str4d/redjubjub.git"
rev = "d5d8c5f3bb704bad8ae88fe4a29ae1f744774cb2" rev = "d5d8c5f3bb704bad8ae88fe4a29ae1f744774cb2"
[dependencies.zcash_note_encryption]
git = "https://github.com/zcash/librustzcash.git"
rev = "cc533a9da4f6a7209a7be05f82b12a03969152c9"
[dev-dependencies] [dev-dependencies]
criterion = "0.3" criterion = "0.3"
hex = "0.4" hex = "0.4"

View File

@ -13,7 +13,7 @@ use crate::{
/// let sk = SpendingKey::from_bytes([7; 32]).unwrap(); /// let sk = SpendingKey::from_bytes([7; 32]).unwrap();
/// let address = FullViewingKey::from(&sk).default_address(); /// let address = FullViewingKey::from(&sk).default_address();
/// ``` /// ```
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Address { pub struct Address {
d: Diversifier, d: Diversifier,
pk_d: DiversifiedTransmissionKey, pk_d: DiversifiedTransmissionKey,
@ -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

@ -16,6 +16,7 @@ use crate::{
FullViewingKey, OutgoingViewingKey, SpendAuthorizingKey, SpendValidatingKey, SpendingKey, FullViewingKey, OutgoingViewingKey, SpendAuthorizingKey, SpendValidatingKey, SpendingKey,
}, },
note::{Note, TransmittedNoteCiphertext}, note::{Note, TransmittedNoteCiphertext},
note_encryption::OrchardNoteEncryption,
primitives::redpallas::{self, Binding, SpendAuth}, primitives::redpallas::{self, Binding, SpendAuth},
tree::{Anchor, MerklePath}, tree::{Anchor, MerklePath},
value::{self, NoteValue, OverflowError, ValueCommitTrapdoor, ValueCommitment, ValueSum}, value::{self, NoteValue, OverflowError, ValueCommitTrapdoor, ValueCommitment, ValueSum},
@ -79,7 +80,7 @@ struct RecipientInfo {
ovk: Option<OutgoingViewingKey>, ovk: Option<OutgoingViewingKey>,
recipient: Address, recipient: Address,
value: NoteValue, value: NoteValue,
memo: Option<()>, memo: Option<[u8; 512]>,
} }
impl RecipientInfo { impl RecipientInfo {
@ -135,21 +136,32 @@ impl ActionInfo {
let alpha = pallas::Scalar::random(&mut rng); let alpha = pallas::Scalar::random(&mut rng);
let rk = ak.randomize(&alpha); let rk = ak.randomize(&alpha);
let note = Note::new(self.output.recipient, self.output.value, nf_old, rng); let note = Note::new(self.output.recipient, self.output.value, nf_old, &mut rng);
let cm_new = note.commitment(); let cm_new = note.commitment();
let cmx = cm_new.into();
let encryptor = OrchardNoteEncryption::new(
self.output.ovk,
note,
self.output.recipient,
self.output.memo.unwrap_or_else(|| {
let mut memo = [0; 512];
memo[0] = 0xf6;
memo
}),
);
// TODO: Note encryption
let encrypted_note = TransmittedNoteCiphertext { let encrypted_note = TransmittedNoteCiphertext {
epk_bytes: [0u8; 32], epk_bytes: encryptor.epk().to_bytes().0,
enc_ciphertext: [0u8; 580], enc_ciphertext: encryptor.encrypt_note_plaintext(),
out_ciphertext: [0u8; 80], out_ciphertext: encryptor.encrypt_outgoing_plaintext(&cv_net, &cmx, &mut rng),
}; };
( (
Action::from_parts( Action::from_parts(
nf_old, nf_old,
rk, rk,
cm_new.into(), cmx,
encrypted_note, encrypted_note,
cv_net, cv_net,
SigningMetadata { SigningMetadata {
@ -220,7 +232,7 @@ impl Builder {
ovk: Option<OutgoingViewingKey>, ovk: Option<OutgoingViewingKey>,
recipient: Address, recipient: Address,
value: NoteValue, value: NoteValue,
memo: Option<()>, memo: Option<[u8; 512]>,
) -> Result<(), &'static str> { ) -> Result<(), &'static str> {
if !self.flags.outputs_enabled() { if !self.flags.outputs_enabled() {
return Err("Outputs are not enabled for this builder"); return Err("Outputs are not enabled for this builder");

View File

@ -4,12 +4,15 @@ use std::convert::TryInto;
use std::mem; use std::mem;
use aes::Aes256; use aes::Aes256;
use blake2b_simd::{Hash as Blake2bHash, Params};
use fpe::ff1::{BinaryNumeralString, FF1}; use fpe::ff1::{BinaryNumeralString, FF1};
use group::GroupEncoding; use group::GroupEncoding;
use halo2::arithmetic::FieldExt; use halo2::arithmetic::FieldExt;
use pasta_curves::pallas; use pasta_curves::pallas;
use rand::RngCore; use rand::RngCore;
use subtle::ConstantTimeEq;
use subtle::CtOption; use subtle::CtOption;
use zcash_note_encryption::EphemeralKeyBytes;
use crate::{ use crate::{
address::Address, address::Address,
@ -20,6 +23,8 @@ use crate::{
}, },
}; };
const KDF_ORCHARD_PERSONALIZATION: &[u8; 16] = b"Zcash_OrchardKDF";
/// A spending key, from which all key material is derived. /// A spending key, from which all key material is derived.
/// ///
/// Defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents]. /// Defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents].
@ -286,10 +291,14 @@ impl DiversifierKey {
/// 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, PartialEq, Eq)]
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
@ -334,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)
} }
} }
@ -367,6 +376,16 @@ impl From<&FullViewingKey> for IncomingViewingKey {
} }
impl IncomingViewingKey { impl IncomingViewingKey {
/// Parses an Orchard incoming viewing key from its raw encoding.
pub fn from_bytes(bytes: &[u8; 64]) -> CtOption<Self> {
NonZeroPallasBase::from_bytes(bytes[32..].try_into().unwrap()).map(|ivk| {
IncomingViewingKey {
dk: DiversifierKey(bytes[..32].try_into().unwrap()),
ivk: KeyAgreementPrivateKey(ivk.into()),
}
})
}
/// Returns the default payment address for this key. /// Returns the default payment address for this key.
pub fn default_address(&self) -> Address { pub fn default_address(&self) -> Address {
self.address(self.dk.default_diversifier()) self.address(self.dk.default_diversifier())
@ -401,35 +420,145 @@ impl From<&FullViewingKey> for OutgoingViewingKey {
} }
} }
impl From<[u8; 32]> for OutgoingViewingKey {
fn from(ovk: [u8; 32]) -> Self {
OutgoingViewingKey(ovk)
}
}
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, PartialEq, Eq)]
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()
} }
} }
/// An ephemeral secret key used to encrypt an output note on-chain.
///
/// `esk` is "ephemeral" in the sense that each secret key is only used once. In
/// practice, `esk` is derived deterministically from the note that it is encrypting.
///
/// $\mathsf{KA}^\mathsf{Orchard}.\mathsf{Private} := \mathbb{F}^{\ast}_{r_P}$
///
/// Defined in [section 5.4.5.5: Orchard Key Agreement][concreteorchardkeyagreement].
///
/// [concreteorchardkeyagreement]: https://zips.z.cash/protocol/nu5.pdf#concreteorchardkeyagreement
#[derive(Debug)]
pub struct EphemeralSecretKey(pub(crate) NonZeroPallasScalar);
impl ConstantTimeEq for EphemeralSecretKey {
fn ct_eq(&self, other: &Self) -> subtle::Choice {
self.0.ct_eq(&other.0)
}
}
impl EphemeralSecretKey {
pub(crate) fn from_bytes(bytes: &[u8; 32]) -> CtOption<Self> {
NonZeroPallasScalar::from_bytes(bytes).map(EphemeralSecretKey)
}
pub(crate) fn derive_public(&self, g_d: NonIdentityPallasPoint) -> EphemeralPublicKey {
EphemeralPublicKey(ka_orchard(&self.0, &g_d))
}
pub(crate) fn agree(&self, pk_d: &DiversifiedTransmissionKey) -> SharedSecret {
SharedSecret(ka_orchard(&self.0, &pk_d.0))
}
}
/// An ephemeral public key used to encrypt an output note on-chain.
///
/// `epk` is "ephemeral" in the sense that each public key is only used once. In practice,
/// `epk` is derived deterministically from the note that it is encrypting.
///
/// $\mathsf{KA}^\mathsf{Orchard}.\mathsf{Public} := \mathbb{P}^{\ast}$
///
/// Defined in [section 5.4.5.5: Orchard Key Agreement][concreteorchardkeyagreement].
///
/// [concreteorchardkeyagreement]: https://zips.z.cash/protocol/nu5.pdf#concreteorchardkeyagreement
#[derive(Debug)]
pub struct EphemeralPublicKey(NonIdentityPallasPoint);
impl EphemeralPublicKey {
pub(crate) fn from_bytes(bytes: &[u8; 32]) -> CtOption<Self> {
NonIdentityPallasPoint::from_bytes(bytes).map(EphemeralPublicKey)
}
pub(crate) fn to_bytes(&self) -> EphemeralKeyBytes {
EphemeralKeyBytes(self.0.to_bytes())
}
pub(crate) fn agree(&self, ivk: &IncomingViewingKey) -> SharedSecret {
SharedSecret(ka_orchard(&ivk.ivk.0, &self.0))
}
}
/// $\mathsf{KA}^\mathsf{Orchard}.\mathsf{SharedSecret} := \mathbb{P}^{\ast}$
///
/// Defined in [section 5.4.5.5: Orchard Key Agreement][concreteorchardkeyagreement].
///
/// [concreteorchardkeyagreement]: https://zips.z.cash/protocol/nu5.pdf#concreteorchardkeyagreement
#[derive(Debug)]
pub struct SharedSecret(NonIdentityPallasPoint);
impl SharedSecret {
/// For checking test vectors only.
#[cfg(test)]
pub(crate) fn to_bytes(&self) -> [u8; 32] {
self.0.to_bytes()
}
/// Defined in [Zcash Protocol Spec § 5.4.5.6: Orchard Key Agreement][concreteorchardkdf].
///
/// [concreteorchardkdf]: https://zips.z.cash/protocol/nu5.pdf#concreteorchardkdf
pub(crate) fn kdf_orchard(self, ephemeral_key: &EphemeralKeyBytes) -> Blake2bHash {
Params::new()
.hash_length(32)
.personal(KDF_ORCHARD_PERSONALIZATION)
.to_state()
.update(&self.0.to_bytes())
.update(&ephemeral_key.0)
.finalize()
}
}
/// Generators for property testing. /// Generators for property testing.
#[cfg(any(test, feature = "test-dependencies"))] #[cfg(any(test, feature = "test-dependencies"))]
pub mod testing { pub mod testing {
use proptest::prelude::*; use proptest::prelude::*;
use super::SpendingKey; use super::{EphemeralSecretKey, SpendingKey};
prop_compose! { prop_compose! {
/// Generate a uniformly distributed fake note commitment value. /// Generate a uniformly distributed fake note commitment value.
@ -444,19 +573,64 @@ pub mod testing {
key.unwrap() key.unwrap()
} }
} }
prop_compose! {
/// Generate a uniformly distributed fake note commitment value.
pub fn arb_esk()(
esk in prop::array::uniform32(prop::num::u8::ANY)
.prop_map(|b| EphemeralSecretKey::from_bytes(&b))
.prop_filter(
"Values must correspond to valid Orchard ephemeral secret keys.",
|opt| bool::from(opt.is_some())
)
) -> EphemeralSecretKey {
esk.unwrap()
}
}
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use ff::PrimeField; use ff::PrimeField;
use proptest::prelude::*;
use super::*; use super::{
testing::{arb_esk, arb_spending_key},
*,
};
use crate::{ use crate::{
note::{ExtractedNoteCommitment, Nullifier}, note::{ExtractedNoteCommitment, Nullifier, RandomSeed},
value::NoteValue, value::NoteValue,
Note, Note,
}; };
#[test]
fn parsers_reject_invalid() {
assert!(bool::from(
EphemeralSecretKey::from_bytes(&[0xff; 32]).is_none()
));
assert!(bool::from(
EphemeralPublicKey::from_bytes(&[0xff; 32]).is_none()
));
}
proptest! {
#[test]
fn key_agreement(
sk in arb_spending_key(),
esk in arb_esk(),
) {
let ivk = IncomingViewingKey::from(&(&sk).into());
let addr = ivk.default_address();
let epk = esk.derive_public(addr.g_d());
assert!(bool::from(
esk.agree(addr.pk_d()).0.ct_eq(&epk.agree(&ivk).0)
));
}
}
#[test] #[test]
fn test_vectors() { fn test_vectors() {
for tv in crate::test_vectors::keys::test_vectors() { for tv in crate::test_vectors::keys::test_vectors() {
@ -492,7 +666,7 @@ mod tests {
addr, addr,
NoteValue::from_raw(tv.note_v), NoteValue::from_raw(tv.note_v),
rho, rho,
tv.note_rseed.into(), RandomSeed::from_bytes(tv.note_rseed, &rho).unwrap(),
); );
let cmx: ExtractedNoteCommitment = note.commitment().into(); let cmx: ExtractedNoteCommitment = note.commitment().into();

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,8 +5,8 @@ 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, PrfExpand}, spec::{to_base, to_scalar, NonZeroPallasScalar, PrfExpand},
value::NoteValue, value::NoteValue,
Address, Address,
}; };
@ -21,17 +21,26 @@ pub use self::nullifier::Nullifier;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub(crate) struct RandomSeed([u8; 32]); pub(crate) struct RandomSeed([u8; 32]);
impl From<[u8; 32]> for RandomSeed {
fn from(rseed: [u8; 32]) -> Self {
RandomSeed(rseed)
}
}
impl RandomSeed { impl RandomSeed {
pub(crate) fn random(rng: &mut impl RngCore) -> Self { pub(crate) fn random(rng: &mut impl RngCore, rho: &Nullifier) -> Self {
loop {
let mut bytes = [0; 32]; let mut bytes = [0; 32];
rng.fill_bytes(&mut bytes); rng.fill_bytes(&mut bytes);
RandomSeed(bytes) let rseed = RandomSeed::from_bytes(bytes, rho);
if rseed.is_some().into() {
break rseed.unwrap();
}
}
}
pub(crate) fn from_bytes(rseed: [u8; 32], rho: &Nullifier) -> CtOption<Self> {
let rseed = RandomSeed(rseed);
let esk = rseed.esk_inner(rho);
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].
@ -44,8 +53,18 @@ impl RandomSeed {
/// 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
fn esk(&self, rho: &Nullifier) -> pallas::Scalar { fn esk_inner(&self, rho: &Nullifier) -> CtOption<NonZeroPallasScalar> {
to_scalar(PrfExpand::Esk.with_ad(&self.0, &rho.to_bytes()[..])) NonZeroPallasScalar::from_scalar(to_scalar(
PrfExpand::Esk.with_ad(&self.0, &rho.to_bytes()[..]),
))
}
/// Defined in [Zcash Protocol Spec § 4.7.3: Sending Notes (Orchard)][orchardsend].
///
/// [orchardsend]: https://zips.z.cash/protocol/nu5.pdf#orchardsend
fn esk(&self, rho: &Nullifier) -> NonZeroPallasScalar {
// We can't construct a RandomSeed for which this unwrap fails.
self.esk_inner(rho).unwrap()
} }
/// Defined in [Zcash Protocol Spec § 4.7.3: Sending Notes (Orchard)][orchardsend]. /// Defined in [Zcash Protocol Spec § 4.7.3: Sending Notes (Orchard)][orchardsend].
@ -76,8 +95,17 @@ pub struct Note {
rseed: RandomSeed, rseed: RandomSeed,
} }
impl PartialEq for Note {
fn eq(&self, other: &Self) -> bool {
// Notes are canonically defined by their commitments.
ExtractedNoteCommitment::from(self.commitment())
.eq(&ExtractedNoteCommitment::from(other.commitment()))
}
}
impl Eq for 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,
@ -108,7 +136,7 @@ impl Note {
recipient, recipient,
value, value,
rho, rho,
rseed: RandomSeed::random(&mut rng), rseed: RandomSeed::random(&mut rng, &rho),
}; };
if note.commitment_inner().is_some().into() { if note.commitment_inner().is_some().into() {
break note; break note;
@ -139,11 +167,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

@ -3,7 +3,7 @@ use std::iter;
use bitvec::{array::BitArray, order::Lsb0}; use bitvec::{array::BitArray, order::Lsb0};
use ff::PrimeFieldBits; use ff::PrimeFieldBits;
use pasta_curves::{arithmetic::FieldExt, pallas}; use pasta_curves::{arithmetic::FieldExt, pallas};
use subtle::CtOption; use subtle::{ConstantTimeEq, CtOption};
use crate::{constants::L_ORCHARD_BASE, primitives::sinsemilla, spec::extract_p, value::NoteValue}; use crate::{constants::L_ORCHARD_BASE, primitives::sinsemilla, spec::extract_p, value::NoteValue};
@ -71,3 +71,23 @@ impl std::ops::Deref for ExtractedNoteCommitment {
&self.0 &self.0
} }
} }
impl From<&ExtractedNoteCommitment> for [u8; 32] {
fn from(cmx: &ExtractedNoteCommitment) -> Self {
cmx.to_bytes()
}
}
impl ConstantTimeEq for ExtractedNoteCommitment {
fn ct_eq(&self, other: &Self) -> subtle::Choice {
self.0.ct_eq(&other.0)
}
}
impl PartialEq for ExtractedNoteCommitment {
fn eq(&self, other: &Self) -> bool {
self.ct_eq(other).into()
}
}
impl Eq for ExtractedNoteCommitment {}

410
src/note_encryption.rs Normal file
View File

@ -0,0 +1,410 @@
//! 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
}
}
struct CompactAction {
ephemeral_key: EphemeralKeyBytes,
cmx: ExtractedNoteCommitment,
enc_ciphertext: [u8; 52],
}
impl<T> From<&Action<T>> for CompactAction {
fn from(action: &Action<T>) -> Self {
CompactAction {
ephemeral_key: action.ephemeral_key(),
cmx: action.cmx().clone(),
enc_ciphertext: action.encrypted_note().enc_ciphertext[..52]
.try_into()
.unwrap(),
}
}
}
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) -> &[u8] {
&self.enc_ciphertext
}
}
#[cfg(test)]
mod tests {
use rand::rngs::OsRng;
use zcash_note_encryption::{
try_compact_note_decryption, try_note_decryption, try_output_recovery_with_ovk,
EphemeralKeyBytes,
};
use super::{prf_ock_orchard, CompactAction, OrchardDomain, OrchardNoteEncryption};
use crate::{
bundle::Action,
keys::{
DiversifiedTransmissionKey, Diversifier, EphemeralSecretKey, IncomingViewingKey,
OutgoingViewingKey,
},
note::{ExtractedNoteCommitment, Nullifier, RandomSeed, TransmittedNoteCiphertext},
primitives::redpallas,
value::{NoteValue, ValueCommitment},
Address, Note,
};
#[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 note = Note::from_parts(recipient, value, rho, rseed);
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.clone(),
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 = 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[..]
);
}
}
}

View File

@ -5,6 +5,9 @@ use std::convert::{TryFrom, TryInto};
use pasta_curves::pallas; use pasta_curves::pallas;
use rand::{CryptoRng, RngCore}; use rand::{CryptoRng, RngCore};
#[cfg(test)]
use rand::rngs::OsRng;
/// A RedPallas signature type. /// A RedPallas signature type.
pub trait SigType: reddsa::SigType + private::Sealed {} pub trait SigType: reddsa::SigType + private::Sealed {}
@ -93,6 +96,12 @@ impl<T: SigType> PartialEq for VerificationKey<T> {
} }
impl VerificationKey<SpendAuth> { impl VerificationKey<SpendAuth> {
/// Used in the note encryption tests.
#[cfg(test)]
pub(crate) fn dummy() -> Self {
VerificationKey((&reddsa::SigningKey::new(OsRng)).into())
}
/// Randomizes this verification key with the given `randomizer`. /// Randomizes this verification key with the given `randomizer`.
/// ///
/// Randomization is only supported for `SpendAuth` keys. /// Randomization is only supported for `SpendAuth` keys.

View File

@ -4,10 +4,11 @@ use std::iter;
use std::ops::Deref; use std::ops::Deref;
use ff::{Field, PrimeField, PrimeFieldBits}; use ff::{Field, PrimeField, PrimeFieldBits};
use group::GroupEncoding;
use group::{Curve, Group}; use group::{Curve, Group};
use halo2::arithmetic::{CurveAffine, CurveExt, FieldExt}; use halo2::arithmetic::{CurveAffine, CurveExt, FieldExt};
use pasta_curves::pallas; use pasta_curves::pallas;
use subtle::CtOption; use subtle::{ConditionallySelectable, CtOption};
use crate::{ use crate::{
constants::L_ORCHARD_BASE, constants::L_ORCHARD_BASE,
@ -18,9 +19,28 @@ mod prf_expand;
pub(crate) use prf_expand::PrfExpand; pub(crate) use prf_expand::PrfExpand;
/// A Pallas point that is guaranteed to not be the identity. /// A Pallas point that is guaranteed to not be the identity.
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct NonIdentityPallasPoint(pallas::Point); pub(crate) struct NonIdentityPallasPoint(pallas::Point);
impl Default for NonIdentityPallasPoint {
fn default() -> Self {
NonIdentityPallasPoint(pallas::Point::generator())
}
}
impl ConditionallySelectable for NonIdentityPallasPoint {
fn conditional_select(a: &Self, b: &Self, choice: subtle::Choice) -> Self {
NonIdentityPallasPoint(pallas::Point::conditional_select(&a.0, &b.0, choice))
}
}
impl NonIdentityPallasPoint {
pub(crate) fn from_bytes(bytes: &[u8; 32]) -> CtOption<Self> {
pallas::Point::from_bytes(bytes)
.and_then(|p| CtOption::new(NonIdentityPallasPoint(p), !p.is_identity()))
}
}
impl Deref for NonIdentityPallasPoint { impl Deref for NonIdentityPallasPoint {
type Target = pallas::Point; type Target = pallas::Point;
@ -30,9 +50,30 @@ impl Deref for NonIdentityPallasPoint {
} }
/// An integer in [1..q_P]. /// An integer in [1..q_P].
#[derive(Clone, Copy, Debug)]
pub(crate) struct NonZeroPallasBase(pallas::Base); pub(crate) struct NonZeroPallasBase(pallas::Base);
impl Default for NonZeroPallasBase {
fn default() -> Self {
NonZeroPallasBase(pallas::Base::one())
}
}
impl ConditionallySelectable for NonZeroPallasBase {
fn conditional_select(a: &Self, b: &Self, choice: subtle::Choice) -> Self {
NonZeroPallasBase(pallas::Base::conditional_select(&a.0, &b.0, choice))
}
}
impl NonZeroPallasBase { impl NonZeroPallasBase {
pub(crate) fn from_bytes(bytes: &[u8; 32]) -> CtOption<Self> {
pallas::Base::from_bytes(bytes).and_then(NonZeroPallasBase::from_base)
}
pub(crate) fn from_base(b: pallas::Base) -> CtOption<Self> {
CtOption::new(NonZeroPallasBase(b), !b.ct_is_zero())
}
/// Constructs a wrapper for a base field element that is guaranteed to be non-zero. /// Constructs a wrapper for a base field element that is guaranteed to be non-zero.
/// ///
/// # Panics /// # Panics
@ -45,16 +86,36 @@ impl NonZeroPallasBase {
} }
/// An integer in [1..r_P]. /// An integer in [1..r_P].
#[derive(Debug)] #[derive(Clone, Copy, Debug)]
pub(crate) struct NonZeroPallasScalar(pallas::Scalar); pub(crate) struct NonZeroPallasScalar(pallas::Scalar);
impl Default for NonZeroPallasScalar {
fn default() -> Self {
NonZeroPallasScalar(pallas::Scalar::one())
}
}
impl From<NonZeroPallasBase> for NonZeroPallasScalar { impl From<NonZeroPallasBase> for NonZeroPallasScalar {
fn from(s: NonZeroPallasBase) -> Self { fn from(s: NonZeroPallasBase) -> Self {
NonZeroPallasScalar::guaranteed(mod_r_p(s.0)) NonZeroPallasScalar::guaranteed(mod_r_p(s.0))
} }
} }
impl ConditionallySelectable for NonZeroPallasScalar {
fn conditional_select(a: &Self, b: &Self, choice: subtle::Choice) -> Self {
NonZeroPallasScalar(pallas::Scalar::conditional_select(&a.0, &b.0, choice))
}
}
impl NonZeroPallasScalar { impl NonZeroPallasScalar {
pub(crate) fn from_bytes(bytes: &[u8; 32]) -> CtOption<Self> {
pallas::Scalar::from_bytes(bytes).and_then(NonZeroPallasScalar::from_scalar)
}
pub(crate) fn from_scalar(s: pallas::Scalar) -> CtOption<Self> {
CtOption::new(NonZeroPallasScalar(s), !s.ct_is_zero())
}
/// Constructs a wrapper for a scalar field element that is guaranteed to be non-zero. /// Constructs a wrapper for a scalar field element that is guaranteed to be non-zero.
/// ///
/// # Panics /// # Panics

View File

@ -1,2 +1,3 @@
pub(crate) mod commitment_tree; pub(crate) mod commitment_tree;
pub(crate) mod keys; pub(crate) mod keys;
pub(crate) mod note_encryption;

File diff suppressed because it is too large Load Diff

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())
} }