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