penumbra_sdk_keys/
symmetric.rs

1use crate::keys::{IncomingViewingKey, OutgoingViewingKey};
2use anyhow::{anyhow, Result};
3use chacha20poly1305::{
4    aead::{Aead, NewAead},
5    ChaCha20Poly1305, Key, Nonce,
6};
7use decaf377_ka as ka;
8use penumbra_sdk_asset::balance;
9use penumbra_sdk_proto::core::keys::v1::{self as pb};
10use penumbra_sdk_tct::StateCommitment;
11use rand::{CryptoRng, RngCore};
12
13pub const PAYLOAD_KEY_LEN_BYTES: usize = 32;
14pub const OVK_WRAPPED_LEN_BYTES: usize = 48;
15pub const MEMOKEY_WRAPPED_LEN_BYTES: usize = 48;
16
17/// Represents the item to be encrypted/decrypted with the [`PayloadKey`].
18pub enum PayloadKind {
19    /// Note is action-scoped.
20    Note,
21    /// MemoKey is action-scoped.
22    MemoKey,
23    /// Memo is transaction-scoped.
24    Memo,
25    /// Swap is action-scoped.
26    Swap,
27}
28
29impl PayloadKind {
30    pub(crate) fn nonce(&self) -> [u8; 12] {
31        match self {
32            Self::Note => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
33            Self::MemoKey => [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
34            Self::Swap => [2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
35            Self::Memo => [3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
36        }
37    }
38}
39
40/// Represents a symmetric `ChaCha20Poly1305` key.
41///
42/// Used for encrypting and decrypting notes, swaps, memos, and memo keys.
43#[derive(Debug, Copy, Clone, PartialEq, Eq)]
44pub struct PayloadKey(Key);
45
46impl PayloadKey {
47    /// Use Blake2b-256 to derive a `PayloadKey`.
48    pub fn derive(shared_secret: &ka::SharedSecret, epk: &ka::Public) -> Self {
49        let mut kdf_params = blake2b_simd::Params::new();
50        kdf_params.personal(b"Penumbra_Payload");
51        kdf_params.hash_length(32);
52        let mut kdf = kdf_params.to_state();
53        kdf.update(&shared_secret.0);
54        kdf.update(&epk.0);
55
56        let key = kdf.finalize();
57        Self(*Key::from_slice(key.as_bytes()))
58    }
59
60    /// Derive a random `PayloadKey`. Used for memo key wrapping.
61    pub fn random_key<R: CryptoRng + RngCore>(rng: &mut R) -> Self {
62        let mut key_bytes = [0u8; 32];
63        rng.fill_bytes(&mut key_bytes);
64        Self(*Key::from_slice(&key_bytes[..]))
65    }
66
67    pub fn to_vec(&self) -> Vec<u8> {
68        self.0.to_vec()
69    }
70
71    /// Encrypt a note, memo, or memo key using the `PayloadKey`.
72    pub fn encrypt(&self, plaintext: Vec<u8>, kind: PayloadKind) -> Vec<u8> {
73        let cipher = ChaCha20Poly1305::new(&self.0);
74        let nonce_bytes = kind.nonce();
75        let nonce = Nonce::from_slice(&nonce_bytes);
76
77        cipher
78            .encrypt(nonce, plaintext.as_ref())
79            .expect("encryption succeeded")
80    }
81
82    /// Decrypt a note, memo, or memo key using the `PayloadKey`.
83    pub fn decrypt(&self, ciphertext: Vec<u8>, kind: PayloadKind) -> Result<Vec<u8>> {
84        let cipher = ChaCha20Poly1305::new(&self.0);
85
86        let nonce_bytes = kind.nonce();
87        let nonce = Nonce::from_slice(&nonce_bytes);
88
89        cipher
90            .decrypt(nonce, ciphertext.as_ref())
91            .map_err(|_| anyhow::anyhow!("decryption error"))
92    }
93
94    /// Use Blake2b-256 to derive an encryption key from the OVK and public fields for swaps.
95    pub fn derive_swap(ovk: &OutgoingViewingKey, cm: StateCommitment) -> Self {
96        let cm_bytes: [u8; 32] = cm.into();
97
98        let mut kdf_params = blake2b_simd::Params::new();
99        kdf_params.personal(b"Penumbra_Payswap");
100        kdf_params.hash_length(32);
101        let mut kdf = kdf_params.to_state();
102        kdf.update(&ovk.to_bytes());
103        kdf.update(&cm_bytes);
104
105        let key = kdf.finalize();
106        Self(*Key::from_slice(key.as_bytes()))
107    }
108
109    /// Encrypt a swap using the `PayloadKey`.
110    pub fn encrypt_swap(&self, plaintext: Vec<u8>) -> Vec<u8> {
111        let cipher = ChaCha20Poly1305::new(&self.0);
112        let nonce_bytes = PayloadKind::Swap.nonce();
113        let nonce = Nonce::from_slice(&nonce_bytes);
114
115        cipher
116            .encrypt(nonce, plaintext.as_ref())
117            .expect("encryption succeeded")
118    }
119
120    /// Decrypt a swap using the `PayloadKey`.
121    pub fn decrypt_swap(&self, ciphertext: Vec<u8>) -> Result<Vec<u8>> {
122        let cipher = ChaCha20Poly1305::new(&self.0);
123
124        let nonce_bytes = PayloadKind::Swap.nonce();
125        let nonce = Nonce::from_slice(&nonce_bytes);
126
127        cipher
128            .decrypt(nonce, ciphertext.as_ref())
129            .map_err(|_| anyhow::anyhow!("decryption error"))
130    }
131}
132
133impl TryFrom<&[u8]> for PayloadKey {
134    type Error = anyhow::Error;
135
136    fn try_from(slice: &[u8]) -> Result<Self, Self::Error> {
137        let bytes: [u8; PAYLOAD_KEY_LEN_BYTES] = slice
138            .try_into()
139            .map_err(|_| anyhow::anyhow!("PayloadKey incorrect len"))?;
140        Ok(Self(*Key::from_slice(&bytes)))
141    }
142}
143
144impl TryFrom<Vec<u8>> for PayloadKey {
145    type Error = anyhow::Error;
146
147    fn try_from(vector: Vec<u8>) -> Result<Self, Self::Error> {
148        vector.as_slice().try_into()
149    }
150}
151
152impl From<[u8; 32]> for PayloadKey {
153    fn from(bytes: [u8; 32]) -> Self {
154        Self(*Key::from_slice(&bytes))
155    }
156}
157
158impl TryFrom<pb::PayloadKey> for PayloadKey {
159    type Error = anyhow::Error;
160    fn try_from(msg: pb::PayloadKey) -> Result<Self, Self::Error> {
161        msg.inner.as_slice().try_into()
162    }
163}
164
165impl From<PayloadKey> for pb::PayloadKey {
166    fn from(msg: PayloadKey) -> Self {
167        pb::PayloadKey {
168            inner: msg.0.to_vec(),
169        }
170    }
171}
172
173/// Represents a symmetric `ChaCha20Poly1305` key.
174///
175/// Used for encrypting and decrypting [`OvkWrappedKey`] material used to decrypt
176/// outgoing notes, and memos.
177pub struct OutgoingCipherKey(Key);
178
179impl OutgoingCipherKey {
180    /// Use Blake2b-256 to derive an encryption key `ock` from the OVK and public fields.
181    pub fn derive(
182        ovk: &OutgoingViewingKey,
183        cv: balance::Commitment,
184        cm: StateCommitment,
185        epk: &ka::Public,
186    ) -> Self {
187        let cv_bytes: [u8; 32] = cv.into();
188        let cm_bytes: [u8; 32] = cm.into();
189
190        let mut kdf_params = blake2b_simd::Params::new();
191        kdf_params.hash_length(32);
192        kdf_params.personal(b"Penumbra_OutCiph");
193        let mut kdf = kdf_params.to_state();
194        kdf.update(&ovk.to_bytes());
195        kdf.update(&cv_bytes);
196        kdf.update(&cm_bytes);
197        kdf.update(&epk.0);
198
199        let key = kdf.finalize();
200        Self(*Key::from_slice(key.as_bytes()))
201    }
202
203    /// Encrypt key material using the `OutgoingCipherKey`.
204    pub fn encrypt(&self, plaintext: Vec<u8>, kind: PayloadKind) -> Vec<u8> {
205        let cipher = ChaCha20Poly1305::new(&self.0);
206
207        // Note: Here we use the same nonce as note encryption, however the keys are different.
208        // For note encryption we derive the `PayloadKey` symmetric key from the shared secret and epk.
209        // However, for the outgoing cipher key, we derive a symmetric key from the
210        // sender's OVK, balance commitment, note commitment, and the epk. Since the keys are
211        // different, it is safe to use the same nonce.
212        //
213        // References:
214        // * Section 5.4.3 of the ZCash protocol spec
215        // * Section 2.3 RFC 7539
216        let nonce_bytes = kind.nonce();
217        let nonce = Nonce::from_slice(&nonce_bytes);
218
219        cipher
220            .encrypt(nonce, plaintext.as_ref())
221            .expect("encryption succeeded")
222    }
223
224    /// Decrypt key material using the `OutgoingCipherKey`.
225    pub fn decrypt(&self, ciphertext: Vec<u8>, kind: PayloadKind) -> Result<Vec<u8>> {
226        let cipher = ChaCha20Poly1305::new(&self.0);
227        let nonce_bytes = kind.nonce();
228        let nonce = Nonce::from_slice(&nonce_bytes);
229
230        cipher
231            .decrypt(nonce, ciphertext.as_ref())
232            .map_err(|_| anyhow::anyhow!("decryption error"))
233    }
234}
235
236/// Represents encrypted key material used to reconstruct a `PayloadKey`.
237#[derive(Clone, Debug)]
238pub struct OvkWrappedKey(pub [u8; OVK_WRAPPED_LEN_BYTES]);
239
240impl OvkWrappedKey {
241    pub fn to_vec(&self) -> Vec<u8> {
242        self.0.to_vec()
243    }
244}
245
246impl TryFrom<Vec<u8>> for OvkWrappedKey {
247    type Error = anyhow::Error;
248
249    fn try_from(vector: Vec<u8>) -> Result<Self, Self::Error> {
250        let bytes: [u8; OVK_WRAPPED_LEN_BYTES] = vector
251            .try_into()
252            .map_err(|_| anyhow::anyhow!("wrapped OVK malformed"))?;
253        Ok(Self(bytes))
254    }
255}
256
257impl TryFrom<&[u8]> for OvkWrappedKey {
258    type Error = anyhow::Error;
259
260    fn try_from(arr: &[u8]) -> Result<Self, Self::Error> {
261        let bytes: [u8; OVK_WRAPPED_LEN_BYTES] = arr
262            .try_into()
263            .map_err(|_| anyhow::anyhow!("wrapped OVK malformed"))?;
264        Ok(Self(bytes))
265    }
266}
267
268/// Represents encrypted key material used to decrypt a `MemoCiphertext`.
269#[derive(Clone, Debug)]
270pub struct WrappedMemoKey(pub [u8; MEMOKEY_WRAPPED_LEN_BYTES]);
271
272impl WrappedMemoKey {
273    pub fn to_vec(&self) -> Vec<u8> {
274        self.0.to_vec()
275    }
276
277    /// Encrypt a memo key using the action-specific `PayloadKey`.
278    pub fn encrypt(
279        memo_key: &PayloadKey,
280        esk: ka::Secret,
281        transmission_key: &ka::Public,
282        diversified_generator: &decaf377::Element,
283    ) -> Self {
284        // 1. Construct the per-action PayloadKey.
285        let epk = esk.diversified_public(diversified_generator);
286        let shared_secret = esk
287            .key_agreement_with(transmission_key)
288            .expect("key agreement succeeded");
289
290        let action_key = PayloadKey::derive(&shared_secret, &epk);
291        // 2. Now use the per-action key to encrypt the memo key.
292        let encrypted_memo_key = action_key.encrypt(memo_key.to_vec(), PayloadKind::MemoKey);
293        let wrapped_memo_key_bytes: [u8; MEMOKEY_WRAPPED_LEN_BYTES] = encrypted_memo_key
294            .try_into()
295            .expect("memo key must fit in wrapped memo key field");
296
297        WrappedMemoKey(wrapped_memo_key_bytes)
298    }
299
300    /// Decrypt a wrapped memo key by first deriving the action-specific `PayloadKey`.
301    pub fn decrypt(&self, epk: ka::Public, ivk: &IncomingViewingKey) -> Result<PayloadKey> {
302        // 1. Construct the per-action PayloadKey.
303        let shared_secret = ivk
304            .key_agreement_with(&epk)
305            .expect("key agreement succeeded");
306
307        let action_key = PayloadKey::derive(&shared_secret, &epk);
308        // 2. Now use the per-action key to decrypt the memo key.
309        let decrypted_memo_key = action_key
310            .decrypt(self.to_vec(), PayloadKind::MemoKey)
311            .map_err(|_| anyhow!("decryption error"))?;
312
313        decrypted_memo_key.try_into()
314    }
315
316    /// Decrypt a wrapped memo key using the action-specific `PayloadKey`.
317    pub fn decrypt_outgoing(&self, action_key: &PayloadKey) -> Result<PayloadKey> {
318        let decrypted_memo_key = action_key
319            .decrypt(self.to_vec(), PayloadKind::MemoKey)
320            .map_err(|_| anyhow!("decryption error"))?;
321        decrypted_memo_key.try_into()
322    }
323}
324
325impl TryFrom<Vec<u8>> for WrappedMemoKey {
326    type Error = anyhow::Error;
327
328    fn try_from(vector: Vec<u8>) -> Result<Self, Self::Error> {
329        let bytes: [u8; MEMOKEY_WRAPPED_LEN_BYTES] = vector
330            .try_into()
331            .map_err(|_| anyhow::anyhow!("wrapped memo key malformed"))?;
332        Ok(Self(bytes))
333    }
334}
335
336impl TryFrom<&[u8]> for WrappedMemoKey {
337    type Error = anyhow::Error;
338
339    fn try_from(arr: &[u8]) -> Result<Self, Self::Error> {
340        let bytes: [u8; MEMOKEY_WRAPPED_LEN_BYTES] = arr
341            .try_into()
342            .map_err(|_| anyhow::anyhow!("wrapped memo key malformed"))?;
343        Ok(Self(bytes))
344    }
345}
346
347/// Represents a symmetric `ChaCha20Poly1305` key used for Spend backreferences.
348#[derive(Debug, Copy, Clone, PartialEq, Eq)]
349pub struct BackreferenceKey(pub Key);
350
351impl BackreferenceKey {
352    pub fn derive(ovk: &OutgoingViewingKey) -> Self {
353        let mut kdf_params = blake2b_simd::Params::new();
354        kdf_params.personal(b"Penumbra_Backref");
355        kdf_params.hash_length(32);
356        let mut kdf = kdf_params.to_state();
357        kdf.update(&ovk.to_bytes());
358
359        let key = kdf.finalize();
360        Self(*Key::from_slice(key.as_bytes()))
361    }
362}