diff --git a/zcash_primitives/CHANGELOG.md b/zcash_primitives/CHANGELOG.md index 6ce7fb42a..2e322818d 100644 --- a/zcash_primitives/CHANGELOG.md +++ b/zcash_primitives/CHANGELOG.md @@ -7,9 +7,19 @@ and this library adheres to Rust's notion of ## [Unreleased] ### Added -- `zcash_primitives::sapling::keys::DiversifiableFullViewingKey` -- `zcash_primitives::sapling::keys::Scope` +- `zcash_primitives::legacy::AccountPrivKey::{to_bytes, from_bytes}` - `zcash_primitives::sapling::NullifierDerivingKey` +- Added in `zcash_primitives::sapling::keys` + - `DecodingError` + - `DiversifiableFullViewingKey` + - `Scope` + - `ExpandedSpendingKey::from_bytes` + - `ExtendedSpendingKey::{from_bytes, to_bytes}` +- Added in `zcash_primitives::zip32` + - `ChainCode::as_bytes` + - `DiversifierIndex::as_bytes` + - `ExtendedSpendingKey::{from_bytes, to_bytes}` + - Implementations of `From` and `From` for `DiversifierIndex` ### Changed - `zcash_primitives::sapling::ViewingKey` now stores `nk` as a diff --git a/zcash_primitives/src/legacy/keys.rs b/zcash_primitives/src/legacy/keys.rs index fa22cca21..483003437 100644 --- a/zcash_primitives/src/legacy/keys.rs +++ b/zcash_primitives/src/legacy/keys.rs @@ -1,4 +1,7 @@ -use hdwallet::{ExtendedPrivKey, ExtendedPubKey, KeyIndex}; +use hdwallet::{ + traits::{Deserialize, Serialize}, + ExtendedPrivKey, ExtendedPubKey, KeyIndex, +}; use ripemd::Digest as RipemdDigest; use secp256k1::PublicKey; use sha2::{Digest as Sha2Digest, Sha256}; @@ -63,6 +66,20 @@ impl AccountPrivKey { .derive_private_key(KeyIndex::Normal(child_index)) .map(|k| k.private_key) } + + /// Returns the `AccountPrivKey` serialized using the encoding for a + /// [BIP 32](https://en.bitcoin.it/wiki/BIP_0032) ExtendedPrivKey + pub fn to_bytes(&self) -> Vec { + self.0.serialize() + } + + /// Decodes the `AccountPrivKey` from the encoding specified for a + /// [BIP 32](https://en.bitcoin.it/wiki/BIP_0032) ExtendedPrivKey + pub fn from_bytes(b: &[u8]) -> Option { + ExtendedPrivKey::deserialize(b) + .map(AccountPrivKey::from_extended_privkey) + .ok() + } } /// A type representing a BIP-44 public key at the account path level diff --git a/zcash_primitives/src/sapling/keys.rs b/zcash_primitives/src/sapling/keys.rs index eeb2d4042..933db9221 100644 --- a/zcash_primitives/src/sapling/keys.rs +++ b/zcash_primitives/src/sapling/keys.rs @@ -17,6 +17,16 @@ use subtle::CtOption; use super::{NullifierDerivingKey, PaymentAddress, ProofGenerationKey, SaplingIvk, ViewingKey}; +/// Errors that can occur in the decoding of Sapling spending keys. +pub enum DecodingError { + /// The length of the byte slice provided for decoding was incorrect. + LengthInvalid { expected: usize, actual: usize }, + /// Could not decode the `ask` bytes to a jubjub field element. + InvalidAsk, + /// Could not decode the `nsk` bytes to a jubjub field element. + InvalidNsk, +} + /// A Sapling expanded spending key #[derive(Clone)] pub struct ExpandedSpendingKey { @@ -49,39 +59,52 @@ impl ExpandedSpendingKey { } } + /// Decodes the expanded spending key from its serialized representation + /// as part of the encoding of the extended spending key as defined in + /// [ZIP 32](https://zips.z.cash/zip-0032) + pub fn from_bytes(b: &[u8]) -> Result { + if b.len() != 96 { + return Err(DecodingError::LengthInvalid { + expected: 96, + actual: b.len(), + }); + } + + let ask = Option::from(jubjub::Fr::from_repr(b[0..32].try_into().unwrap())) + .ok_or(DecodingError::InvalidAsk)?; + let nsk = Option::from(jubjub::Fr::from_repr(b[32..64].try_into().unwrap())) + .ok_or(DecodingError::InvalidNsk)?; + let ovk = OutgoingViewingKey(b[64..96].try_into().unwrap()); + + Ok(ExpandedSpendingKey { ask, nsk, ovk }) + } + pub fn read(mut reader: R) -> io::Result { - let mut ask_repr = [0u8; 32]; - reader.read_exact(ask_repr.as_mut())?; - let ask = Option::from(jubjub::Fr::from_repr(ask_repr)) - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "ask not in field"))?; - - let mut nsk_repr = [0u8; 32]; - reader.read_exact(nsk_repr.as_mut())?; - let nsk = Option::from(jubjub::Fr::from_repr(nsk_repr)) - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "nsk not in field"))?; - - let mut ovk = [0u8; 32]; - reader.read_exact(&mut ovk)?; - - Ok(ExpandedSpendingKey { - ask, - nsk, - ovk: OutgoingViewingKey(ovk), + let mut repr = [0u8; 96]; + reader.read_exact(repr.as_mut())?; + Self::from_bytes(&repr).map_err(|e| match e { + DecodingError::InvalidAsk => { + io::Error::new(io::ErrorKind::InvalidData, "ask not in field") + } + DecodingError::InvalidNsk => { + io::Error::new(io::ErrorKind::InvalidData, "nsk not in field") + } + DecodingError::LengthInvalid { .. } => unreachable!(), }) } pub fn write(&self, mut writer: W) -> io::Result<()> { - writer.write_all(self.ask.to_repr().as_ref())?; - writer.write_all(self.nsk.to_repr().as_ref())?; - writer.write_all(&self.ovk.0)?; - - Ok(()) + writer.write_all(&self.to_bytes()) } + /// Encodes the expanded spending key to the its seralized representation + /// as part of the encoding of the extended spending key as defined in + /// [ZIP 32](https://zips.z.cash/zip-0032) pub fn to_bytes(&self) -> [u8; 96] { let mut result = [0u8; 96]; - self.write(&mut result[..]) - .expect("should be able to serialize an ExpandedSpendingKey"); + (&mut result[0..32]).copy_from_slice(&self.ask.to_repr()); + (&mut result[32..64]).copy_from_slice(&self.nsk.to_repr()); + (&mut result[64..96]).copy_from_slice(&self.ovk.0); result } } diff --git a/zcash_primitives/src/zip32.rs b/zcash_primitives/src/zip32.rs index 6e4767d19..e2bb8af89 100644 --- a/zcash_primitives/src/zip32.rs +++ b/zcash_primitives/src/zip32.rs @@ -18,7 +18,7 @@ use std::io::{self, Read, Write}; use crate::{ keys::{prf_expand, prf_expand_vec, OutgoingViewingKey}, sapling::{ - keys::{ExpandedSpendingKey, FullViewingKey}, + keys::{DecodingError, ExpandedSpendingKey, FullViewingKey}, NullifierDerivingKey, }, }; @@ -91,6 +91,10 @@ impl FvkTag { fn master() -> Self { FvkTag([0u8; 4]) } + + fn as_bytes(&self) -> &[u8; 4] { + &self.0 + } } /// A child index for a derived key @@ -124,6 +128,14 @@ impl ChildIndex { #[derive(Clone, Copy, Debug, PartialEq)] pub struct ChainCode([u8; 32]); +impl ChainCode { + /// Returns byte representation of the chain code, as required for + /// [ZIP 32](https://zips.z.cash/zip-0032) encoding. + fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } +} + #[derive(Clone, Copy, Debug, PartialEq)] pub struct DiversifierIndex(pub [u8; 11]); @@ -176,6 +188,16 @@ impl DiversifierKey { DiversifierKey(dk_m) } + /// Constructs the diversifier key from its constituent bytes. + pub fn from_bytes(key: [u8; 32]) -> Self { + DiversifierKey(key) + } + + /// Returns the byte representation of the diversifier key. + pub fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } + fn derive_child(&self, i_l: &[u8]) -> Self { let mut dk = [0u8; 32]; dk.copy_from_slice(&prf_expand_vec(i_l, &[&[0x16], &self.0]).as_bytes()[..32]); @@ -402,6 +424,45 @@ impl ExtendedSpendingKey { } } + /// Decodes the extended spending key from its serialized representation as defined in + /// [ZIP 32](https://zips.z.cash/zip-0032) + pub fn from_bytes(b: &[u8]) -> Result { + if b.len() != 169 { + return Err(DecodingError::LengthInvalid { + expected: 169, + actual: b.len(), + }); + } + + let depth = b[0]; + + let mut parent_fvk_tag = FvkTag([0; 4]); + (&mut parent_fvk_tag.0[..]).copy_from_slice(&b[1..5]); + + let mut ci_bytes = [0u8; 4]; + (&mut ci_bytes[..]).copy_from_slice(&b[5..9]); + let child_index = ChildIndex::from_index(u32::from_le_bytes(ci_bytes)); + + let mut chain_code = ChainCode([0u8; 32]); + (&mut chain_code.0[..]).copy_from_slice(&b[9..41]); + + let expsk = ExpandedSpendingKey::from_bytes(&b[41..137])?; + + let mut dk = DiversifierKey([0u8; 32]); + (&mut dk.0[..]).copy_from_slice(&b[137..169]); + + Ok(ExtendedSpendingKey { + depth, + parent_fvk_tag, + child_index, + chain_code, + expsk, + dk, + }) + } + + /// Reads and decodes the encoded form of the extended spending key as define in + /// [ZIP 32](https://zips.z.cash/zip-0032) from the provided reader. pub fn read(mut reader: R) -> io::Result { let depth = reader.read_u8()?; let mut tag = [0; 4]; @@ -423,15 +484,23 @@ impl ExtendedSpendingKey { }) } - pub fn write(&self, mut writer: W) -> io::Result<()> { - writer.write_u8(self.depth)?; - writer.write_all(&self.parent_fvk_tag.0)?; - writer.write_u32::(self.child_index.value())?; - writer.write_all(&self.chain_code.0)?; - writer.write_all(&self.expsk.to_bytes())?; - writer.write_all(&self.dk.0)?; + /// Encodes the extended spending key to the its seralized representation as defined in + /// [ZIP 32](https://zips.z.cash/zip-0032) + pub fn to_bytes(&self) -> [u8; 169] { + let mut result = [0u8; 169]; + result[0] = self.depth; + (&mut result[1..5]).copy_from_slice(&self.parent_fvk_tag.as_bytes()[..]); + (&mut result[5..9]).copy_from_slice(&self.child_index.value().to_le_bytes()[..]); + (&mut result[9..41]).copy_from_slice(&self.chain_code.as_bytes()[..]); + (&mut result[41..137]).copy_from_slice(&self.expsk.to_bytes()[..]); + (&mut result[137..169]).copy_from_slice(&self.dk.as_bytes()[..]); + result + } - Ok(()) + /// Writes the encoded form of the extended spending key as define in + /// [ZIP 32](https://zips.z.cash/zip-0032) to the provided writer. + pub fn write(&self, mut writer: W) -> io::Result<()> { + writer.write_all(&self.to_bytes()) } /// Returns the child key corresponding to the path derived from the master key