penumbra_sdk_transaction/
memo.rs1use std::{
2 convert::{TryFrom, TryInto},
3 fmt::Debug,
4};
5
6use anyhow::anyhow;
7
8use decaf377_ka as ka;
9use penumbra_sdk_asset::balance;
10use penumbra_sdk_keys::{
11 address::ADDRESS_LEN_BYTES,
12 keys::OutgoingViewingKey,
13 symmetric::{OvkWrappedKey, PayloadKey, PayloadKind, WrappedMemoKey},
14 Address,
15};
16use penumbra_sdk_proto::{core::transaction::v1 as pbt, DomainType};
17use penumbra_sdk_shielded_pool::{note, Note};
18use penumbra_sdk_txhash::{EffectHash, EffectingData};
19
20pub const MEMO_CIPHERTEXT_LEN_BYTES: usize = 528;
21
22pub const MEMO_LEN_BYTES: usize = 512;
24
25const MAX_TEXT_LEN: usize = MEMO_LEN_BYTES - ADDRESS_LEN_BYTES;
27
28fn raw_bytes_to_text(data: &[u8]) -> String {
30 String::from_utf8_lossy(data)
31 .trim_end_matches(0u8 as char)
32 .to_string()
33}
34
35#[derive(Clone, Debug)]
36pub struct MemoCiphertext(pub [u8; MEMO_CIPHERTEXT_LEN_BYTES]);
37
38impl EffectingData for MemoCiphertext {
39 fn effect_hash(&self) -> EffectHash {
40 EffectHash::from_proto_effecting_data(&self.to_proto())
41 }
42}
43
44#[derive(Clone, Debug, PartialEq)]
45pub struct MemoPlaintext {
46 return_address: Address,
47 text: String,
48}
49
50impl MemoPlaintext {
51 pub fn new(return_address: Address, text: String) -> anyhow::Result<Self> {
55 if text.len() > MAX_TEXT_LEN {
56 anyhow::bail!(
57 "memo text length must be <= {}, found {}",
58 MAX_TEXT_LEN,
59 text.len()
60 );
61 }
62 Ok(Self {
63 return_address,
64 text,
65 })
66 }
67
68 pub fn return_address(&self) -> Address {
69 self.return_address.clone()
70 }
71
72 pub fn text(&self) -> &str {
73 self.text.as_str()
74 }
75}
76
77impl From<&MemoPlaintext> for Vec<u8> {
78 fn from(plaintext: &MemoPlaintext) -> Vec<u8> {
79 let mut bytes = vec![];
80 bytes.extend_from_slice(&plaintext.return_address.to_vec());
81 bytes.extend_from_slice(plaintext.text.as_bytes());
82 bytes
83 }
84}
85
86impl TryFrom<Vec<u8>> for MemoPlaintext {
87 type Error = anyhow::Error;
88
89 fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
90 if bytes.len() < 80 {
91 anyhow::bail!("malformed memo plaintext: missing return address");
92 }
93 let return_address_bytes = &bytes[..80];
94 let return_address: Address = return_address_bytes.try_into()?;
95 let text = raw_bytes_to_text(&bytes[80..]);
96
97 MemoPlaintext::new(return_address, text)
98 }
99}
100
101impl MemoPlaintext {
102 pub fn to_vec(&self) -> Vec<u8> {
103 self.into()
104 }
105
106 pub fn blank_memo(return_address: Address) -> MemoPlaintext {
107 MemoPlaintext {
108 return_address,
109 text: String::new(),
110 }
111 }
112}
113
114impl MemoCiphertext {
115 pub fn encrypt(memo_key: PayloadKey, memo: &MemoPlaintext) -> anyhow::Result<MemoCiphertext> {
117 let memo_bytes: Vec<u8> = memo.into();
118 let memo_len = memo_bytes.len();
119 if memo_len > MEMO_LEN_BYTES {
120 anyhow::bail!(
121 "provided memo plaintext of length {memo_len} exceeds maximum memo length of {MEMO_LEN_BYTES}"
122 );
123 }
124 let mut m = [0u8; MEMO_LEN_BYTES];
125 m[..memo_len].copy_from_slice(&memo_bytes);
126
127 let encryption_result = memo_key.encrypt(m.to_vec(), PayloadKind::Memo);
128 let ciphertext: [u8; MEMO_CIPHERTEXT_LEN_BYTES] = encryption_result
129 .try_into()
130 .expect("memo encryption result fits in ciphertext len");
131
132 Ok(MemoCiphertext(ciphertext))
133 }
134
135 pub fn decrypt(
137 memo_key: &PayloadKey,
138 ciphertext: MemoCiphertext,
139 ) -> anyhow::Result<MemoPlaintext> {
140 let plaintext_bytes = MemoCiphertext::decrypt_bytes(memo_key, ciphertext)?;
141
142 let return_address_bytes = &plaintext_bytes[..80];
143 let return_address: Address = return_address_bytes.try_into()?;
144 let text = raw_bytes_to_text(&plaintext_bytes[80..]);
145
146 MemoPlaintext::new(return_address, text)
147 }
148
149 pub fn decrypt_bytes(
151 memo_key: &PayloadKey,
152 ciphertext: MemoCiphertext,
153 ) -> anyhow::Result<[u8; MEMO_LEN_BYTES]> {
154 let decryption_result = memo_key
155 .decrypt(ciphertext.0.to_vec(), PayloadKind::Memo)
156 .map_err(|_| anyhow!("decryption error"))?;
157 let plaintext_bytes: [u8; MEMO_LEN_BYTES] = decryption_result.try_into().map_err(|_| {
158 anyhow!("post-decryption, could not fit plaintext into memo size {MEMO_LEN_BYTES}")
159 })?;
160 Ok(plaintext_bytes)
161 }
162
163 pub fn decrypt_outgoing(
165 wrapped_memo_key: &WrappedMemoKey,
166 wrapped_ovk: OvkWrappedKey,
167 cm: note::StateCommitment,
168 cv: balance::Commitment,
169 ovk: &OutgoingViewingKey,
170 epk: &ka::Public,
171 ciphertext: MemoCiphertext,
172 ) -> anyhow::Result<MemoPlaintext> {
173 let shared_secret = Note::decrypt_key(wrapped_ovk, cm, cv, ovk, epk)
174 .map_err(|_| anyhow!("key decryption error"))?;
175
176 let action_key = PayloadKey::derive(&shared_secret, epk);
177 let memo_key = wrapped_memo_key
178 .decrypt_outgoing(&action_key)
179 .map_err(|_| anyhow!("could not decrypt wrapped memo key"))?;
180
181 let plaintext = memo_key
182 .decrypt(ciphertext.0.to_vec(), PayloadKind::Memo)
183 .map_err(|_| anyhow!("decryption error"))?;
184
185 let plaintext_bytes: [u8; MEMO_LEN_BYTES] = plaintext.try_into().map_err(|_| {
186 anyhow!("post-decryption, could not fit plaintext into memo size {MEMO_LEN_BYTES}")
187 })?;
188
189 let return_address_bytes = &plaintext_bytes[..80];
190 let return_address: Address = return_address_bytes.try_into()?;
191 let text = raw_bytes_to_text(&plaintext_bytes[80..]);
192
193 MemoPlaintext::new(return_address, text)
194 }
195}
196
197impl TryFrom<&[u8]> for MemoCiphertext {
198 type Error = anyhow::Error;
199
200 fn try_from(input: &[u8]) -> Result<MemoCiphertext, Self::Error> {
201 if input.len() > MEMO_CIPHERTEXT_LEN_BYTES {
202 anyhow::bail!("provided memo ciphertext exceeds maximum memo size");
203 }
204 let mut mc = [0u8; MEMO_CIPHERTEXT_LEN_BYTES];
205 mc[..input.len()].copy_from_slice(input);
206
207 Ok(MemoCiphertext(mc))
208 }
209}
210
211impl From<MemoPlaintext> for pbt::MemoPlaintext {
212 fn from(plaintext: MemoPlaintext) -> pbt::MemoPlaintext {
213 pbt::MemoPlaintext {
214 return_address: Some(plaintext.return_address.into()),
215 text: plaintext.text,
216 }
217 }
218}
219
220impl TryFrom<pbt::MemoCiphertext> for MemoCiphertext {
221 type Error = anyhow::Error;
222
223 fn try_from(msg: pbt::MemoCiphertext) -> Result<Self, Self::Error> {
224 MemoCiphertext::try_from(msg.inner.to_vec().as_slice())
225 }
226}
227
228impl From<MemoCiphertext> for pbt::MemoCiphertext {
229 fn from(ciphertext: MemoCiphertext) -> pbt::MemoCiphertext {
230 pbt::MemoCiphertext {
231 inner: ciphertext.0.to_vec(),
232 }
233 }
234}
235
236impl DomainType for MemoCiphertext {
237 type Proto = pbt::MemoCiphertext;
238}
239
240impl TryFrom<pbt::MemoPlaintext> for MemoPlaintext {
241 type Error = anyhow::Error;
242
243 fn try_from(msg: pbt::MemoPlaintext) -> Result<Self, Self::Error> {
244 let sender = msg
245 .return_address
246 .ok_or_else(|| anyhow::anyhow!("message missing return address"))?
247 .try_into()?;
248 if (msg.text).len() > MEMO_LEN_BYTES - ADDRESS_LEN_BYTES {
249 anyhow::bail!(
250 "provided memo text exceeds {} bytes",
251 MEMO_LEN_BYTES - ADDRESS_LEN_BYTES
252 );
253 }
254 Ok(Self {
255 return_address: sender,
256 text: msg.text,
257 })
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use rand_core::OsRng;
264
265 use super::*;
266 use decaf377::Fr;
267 use penumbra_sdk_asset::{asset, Value};
268 use penumbra_sdk_keys::keys::{Bip44Path, SeedPhrase, SpendKey};
269
270 use proptest::prelude::*;
271
272 #[test]
273 fn test_memo_encryption_and_decryption() {
274 let mut rng = OsRng;
275 let seed_phrase = SeedPhrase::generate(rng);
276 let sk = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
277 let fvk = sk.full_viewing_key();
278 let ivk = fvk.incoming();
279 let (dest, _dtk_d) = ivk.payment_address(0u32.into());
280
281 let esk = ka::Secret::new(&mut rng);
282
283 let memo = MemoPlaintext {
286 return_address: dest.clone(),
287 text: String::from("Hi"),
288 };
289 let memo_key = PayloadKey::random_key(&mut OsRng);
290 let ciphertext =
291 MemoCiphertext::encrypt(memo_key.clone(), &memo).expect("can encrypt memo");
292 let wrapped_memo_key = WrappedMemoKey::encrypt(
293 &memo_key,
294 esk.clone(),
295 dest.transmission_key(),
296 dest.diversified_generator(),
297 );
298
299 let epk = esk.diversified_public(dest.diversified_generator());
301 let decrypted_memo_key = wrapped_memo_key
302 .decrypt(epk, ivk)
303 .expect("can decrypt memo key");
304 let plaintext =
305 MemoCiphertext::decrypt(&decrypted_memo_key, ciphertext).expect("can decrypt memo");
306
307 assert_eq!(memo_key, decrypted_memo_key);
308 assert_eq!(plaintext, memo);
309 }
310
311 #[test]
312 fn test_memo_encryption_and_sender_decryption() -> anyhow::Result<()> {
313 let mut rng = OsRng;
314
315 let seed_phrase = SeedPhrase::generate(rng);
316 let sk = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
317 let fvk = sk.full_viewing_key();
318 let ivk = fvk.incoming();
319 let ovk = fvk.outgoing();
320 let (dest, _dtk_d) = ivk.payment_address(0u32.into());
321
322 let value = Value {
323 amount: 10u64.into(),
324 asset_id: asset::Cache::with_known_assets()
325 .get_unit("upenumbra")
326 .unwrap()
327 .id(),
328 };
329 let note = Note::generate(&mut rng, &dest, value);
330
331 let memo = MemoPlaintext::new(dest.clone(), "Hello, friend".into())?;
334 let memo_key = PayloadKey::random_key(&mut OsRng);
335 let ciphertext =
336 MemoCiphertext::encrypt(memo_key.clone(), &memo).expect("can encrypt memo");
337 let esk = note.ephemeral_secret_key();
338 let wrapped_memo_key = WrappedMemoKey::encrypt(
339 &memo_key,
340 esk.clone(),
341 dest.transmission_key(),
342 dest.diversified_generator(),
343 );
344
345 let value_blinding = Fr::rand(&mut rng);
346 let cv = note.value().commit(value_blinding);
347 let wrapped_ovk = note.encrypt_key(ovk, cv);
348
349 let epk = esk.diversified_public(dest.diversified_generator());
351 let plaintext = MemoCiphertext::decrypt_outgoing(
352 &wrapped_memo_key,
353 wrapped_ovk,
354 note.commit(),
355 cv,
356 ovk,
357 &epk,
358 ciphertext,
359 )
360 .expect("can decrypt memo");
361
362 assert_eq!(plaintext, memo);
363
364 Ok(())
365 }
366
367 proptest! {
368 #[test]
376 fn test_memo_size_limit(s in "\\PC{0,10000}") {
377 let mut rng = OsRng;
378 let memo_key = PayloadKey::random_key(&mut rng);
379 let memo_address = Address::dummy(&mut rng);
380 let memo_text = s;
381 let memo = {
382 let text_len = memo_text.len();
383 let memo = MemoPlaintext::new(memo_address, memo_text);
384 if text_len > MAX_TEXT_LEN {
385 assert!(memo.is_err());
386 return Ok(());
387 }
388 assert!(memo.is_ok());
389 memo.unwrap()
390 };
391 let ciphertext_result = MemoCiphertext::encrypt(memo_key.clone(), &memo);
392 if memo.to_vec().len() > MEMO_LEN_BYTES {
393 assert!(ciphertext_result.is_err());
394 } else {
395 assert!(ciphertext_result.is_ok());
396 let plaintext = MemoCiphertext::decrypt(&memo_key, ciphertext_result.unwrap()).unwrap();
397 assert_eq!(plaintext, memo);
398 }
399 }
400 }
401}