Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

crypto: v2 address format #1262

Merged
merged 9 commits into from Aug 16, 2022
424 changes: 238 additions & 186 deletions Cargo.lock

Large diffs are not rendered by default.

9 changes: 3 additions & 6 deletions crypto/Cargo.toml
Expand Up @@ -15,10 +15,8 @@ penumbra-tct = { path = "../tct/" }
# Git deps
decaf377 = { git = "https://github.com/penumbra-zone/decaf377" }
decaf377-rdsa = { version = "0.5", git = "https://github.com/penumbra-zone/decaf377-rdsa" }
# Temporarily pinning poseidon377 until new parameters are audited and the other address-breaking
# changes are ready to ship.
poseidon377 = { git = "https://github.com/penumbra-zone/poseidon377", branch = "oldparams" }
poseidon-paramgen = { git = "https://github.com/penumbra-zone/poseidon377", branch = "oldparams" }
poseidon377 = { git = "https://github.com/penumbra-zone/poseidon377", branch = "main" }
poseidon-paramgen = { git = "https://github.com/penumbra-zone/poseidon377", branch = "main" }
jmt = { git = "https://github.com/penumbra-zone/jellyfish-merkle.git", branch = "main" }
f4jumble = { git = "https://github.com/zcash/librustzcash", rev="2425a0869098e3b0588ccd73c42716bcf418612c" }

Expand All @@ -29,8 +27,7 @@ ark-serialize = "0.3"
regex = "1.5"
sha2 = "0.10.1"
bech32 = "0.8.1"
fpe = "0.5"
aes = "0.7"
aes = "0.8.1"
anyhow = "1"
thiserror = "1"
bytes = "1"
Expand Down
40 changes: 1 addition & 39 deletions crypto/src/address.rs
Expand Up @@ -7,10 +7,6 @@ use serde::{Deserialize, Serialize};

use crate::{fmd, ka, keys::Diversifier, Fq};

// We pad addresses to 80 bytes (before jumbling and Bech32m encoding)
// using this 5 byte padding.
const ADDR_PADDING: &[u8] = "pen00".as_bytes();

