Skip to content

Commit

Permalink
crypto: add helper function for decrypting transaction memo (#3088)
Browse files Browse the repository at this point in the history
  • Loading branch information
redshiftzero authored Sep 22, 2023
1 parent 1a666ea commit 7e768ea
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 10 deletions.
18 changes: 8 additions & 10 deletions crates/core/transaction/src/effect_hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,10 @@ mod tests {

let mut rng = OsRng;

let memo_plaintext = MemoPlaintext {
sender: Address::dummy(&mut rng),
text: "".to_string(),
};
let plan = TransactionPlan {
expiry_height: 0,
fee: Fee::default(),
Expand All @@ -625,16 +629,7 @@ mod tests {
SwapPlan::new(&mut OsRng, swap_plaintext).into(),
],
clue_plans: vec![CluePlan::new(&mut OsRng, addr, 1)],
memo_plan: Some(
MemoPlan::new(
&mut OsRng,
MemoPlaintext {
sender: Address::dummy(&mut rng),
text: "".to_string(),
},
)
.unwrap(),
),
memo_plan: Some(MemoPlan::new(&mut OsRng, memo_plaintext.clone()).unwrap()),
};

println!("{}", serde_json::to_string_pretty(&plan).unwrap());
Expand Down Expand Up @@ -664,6 +659,9 @@ mod tests {

assert_eq!(plan_effect_hash, transaction_effect_hash);

let decrypted_memo = transaction.decrypt_memo(fvk).expect("can decrypt memo");
assert_eq!(decrypted_memo, memo_plaintext);

// TODO: fix this and move into its own test?
// // Also check the concurrent build results in the same effect hash.
// let rt = Runtime::new().unwrap();
Expand Down
48 changes: 48 additions & 0 deletions crates/core/transaction/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,54 @@ impl Transaction {
.sum()
}

/// Helper function for decrypting the memo on the transaction given an FVK.
///
/// Will return an Error if there is no memo.
pub fn decrypt_memo(&self, fvk: &FullViewingKey) -> anyhow::Result<MemoPlaintext> {
// Error if we don't have an encrypted memo field to decrypt.
if self.transaction_body().memo.is_none() {
return Err(anyhow::anyhow!("no memo"));
}

// Iterate through the outputs until we find an output that lets us decrypt the memo.
for output in self.outputs() {
// First decrypt the wrapped memo key on the output.
let ovk_wrapped_key = output.body.ovk_wrapped_key.clone();
let shared_secret = Note::decrypt_key(
ovk_wrapped_key,
output.body.note_payload.note_commitment,
output.body.balance_commitment,
fvk.outgoing(),
&output.body.note_payload.ephemeral_key,
);

let wrapped_memo_key = &output.body.wrapped_memo_key;
let memo_key: PayloadKey = match shared_secret {
Ok(shared_secret) => {
let payload_key =
PayloadKey::derive(&shared_secret, &output.body.note_payload.ephemeral_key);
wrapped_memo_key.decrypt_outgoing(&payload_key)?
}
Err(_) => wrapped_memo_key
.decrypt(output.body.note_payload.ephemeral_key, fvk.incoming())?,
};

// Now we can use the memo key to decrypt the memo.
let tx_body = self.transaction_body();
let memo_ciphertext = tx_body
.memo
.as_ref()
.expect("memo field exists on this transaction");
let decrypted_memo = MemoCiphertext::decrypt(&memo_key, memo_ciphertext.clone())?;

// The memo is shared across all outputs, so we can stop here.
return Ok(decrypted_memo);
}

// If we got here, we were unable to find an output to decrypt the memo.
Err(anyhow::anyhow!("unable to decrypt memo"))
}

pub fn payload_keys(
&self,
fvk: &FullViewingKey,
Expand Down

0 comments on commit 7e768ea

Please sign in to comment.