solana_zk_sdk/encryption/pod/
elgamal.rs

1//! Plain Old Data types for the ElGamal encryption scheme.
2
3#[cfg(not(target_os = "solana"))]
4use {
5    crate::{
6        encryption::elgamal::{DecryptHandle, ElGamalCiphertext, ElGamalPubkey},
7        errors::ElGamalError,
8    },
9    curve25519_dalek::ristretto::CompressedRistretto,
10};
11use {
12    crate::{
13        encryption::{DECRYPT_HANDLE_LEN, ELGAMAL_CIPHERTEXT_LEN, ELGAMAL_PUBKEY_LEN},
14        pod::{impl_from_bytes, impl_from_str},
15    },
16    base64::{prelude::BASE64_STANDARD, Engine},
17    bytemuck::Zeroable,
18    std::fmt,
19};
20#[cfg(target_arch = "wasm32")]
21use {
22    js_sys::{Array, Uint8Array},
23    wasm_bindgen::prelude::*,
24};
25
26/// Maximum length of a base64 encoded ElGamal public key
27const ELGAMAL_PUBKEY_MAX_BASE64_LEN: usize = 44;
28
29/// Maximum length of a base64 encoded ElGamal ciphertext
30const ELGAMAL_CIPHERTEXT_MAX_BASE64_LEN: usize = 88;
31
32/// Maximum length of a base64 encoded ElGamal decrypt handle
33const DECRYPT_HANDLE_MAX_BASE64_LEN: usize = 44;
34
35/// The `ElGamalCiphertext` type as a `Pod`.
36#[derive(Clone, Copy, bytemuck_derive::Pod, bytemuck_derive::Zeroable, PartialEq, Eq)]
37#[repr(transparent)]
38pub struct PodElGamalCiphertext(pub(crate) [u8; ELGAMAL_CIPHERTEXT_LEN]);
39
40impl fmt::Debug for PodElGamalCiphertext {
41    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
42        write!(f, "{:?}", self.0)
43    }
44}
45
46impl fmt::Display for PodElGamalCiphertext {
47    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
48        write!(f, "{}", BASE64_STANDARD.encode(self.0))
49    }
50}
51
52impl Default for PodElGamalCiphertext {
53    fn default() -> Self {
54        Self::zeroed()
55    }
56}
57
58impl_from_str!(
59    TYPE = PodElGamalCiphertext,
60    BYTES_LEN = ELGAMAL_CIPHERTEXT_LEN,
61    BASE64_LEN = ELGAMAL_CIPHERTEXT_MAX_BASE64_LEN
62);
63
64impl_from_bytes!(
65    TYPE = PodElGamalCiphertext,
66    BYTES_LEN = ELGAMAL_CIPHERTEXT_LEN
67);
68
69#[cfg(not(target_os = "solana"))]
70impl From<ElGamalCiphertext> for PodElGamalCiphertext {
71    fn from(decoded_ciphertext: ElGamalCiphertext) -> Self {
72        Self(decoded_ciphertext.to_bytes())
73    }
74}
75
76#[cfg(not(target_os = "solana"))]
77impl TryFrom<PodElGamalCiphertext> for ElGamalCiphertext {
78    type Error = ElGamalError;
79
80    fn try_from(pod_ciphertext: PodElGamalCiphertext) -> Result<Self, Self::Error> {
81        Self::from_bytes(&pod_ciphertext.0).ok_or(ElGamalError::CiphertextDeserialization)
82    }
83}
84
85/// The `ElGamalPubkey` type as a `Pod`.
86#[derive(Clone, Copy, Default, bytemuck_derive::Pod, bytemuck_derive::Zeroable, PartialEq, Eq)]
87#[repr(transparent)]
88#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
89pub struct PodElGamalPubkey(pub(crate) [u8; ELGAMAL_PUBKEY_LEN]);
90
91#[cfg(target_arch = "wasm32")]
92#[allow(non_snake_case)]
93#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
94impl PodElGamalPubkey {
95    /// Create a new `PodElGamalPubkey` object
96    ///
97    /// * `value` - optional public key as a base64 encoded string, `Uint8Array`, `[number]`
98    #[wasm_bindgen(constructor)]
99    pub fn constructor(value: JsValue) -> Result<PodElGamalPubkey, JsValue> {
100        if let Some(base64_str) = value.as_string() {
101            base64_str
102                .parse::<PodElGamalPubkey>()
103                .map_err(|e| e.to_string().into())
104        } else if let Some(uint8_array) = value.dyn_ref::<Uint8Array>() {
105            bytemuck::try_from_bytes(&uint8_array.to_vec())
106                .map_err(|err| JsValue::from(format!("Invalid Uint8Array ElGamalPubkey: {err:?}")))
107                .map(|pubkey| *pubkey)
108        } else if let Some(array) = value.dyn_ref::<Array>() {
109            let mut bytes = vec![];
110            let iterator = js_sys::try_iter(&array.values())?.expect("array to be iterable");
111            for x in iterator {
112                let x = x?;
113
114                if let Some(n) = x.as_f64() {
115                    if (0. ..=255.).contains(&n) {
116                        bytes.push(n as u8);
117                        continue;
118                    }
119                }
120                return Err(format!("Invalid array argument: {:?}", x).into());
121            }
122
123            bytemuck::try_from_bytes(&bytes)
124                .map_err(|err| JsValue::from(format!("Invalid Array pubkey: {err:?}")))
125                .map(|pubkey| *pubkey)
126        } else if value.is_undefined() {
127            Ok(PodElGamalPubkey::default())
128        } else {
129            Err("Unsupported argument".into())
130        }
131    }
132
133    /// Return the base64 string representation of the public key
134    pub fn toString(&self) -> String {
135        self.to_string()
136    }
137
138    /// Checks if two `ElGamalPubkey`s are equal
139    pub fn equals(&self, other: &PodElGamalPubkey) -> bool {
140        self == other
141    }
142
143    /// Return the `Uint8Array` representation of the public key
144    pub fn toBytes(&self) -> Box<[u8]> {
145        self.0.into()
146    }
147
148    pub fn compressed(decoded: &ElGamalPubkey) -> PodElGamalPubkey {
149        (*decoded).into()
150    }
151
152    pub fn decompressed(&self) -> Result<ElGamalPubkey, JsValue> {
153        (*self)
154            .try_into()
155            .map_err(|err| JsValue::from(format!("Invalid ElGamalPubkey: {err:?}")))
156    }
157}
158
159impl fmt::Debug for PodElGamalPubkey {
160    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
161        write!(f, "{:?}", self.0)
162    }
163}
164
165impl fmt::Display for PodElGamalPubkey {
166    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
167        write!(f, "{}", BASE64_STANDARD.encode(self.0))
168    }
169}
170
171impl_from_str!(
172    TYPE = PodElGamalPubkey,
173    BYTES_LEN = ELGAMAL_PUBKEY_LEN,
174    BASE64_LEN = ELGAMAL_PUBKEY_MAX_BASE64_LEN
175);
176
177impl_from_bytes!(TYPE = PodElGamalPubkey, BYTES_LEN = ELGAMAL_PUBKEY_LEN);
178
179#[cfg(not(target_os = "solana"))]
180impl From<ElGamalPubkey> for PodElGamalPubkey {
181    fn from(decoded_pubkey: ElGamalPubkey) -> Self {
182        Self(decoded_pubkey.into())
183    }
184}
185
186#[cfg(not(target_os = "solana"))]
187impl TryFrom<PodElGamalPubkey> for ElGamalPubkey {
188    type Error = ElGamalError;
189
190    fn try_from(pod_pubkey: PodElGamalPubkey) -> Result<Self, Self::Error> {
191        Self::try_from(pod_pubkey.0.as_slice())
192    }
193}
194
195/// The `DecryptHandle` type as a `Pod`.
196#[derive(Clone, Copy, Default, bytemuck_derive::Pod, bytemuck_derive::Zeroable, PartialEq, Eq)]
197#[repr(transparent)]
198pub struct PodDecryptHandle(pub(crate) [u8; DECRYPT_HANDLE_LEN]);
199
200impl fmt::Debug for PodDecryptHandle {
201    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
202        write!(f, "{:?}", self.0)
203    }
204}
205
206#[cfg(not(target_os = "solana"))]
207impl From<DecryptHandle> for PodDecryptHandle {
208    fn from(decoded_handle: DecryptHandle) -> Self {
209        Self(decoded_handle.to_bytes())
210    }
211}
212
213// For proof verification, interpret pod::DecryptHandle as CompressedRistretto
214#[cfg(not(target_os = "solana"))]
215impl From<PodDecryptHandle> for CompressedRistretto {
216    fn from(pod_handle: PodDecryptHandle) -> Self {
217        Self(pod_handle.0)
218    }
219}
220
221#[cfg(not(target_os = "solana"))]
222impl TryFrom<PodDecryptHandle> for DecryptHandle {
223    type Error = ElGamalError;
224
225    fn try_from(pod_handle: PodDecryptHandle) -> Result<Self, Self::Error> {
226        Self::from_bytes(&pod_handle.0).ok_or(ElGamalError::CiphertextDeserialization)
227    }
228}
229
230impl fmt::Display for PodDecryptHandle {
231    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
232        write!(f, "{}", BASE64_STANDARD.encode(self.0))
233    }
234}
235
236impl_from_str!(
237    TYPE = PodDecryptHandle,
238    BYTES_LEN = DECRYPT_HANDLE_LEN,
239    BASE64_LEN = DECRYPT_HANDLE_MAX_BASE64_LEN
240);
241
242impl_from_bytes!(TYPE = PodDecryptHandle, BYTES_LEN = DECRYPT_HANDLE_LEN);
243
244#[cfg(test)]
245mod tests {
246    use {super::*, crate::encryption::elgamal::ElGamalKeypair, std::str::FromStr};
247
248    #[test]
249    fn elgamal_pubkey_fromstr() {
250        let elgamal_keypair = ElGamalKeypair::new_rand();
251        let expected_elgamal_pubkey: PodElGamalPubkey = (*elgamal_keypair.pubkey()).into();
252
253        let elgamal_pubkey_base64_str = format!("{}", expected_elgamal_pubkey);
254        let computed_elgamal_pubkey =
255            PodElGamalPubkey::from_str(&elgamal_pubkey_base64_str).unwrap();
256
257        assert_eq!(expected_elgamal_pubkey, computed_elgamal_pubkey);
258    }
259
260    #[test]
261    fn elgamal_ciphertext_fromstr() {
262        let elgamal_keypair = ElGamalKeypair::new_rand();
263        let expected_elgamal_ciphertext: PodElGamalCiphertext =
264            elgamal_keypair.pubkey().encrypt(0_u64).into();
265
266        let elgamal_ciphertext_base64_str = format!("{}", expected_elgamal_ciphertext);
267        let computed_elgamal_ciphertext =
268            PodElGamalCiphertext::from_str(&elgamal_ciphertext_base64_str).unwrap();
269
270        assert_eq!(expected_elgamal_ciphertext, computed_elgamal_ciphertext);
271    }
272}