solana_zk_sdk/encryption/
grouped_elgamal.rs

1//! The twisted ElGamal group encryption implementation.
2//!
3//! The message space consists of any number that is representable as a scalar (a.k.a. "exponent")
4//! for Curve25519.
5//!
6//! A regular twisted ElGamal ciphertext consists of two components:
7//! - A Pedersen commitment that encodes a message to be encrypted
8//! - A "decryption handle" that binds the Pedersen opening to a specific public key
9//!
10//! The ciphertext can be generalized to hold not a single decryption handle, but multiple handles
11//! pertaining to multiple ElGamal public keys. These ciphertexts are referred to as a "grouped"
12//! ElGamal ciphertext.
13//!
14
15#[cfg(not(target_arch = "wasm32"))]
16use crate::encryption::{discrete_log::DiscreteLog, elgamal::ElGamalSecretKey};
17#[cfg(target_arch = "wasm32")]
18pub use grouped_elgamal_wasm::*;
19#[cfg(target_arch = "wasm32")]
20use wasm_bindgen::prelude::*;
21use {
22    crate::{
23        encryption::{
24            elgamal::{DecryptHandle, ElGamalCiphertext, ElGamalPubkey},
25            pedersen::{Pedersen, PedersenCommitment, PedersenOpening},
26        },
27        RISTRETTO_POINT_LEN,
28    },
29    curve25519_dalek::scalar::Scalar,
30    thiserror::Error,
31};
32
33#[derive(Error, Clone, Debug, Eq, PartialEq)]
34pub enum GroupedElGamalError {
35    #[error("index out of bounds")]
36    IndexOutOfBounds,
37}
38
39/// Algorithm handle for the grouped ElGamal encryption
40pub struct GroupedElGamal<const N: usize>;
41impl<const N: usize> GroupedElGamal<N> {
42    /// Encrypts an amount under an array of ElGamal public keys.
43    ///
44    /// This function is randomized. It internally samples a scalar element using `OsRng`.
45    pub fn encrypt<T: Into<Scalar>>(
46        pubkeys: [&ElGamalPubkey; N],
47        amount: T,
48    ) -> GroupedElGamalCiphertext<N> {
49        let (commitment, opening) = Pedersen::new(amount);
50        let handles: [DecryptHandle; N] = pubkeys
51            .iter()
52            .map(|handle| handle.decrypt_handle(&opening))
53            .collect::<Vec<DecryptHandle>>()
54            .try_into()
55            .unwrap();
56
57        GroupedElGamalCiphertext {
58            commitment,
59            handles,
60        }
61    }
62
63    /// Encrypts an amount under an array of ElGamal public keys using a specified Pedersen
64    /// opening.
65    pub fn encrypt_with<T: Into<Scalar>>(
66        pubkeys: [&ElGamalPubkey; N],
67        amount: T,
68        opening: &PedersenOpening,
69    ) -> GroupedElGamalCiphertext<N> {
70        let commitment = Pedersen::with(amount, opening);
71        let handles: [DecryptHandle; N] = pubkeys
72            .iter()
73            .map(|handle| handle.decrypt_handle(opening))
74            .collect::<Vec<DecryptHandle>>()
75            .try_into()
76            .unwrap();
77
78        GroupedElGamalCiphertext {
79            commitment,
80            handles,
81        }
82    }
83
84    /// Converts a grouped ElGamal ciphertext into a regular ElGamal ciphertext using the decrypt
85    /// handle at a specified index.
86    fn to_elgamal_ciphertext(
87        grouped_ciphertext: &GroupedElGamalCiphertext<N>,
88        index: usize,
89    ) -> Result<ElGamalCiphertext, GroupedElGamalError> {
90        let handle = grouped_ciphertext
91            .handles
92            .get(index)
93            .ok_or(GroupedElGamalError::IndexOutOfBounds)?;
94
95        Ok(ElGamalCiphertext {
96            commitment: grouped_ciphertext.commitment,
97            handle: *handle,
98        })
99    }
100}
101
102#[cfg(not(target_arch = "wasm32"))]
103impl<const N: usize> GroupedElGamal<N> {
104    /// Decrypts a grouped ElGamal ciphertext using an ElGamal secret key pertaining to a
105    /// decryption handle at a specified index.
106    ///
107    /// The output of this function is of type `DiscreteLog`. To recover the originally encrypted
108    /// amount, use `DiscreteLog::decode`.
109    fn decrypt(
110        grouped_ciphertext: &GroupedElGamalCiphertext<N>,
111        secret: &ElGamalSecretKey,
112        index: usize,
113    ) -> Result<DiscreteLog, GroupedElGamalError> {
114        Self::to_elgamal_ciphertext(grouped_ciphertext, index)
115            .map(|ciphertext| ciphertext.decrypt(secret))
116    }
117
118    /// Decrypts a grouped ElGamal ciphertext to a number that is interpreted as a positive 32-bit
119    /// number (but still of type `u64`).
120    ///
121    /// If the originally encrypted amount is not a positive 32-bit number, then the function
122    /// Result contains `None`.
123    fn decrypt_u32(
124        grouped_ciphertext: &GroupedElGamalCiphertext<N>,
125        secret: &ElGamalSecretKey,
126        index: usize,
127    ) -> Result<Option<u64>, GroupedElGamalError> {
128        Self::to_elgamal_ciphertext(grouped_ciphertext, index)
129            .map(|ciphertext| ciphertext.decrypt_u32(secret))
130    }
131}
132
133/// A grouped ElGamal ciphertext.
134///
135/// The type is defined with a generic constant parameter that specifies the number of
136/// decryption handles that the ciphertext holds.
137#[derive(Clone, Copy, Debug, Eq, PartialEq)]
138pub struct GroupedElGamalCiphertext<const N: usize> {
139    pub commitment: PedersenCommitment,
140    pub handles: [DecryptHandle; N],
141}
142
143impl<const N: usize> GroupedElGamalCiphertext<N> {
144    /// Converts a grouped ElGamal ciphertext into a regular ElGamal ciphertext using the decrypt
145    /// handle at a specified index.
146    pub fn to_elgamal_ciphertext(
147        &self,
148        index: usize,
149    ) -> Result<ElGamalCiphertext, GroupedElGamalError> {
150        GroupedElGamal::to_elgamal_ciphertext(self, index)
151    }
152
153    /// The expected length of a serialized grouped ElGamal ciphertext.
154    ///
155    /// A grouped ElGamal ciphertext consists of a Pedersen commitment and an array of decryption
156    /// handles. The commitment and decryption handles are each a single Curve25519 group element
157    /// that is serialized as 32 bytes. Therefore, the total byte length of a grouped ciphertext is
158    /// `(N+1) * 32`.
159    fn expected_byte_length() -> usize {
160        N.checked_add(1)
161            .and_then(|length| length.checked_mul(RISTRETTO_POINT_LEN))
162            .unwrap()
163    }
164
165    pub fn to_bytes(&self) -> Vec<u8> {
166        let mut buf = Vec::with_capacity(Self::expected_byte_length());
167        buf.extend_from_slice(&self.commitment.to_bytes());
168        self.handles
169            .iter()
170            .for_each(|handle| buf.extend_from_slice(&handle.to_bytes()));
171        buf
172    }
173
174    pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
175        if bytes.len() != Self::expected_byte_length() {
176            return None;
177        }
178
179        let mut iter = bytes.chunks(RISTRETTO_POINT_LEN);
180        let commitment = PedersenCommitment::from_bytes(iter.next()?)?;
181
182        let mut handles = Vec::with_capacity(N);
183        for handle_bytes in iter {
184            handles.push(DecryptHandle::from_bytes(handle_bytes)?);
185        }
186
187        Some(Self {
188            commitment,
189            handles: handles.try_into().unwrap(),
190        })
191    }
192}
193
194#[cfg(not(target_arch = "wasm32"))]
195impl<const N: usize> GroupedElGamalCiphertext<N> {
196    /// Decrypts the grouped ElGamal ciphertext using an ElGamal secret key pertaining to a
197    /// specified index.
198    ///
199    /// The output of this function is of type `DiscreteLog`. To recover the originally encrypted
200    /// amount, use `DiscreteLog::decode`.
201    pub fn decrypt(
202        &self,
203        secret: &ElGamalSecretKey,
204        index: usize,
205    ) -> Result<DiscreteLog, GroupedElGamalError> {
206        GroupedElGamal::decrypt(self, secret, index)
207    }
208
209    /// Decrypts the grouped ElGamal ciphertext to a number that is interpreted as a positive 32-bit
210    /// number (but still of type `u64`).
211    ///
212    /// If the originally encrypted amount is not a positive 32-bit number, then the function
213    /// returns `None`.
214    pub fn decrypt_u32(
215        &self,
216        secret: &ElGamalSecretKey,
217        index: usize,
218    ) -> Result<Option<u64>, GroupedElGamalError> {
219        GroupedElGamal::decrypt_u32(self, secret, index)
220    }
221}
222
223// Define specific grouped ElGamal ciphertext types for 2 and 3 handles since
224// `wasm_bindgen` do not yet support the export of types that take on generic
225// type parameters.
226#[cfg(target_arch = "wasm32")]
227mod grouped_elgamal_wasm {
228    use super::*;
229
230    #[wasm_bindgen]
231    pub struct GroupedElGamalCiphertext2Handles(pub(crate) GroupedElGamalCiphertext<2>);
232
233    #[wasm_bindgen]
234    impl GroupedElGamalCiphertext2Handles {
235        #[wasm_bindgen(js_name = encryptU64)]
236        pub fn encrypt_u64(
237            first_pubkey: &ElGamalPubkey,
238            second_pubkey: &ElGamalPubkey,
239            amount: u64,
240        ) -> Self {
241            Self(GroupedElGamal::<2>::encrypt(
242                [first_pubkey, second_pubkey],
243                amount,
244            ))
245        }
246
247        #[wasm_bindgen(js_name = encryptionWithU64)]
248        pub fn encryption_with_u64(
249            first_pubkey: &ElGamalPubkey,
250            second_pubkey: &ElGamalPubkey,
251            amount: u64,
252            opening: &PedersenOpening,
253        ) -> Self {
254            Self(GroupedElGamal::<2>::encrypt_with(
255                [first_pubkey, second_pubkey],
256                amount,
257                opening,
258            ))
259        }
260    }
261
262    #[wasm_bindgen]
263    pub struct GroupedElGamalCiphertext3Handles(pub(crate) GroupedElGamalCiphertext<3>);
264
265    #[wasm_bindgen]
266    impl GroupedElGamalCiphertext3Handles {
267        #[wasm_bindgen(js_name = encryptU64)]
268        pub fn encrypt_u64(
269            first_pubkey: &ElGamalPubkey,
270            second_pubkey: &ElGamalPubkey,
271            third_pubkey: &ElGamalPubkey,
272            amount: u64,
273        ) -> Self {
274            Self(GroupedElGamal::<3>::encrypt(
275                [first_pubkey, second_pubkey, third_pubkey],
276                amount,
277            ))
278        }
279
280        #[wasm_bindgen(js_name = encryptionWithU64)]
281        pub fn encryption_with_u64(
282            first_pubkey: &ElGamalPubkey,
283            second_pubkey: &ElGamalPubkey,
284            third_pubkey: &ElGamalPubkey,
285            amount: u64,
286            opening: &PedersenOpening,
287        ) -> Self {
288            Self(GroupedElGamal::<3>::encrypt_with(
289                [first_pubkey, second_pubkey, third_pubkey],
290                amount,
291                opening,
292            ))
293        }
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use {super::*, crate::encryption::elgamal::ElGamalKeypair};
300
301    #[test]
302    fn test_grouped_elgamal_encrypt_decrypt_correctness() {
303        let elgamal_keypair_0 = ElGamalKeypair::new_rand();
304        let elgamal_keypair_1 = ElGamalKeypair::new_rand();
305        let elgamal_keypair_2 = ElGamalKeypair::new_rand();
306
307        let amount: u64 = 10;
308        let grouped_ciphertext = GroupedElGamal::encrypt(
309            [
310                elgamal_keypair_0.pubkey(),
311                elgamal_keypair_1.pubkey(),
312                elgamal_keypair_2.pubkey(),
313            ],
314            amount,
315        );
316
317        assert_eq!(
318            Some(amount),
319            grouped_ciphertext
320                .decrypt_u32(elgamal_keypair_0.secret(), 0)
321                .unwrap()
322        );
323
324        assert_eq!(
325            Some(amount),
326            grouped_ciphertext
327                .decrypt_u32(elgamal_keypair_1.secret(), 1)
328                .unwrap()
329        );
330
331        assert_eq!(
332            Some(amount),
333            grouped_ciphertext
334                .decrypt_u32(elgamal_keypair_2.secret(), 2)
335                .unwrap()
336        );
337
338        assert_eq!(
339            GroupedElGamalError::IndexOutOfBounds,
340            grouped_ciphertext
341                .decrypt_u32(elgamal_keypair_0.secret(), 3)
342                .unwrap_err()
343        );
344    }
345
346    #[test]
347    fn test_grouped_ciphertext_bytes() {
348        let elgamal_keypair_0 = ElGamalKeypair::new_rand();
349        let elgamal_keypair_1 = ElGamalKeypair::new_rand();
350        let elgamal_keypair_2 = ElGamalKeypair::new_rand();
351
352        let amount: u64 = 10;
353        let grouped_ciphertext = GroupedElGamal::encrypt(
354            [
355                elgamal_keypair_0.pubkey(),
356                elgamal_keypair_1.pubkey(),
357                elgamal_keypair_2.pubkey(),
358            ],
359            amount,
360        );
361
362        let produced_bytes = grouped_ciphertext.to_bytes();
363        assert_eq!(produced_bytes.len(), 128);
364
365        let decoded_grouped_ciphertext =
366            GroupedElGamalCiphertext::<3>::from_bytes(&produced_bytes).unwrap();
367        assert_eq!(
368            Some(amount),
369            decoded_grouped_ciphertext
370                .decrypt_u32(elgamal_keypair_0.secret(), 0)
371                .unwrap()
372        );
373
374        assert_eq!(
375            Some(amount),
376            decoded_grouped_ciphertext
377                .decrypt_u32(elgamal_keypair_1.secret(), 1)
378                .unwrap()
379        );
380
381        assert_eq!(
382            Some(amount),
383            decoded_grouped_ciphertext
384                .decrypt_u32(elgamal_keypair_2.secret(), 2)
385                .unwrap()
386        );
387    }
388}