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