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