penumbra_sdk_transaction/
memo.rs

1use 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
22// This is the `MEMO_CIPHERTEXT_LEN_BYTES` - MAC size (16 bytes).
23pub const MEMO_LEN_BYTES: usize = 512;
24
25// This is the largest text length we can support
26const MAX_TEXT_LEN: usize = MEMO_LEN_BYTES - ADDRESS_LEN_BYTES;
27
28/// A method which reads out bytes in a lossy way, and trims out null bytes
29fn 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    /// Create a new MemoPlaintext, checking that the text isn't long enough.
52    ///
53    /// The text being too long is the only reason this function will fail.
54    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    /// Encrypt a memo, returning its ciphertext.
116    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    /// Decrypt a [`MemoCiphertext`] to generate a plaintext [`MemoPlaintext`].
136    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    /// Decrypt a [`MemoCiphertext`] to generate a fixed-length slice of bytes.
150    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    /// Decrypt a [`MemoCiphertext`] using the wrapped OVK to generate a plaintext [`Memo`].
164    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        // On the sender side, we have to encrypt the memo to put into the transaction-level,
284        // and also the memo key to put on the action-level (output).
285        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        // On the recipient side, we have to decrypt the wrapped memo key, and then the memo.
300        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        // On the sender side, we have to encrypt the memo to put into the transaction-level,
332        // and also the memo key to put on the action-level (output).
333        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        // Later, still on the sender side, we decrypt the memo by using the decrypt_outgoing method.
350        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        // We generate random strings, up to 10k chars long.
369        // Since UTF-8 represents each char using 1 to 4 bytes,
370        // we need to test strings up to (MEMO_LEN_BYTES * 4 = 2048)
371        // chars in length. That's the intended upper bound of what
372        // the memo parsing will handle, but for the sake of tests,
373        // let's raise it 2048 -> 10,000. Doing so only adds a fraction
374        // of a second to the length of the test run.
375        #[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}