solana_zk_sdk/encryption/
elgamal.rs

1//! The twisted ElGamal 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 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//! In contrast to the traditional ElGamal encryption scheme, the twisted ElGamal encodes messages
11//! directly as a Pedersen commitment. Therefore, proof systems that are designed specifically for
12//! Pedersen commitments can be used on the twisted ElGamal ciphertexts.
13//!
14//! As the messages are encrypted as scalar elements (a.k.a. in the "exponent"), one must solve the
15//! discrete log to recover the originally encrypted value.
16
17#[cfg(target_arch = "wasm32")]
18use wasm_bindgen::prelude::*;
19// Currently, `wasm_bindgen` exports types and functions included in the current crate, but all
20// types and functions exported for wasm targets in all of its dependencies
21// (https://github.com/rustwasm/wasm-bindgen/issues/3759). We specifically exclude some of the
22// dependencies that will cause unnecessary bloat to the wasm binary.
23use {
24    crate::{
25        encryption::{
26            discrete_log::DiscreteLog,
27            pedersen::{Pedersen, PedersenCommitment, PedersenOpening, G, H},
28            DECRYPT_HANDLE_LEN, ELGAMAL_CIPHERTEXT_LEN, ELGAMAL_KEYPAIR_LEN, ELGAMAL_PUBKEY_LEN,
29            ELGAMAL_SECRET_KEY_LEN, PEDERSEN_COMMITMENT_LEN,
30        },
31        errors::ElGamalError,
32    },
33    base64::{prelude::BASE64_STANDARD, Engine},
34    core::ops::{Add, Mul, Sub},
35    curve25519_dalek::{
36        ristretto::{CompressedRistretto, RistrettoPoint},
37        scalar::Scalar,
38        traits::Identity,
39    },
40    rand::rngs::OsRng,
41    serde::{Deserialize, Serialize},
42    sha3::Sha3_512,
43    std::{convert::TryInto, fmt},
44    subtle::{Choice, ConstantTimeEq},
45    zeroize::Zeroize,
46};
47#[cfg(not(target_arch = "wasm32"))]
48use {
49    sha3::Digest,
50    solana_derivation_path::DerivationPath,
51    solana_seed_derivable::SeedDerivable,
52    solana_seed_phrase::generate_seed_from_seed_phrase_and_passphrase,
53    solana_signature::Signature,
54    solana_signer::{EncodableKey, EncodableKeypair, Signer, SignerError},
55    std::{
56        error,
57        io::{Read, Write},
58        path::Path,
59    },
60};
61
62/// Algorithm handle for the twisted ElGamal encryption scheme
63pub struct ElGamal;
64impl ElGamal {
65    /// Generates an ElGamal keypair.
66    ///
67    /// This function is randomized. It internally samples a scalar element using `OsRng`.
68    fn keygen() -> ElGamalKeypair {
69        // secret scalar should be non-zero except with negligible probability
70        let mut s = Scalar::random(&mut OsRng);
71        let keypair = Self::keygen_with_scalar(&s);
72
73        s.zeroize();
74        keypair
75    }
76
77    /// Generates an ElGamal keypair from a scalar input that determines the ElGamal private key.
78    ///
79    /// This function panics if the input scalar is zero, which is not a valid key.
80    fn keygen_with_scalar(s: &Scalar) -> ElGamalKeypair {
81        let secret = ElGamalSecretKey(*s);
82        let public = ElGamalPubkey::new(&secret);
83
84        ElGamalKeypair { public, secret }
85    }
86
87    /// On input an ElGamal public key and an amount to be encrypted, the function returns a
88    /// corresponding ElGamal ciphertext.
89    ///
90    /// This function is randomized. It internally samples a scalar element using `OsRng`.
91    fn encrypt<T: Into<Scalar>>(public: &ElGamalPubkey, amount: T) -> ElGamalCiphertext {
92        let (commitment, opening) = Pedersen::new(amount);
93        let handle = public.decrypt_handle(&opening);
94
95        ElGamalCiphertext { commitment, handle }
96    }
97
98    /// On input a public key, amount, and Pedersen opening, the function returns the corresponding
99    /// ElGamal ciphertext.
100    fn encrypt_with<T: Into<Scalar>>(
101        amount: T,
102        public: &ElGamalPubkey,
103        opening: &PedersenOpening,
104    ) -> ElGamalCiphertext {
105        let commitment = Pedersen::with(amount, opening);
106        let handle = public.decrypt_handle(opening);
107
108        ElGamalCiphertext { commitment, handle }
109    }
110
111    /// On input an amount, the function returns a twisted ElGamal ciphertext where the associated
112    /// Pedersen opening is always zero. Since the opening is zero, any twisted ElGamal ciphertext
113    /// of this form is a valid ciphertext under any ElGamal public key.
114    pub fn encode<T: Into<Scalar>>(amount: T) -> ElGamalCiphertext {
115        let commitment = Pedersen::encode(amount);
116        let handle = DecryptHandle(RistrettoPoint::identity());
117
118        ElGamalCiphertext { commitment, handle }
119    }
120
121    /// On input a secret key and a ciphertext, the function returns the discrete log encoding of
122    /// original amount.
123    ///
124    /// The output of this function is of type `DiscreteLog`. To recover, the originally encrypted
125    /// amount, use `DiscreteLog::decode`.
126    fn decrypt(secret: &ElGamalSecretKey, ciphertext: &ElGamalCiphertext) -> DiscreteLog {
127        DiscreteLog::new(
128            G,
129            ciphertext.commitment.get_point() - &(&secret.0 * &ciphertext.handle.0),
130        )
131    }
132
133    /// On input a secret key and a ciphertext, the function returns the decrypted amount
134    /// interpreted as a positive 32-bit number (but still of type `u64`).
135    ///
136    /// If the originally encrypted amount is not a positive 32-bit number, then the function
137    /// returns `None`.
138    ///
139    /// NOTE: This function is not constant time.
140    fn decrypt_u32(secret: &ElGamalSecretKey, ciphertext: &ElGamalCiphertext) -> Option<u64> {
141        let discrete_log_instance = Self::decrypt(secret, ciphertext);
142        discrete_log_instance.decode_u32()
143    }
144}
145
146/// A (twisted) ElGamal encryption keypair.
147///
148/// The instances of the secret key are zeroized on drop.
149#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
150#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, Zeroize)]
151pub struct ElGamalKeypair {
152    /// The public half of this keypair.
153    public: ElGamalPubkey,
154    /// The secret half of this keypair.
155    secret: ElGamalSecretKey,
156}
157
158#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
159impl ElGamalKeypair {
160    /// Generates the public and secret keys for ElGamal encryption.
161    ///
162    /// This function is randomized. It internally samples a scalar element using `OsRng`.
163    #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = newRand))]
164    pub fn new_rand() -> Self {
165        ElGamal::keygen()
166    }
167
168    #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = pubkeyOwned))]
169    pub fn pubkey_owned(&self) -> ElGamalPubkey {
170        self.public
171    }
172}
173
174impl ElGamalKeypair {
175    pub fn pubkey(&self) -> &ElGamalPubkey {
176        &self.public
177    }
178
179    pub fn secret(&self) -> &ElGamalSecretKey {
180        &self.secret
181    }
182}
183
184#[cfg(not(target_arch = "wasm32"))]
185impl ElGamalKeypair {
186    /// Create an ElGamal keypair from an ElGamal public key and an ElGamal secret key.
187    ///
188    /// An ElGamal keypair should never be instantiated manually; `ElGamalKeypair::new`,
189    /// `ElGamalKeypair::new_rand` or `ElGamalKeypair::new_from_signer` should be used instead.
190    /// This function exists to create custom ElGamal keypairs for tests.
191    pub fn new_for_tests(public: ElGamalPubkey, secret: ElGamalSecretKey) -> Self {
192        Self { public, secret }
193    }
194
195    /// Convert an ElGamal secret key to an ElGamal keypair.
196    pub fn new(secret: ElGamalSecretKey) -> Self {
197        let public = ElGamalPubkey::new(&secret);
198        Self { public, secret }
199    }
200
201    /// Deterministically derives an ElGamal keypair from a Solana signer and a public seed.
202    ///
203    /// This function exists for applications where a user may not wish to maintain a Solana signer
204    /// and an ElGamal keypair separately. Instead, a user can derive the ElGamal keypair
205    /// on-the-fly whenever encryption/decryption is needed.
206    ///
207    /// For the spl-token-2022 confidential extension, the ElGamal public key is specified in a
208    /// token account. A natural way to derive an ElGamal keypair is to define it from the hash of
209    /// a Solana keypair and a Solana address as the public seed. However, for general hardware
210    /// wallets, the signing key is not exposed in the API. Therefore, this function uses a signer
211    /// to sign a public seed and the resulting signature is then hashed to derive an ElGamal
212    /// keypair.
213    pub fn new_from_signer(
214        signer: &dyn Signer,
215        public_seed: &[u8],
216    ) -> Result<Self, Box<dyn error::Error>> {
217        let secret = ElGamalSecretKey::new_from_signer(signer, public_seed)?;
218        Ok(Self::new(secret))
219    }
220
221    /// Derive an ElGamal keypair from a signature.
222    pub fn new_from_signature(signature: &Signature) -> Result<Self, Box<dyn error::Error>> {
223        let secret = ElGamalSecretKey::new_from_signature(signature)?;
224        Ok(Self::new(secret))
225    }
226
227    /// Reads a JSON-encoded keypair from a `Reader` implementor
228    pub fn read_json<R: Read>(reader: &mut R) -> Result<Self, Box<dyn error::Error>> {
229        let bytes: Vec<u8> = serde_json::from_reader(reader)?;
230        Self::try_from(bytes.as_slice())
231            .ok()
232            .ok_or_else(|| std::io::Error::other("Invalid ElGamalKeypair").into())
233    }
234
235    /// Reads keypair from a file
236    pub fn read_json_file<F: AsRef<Path>>(path: F) -> Result<Self, Box<dyn error::Error>> {
237        Self::read_from_file(path)
238    }
239
240    /// Writes to a `Write` implementer with JSON-encoding
241    pub fn write_json<W: Write>(&self, writer: &mut W) -> Result<String, Box<dyn error::Error>> {
242        let json =
243            serde_json::to_string(&Into::<[u8; ELGAMAL_KEYPAIR_LEN]>::into(self).as_slice())?;
244        writer.write_all(&json.clone().into_bytes())?;
245        Ok(json)
246    }
247
248    /// Write keypair to a file with JSON-encoding
249    pub fn write_json_file<F: AsRef<Path>>(
250        &self,
251        outfile: F,
252    ) -> Result<String, Box<dyn std::error::Error>> {
253        self.write_to_file(outfile)
254    }
255}
256
257#[cfg(not(target_arch = "wasm32"))]
258impl EncodableKey for ElGamalKeypair {
259    fn read<R: Read>(reader: &mut R) -> Result<Self, Box<dyn error::Error>> {
260        Self::read_json(reader)
261    }
262
263    fn write<W: Write>(&self, writer: &mut W) -> Result<String, Box<dyn error::Error>> {
264        self.write_json(writer)
265    }
266}
267
268impl TryFrom<&[u8]> for ElGamalKeypair {
269    type Error = ElGamalError;
270    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
271        if bytes.len() != ELGAMAL_KEYPAIR_LEN {
272            return Err(ElGamalError::KeypairDeserialization);
273        }
274
275        Ok(Self {
276            public: ElGamalPubkey::try_from(&bytes[..ELGAMAL_PUBKEY_LEN])?,
277            secret: ElGamalSecretKey::try_from(&bytes[ELGAMAL_PUBKEY_LEN..])?,
278        })
279    }
280}
281
282impl From<ElGamalKeypair> for [u8; ELGAMAL_KEYPAIR_LEN] {
283    fn from(keypair: ElGamalKeypair) -> Self {
284        let mut bytes = [0u8; ELGAMAL_KEYPAIR_LEN];
285        bytes[..ELGAMAL_PUBKEY_LEN]
286            .copy_from_slice(&Into::<[u8; ELGAMAL_PUBKEY_LEN]>::into(keypair.public));
287        bytes[ELGAMAL_PUBKEY_LEN..].copy_from_slice(keypair.secret.as_bytes());
288        bytes
289    }
290}
291
292impl From<&ElGamalKeypair> for [u8; ELGAMAL_KEYPAIR_LEN] {
293    fn from(keypair: &ElGamalKeypair) -> Self {
294        let mut bytes = [0u8; ELGAMAL_KEYPAIR_LEN];
295        bytes[..ELGAMAL_PUBKEY_LEN]
296            .copy_from_slice(&Into::<[u8; ELGAMAL_PUBKEY_LEN]>::into(keypair.public));
297        bytes[ELGAMAL_PUBKEY_LEN..].copy_from_slice(keypair.secret.as_bytes());
298        bytes
299    }
300}
301
302#[cfg(not(target_arch = "wasm32"))]
303impl SeedDerivable for ElGamalKeypair {
304    fn from_seed(seed: &[u8]) -> Result<Self, Box<dyn error::Error>> {
305        let secret = ElGamalSecretKey::from_seed(seed)?;
306        let public = ElGamalPubkey::new(&secret);
307        Ok(ElGamalKeypair { public, secret })
308    }
309
310    fn from_seed_and_derivation_path(
311        _seed: &[u8],
312        _derivation_path: Option<DerivationPath>,
313    ) -> Result<Self, Box<dyn error::Error>> {
314        Err(ElGamalError::DerivationMethodNotSupported.into())
315    }
316
317    fn from_seed_phrase_and_passphrase(
318        seed_phrase: &str,
319        passphrase: &str,
320    ) -> Result<Self, Box<dyn error::Error>> {
321        Self::from_seed(&generate_seed_from_seed_phrase_and_passphrase(
322            seed_phrase,
323            passphrase,
324        ))
325    }
326}
327
328#[cfg(not(target_arch = "wasm32"))]
329impl EncodableKeypair for ElGamalKeypair {
330    type Pubkey = ElGamalPubkey;
331
332    fn encodable_pubkey(&self) -> Self::Pubkey {
333        self.public
334    }
335}
336
337/// Public key for the ElGamal encryption scheme.
338#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
339#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, Zeroize)]
340pub struct ElGamalPubkey(RistrettoPoint);
341impl ElGamalPubkey {
342    /// Derives the `ElGamalPubkey` that uniquely corresponds to an `ElGamalSecretKey`.
343    pub fn new(secret: &ElGamalSecretKey) -> Self {
344        let s = &secret.0;
345        assert!(s != &Scalar::ZERO);
346
347        ElGamalPubkey(s.invert() * &(*H))
348    }
349
350    pub fn get_point(&self) -> &RistrettoPoint {
351        &self.0
352    }
353
354    /// Encrypts an amount under the public key.
355    ///
356    /// This function is randomized. It internally samples a scalar element using `OsRng`.
357    pub fn encrypt<T: Into<Scalar>>(&self, amount: T) -> ElGamalCiphertext {
358        ElGamal::encrypt(self, amount)
359    }
360
361    /// Encrypts an amount under the public key and an input Pedersen opening.
362    pub fn encrypt_with<T: Into<Scalar>>(
363        &self,
364        amount: T,
365        opening: &PedersenOpening,
366    ) -> ElGamalCiphertext {
367        ElGamal::encrypt_with(amount, self, opening)
368    }
369
370    /// Generates a decryption handle for an ElGamal public key under a Pedersen
371    /// opening.
372    pub fn decrypt_handle(self, opening: &PedersenOpening) -> DecryptHandle {
373        DecryptHandle::new(&self, opening)
374    }
375}
376
377#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
378impl ElGamalPubkey {
379    #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = encryptU64))]
380    pub fn encrypt_u64(&self, amount: u64) -> ElGamalCiphertext {
381        ElGamal::encrypt(self, amount)
382    }
383
384    #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = encryptWithU64))]
385    pub fn encrypt_with_u64(&self, amount: u64, opening: &PedersenOpening) -> ElGamalCiphertext {
386        ElGamal::encrypt_with(amount, self, opening)
387    }
388}
389
390#[cfg(not(target_arch = "wasm32"))]
391impl EncodableKey for ElGamalPubkey {
392    fn read<R: Read>(reader: &mut R) -> Result<Self, Box<dyn error::Error>> {
393        let bytes: Vec<u8> = serde_json::from_reader(reader)?;
394        Self::try_from(bytes.as_slice())
395            .ok()
396            .ok_or_else(|| std::io::Error::other("Invalid ElGamalPubkey").into())
397    }
398
399    fn write<W: Write>(&self, writer: &mut W) -> Result<String, Box<dyn error::Error>> {
400        let bytes = Into::<[u8; ELGAMAL_PUBKEY_LEN]>::into(*self);
401        let json = serde_json::to_string(&bytes.to_vec())?;
402        writer.write_all(&json.clone().into_bytes())?;
403        Ok(json)
404    }
405}
406
407impl fmt::Display for ElGamalPubkey {
408    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
409        write!(
410            f,
411            "{}",
412            BASE64_STANDARD.encode(Into::<[u8; ELGAMAL_PUBKEY_LEN]>::into(*self))
413        )
414    }
415}
416
417impl TryFrom<&[u8]> for ElGamalPubkey {
418    type Error = ElGamalError;
419    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
420        if bytes.len() != ELGAMAL_PUBKEY_LEN {
421            return Err(ElGamalError::PubkeyDeserialization);
422        }
423        let Ok(compressed_ristretto) = CompressedRistretto::from_slice(bytes) else {
424            return Err(ElGamalError::PubkeyDeserialization);
425        };
426
427        Ok(ElGamalPubkey(
428            compressed_ristretto
429                .decompress()
430                .ok_or(ElGamalError::PubkeyDeserialization)?,
431        ))
432    }
433}
434
435impl From<ElGamalPubkey> for [u8; ELGAMAL_PUBKEY_LEN] {
436    fn from(pubkey: ElGamalPubkey) -> Self {
437        pubkey.0.compress().to_bytes()
438    }
439}
440
441impl From<&ElGamalPubkey> for [u8; ELGAMAL_PUBKEY_LEN] {
442    fn from(pubkey: &ElGamalPubkey) -> Self {
443        pubkey.0.compress().to_bytes()
444    }
445}
446
447/// Secret key for the ElGamal encryption scheme.
448///
449/// Instances of ElGamal secret key are zeroized on drop.
450#[derive(Clone, Debug, Deserialize, Serialize, Zeroize)]
451#[zeroize(drop)]
452pub struct ElGamalSecretKey(Scalar);
453impl ElGamalSecretKey {
454    /// Randomly samples an ElGamal secret key.
455    ///
456    /// This function is randomized. It internally samples a scalar element using `OsRng`.
457    pub fn new_rand() -> Self {
458        ElGamalSecretKey(Scalar::random(&mut OsRng))
459    }
460
461    /// Derive an ElGamal secret key from an entropy seed.
462    pub fn from_seed(seed: &[u8]) -> Result<Self, ElGamalError> {
463        const MINIMUM_SEED_LEN: usize = ELGAMAL_SECRET_KEY_LEN;
464        const MAXIMUM_SEED_LEN: usize = 65535;
465
466        if seed.len() < MINIMUM_SEED_LEN {
467            return Err(ElGamalError::SeedLengthTooShort);
468        }
469        if seed.len() > MAXIMUM_SEED_LEN {
470            return Err(ElGamalError::SeedLengthTooLong);
471        }
472        Ok(ElGamalSecretKey(Scalar::hash_from_bytes::<Sha3_512>(seed)))
473    }
474
475    pub fn get_scalar(&self) -> &Scalar {
476        &self.0
477    }
478
479    pub fn as_bytes(&self) -> &[u8; ELGAMAL_SECRET_KEY_LEN] {
480        self.0.as_bytes()
481    }
482
483    /// Decrypts a ciphertext using the ElGamal secret key.
484    ///
485    /// The output of this function is of type `DiscreteLog`. To recover, the originally encrypted
486    /// message, use `DiscreteLog::decode`.
487    pub fn decrypt(&self, ciphertext: &ElGamalCiphertext) -> DiscreteLog {
488        ElGamal::decrypt(self, ciphertext)
489    }
490
491    /// Decrypts a ciphertext using the ElGamal secret key interpreting the message as type `u32`.
492    ///
493    /// NOTE: This function is not constant time.
494    pub fn decrypt_u32(&self, ciphertext: &ElGamalCiphertext) -> Option<u64> {
495        ElGamal::decrypt_u32(self, ciphertext)
496    }
497}
498
499#[cfg(not(target_arch = "wasm32"))]
500impl ElGamalSecretKey {
501    /// Deterministically derives an ElGamal secret key from a Solana signer and a public seed.
502    ///
503    /// See `ElGamalKeypair::new_from_signer` for more context on the key derivation.
504    pub fn new_from_signer(
505        signer: &dyn Signer,
506        public_seed: &[u8],
507    ) -> Result<Self, Box<dyn error::Error>> {
508        let seed = Self::seed_from_signer(signer, public_seed)?;
509        let key = Self::from_seed(&seed)?;
510        Ok(key)
511    }
512
513    /// Derive a seed from a Solana signer used to generate an ElGamal secret key.
514    ///
515    /// The seed is derived as the hash of the signature of a public seed.
516    pub fn seed_from_signer(
517        signer: &dyn Signer,
518        public_seed: &[u8],
519    ) -> Result<Vec<u8>, SignerError> {
520        let message = [b"ElGamalSecretKey", public_seed].concat();
521        let signature = signer.try_sign_message(&message)?;
522
523        // Some `Signer` implementations return the default signature, which is not suitable for
524        // use as key material
525        if bool::from(signature.as_ref().ct_eq(Signature::default().as_ref())) {
526            return Err(SignerError::Custom("Rejecting default signatures".into()));
527        }
528
529        Ok(Self::seed_from_signature(&signature))
530    }
531
532    /// Derive an ElGamal secret key from a signature.
533    pub fn new_from_signature(signature: &Signature) -> Result<Self, Box<dyn error::Error>> {
534        let seed = Self::seed_from_signature(signature);
535        let key = Self::from_seed(&seed)?;
536        Ok(key)
537    }
538
539    /// Derive an ElGamal secret key from a signature.
540    ///
541    /// TODO: This function uses a non-standard KDF and should be refactored.
542    /// See: https://github.com/solana-program/zk-elgamal-proof/issues/35
543    pub fn seed_from_signature(signature: &Signature) -> Vec<u8> {
544        let mut hasher = Sha3_512::new();
545        hasher.update(signature.as_ref());
546        let result = hasher.finalize();
547
548        result.to_vec()
549    }
550}
551
552#[cfg(not(target_arch = "wasm32"))]
553impl EncodableKey for ElGamalSecretKey {
554    fn read<R: Read>(reader: &mut R) -> Result<Self, Box<dyn error::Error>> {
555        let bytes: Vec<u8> = serde_json::from_reader(reader)?;
556        Self::try_from(bytes.as_slice())
557            .ok()
558            .ok_or_else(|| std::io::Error::other("Invalid ElGamalSecretKey").into())
559    }
560
561    fn write<W: Write>(&self, writer: &mut W) -> Result<String, Box<dyn error::Error>> {
562        let bytes = Into::<[u8; ELGAMAL_SECRET_KEY_LEN]>::into(self);
563        let json = serde_json::to_string(&bytes.to_vec())?;
564        writer.write_all(&json.clone().into_bytes())?;
565        Ok(json)
566    }
567}
568
569#[cfg(not(target_arch = "wasm32"))]
570impl SeedDerivable for ElGamalSecretKey {
571    fn from_seed(seed: &[u8]) -> Result<Self, Box<dyn error::Error>> {
572        let key = Self::from_seed(seed)?;
573        Ok(key)
574    }
575
576    fn from_seed_and_derivation_path(
577        _seed: &[u8],
578        _derivation_path: Option<DerivationPath>,
579    ) -> Result<Self, Box<dyn error::Error>> {
580        Err(ElGamalError::DerivationMethodNotSupported.into())
581    }
582
583    fn from_seed_phrase_and_passphrase(
584        seed_phrase: &str,
585        passphrase: &str,
586    ) -> Result<Self, Box<dyn error::Error>> {
587        let key = Self::from_seed(&generate_seed_from_seed_phrase_and_passphrase(
588            seed_phrase,
589            passphrase,
590        ))?;
591        Ok(key)
592    }
593}
594
595impl From<Scalar> for ElGamalSecretKey {
596    fn from(scalar: Scalar) -> ElGamalSecretKey {
597        ElGamalSecretKey(scalar)
598    }
599}
600
601impl TryFrom<&[u8]> for ElGamalSecretKey {
602    type Error = ElGamalError;
603    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
604        match bytes.try_into() {
605            Ok(bytes) => Ok(ElGamalSecretKey::from(
606                Scalar::from_canonical_bytes(bytes)
607                    .into_option()
608                    .ok_or(ElGamalError::SecretKeyDeserialization)?,
609            )),
610            _ => Err(ElGamalError::SecretKeyDeserialization),
611        }
612    }
613}
614
615impl From<ElGamalSecretKey> for [u8; ELGAMAL_SECRET_KEY_LEN] {
616    fn from(secret_key: ElGamalSecretKey) -> Self {
617        secret_key.0.to_bytes()
618    }
619}
620
621impl From<&ElGamalSecretKey> for [u8; ELGAMAL_SECRET_KEY_LEN] {
622    fn from(secret_key: &ElGamalSecretKey) -> Self {
623        secret_key.0.to_bytes()
624    }
625}
626
627impl Eq for ElGamalSecretKey {}
628impl PartialEq for ElGamalSecretKey {
629    fn eq(&self, other: &Self) -> bool {
630        self.ct_eq(other).unwrap_u8() == 1u8
631    }
632}
633impl ConstantTimeEq for ElGamalSecretKey {
634    fn ct_eq(&self, other: &Self) -> Choice {
635        self.0.ct_eq(&other.0)
636    }
637}
638
639/// Ciphertext for the ElGamal encryption scheme.
640#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
641#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
642pub struct ElGamalCiphertext {
643    pub commitment: PedersenCommitment,
644    pub handle: DecryptHandle,
645}
646impl ElGamalCiphertext {
647    pub fn add_amount<T: Into<Scalar>>(&self, amount: T) -> Self {
648        let point = amount.into() * G;
649        let commitment_to_add = PedersenCommitment::new(point);
650        ElGamalCiphertext {
651            commitment: &self.commitment + &commitment_to_add,
652            handle: self.handle,
653        }
654    }
655
656    pub fn subtract_amount<T: Into<Scalar>>(&self, amount: T) -> Self {
657        let point = amount.into() * &G;
658        let commitment_to_subtract = PedersenCommitment::new(point);
659        ElGamalCiphertext {
660            commitment: &self.commitment - &commitment_to_subtract,
661            handle: self.handle,
662        }
663    }
664
665    pub fn to_bytes(&self) -> [u8; ELGAMAL_CIPHERTEXT_LEN] {
666        let mut bytes = [0u8; ELGAMAL_CIPHERTEXT_LEN];
667        bytes[..PEDERSEN_COMMITMENT_LEN].copy_from_slice(&self.commitment.to_bytes());
668        bytes[PEDERSEN_COMMITMENT_LEN..].copy_from_slice(&self.handle.to_bytes());
669        bytes
670    }
671
672    pub fn from_bytes(bytes: &[u8]) -> Option<ElGamalCiphertext> {
673        if bytes.len() != ELGAMAL_CIPHERTEXT_LEN {
674            return None;
675        }
676
677        Some(ElGamalCiphertext {
678            commitment: PedersenCommitment::from_bytes(&bytes[..PEDERSEN_COMMITMENT_LEN])?,
679            handle: DecryptHandle::from_bytes(&bytes[PEDERSEN_COMMITMENT_LEN..])?,
680        })
681    }
682
683    /// Decrypts the ciphertext using an ElGamal secret key.
684    ///
685    /// The output of this function is of type `DiscreteLog`. To recover, the originally encrypted
686    /// amount, use `DiscreteLog::decode`.
687    pub fn decrypt(&self, secret: &ElGamalSecretKey) -> DiscreteLog {
688        ElGamal::decrypt(secret, self)
689    }
690
691    /// Decrypts the ciphertext using an ElGamal secret key assuming that the message is a positive
692    /// 32-bit number.
693    ///
694    /// If the originally encrypted amount is not a positive 32-bit number, then the function
695    /// returns `None`.
696    ///
697    /// NOTE: This function is not constant time.
698    pub fn decrypt_u32(&self, secret: &ElGamalSecretKey) -> Option<u64> {
699        ElGamal::decrypt_u32(secret, self)
700    }
701}
702
703impl fmt::Display for ElGamalCiphertext {
704    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
705        write!(f, "{}", BASE64_STANDARD.encode(self.to_bytes()))
706    }
707}
708
709impl<'b> Add<&'b ElGamalCiphertext> for &ElGamalCiphertext {
710    type Output = ElGamalCiphertext;
711
712    fn add(self, ciphertext: &'b ElGamalCiphertext) -> ElGamalCiphertext {
713        ElGamalCiphertext {
714            commitment: &self.commitment + &ciphertext.commitment,
715            handle: &self.handle + &ciphertext.handle,
716        }
717    }
718}
719
720define_add_variants!(
721    LHS = ElGamalCiphertext,
722    RHS = ElGamalCiphertext,
723    Output = ElGamalCiphertext
724);
725
726impl<'b> Sub<&'b ElGamalCiphertext> for &ElGamalCiphertext {
727    type Output = ElGamalCiphertext;
728
729    fn sub(self, ciphertext: &'b ElGamalCiphertext) -> ElGamalCiphertext {
730        ElGamalCiphertext {
731            commitment: &self.commitment - &ciphertext.commitment,
732            handle: &self.handle - &ciphertext.handle,
733        }
734    }
735}
736
737define_sub_variants!(
738    LHS = ElGamalCiphertext,
739    RHS = ElGamalCiphertext,
740    Output = ElGamalCiphertext
741);
742
743impl<'b> Mul<&'b Scalar> for &ElGamalCiphertext {
744    type Output = ElGamalCiphertext;
745
746    fn mul(self, scalar: &'b Scalar) -> ElGamalCiphertext {
747        ElGamalCiphertext {
748            commitment: &self.commitment * scalar,
749            handle: &self.handle * scalar,
750        }
751    }
752}
753
754define_mul_variants!(
755    LHS = ElGamalCiphertext,
756    RHS = Scalar,
757    Output = ElGamalCiphertext
758);
759
760impl<'b> Mul<&'b ElGamalCiphertext> for &Scalar {
761    type Output = ElGamalCiphertext;
762
763    fn mul(self, ciphertext: &'b ElGamalCiphertext) -> ElGamalCiphertext {
764        ElGamalCiphertext {
765            commitment: self * &ciphertext.commitment,
766            handle: self * &ciphertext.handle,
767        }
768    }
769}
770
771define_mul_variants!(
772    LHS = Scalar,
773    RHS = ElGamalCiphertext,
774    Output = ElGamalCiphertext
775);
776
777/// Decryption handle for Pedersen commitment.
778#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
779#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
780pub struct DecryptHandle(RistrettoPoint);
781impl DecryptHandle {
782    pub fn new(public: &ElGamalPubkey, opening: &PedersenOpening) -> Self {
783        Self(&public.0 * opening.get_scalar())
784    }
785
786    pub fn get_point(&self) -> &RistrettoPoint {
787        &self.0
788    }
789
790    pub fn to_bytes(&self) -> [u8; DECRYPT_HANDLE_LEN] {
791        self.0.compress().to_bytes()
792    }
793
794    pub fn from_bytes(bytes: &[u8]) -> Option<DecryptHandle> {
795        if bytes.len() != DECRYPT_HANDLE_LEN {
796            return None;
797        }
798        let Ok(compressed_ristretto) = CompressedRistretto::from_slice(bytes) else {
799            return None;
800        };
801
802        compressed_ristretto.decompress().map(DecryptHandle)
803    }
804}
805
806impl<'b> Add<&'b DecryptHandle> for &DecryptHandle {
807    type Output = DecryptHandle;
808
809    fn add(self, handle: &'b DecryptHandle) -> DecryptHandle {
810        DecryptHandle(&self.0 + &handle.0)
811    }
812}
813
814define_add_variants!(
815    LHS = DecryptHandle,
816    RHS = DecryptHandle,
817    Output = DecryptHandle
818);
819
820impl<'b> Sub<&'b DecryptHandle> for &DecryptHandle {
821    type Output = DecryptHandle;
822
823    fn sub(self, handle: &'b DecryptHandle) -> DecryptHandle {
824        DecryptHandle(&self.0 - &handle.0)
825    }
826}
827
828define_sub_variants!(
829    LHS = DecryptHandle,
830    RHS = DecryptHandle,
831    Output = DecryptHandle
832);
833
834impl<'b> Mul<&'b Scalar> for &DecryptHandle {
835    type Output = DecryptHandle;
836
837    fn mul(self, scalar: &'b Scalar) -> DecryptHandle {
838        DecryptHandle(&self.0 * scalar)
839    }
840}
841
842define_mul_variants!(LHS = DecryptHandle, RHS = Scalar, Output = DecryptHandle);
843
844impl<'b> Mul<&'b DecryptHandle> for &Scalar {
845    type Output = DecryptHandle;
846
847    fn mul(self, handle: &'b DecryptHandle) -> DecryptHandle {
848        DecryptHandle(self * &handle.0)
849    }
850}
851
852define_mul_variants!(LHS = Scalar, RHS = DecryptHandle, Output = DecryptHandle);
853
854#[cfg(test)]
855mod tests {
856    use {
857        super::*,
858        crate::encryption::pedersen::Pedersen,
859        bip39::{Language, Mnemonic, MnemonicType, Seed},
860        solana_keypair::Keypair,
861        solana_pubkey::Pubkey,
862        solana_signer::null_signer::NullSigner,
863        std::fs::{self, File},
864    };
865
866    #[test]
867    fn test_encrypt_decrypt_correctness() {
868        let ElGamalKeypair { public, secret } = ElGamalKeypair::new_rand();
869        let amount: u32 = 57;
870        let ciphertext = ElGamal::encrypt(&public, amount);
871
872        let expected_instance = DiscreteLog::new(G, Scalar::from(amount) * &G);
873
874        assert_eq!(expected_instance, ElGamal::decrypt(&secret, &ciphertext));
875        assert_eq!(57_u64, secret.decrypt_u32(&ciphertext).unwrap());
876    }
877
878    #[cfg(not(target_arch = "wasm32"))]
879    #[test]
880    fn test_encrypt_decrypt_correctness_multithreaded() {
881        let ElGamalKeypair { public, secret } = ElGamalKeypair::new_rand();
882        let amount: u32 = 57;
883        let ciphertext = ElGamal::encrypt(&public, amount);
884
885        let mut instance = ElGamal::decrypt(&secret, &ciphertext);
886        instance.num_threads(4.try_into().unwrap()).unwrap();
887        assert_eq!(57_u64, instance.decode_u32().unwrap());
888    }
889
890    #[test]
891    fn test_decrypt_handle() {
892        let ElGamalKeypair {
893            public: public_0,
894            secret: secret_0,
895        } = ElGamalKeypair::new_rand();
896        let ElGamalKeypair {
897            public: public_1,
898            secret: secret_1,
899        } = ElGamalKeypair::new_rand();
900
901        let amount: u32 = 77;
902        let (commitment, opening) = Pedersen::new(amount);
903
904        let handle_0 = public_0.decrypt_handle(&opening);
905        let handle_1 = public_1.decrypt_handle(&opening);
906
907        let ciphertext_0 = ElGamalCiphertext {
908            commitment,
909            handle: handle_0,
910        };
911        let ciphertext_1 = ElGamalCiphertext {
912            commitment,
913            handle: handle_1,
914        };
915
916        let expected_instance = DiscreteLog::new(G, Scalar::from(amount) * &G);
917
918        assert_eq!(expected_instance, secret_0.decrypt(&ciphertext_0));
919        assert_eq!(expected_instance, secret_1.decrypt(&ciphertext_1));
920    }
921
922    #[test]
923    fn test_homomorphic_addition() {
924        let ElGamalKeypair { public, secret: _ } = ElGamalKeypair::new_rand();
925        let amount_0: u64 = 57;
926        let amount_1: u64 = 77;
927
928        // Add two ElGamal ciphertexts
929        let opening_0 = PedersenOpening::new_rand();
930        let opening_1 = PedersenOpening::new_rand();
931
932        let ciphertext_0 = ElGamal::encrypt_with(amount_0, &public, &opening_0);
933        let ciphertext_1 = ElGamal::encrypt_with(amount_1, &public, &opening_1);
934
935        let ciphertext_sum =
936            ElGamal::encrypt_with(amount_0 + amount_1, &public, &(&opening_0 + &opening_1));
937
938        assert_eq!(ciphertext_sum, ciphertext_0 + ciphertext_1);
939
940        // Add to ElGamal ciphertext
941        let opening = PedersenOpening::new_rand();
942        let ciphertext = ElGamal::encrypt_with(amount_0, &public, &opening);
943        let ciphertext_sum = ElGamal::encrypt_with(amount_0 + amount_1, &public, &opening);
944
945        assert_eq!(ciphertext_sum, ciphertext.add_amount(amount_1));
946    }
947
948    #[test]
949    fn test_homomorphic_subtraction() {
950        let ElGamalKeypair { public, secret: _ } = ElGamalKeypair::new_rand();
951        let amount_0: u64 = 77;
952        let amount_1: u64 = 55;
953
954        // Subtract two ElGamal ciphertexts
955        let opening_0 = PedersenOpening::new_rand();
956        let opening_1 = PedersenOpening::new_rand();
957
958        let ciphertext_0 = ElGamal::encrypt_with(amount_0, &public, &opening_0);
959        let ciphertext_1 = ElGamal::encrypt_with(amount_1, &public, &opening_1);
960
961        let ciphertext_sub =
962            ElGamal::encrypt_with(amount_0 - amount_1, &public, &(&opening_0 - &opening_1));
963
964        assert_eq!(ciphertext_sub, ciphertext_0 - ciphertext_1);
965
966        // Subtract to ElGamal ciphertext
967        let opening = PedersenOpening::new_rand();
968        let ciphertext = ElGamal::encrypt_with(amount_0, &public, &opening);
969        let ciphertext_sub = ElGamal::encrypt_with(amount_0 - amount_1, &public, &opening);
970
971        assert_eq!(ciphertext_sub, ciphertext.subtract_amount(amount_1));
972    }
973
974    #[test]
975    fn test_homomorphic_multiplication() {
976        let ElGamalKeypair { public, secret: _ } = ElGamalKeypair::new_rand();
977        let amount_0: u64 = 57;
978        let amount_1: u64 = 77;
979
980        let opening = PedersenOpening::new_rand();
981
982        let ciphertext = ElGamal::encrypt_with(amount_0, &public, &opening);
983        let scalar = Scalar::from(amount_1);
984
985        let ciphertext_prod =
986            ElGamal::encrypt_with(amount_0 * amount_1, &public, &(&opening * scalar));
987
988        assert_eq!(ciphertext_prod, ciphertext * scalar);
989        assert_eq!(ciphertext_prod, scalar * ciphertext);
990    }
991
992    #[test]
993    fn test_serde_ciphertext() {
994        let ElGamalKeypair { public, secret: _ } = ElGamalKeypair::new_rand();
995        let amount: u64 = 77;
996        let ciphertext = public.encrypt(amount);
997
998        let encoded = bincode::serialize(&ciphertext).unwrap();
999        let decoded: ElGamalCiphertext = bincode::deserialize(&encoded).unwrap();
1000
1001        assert_eq!(ciphertext, decoded);
1002    }
1003
1004    #[test]
1005    fn test_serde_pubkey() {
1006        let ElGamalKeypair { public, secret: _ } = ElGamalKeypair::new_rand();
1007
1008        let encoded = bincode::serialize(&public).unwrap();
1009        let decoded: ElGamalPubkey = bincode::deserialize(&encoded).unwrap();
1010
1011        assert_eq!(public, decoded);
1012    }
1013
1014    #[test]
1015    fn test_serde_secretkey() {
1016        let ElGamalKeypair { public: _, secret } = ElGamalKeypair::new_rand();
1017
1018        let encoded = bincode::serialize(&secret).unwrap();
1019        let decoded: ElGamalSecretKey = bincode::deserialize(&encoded).unwrap();
1020
1021        assert_eq!(secret, decoded);
1022    }
1023
1024    fn tmp_file_path(name: &str) -> String {
1025        use std::env;
1026        let out_dir = env::var("FARF_DIR").unwrap_or_else(|_| "farf".to_string());
1027        let keypair = ElGamalKeypair::new_rand();
1028        format!("{}/tmp/{}-{}", out_dir, name, keypair.public)
1029    }
1030
1031    #[test]
1032    fn test_write_keypair_file() {
1033        let outfile = tmp_file_path("test_write_keypair_file.json");
1034        let serialized_keypair = ElGamalKeypair::new_rand()
1035            .write_json_file(&outfile)
1036            .unwrap();
1037        let keypair_vec: Vec<u8> = serde_json::from_str(&serialized_keypair).unwrap();
1038        assert!(Path::new(&outfile).exists());
1039        assert_eq!(
1040            keypair_vec,
1041            Into::<[u8; ELGAMAL_KEYPAIR_LEN]>::into(
1042                ElGamalKeypair::read_json_file(&outfile).unwrap()
1043            )
1044            .to_vec()
1045        );
1046
1047        #[cfg(unix)]
1048        {
1049            use std::os::unix::fs::PermissionsExt;
1050            assert_eq!(
1051                File::open(&outfile)
1052                    .expect("open")
1053                    .metadata()
1054                    .expect("metadata")
1055                    .permissions()
1056                    .mode()
1057                    & 0o777,
1058                0o600
1059            );
1060        }
1061        fs::remove_file(&outfile).unwrap();
1062    }
1063
1064    #[test]
1065    fn test_write_keypair_file_overwrite_ok() {
1066        let outfile = tmp_file_path("test_write_keypair_file_overwrite_ok.json");
1067
1068        ElGamalKeypair::new_rand()
1069            .write_json_file(&outfile)
1070            .unwrap();
1071        ElGamalKeypair::new_rand()
1072            .write_json_file(&outfile)
1073            .unwrap();
1074    }
1075
1076    #[test]
1077    fn test_write_keypair_file_truncate() {
1078        let outfile = tmp_file_path("test_write_keypair_file_truncate.json");
1079
1080        ElGamalKeypair::new_rand()
1081            .write_json_file(&outfile)
1082            .unwrap();
1083        ElGamalKeypair::read_json_file(&outfile).unwrap();
1084
1085        // Ensure outfile is truncated
1086        {
1087            let mut f = File::create(&outfile).unwrap();
1088            f.write_all(String::from_utf8([b'a'; 2048].to_vec()).unwrap().as_bytes())
1089                .unwrap();
1090        }
1091        ElGamalKeypair::new_rand()
1092            .write_json_file(&outfile)
1093            .unwrap();
1094        ElGamalKeypair::read_json_file(&outfile).unwrap();
1095    }
1096
1097    #[test]
1098    fn test_secret_key_new_from_signer() {
1099        let keypair1 = Keypair::new();
1100        let keypair2 = Keypair::new();
1101
1102        assert_ne!(
1103            ElGamalSecretKey::new_from_signer(&keypair1, Pubkey::default().as_ref())
1104                .unwrap()
1105                .0,
1106            ElGamalSecretKey::new_from_signer(&keypair2, Pubkey::default().as_ref())
1107                .unwrap()
1108                .0,
1109        );
1110
1111        let null_signer = NullSigner::new(&Pubkey::default());
1112        assert!(
1113            ElGamalSecretKey::new_from_signer(&null_signer, Pubkey::default().as_ref()).is_err()
1114        );
1115    }
1116
1117    #[test]
1118    fn test_keypair_from_seed() {
1119        let good_seed = vec![0; 32];
1120        assert!(ElGamalKeypair::from_seed(&good_seed).is_ok());
1121
1122        let too_short_seed = vec![0; 31];
1123        assert!(ElGamalKeypair::from_seed(&too_short_seed).is_err());
1124
1125        let too_long_seed = vec![0; 65536];
1126        assert!(ElGamalKeypair::from_seed(&too_long_seed).is_err());
1127    }
1128
1129    #[test]
1130    fn test_keypair_from_seed_phrase_and_passphrase() {
1131        let mnemonic = Mnemonic::new(MnemonicType::Words12, Language::English);
1132        let passphrase = "42";
1133        let seed = Seed::new(&mnemonic, passphrase);
1134        let expected_keypair = ElGamalKeypair::from_seed(seed.as_bytes()).unwrap();
1135        let keypair =
1136            ElGamalKeypair::from_seed_phrase_and_passphrase(mnemonic.phrase(), passphrase).unwrap();
1137        assert_eq!(keypair.public, expected_keypair.public);
1138    }
1139
1140    #[test]
1141    fn test_decrypt_handle_bytes() {
1142        let handle = DecryptHandle(RistrettoPoint::default());
1143
1144        let encoded = handle.to_bytes();
1145        let decoded = DecryptHandle::from_bytes(&encoded).unwrap();
1146
1147        assert_eq!(handle, decoded);
1148    }
1149
1150    #[test]
1151    fn test_serde_decrypt_handle() {
1152        let handle = DecryptHandle(RistrettoPoint::default());
1153
1154        let encoded = bincode::serialize(&handle).unwrap();
1155        let decoded: DecryptHandle = bincode::deserialize(&encoded).unwrap();
1156
1157        assert_eq!(handle, decoded);
1158    }
1159
1160    #[test]
1161    fn test_decryption_with_wrong_key() {
1162        let keypair1 = ElGamalKeypair::new_rand();
1163        let keypair2 = ElGamalKeypair::new_rand(); // A different key
1164        let amount: u64 = 100;
1165
1166        let ciphertext = keypair1.pubkey().encrypt(amount);
1167
1168        // Decryption with the wrong key should fail
1169        assert!(keypair2.secret().decrypt_u32(&ciphertext).is_none());
1170    }
1171
1172    #[test]
1173    fn test_elgamal_encode_decryption() {
1174        let keypair1 = ElGamalKeypair::new_rand();
1175        let keypair2 = ElGamalKeypair::new_rand();
1176        let amount: u64 = 12345;
1177
1178        let encoded_ciphertext = ElGamal::encode(amount);
1179
1180        // ANY key should be able to "decrypt" the message
1181        assert_eq!(
1182            encoded_ciphertext.decrypt_u32(keypair1.secret()).unwrap(),
1183            amount
1184        );
1185        assert_eq!(
1186            encoded_ciphertext.decrypt_u32(keypair2.secret()).unwrap(),
1187            amount
1188        );
1189    }
1190}