/// A valid payment address.
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(try_from = "pb::Address", into = "pb::Address")]
Expand Down Expand Up @@ -86,9 +82,6 @@ impl From<Address> for pb::Address {
bytes
.write_all(&a.clue_key().0)
.expect("can write clue key into vec");
bytes
.write_all(ADDR_PADDING)
.expect("can write padding into vec");

let jumbled_bytes = f4jumble(bytes.get_ref()).expect("can jumble");
pb::Address {
Expand All @@ -104,7 +97,7 @@ impl TryFrom<pb::Address> for Address {
f4jumble_inv(&value.inner).ok_or_else(|| anyhow::anyhow!("invalid address"))?;
let mut bytes = Cursor::new(unjumbled_bytes);

let mut diversifier_bytes = [0u8; 11];
let mut diversifier_bytes = [0u8; 16];
bytes.read_exact(&mut diversifier_bytes)?;

let mut pk_d_bytes = [0u8; 32];
Expand All @@ -113,13 +106,6 @@ impl TryFrom<pb::Address> for Address {
let mut clue_key_bytes = [0; 32];
bytes.read_exact(&mut clue_key_bytes)?;

let mut padding_bytes = [0; 5];
bytes.read_exact(&mut padding_bytes)?;

if padding_bytes != ADDR_PADDING {
return Err(anyhow::anyhow!("invalid address"));
}

let diversifier = Diversifier(diversifier_bytes);
Address::from_components(
diversifier,
Expand Down Expand Up @@ -159,30 +145,6 @@ impl std::str::FromStr for Address {
}
}

/// Parse v0 testnet address string (temporary migration used in `pcli`)
pub fn parse_v0_testnet_address(v0_address: String) -> Result<Address, anyhow::Error> {
let decoded_bytes = &bech32str::decode(&v0_address, "penumbrav0t", bech32str::Bech32m)?;

let mut bytes = Cursor::new(decoded_bytes);
let mut diversifier_bytes = [0u8; 11];
bytes.read_exact(&mut diversifier_bytes)?;
let mut pk_d_bytes = [0u8; 32];
bytes.read_exact(&mut pk_d_bytes)?;
let mut clue_key_bytes = [0; 32];
bytes.read_exact(&mut clue_key_bytes)?;

let diversifier = Diversifier(diversifier_bytes);
let addr = Address::from_components(
diversifier,
diversifier.diversified_generator(),
ka::Public(pk_d_bytes),
fmd::ClueKey(clue_key_bytes),
)
.ok_or_else(|| anyhow::anyhow!("invalid address"))?;

Ok(addr)
}

#[cfg(test)]
mod tests {
use std::str::FromStr;
Expand Down
95 changes: 48 additions & 47 deletions crypto/src/keys/diversifier.rs
@@ -1,17 +1,18 @@
use std::convert::{TryFrom, TryInto};
use std::fmt::Debug;

use aes::Aes256;
use aes::cipher::{generic_array::GenericArray, BlockDecrypt, BlockEncrypt, KeyInit};
use aes::Aes128;

use anyhow::anyhow;
use ark_ff::PrimeField;
use derivative::Derivative;
use fpe::ff1;
use penumbra_proto::{crypto as pb, Protobuf};
use serde::{Deserialize, Serialize};

use crate::Fq;

pub const DIVERSIFIER_LEN_BYTES: usize = 11;
pub const DIVERSIFIER_LEN_BYTES: usize = 16;

#[derive(Copy, Clone, PartialEq, Eq, Derivative, Serialize, Deserialize)]
#[derivative(Debug)]
Expand Down Expand Up @@ -43,13 +44,13 @@ impl TryFrom<&[u8]> for Diversifier {
fn try_from(slice: &[u8]) -> Result<Diversifier, Self::Error> {
if slice.len() != DIVERSIFIER_LEN_BYTES {
return Err(anyhow!(
"diversifier must be 11 bytes, got {:?}",
"diversifier must be 16 bytes, got {:?}",
slice.len()
));
}

let mut bytes = [0u8; DIVERSIFIER_LEN_BYTES];
bytes.copy_from_slice(&slice[0..11]);
bytes.copy_from_slice(&slice[0..16]);
Ok(Diversifier(bytes))
}
}
Expand All @@ -75,36 +76,40 @@ impl TryFrom<pb::Diversifier> for Diversifier {
#[derive(Clone, Derivative)]
#[derivative(Debug)]
pub struct DiversifierKey(
#[derivative(Debug(bound = "", format_with = "crate::fmt_hex"))] pub(super) [u8; 32],
#[derivative(Debug(bound = "", format_with = "crate::fmt_hex"))] pub(super) [u8; 16],
);

impl DiversifierKey {
pub fn diversifier_for_index(&self, index: &AddressIndex) -> Diversifier {
let enc_index = ff1::FF1::<Aes256>::new(&self.0, 2)
.expect("radix 2 is in range")
.encrypt(
b"",
&ff1::BinaryNumeralString::from_bytes_le(&index.to_bytes()),
)
.expect("binary string is the configured radix (2)");

let mut diversifier_bytes = [0; 11];
diversifier_bytes.copy_from_slice(&enc_index.to_bytes_le());
Diversifier(diversifier_bytes)
let mut key_bytes = [0u8; 16];
key_bytes.copy_from_slice(&self.0);
let key = GenericArray::from(key_bytes);

let mut plaintext_bytes = [0u8; 16];
plaintext_bytes.copy_from_slice(&index.to_bytes());
let mut block = GenericArray::from(plaintext_bytes);

let cipher = Aes128::new(&key);
cipher.encrypt_block(&mut block);

let mut ciphertext_bytes = [0u8; 16];
ciphertext_bytes.copy_from_slice(&*block);
Diversifier(ciphertext_bytes)
}

pub fn index_for_diversifier(&self, diversifier: &Diversifier) -> AddressIndex {
let index = ff1::FF1::<Aes256>::new(&self.0, 2)
.expect("radix 2 is in range")
.decrypt(
b"",
&ff1::BinaryNumeralString::from_bytes_le(&diversifier.0),
)
.expect("binary string is in the configured radix (2)");

let mut index_bytes = [0; 11];
index_bytes.copy_from_slice(&index.to_bytes_le());
if index_bytes[8] == 0 && index_bytes[9] == 0 && index_bytes[10] == 0 {
let mut key_bytes = [0u8; 16];
key_bytes.copy_from_slice(&self.0);
let key = GenericArray::from(key_bytes);

let mut block = GenericArray::from(diversifier.0);

let cipher = Aes128::new(&key);
cipher.decrypt_block(&mut block);

let mut index_bytes = [0; DIVERSIFIER_LEN_BYTES];
index_bytes.copy_from_slice(&*block);
if index_bytes[8..16] == [0u8; 8] {
AddressIndex::Numeric(u64::from_le_bytes(
index_bytes[0..8].try_into().expect("can form 8 byte array"),
))
Expand All @@ -120,7 +125,7 @@ pub enum AddressIndex {
/// Reserved for client applications.
Numeric(u64),
/// Randomly generated.
Random([u8; 11]),
Random([u8; 16]),
}

// Workaround for https://github.com/mcarton/rust-derivative/issues/91
Expand All @@ -131,10 +136,10 @@ impl Debug for AddressIndex {
}

impl AddressIndex {
pub fn to_bytes(&self) -> [u8; 11] {
pub fn to_bytes(&self) -> [u8; 16] {
match self {
Self::Numeric(x) => {
let mut bytes = [0; 11];
let mut bytes = [0; DIVERSIFIER_LEN_BYTES];
bytes[0..8].copy_from_slice(&x.to_le_bytes());
bytes
}
Expand Down Expand Up @@ -163,7 +168,7 @@ impl From<u32> for AddressIndex {

impl From<u64> for AddressIndex {
fn from(x: u64) -> Self {
AddressIndex::Numeric(x)
AddressIndex::Numeric(x as u64)
}
}

Expand All @@ -177,11 +182,7 @@ impl From<AddressIndex> for u128 {
fn from(x: AddressIndex) -> Self {
match x {
AddressIndex::Numeric(x) => u128::from(x),
AddressIndex::Random(x) => {
let mut bytes = [0; 16];
bytes[0..11].copy_from_slice(&x);
u128::from_le_bytes(bytes)
}
AddressIndex::Random(x) => u128::from_le_bytes(x),
}
}
}
Expand All @@ -192,11 +193,11 @@ impl TryFrom<AddressIndex> for u64 {
match address_index {
AddressIndex::Numeric(x) => Ok(x),
AddressIndex::Random(bytes) => {
if bytes[8] == 0 && bytes[9] == 0 && bytes[10] == 0 {
if bytes[8..16] == [0u8; 8] {
Ok(u64::from_le_bytes(
bytes[0..8]
.try_into()
.expect("can take first 8 bytes of 11-byte array"),
.expect("can take first 8 bytes of 16-byte array"),
))
} else {
Err(anyhow::anyhow!("address index out of range"))
Expand All @@ -212,19 +213,19 @@ impl TryFrom<&[u8]> for AddressIndex {
fn try_from(slice: &[u8]) -> Result<AddressIndex, Self::Error> {
if slice.len() != DIVERSIFIER_LEN_BYTES {
return Err(anyhow!(
"address index must be 11 bytes, got {:?}",
"address index must be 16 bytes, got {:?}",
slice.len()
));
}

// Numeric addresses have the last three bytes as zero.
if slice[8] == 0 && slice[9] == 0 && slice[10] == 0 {
// Numeric addresses have the last eight bytes as zero.
if slice[8..16] == [0u8; 8] {
let mut bytes = [0; 8];
bytes[0..8].copy_from_slice(&slice[0..8]);
Ok(AddressIndex::Numeric(u64::from_le_bytes(bytes)))
} else {
let mut bytes = [0u8; DIVERSIFIER_LEN_BYTES];
bytes.copy_from_slice(&slice[0..11]);
bytes.copy_from_slice(&slice[0..16]);
Ok(AddressIndex::Random(bytes))
}
}
Expand All @@ -236,7 +237,7 @@ impl From<AddressIndex> for pb::AddressIndex {
fn from(d: AddressIndex) -> pb::AddressIndex {
match d {
AddressIndex::Numeric(x) => {
let mut bytes = [0; 11];
let mut bytes = [0; DIVERSIFIER_LEN_BYTES];
bytes[0..8].copy_from_slice(&x.to_le_bytes());
pb::AddressIndex {
inner: bytes.to_vec(),
Expand Down Expand Up @@ -266,11 +267,11 @@ mod tests {
}

fn address_index_strategy_random() -> BoxedStrategy<AddressIndex> {
any::<[u8; 11]>().prop_map(AddressIndex::Random).boxed()
any::<[u8; 16]>().prop_map(AddressIndex::Random).boxed()
}

fn diversifier_key_strategy() -> BoxedStrategy<DiversifierKey> {
any::<[u8; 32]>().prop_map(DiversifierKey).boxed()
any::<[u8; 16]>().prop_map(DiversifierKey).boxed()
}

proptest! {
Expand All @@ -281,7 +282,7 @@ mod tests {
) {
let diversifier = key.diversifier_for_index(&index);
let index2 = key.index_for_diversifier(&diversifier);
assert_eq!(index2, index );
assert_eq!(index2, index);
}

#[test]
Expand Down
15 changes: 9 additions & 6 deletions crypto/src/keys/fvk.rs
Expand Up @@ -40,15 +40,18 @@ impl FullViewingKey {

/// Construct a full viewing key from its components.
pub fn from_components(ak: VerificationKey<SpendAuth>, nk: NullifierKey) -> Self {
let (ovk, dk) = {
let hash_result = prf::expand(b"Penumbra_ExpndVK", &nk.0.to_bytes(), ak.as_ref());

let ovk = {
let hash_result = prf::expand(b"Penumbra_DeriOVK", &nk.0.to_bytes(), ak.as_ref());
let mut ovk = [0; 32];
let mut dk = [0; 32];
ovk.copy_from_slice(&hash_result.as_bytes()[0..32]);
dk.copy_from_slice(&hash_result.as_bytes()[32..64]);
ovk
};

(ovk, dk)
let dk = {
let hash_result = prf::expand(b"Penumbra_DerivDK", &nk.0.to_bytes(), ak.as_ref());
let mut dk = [0; 16];
dk.copy_from_slice(&hash_result.as_bytes()[0..16]);
dk
};

let ivk = {
Expand Down
2 changes: 1 addition & 1 deletion crypto/src/keys/ivk.rs
Expand Up @@ -37,7 +37,7 @@ impl IncomingViewingKey {
&self,
mut rng: R,
) -> (Address, fmd::DetectionKey) {
let mut random_index = [0u8; 11];
let mut random_index = [0u8; 16];
rng.fill_bytes(&mut random_index);
let index = AddressIndex::Random(random_index);
self.payment_address(index)
Expand Down
3 changes: 0 additions & 3 deletions crypto/src/lib.rs
Expand Up @@ -33,9 +33,6 @@ pub use note_payload::NotePayload;
pub use nullifier::Nullifier;
pub use value::Value;

// Temporary for v0 to v1 testnet address migration.
pub use address::parse_v0_testnet_address;

fn fmt_hex<T: AsRef<[u8]>>(data: T, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", hex::encode(data))
}
Expand Down