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