Skip to content

Commit

Permalink
crypto: use 16 byte diversifiers (closes #1077)
Browse files Browse the repository at this point in the history
  • Loading branch information
redshiftzero committed Aug 15, 2022
1 parent e1018c8 commit 86eed55
Show file tree
Hide file tree
Showing 7 changed files with 42 additions and 87 deletions.
40 changes: 1 addition & 39 deletions crypto/src/address.rs
Original file line number Diff line number Diff line change
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
44 changes: 20 additions & 24 deletions crypto/src/keys/diversifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ 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 +43,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 Down Expand Up @@ -88,7 +88,7 @@ impl DiversifierKey {
)
.expect("binary string is the configured radix (2)");

let mut diversifier_bytes = [0; 11];
let mut diversifier_bytes = [0; DIVERSIFIER_LEN_BYTES];
diversifier_bytes.copy_from_slice(&enc_index.to_bytes_le());
Diversifier(diversifier_bytes)
}
Expand All @@ -102,9 +102,9 @@ impl DiversifierKey {
)
.expect("binary string is in the configured radix (2)");

let mut index_bytes = [0; 11];
let mut index_bytes = [0; DIVERSIFIER_LEN_BYTES];
index_bytes.copy_from_slice(&index.to_bytes_le());
if index_bytes[8] == 0 && index_bytes[9] == 0 && index_bytes[10] == 0 {
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 +120,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 +131,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 +163,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 +177,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 +188,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 +208,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 +232,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,7 +262,7 @@ 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> {
Expand All @@ -281,7 +277,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
2 changes: 1 addition & 1 deletion crypto/src/keys/ivk.rs
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
24 changes: 12 additions & 12 deletions crypto/src/note.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ use crate::{
value, Fq, Value,
};

pub const NOTE_LEN_BYTES: usize = 116;
pub const NOTE_CIPHERTEXT_BYTES: usize = 132;
pub const NOTE_LEN_BYTES: usize = 121;
pub const NOTE_CIPHERTEXT_BYTES: usize = 137;
pub const OVK_WRAPPED_LEN_BYTES: usize = 80;

/// The nonce used for note encryption.
Expand Down Expand Up @@ -403,11 +403,11 @@ impl From<&Note> for [u8; NOTE_LEN_BYTES] {
fn from(note: &Note) -> [u8; NOTE_LEN_BYTES] {
let mut bytes = [0u8; NOTE_LEN_BYTES];
bytes[0] = NOTE_TYPE;
bytes[1..12].copy_from_slice(&note.diversifier.0);
bytes[12..20].copy_from_slice(&note.value.amount.to_le_bytes());
bytes[20..52].copy_from_slice(&note.value.asset_id.0.to_bytes());
bytes[52..84].copy_from_slice(&note.note_blinding.to_bytes());
bytes[84..116].copy_from_slice(&note.transmission_key.0);
bytes[1..17].copy_from_slice(&note.diversifier.0);
bytes[17..25].copy_from_slice(&note.value.amount.to_le_bytes());
bytes[25..57].copy_from_slice(&note.value.asset_id.0.to_bytes());
bytes[57..89].copy_from_slice(&note.note_blinding.to_bytes());
bytes[89..121].copy_from_slice(&note.transmission_key.0);
bytes
}
}
Expand Down Expand Up @@ -442,21 +442,21 @@ impl TryFrom<&[u8]> for Note {
return Err(Error::NoteTypeUnsupported);
}

let amount_bytes: [u8; 8] = bytes[12..20]
let amount_bytes: [u8; 8] = bytes[17..25]
.try_into()
.map_err(|_| Error::NoteDeserializationError)?;
let asset_id_bytes: [u8; 32] = bytes[20..52]
let asset_id_bytes: [u8; 32] = bytes[25..57]
.try_into()
.map_err(|_| Error::NoteDeserializationError)?;
let note_blinding_bytes: [u8; 32] = bytes[52..84]
let note_blinding_bytes: [u8; 32] = bytes[57..89]
.try_into()
.map_err(|_| Error::NoteDeserializationError)?;

Note::from_parts(
bytes[1..12]
bytes[1..17]
.try_into()
.map_err(|_| Error::NoteDeserializationError)?,
bytes[84..116]
bytes[89..121]
.try_into()
.map_err(|_| Error::NoteDeserializationError)?,
Value {
Expand Down
2 changes: 1 addition & 1 deletion docs/protocol/src/concepts/addresses_keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ From bottom to top:
- the *incoming viewing key* represents the capability to view only incoming transactions, and is used to scan the block chain for incoming transactions.

Penumbra allows the same spending authority to present multiple, publicly
unlinkable addresses, keyed by an 11-byte *address index*. Each choice of
unlinkable addresses, keyed by an 16-byte *address index*. Each choice of
address index gives a distinct shielded payment address. Because these
addresses share a common incoming viewing key, the cost of scanning the
blockchain does not increase with the number of addresses in use.
Expand Down
14 changes: 7 additions & 7 deletions docs/protocol/src/protocol/addresses_keys/addresses.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@ scanning service.

## Diversifiers

Addresses are parameterized by *diversifiers*, 11-byte tags used to derive up to
$2^{88}$ distinct addresses for each spending authority. The diversifier is
Addresses are parameterized by *diversifiers*, 16-byte tags used to derive up to
$2^{128}$ distinct addresses for each spending authority. The diversifier is
included in the address, so it should be uniformly random. To ensure this,
diversifiers are indexed by a *address index* $i \in \{0, \ldots, 2^{88} -
diversifiers are indexed by a *address index* $i \in \{0, \ldots, 2^{128} -
1\}$; the $i$-th diversifier $d_i$ is the encryption of $i$ using [AES-FF1] with
the diversifier key $\mathsf{dk}$.[^1]

Each diversifier $d$ is used to generate a *diversified basepoint* $B_d$ as
$$B_d = H_{\mathbb G}^{\mathsf d}(d),$$
where
$$H_{\mathbb G}^{\mathsf d} : \{0, 1\}^{88} \rightarrow \mathbb G$$
$$H_{\mathbb G}^{\mathsf d} : \{0, 1\}^{128} \rightarrow \mathbb G$$
performs [hash-to-group] for `decaf377` as follows: first, apply BLAKE2b-512
with personalization `b"Penumbra_Divrsfy"` to the input, then, interpret the
64-byte output as an integer in little-endian byte order and reduce it modulo
Expand Down Expand Up @@ -67,9 +67,9 @@ The clue key is $\mathsf{ck_d}$ is derived as $\mathsf{ck_d} =

### Address Encodings

The raw binary encoding of a payment address is the 75-byte string `d || pk_d ||
ck_d`. We pad this string to 80 bytes, then apply the [F4Jumble] algorithm to
this padded string. This mitigates attacks where an attacker replaces a valid
The raw binary encoding of a payment address is the 80-byte string `d || pk_d ||
ck_d`. We then apply the [F4Jumble] algorithm to
this string. This mitigates attacks where an attacker replaces a valid
address with one derived from an attacker controlled key that encodes to an
address with a subset of characters that collide with the target valid address.
For example, an attacker may try to generate an address with the first
Expand Down

0 comments on commit 86eed55

Please sign in to comment.