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

protocol: lift memos to transaction level #1371

Merged
merged 2 commits into from
Sep 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions component/src/ibc/component/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,7 @@ mod tests {
chain_id: "".to_string(),
fee: Default::default(),
fmd_clues: vec![],
memo: None,
},
anchor: tct::Tree::new().root(),
binding_sig: [0u8; 64].into(),
Expand All @@ -699,6 +700,7 @@ mod tests {
chain_id: "".to_string(),
fee: Default::default(),
fmd_clues: vec![],
memo: None,
},
binding_sig: [0u8; 64].into(),
anchor: tct::Tree::new().root(),
Expand Down Expand Up @@ -748,6 +750,7 @@ mod tests {
chain_id: "".to_string(),
fee: Default::default(),
fmd_clues: vec![],
memo: None,
},
anchor: tct::Tree::new().root(),
binding_sig: [0u8; 64].into(),
Expand Down
106 changes: 72 additions & 34 deletions crypto/src/memo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ use anyhow::anyhow;

use crate::{
ka,
keys::{IncomingViewingKey, OutgoingViewingKey},
keys::OutgoingViewingKey,
note,
symmetric::{OvkWrappedKey, PayloadKey, PayloadKind},
value, Address, Note,
symmetric::{OvkWrappedKey, PayloadKey, PayloadKind, WrappedMemoKey},
value, Note,
};

pub const MEMO_CIPHERTEXT_LEN_BYTES: usize = 528;
Expand Down Expand Up @@ -49,16 +49,13 @@ impl TryFrom<&[u8]> for MemoPlaintext {
}
}

#[derive(Clone, Debug)]
pub struct MemoCiphertext(pub [u8; MEMO_CIPHERTEXT_LEN_BYTES]);

impl MemoPlaintext {
/// Encrypt a memo, returning its ciphertext.
pub fn encrypt(&self, esk: &ka::Secret, address: &Address) -> MemoCiphertext {
let epk = esk.diversified_public(address.diversified_generator());
let shared_secret = esk
.key_agreement_with(address.transmission_key())
.expect("key agreement succeeds");

let key = PayloadKey::derive(&shared_secret, &epk);
let encryption_result = key.encrypt(self.0.to_vec(), PayloadKind::Memo);
pub fn encrypt(&self, memo_key: PayloadKey) -> MemoCiphertext {
let encryption_result = memo_key.encrypt(self.0.to_vec(), PayloadKind::Memo);
let ciphertext: [u8; MEMO_CIPHERTEXT_LEN_BYTES] = encryption_result
.try_into()
.expect("memo encryption result fits in ciphertext len");
Expand All @@ -69,19 +66,12 @@ impl MemoPlaintext {
/// Decrypt a `MemoCiphertext` to generate a plaintext `Memo`.
pub fn decrypt(
ciphertext: MemoCiphertext,
ivk: &IncomingViewingKey,
epk: &ka::Public,
memo_key: &PayloadKey,
) -> Result<MemoPlaintext, anyhow::Error> {
let shared_secret = ivk
.key_agreement_with(epk)
.map_err(|_| anyhow!("could not perform key agreement"))?;

let key = PayloadKey::derive(&shared_secret, epk);
let plaintext = key
let encryption_result = memo_key
.decrypt(ciphertext.0.to_vec(), PayloadKind::Memo)
.map_err(|_| anyhow!("decryption error"))?;

let plaintext_bytes: [u8; MEMO_LEN_BYTES] = plaintext
let plaintext_bytes: [u8; MEMO_LEN_BYTES] = encryption_result
.try_into()
.map_err(|_| anyhow!("could not fit plaintext into memo size"))?;

Expand All @@ -96,12 +86,17 @@ impl MemoPlaintext {
cv: value::Commitment,
ovk: &OutgoingViewingKey,
epk: &ka::Public,
wrapped_memo_key: &WrappedMemoKey,
) -> Result<MemoPlaintext, anyhow::Error> {
let shared_secret = Note::decrypt_key(wrapped_ovk, cm, cv, ovk, epk)
.map_err(|_| anyhow!("key decryption error"))?;

let key = PayloadKey::derive(&shared_secret, epk);
let plaintext = key
let action_key = PayloadKey::derive(&shared_secret, epk);
let memo_key = wrapped_memo_key
.decrypt_outgoing(&action_key)
.map_err(|_| anyhow!("could not decrypt wrapped memo key"))?;

let plaintext = memo_key
.decrypt(ciphertext.0.to_vec(), PayloadKind::Memo)
.map_err(|_| anyhow!("decryption error"))?;

Expand All @@ -113,8 +108,21 @@ impl MemoPlaintext {
}
}

#[derive(Clone, Debug)]
pub struct MemoCiphertext(pub [u8; MEMO_CIPHERTEXT_LEN_BYTES]);
impl TryFrom<&[u8]> for MemoCiphertext {
type Error = anyhow::Error;

fn try_from(input: &[u8]) -> Result<MemoCiphertext, Self::Error> {
if input.len() > MEMO_CIPHERTEXT_LEN_BYTES {
return Err(anyhow::anyhow!(
"provided memo ciphertext exceeds maximum memo size"
));
}
let mut mc = [0u8; MEMO_CIPHERTEXT_LEN_BYTES];
mc[..input.len()].copy_from_slice(input);

Ok(MemoCiphertext(mc))
}
}

#[cfg(test)]
mod tests {
Expand Down Expand Up @@ -144,13 +152,27 @@ mod tests {

let esk = ka::Secret::new(&mut rng);

// On the sender side, we have to encrypt the memo to put into the transaction-level,
// and also the memo key to put on the action-level (output).
let memo = MemoPlaintext(memo_bytes);

let ciphertext = memo.encrypt(&esk, &dest);

let memo_key = PayloadKey::random_key(&mut OsRng);
let ciphertext = memo.encrypt(memo_key.clone());
let wrapped_memo_key = WrappedMemoKey::encrypt(
&memo_key,
esk.clone(),
dest.transmission_key(),
dest.diversified_generator(),
);

// On the recipient side, we have to decrypt the wrapped memo key, and then the memo.
let epk = esk.diversified_public(dest.diversified_generator());
let plaintext = MemoPlaintext::decrypt(ciphertext, ivk, &epk).expect("can decrypt memo");
let decrypted_memo_key = wrapped_memo_key
.decrypt(epk, ivk)
.expect("can decrypt memo key");
let plaintext =
MemoPlaintext::decrypt(ciphertext, &decrypted_memo_key).expect("can decrypt memo");

assert_eq!(memo_key, decrypted_memo_key);
assert_eq!(plaintext, memo);
}

Expand All @@ -176,18 +198,34 @@ mod tests {
};
let note = Note::generate(&mut rng, &dest, value);

// On the sender side, we have to encrypt the memo to put into the transaction-level,
// and also the memo key to put on the action-level (output).
let memo = MemoPlaintext(memo_bytes);
let memo_key = PayloadKey::random_key(&mut OsRng);
let ciphertext = memo.encrypt(memo_key.clone());
let wrapped_memo_key = WrappedMemoKey::encrypt(
&memo_key,
esk.clone(),
dest.transmission_key(),
dest.diversified_generator(),
);

let value_blinding = Fr::rand(&mut rng);
let cv = note.value().commit(value_blinding);

let wrapped_ovk = note.encrypt_key(&esk, ovk, cv);
let ciphertext = memo.encrypt(&esk, &dest);

// Later, still on the sender side, we decrypt the memo by using the decrypt_outgoing method.
let epk = esk.diversified_public(dest.diversified_generator());
let plaintext =
MemoPlaintext::decrypt_outgoing(ciphertext, wrapped_ovk, note.commit(), cv, ovk, &epk)
.expect("can decrypt memo");
let plaintext = MemoPlaintext::decrypt_outgoing(
ciphertext,
wrapped_ovk,
note.commit(),
cv,
ovk,
&epk,
&wrapped_memo_key,
)
.expect("can decrypt memo");

assert_eq!(plaintext, memo);
}
Expand Down
125 changes: 118 additions & 7 deletions crypto/src/symmetric.rs
Original file line number Diff line number Diff line change
@@ -1,33 +1,43 @@
use anyhow::Result;
use anyhow::{anyhow, Result};
use chacha20poly1305::{
aead::{Aead, NewAead},
ChaCha20Poly1305, Key, Nonce,
};
use rand::{CryptoRng, RngCore};

use crate::{ka, keys::OutgoingViewingKey, note, value};
use crate::{
ka,
keys::{IncomingViewingKey, OutgoingViewingKey},
note, value,
};

pub const PAYLOAD_KEY_LEN_BYTES: usize = 32;
pub const OVK_WRAPPED_LEN_BYTES: usize = 48;
pub const MEMOKEY_WRAPPED_LEN_BYTES: usize = 48;

/// Represents the item to be encrypted/decrypted with the [`PayloadKey`].
pub enum PayloadKind {
Note,
Memo,
MemoKey,
Swap,
Memo,
}

impl PayloadKind {
pub(crate) fn nonce(&self) -> [u8; 12] {
match self {
Self::Note => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
Self::Memo => [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
Self::MemoKey => [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
Self::Swap => [2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
Self::Memo => [3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
}
}
}

/// Represents a symmetric `ChaCha20Poly1305` key.
///
/// Used for encrypting and decrypting notes, memos and swaps.
/// Used for encrypting and decrypting notes, memos, memo keys, and swaps.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PayloadKey(Key);

impl PayloadKey {
Expand All @@ -43,7 +53,18 @@ impl PayloadKey {
Self(*Key::from_slice(key.as_bytes()))
}

/// Encrypt a note, swap, or memo using the `PayloadKey`.
/// Derive a random `PayloadKey`. Used for memo key wrapping.
pub fn random_key<R: CryptoRng + RngCore>(rng: &mut R) -> Self {
let mut key_bytes = [0u8; 32];
rng.fill_bytes(&mut key_bytes);
Self(*Key::from_slice(&key_bytes[..]))
}

pub fn to_vec(&self) -> Vec<u8> {
self.0.to_vec()
}

/// Encrypt a note, swap, memo, or memo key using the `PayloadKey`.
pub fn encrypt(&self, plaintext: Vec<u8>, kind: PayloadKind) -> Vec<u8> {
let cipher = ChaCha20Poly1305::new(&self.0);
let nonce_bytes = kind.nonce();
Expand All @@ -54,7 +75,7 @@ impl PayloadKey {
.expect("encryption succeeded")
}

/// Decrypt a note, swap, or memo using the `PayloadKey`.
/// Decrypt a note, swap, memo, or memo key using the `PayloadKey`.
pub fn decrypt(&self, ciphertext: Vec<u8>, kind: PayloadKind) -> Result<Vec<u8>> {
let cipher = ChaCha20Poly1305::new(&self.0);
let nonce_bytes = kind.nonce();
Expand All @@ -66,6 +87,17 @@ impl PayloadKey {
}
}

impl TryFrom<Vec<u8>> for PayloadKey {
type Error = anyhow::Error;

fn try_from(vector: Vec<u8>) -> Result<Self, Self::Error> {
let bytes: [u8; PAYLOAD_KEY_LEN_BYTES] = vector
.try_into()
.map_err(|_| anyhow::anyhow!("PayloadKey incorrect len"))?;
Ok(Self(*Key::from_slice(&bytes)))
}
}

/// Represents a symmetric `ChaCha20Poly1305` key.
///
/// Used for encrypting and decrypting [`OvkWrappedKey`] material used to decrypt
Expand Down Expand Up @@ -159,3 +191,82 @@ impl TryFrom<&[u8]> for OvkWrappedKey {
Ok(Self(bytes))
}
}

/// Represents encrypted key material used to decrypt a `MemoCiphertext`.
#[derive(Clone, Debug)]
pub struct WrappedMemoKey(pub [u8; MEMOKEY_WRAPPED_LEN_BYTES]);

impl WrappedMemoKey {
pub fn to_vec(&self) -> Vec<u8> {
self.0.to_vec()
}

/// Encrypt a memo key using the action-specific `PayloadKey`.
pub fn encrypt(
memo_key: &PayloadKey,
esk: ka::Secret,
transmission_key: &ka::Public,
diversified_generator: &decaf377::Element,
) -> Self {
// 1. Construct the per-action PayloadKey.
let epk = esk.diversified_public(diversified_generator);
let shared_secret = esk
.key_agreement_with(transmission_key)
.expect("key agreement succeeded");

let action_key = PayloadKey::derive(&shared_secret, &epk);
// 2. Now use the per-action key to encrypt the memo key.
let encrypted_memo_key = action_key.encrypt(memo_key.to_vec(), PayloadKind::MemoKey);
let wrapped_memo_key_bytes: [u8; MEMOKEY_WRAPPED_LEN_BYTES] = encrypted_memo_key
.try_into()
.expect("memo key must fit in wrapped memo key field");

WrappedMemoKey(wrapped_memo_key_bytes)
}

/// Decrypt a wrapped memo key by first deriving the action-specific `PayloadKey`.
pub fn decrypt(&self, epk: ka::Public, ivk: &IncomingViewingKey) -> Result<PayloadKey> {
// 1. Construct the per-action PayloadKey.
let shared_secret = ivk
.key_agreement_with(&epk)
.expect("key agreement succeeded");

let action_key = PayloadKey::derive(&shared_secret, &epk);
// 2. Now use the per-action key to decrypt the memo key.
let decrypted_memo_key = action_key
.decrypt(self.to_vec(), PayloadKind::MemoKey)
.map_err(|_| anyhow!("decryption error"))?;

decrypted_memo_key.try_into()
}

/// Decrypt a wrapped memo key using the action-specific `PayloadKey`.
pub fn decrypt_outgoing(&self, action_key: &PayloadKey) -> Result<PayloadKey> {
let decrypted_memo_key = action_key
.decrypt(self.to_vec(), PayloadKind::MemoKey)
.map_err(|_| anyhow!("decryption error"))?;
decrypted_memo_key.try_into()
}
}

impl TryFrom<Vec<u8>> for WrappedMemoKey {
type Error = anyhow::Error;

fn try_from(vector: Vec<u8>) -> Result<Self, Self::Error> {
let bytes: [u8; MEMOKEY_WRAPPED_LEN_BYTES] = vector
.try_into()
.map_err(|_| anyhow::anyhow!("wrapped memo key malformed"))?;
Ok(Self(bytes))
}
}

impl TryFrom<&[u8]> for WrappedMemoKey {
type Error = anyhow::Error;

fn try_from(arr: &[u8]) -> Result<Self, Self::Error> {
let bytes: [u8; MEMOKEY_WRAPPED_LEN_BYTES] = arr
.try_into()
.map_err(|_| anyhow::anyhow!("wrapped memo key malformed"))?;
Ok(Self(bytes))
}
}