waffles_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//! In contrast to the traditional ElGamal encryption scheme, the twisted ElGamal encodes messages
10//! directly as a Pedersen commitment. Therefore, proof systems that are designed specifically for
11//! Pedersen commitments can be used on the twisted ElGamal ciphertexts.
12//!
13//! As the messages are encrypted as scalar elements (a.k.a. in the "exponent"), one must solve the
14//! discrete log to recover the originally encrypted value.
15
16use {
17    crate::encryption::{
18        discrete_log::DiscreteLog,
19        pedersen::{Pedersen, PedersenCommitment, PedersenOpening, G, H},
20    },
21    core::ops::{Add, Mul, Sub},
22    curve25519_dalek::{
23        ristretto::{CompressedRistretto, RistrettoPoint},
24        scalar::Scalar,
25        traits::Identity,
26    },
27    serde::{Deserialize, Serialize},
28    solana_sdk::{
29        instruction::Instruction,
30        message::Message,
31        pubkey::Pubkey,
32        signature::Signature,
33        signer::{Signer, SignerError},
34    },
35    std::convert::TryInto,
36    subtle::{Choice, ConstantTimeEq},
37    zeroize::Zeroize,
38};
39#[cfg(not(target_os = "solana"))]
40use {
41    rand::rngs::OsRng,
42    sha3::Sha3_512,
43    std::{
44        fmt,
45        fs::{self, File, OpenOptions},
46        io::{Read, Write},
47        path::Path,
48    },
49};
50
51/// Algorithm handle for the twisted ElGamal encryption scheme
52pub struct ElGamal;
53impl ElGamal {
54    /// Generates an ElGamal keypair.
55    ///
56    /// This function is randomized. It internally samples a scalar element using `OsRng`.
57    #[cfg(not(target_os = "solana"))]
58    #[allow(non_snake_case)]
59    fn keygen() -> ElGamalKeypair {
60        // secret scalar should be non-zero except with negligible probability
61        let mut s = Scalar::random(&mut OsRng);
62        let keypair = Self::keygen_with_scalar(&s);
63
64        s.zeroize();
65        keypair
66    }
67
68    /// Generates an ElGamal keypair from a scalar input that determines the ElGamal private key.
69    ///
70    /// This function panics if the input scalar is zero, which is not a valid key.
71    #[cfg(not(target_os = "solana"))]
72    #[allow(non_snake_case)]
73    fn keygen_with_scalar(s: &Scalar) -> ElGamalKeypair {
74        let secret = ElGamalSecretKey(*s);
75        let public = ElGamalPubkey::new(&secret);
76
77        ElGamalKeypair { public, secret }
78    }
79
80    /// On input an ElGamal public key and an amount to be encrypted, the function returns a
81    /// corresponding ElGamal ciphertext.
82    ///
83    /// This function is randomized. It internally samples a scalar element using `OsRng`.
84    #[cfg(not(target_os = "solana"))]
85    fn encrypt<T: Into<Scalar>>(public: &ElGamalPubkey, amount: T) -> ElGamalCiphertext {
86        let (commitment, opening) = Pedersen::new(amount);
87        let handle = public.decrypt_handle(&opening);
88
89        ElGamalCiphertext { commitment, handle }
90    }
91
92    /// On input a public key, amount, and Pedersen opening, the function returns the corresponding
93    /// ElGamal ciphertext.
94    #[cfg(not(target_os = "solana"))]
95    fn encrypt_with<T: Into<Scalar>>(
96        amount: T,
97        public: &ElGamalPubkey,
98        opening: &PedersenOpening,
99    ) -> ElGamalCiphertext {
100        let commitment = Pedersen::with(amount, opening);
101        let handle = public.decrypt_handle(opening);
102
103        ElGamalCiphertext { commitment, handle }
104    }
105
106    /// On input an amount, the function returns a twisted ElGamal ciphertext where the associated
107    /// Pedersen opening is always zero. Since the opening is zero, any twisted ElGamal ciphertext
108    /// of this form is a valid ciphertext under any ElGamal public key.
109    #[cfg(not(target_os = "solana"))]
110    pub fn encode<T: Into<Scalar>>(amount: T) -> ElGamalCiphertext {
111        let commitment = Pedersen::encode(amount);
112        let handle = DecryptHandle(RistrettoPoint::identity());
113
114        ElGamalCiphertext { commitment, handle }
115    }
116
117    /// On input a secret key and a ciphertext, the function returns the discrete log encoding of
118    /// original amount.
119    ///
120    /// The output of this function is of type `DiscreteLog`. To recover, the originally encrypted
121    /// amount, use `DiscreteLog::decode`.
122    #[cfg(not(target_os = "solana"))]
123    fn decrypt(secret: &ElGamalSecretKey, ciphertext: &ElGamalCiphertext) -> DiscreteLog {
124        DiscreteLog::new(
125            *G,
126            &ciphertext.commitment.0 - &(&secret.0 * &ciphertext.handle.0),
127        )
128    }
129
130    /// On input a secret key and a ciphertext, the function returns the decrypted amount
131    /// interpretted as a positive 32-bit number (but still of type `u64`).
132    ///
133    /// If the originally encrypted amount is not a positive 32-bit number, then the function
134    /// returns `None`.
135    #[cfg(not(target_os = "solana"))]
136    fn decrypt_u32(secret: &ElGamalSecretKey, ciphertext: &ElGamalCiphertext) -> Option<u64> {
137        let discrete_log_instance = Self::decrypt(secret, ciphertext);
138        discrete_log_instance.decode_u32()
139    }
140}
141
142/// A (twisted) ElGamal encryption keypair.
143///
144/// The instances of the secret key are zeroized on drop.
145#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, Zeroize)]
146pub struct ElGamalKeypair {
147    /// The public half of this keypair.
148    pub public: ElGamalPubkey,
149    /// The secret half of this keypair.
150    pub secret: ElGamalSecretKey,
151}
152
153impl ElGamalKeypair {
154    /// Deterministically derives an ElGamal keypair from an Ed25519 signing key and a Solana
155    /// address.
156    ///
157    /// This function exists for applications where a user may not wish to maintin a Solana
158    /// (Ed25519) keypair and an ElGamal keypair separately. A user may wish to solely maintain the
159    /// Solana keypair and then derive the ElGamal keypair on-the-fly whenever
160    /// encryption/decryption is needed.
161    ///
162    /// For the spl token-2022 confidential extension application, the ElGamal encryption public
163    /// key is specified in a token account address. A natural way to derive an ElGamal keypair is
164    /// then to define it from the hash of a Solana keypair and a Solana address. However, for
165    /// general hardware wallets, the signing key is not exposed in the API. Therefore, this
166    /// function uses a signer to sign a pre-specified message with respect to a Solana address.
167    /// The resulting signature is then hashed to derive an ElGamal keypair.
168    #[cfg(not(target_os = "solana"))]
169    #[allow(non_snake_case)]
170    pub fn new(signer: &dyn Signer, address: &Pubkey) -> Result<Self, SignerError> {
171        let secret = ElGamalSecretKey::new(signer, address)?;
172        let public = ElGamalPubkey::new(&secret);
173        Ok(ElGamalKeypair { public, secret })
174    }
175
176    /// Generates the public and secret keys for ElGamal encryption.
177    ///
178    /// This function is randomized. It internally samples a scalar element using `OsRng`.
179    #[cfg(not(target_os = "solana"))]
180    #[allow(clippy::new_ret_no_self)]
181    pub fn new_rand() -> Self {
182        ElGamal::keygen()
183    }
184
185    pub fn to_bytes(&self) -> [u8; 64] {
186        let mut bytes = [0u8; 64];
187        bytes[..32].copy_from_slice(&self.public.to_bytes());
188        bytes[32..].copy_from_slice(self.secret.as_bytes());
189        bytes
190    }
191
192    pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
193        if bytes.len() != 64 {
194            return None;
195        }
196
197        Some(Self {
198            public: ElGamalPubkey::from_bytes(&bytes[..32])?,
199            secret: ElGamalSecretKey::from_bytes(bytes[32..].try_into().ok()?)?,
200        })
201    }
202
203    /// Reads a JSON-encoded keypair from a `Reader` implementor
204    pub fn read_json<R: Read>(reader: &mut R) -> Result<Self, Box<dyn std::error::Error>> {
205        let bytes: Vec<u8> = serde_json::from_reader(reader)?;
206        Self::from_bytes(&bytes).ok_or_else(|| {
207            std::io::Error::new(std::io::ErrorKind::Other, "Invalid ElGamalKeypair").into()
208        })
209    }
210
211    /// Reads keypair from a file
212    pub fn read_json_file<F: AsRef<Path>>(path: F) -> Result<Self, Box<dyn std::error::Error>> {
213        let mut file = File::open(path.as_ref())?;
214        Self::read_json(&mut file)
215    }
216
217    /// Writes to a `Write` implementer with JSON-encoding
218    pub fn write_json<W: Write>(
219        &self,
220        writer: &mut W,
221    ) -> Result<String, Box<dyn std::error::Error>> {
222        let bytes = self.to_bytes();
223        let json = serde_json::to_string(&bytes.to_vec())?;
224        writer.write_all(&json.clone().into_bytes())?;
225        Ok(json)
226    }
227
228    /// Write keypair to a file with JSON-encoding
229    pub fn write_json_file<F: AsRef<Path>>(
230        &self,
231        outfile: F,
232    ) -> Result<String, Box<dyn std::error::Error>> {
233        let outfile = outfile.as_ref();
234
235        if let Some(outdir) = outfile.parent() {
236            fs::create_dir_all(outdir)?;
237        }
238
239        let mut f = {
240            #[cfg(not(unix))]
241            {
242                OpenOptions::new()
243            }
244            #[cfg(unix)]
245            {
246                use std::os::unix::fs::OpenOptionsExt;
247                OpenOptions::new().mode(0o600)
248            }
249        }
250        .write(true)
251        .truncate(true)
252        .create(true)
253        .open(outfile)?;
254
255        self.write_json(&mut f)
256    }
257}
258
259/// Public key for the ElGamal encryption scheme.
260#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, Zeroize)]
261pub struct ElGamalPubkey(RistrettoPoint);
262impl ElGamalPubkey {
263    /// Derives the `ElGamalPubkey` that uniquely corresponds to an `ElGamalSecretKey`.
264    #[allow(non_snake_case)]
265    pub fn new(secret: &ElGamalSecretKey) -> Self {
266        let s = &secret.0;
267        assert!(s != &Scalar::zero());
268
269        ElGamalPubkey(s.invert() * &(*H))
270    }
271
272    pub fn get_point(&self) -> &RistrettoPoint {
273        &self.0
274    }
275
276    pub fn to_bytes(&self) -> [u8; 32] {
277        self.0.compress().to_bytes()
278    }
279
280    pub fn from_bytes(bytes: &[u8]) -> Option<ElGamalPubkey> {
281        if bytes.len() != 32 {
282            return None;
283        }
284
285        Some(ElGamalPubkey(
286            CompressedRistretto::from_slice(bytes).decompress()?,
287        ))
288    }
289
290    /// Encrypts an amount under the public key.
291    ///
292    /// This function is randomized. It internally samples a scalar element using `OsRng`.
293    #[cfg(not(target_os = "solana"))]
294    pub fn encrypt<T: Into<Scalar>>(&self, amount: T) -> ElGamalCiphertext {
295        ElGamal::encrypt(self, amount)
296    }
297
298    /// Encrypts an amount under the public key and an input Pedersen opening.
299    pub fn encrypt_with<T: Into<Scalar>>(
300        &self,
301        amount: T,
302        opening: &PedersenOpening,
303    ) -> ElGamalCiphertext {
304        ElGamal::encrypt_with(amount, self, opening)
305    }
306
307    /// Generates a decryption handle for an ElGamal public key under a Pedersen
308    /// opening.
309    pub fn decrypt_handle(self, opening: &PedersenOpening) -> DecryptHandle {
310        DecryptHandle::new(&self, opening)
311    }
312}
313
314impl fmt::Display for ElGamalPubkey {
315    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
316        write!(f, "{}", base64::encode(self.to_bytes()))
317    }
318}
319
320/// Secret key for the ElGamal encryption scheme.
321///
322/// Instances of ElGamal secret key are zeroized on drop.
323#[derive(Clone, Debug, Deserialize, Serialize, Zeroize)]
324#[zeroize(drop)]
325pub struct ElGamalSecretKey(Scalar);
326impl ElGamalSecretKey {
327    /// Deterministically derives an ElGamal keypair from an Ed25519 signing key and a Solana
328    /// address.
329    ///
330    /// See `ElGamalKeypair::new` for more context on the key derivation.
331    pub fn new(signer: &dyn Signer, address: &Pubkey) -> Result<Self, SignerError> {
332        let message = Message::new(
333            &[Instruction::new_with_bytes(
334                *address,
335                b"ElGamalSecretKey",
336                vec![],
337            )],
338            Some(&signer.try_pubkey()?),
339        );
340        let signature = signer.try_sign_message(&message.serialize())?;
341
342        // Some `Signer` implementations return the default signature, which is not suitable for
343        // use as key material
344        if bool::from(signature.as_ref().ct_eq(Signature::default().as_ref())) {
345            return Err(SignerError::Custom("Rejecting default signatures".into()));
346        }
347
348        Ok(ElGamalSecretKey(Scalar::hash_from_bytes::<Sha3_512>(
349            signature.as_ref(),
350        )))
351    }
352
353    /// Randomly samples an ElGamal secret key.
354    ///
355    /// This function is randomized. It internally samples a scalar element using `OsRng`.
356    pub fn new_rand() -> Self {
357        ElGamalSecretKey(Scalar::random(&mut OsRng))
358    }
359
360    pub fn get_scalar(&self) -> &Scalar {
361        &self.0
362    }
363
364    /// Decrypts a ciphertext using the ElGamal secret key.
365    ///
366    /// The output of this function is of type `DiscreteLog`. To recover, the originally encrypted
367    /// message, use `DiscreteLog::decode`.
368    pub fn decrypt(&self, ciphertext: &ElGamalCiphertext) -> DiscreteLog {
369        ElGamal::decrypt(self, ciphertext)
370    }
371
372    /// Decrypts a ciphertext using the ElGamal secret key interpretting the message as type `u32`.
373    pub fn decrypt_u32(&self, ciphertext: &ElGamalCiphertext) -> Option<u64> {
374        ElGamal::decrypt_u32(self, ciphertext)
375    }
376
377    pub fn as_bytes(&self) -> &[u8; 32] {
378        self.0.as_bytes()
379    }
380
381    pub fn to_bytes(&self) -> [u8; 32] {
382        self.0.to_bytes()
383    }
384
385    pub fn from_bytes(bytes: &[u8]) -> Option<ElGamalSecretKey> {
386        match bytes.try_into() {
387            Ok(bytes) => Scalar::from_canonical_bytes(bytes).map(ElGamalSecretKey),
388            _ => None,
389        }
390    }
391}
392
393impl From<Scalar> for ElGamalSecretKey {
394    fn from(scalar: Scalar) -> ElGamalSecretKey {
395        ElGamalSecretKey(scalar)
396    }
397}
398
399impl Eq for ElGamalSecretKey {}
400impl PartialEq for ElGamalSecretKey {
401    fn eq(&self, other: &Self) -> bool {
402        self.ct_eq(other).unwrap_u8() == 1u8
403    }
404}
405impl ConstantTimeEq for ElGamalSecretKey {
406    fn ct_eq(&self, other: &Self) -> Choice {
407        self.0.ct_eq(&other.0)
408    }
409}
410
411/// Ciphertext for the ElGamal encryption scheme.
412#[allow(non_snake_case)]
413#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
414pub struct ElGamalCiphertext {
415    pub commitment: PedersenCommitment,
416    pub handle: DecryptHandle,
417}
418impl ElGamalCiphertext {
419    pub fn add_amount<T: Into<Scalar>>(&self, amount: T) -> Self {
420        let commitment_to_add = PedersenCommitment(amount.into() * &(*G));
421        ElGamalCiphertext {
422            commitment: &self.commitment + &commitment_to_add,
423            handle: self.handle,
424        }
425    }
426
427    pub fn subtract_amount<T: Into<Scalar>>(&self, amount: T) -> Self {
428        let commitment_to_subtract = PedersenCommitment(amount.into() * &(*G));
429        ElGamalCiphertext {
430            commitment: &self.commitment - &commitment_to_subtract,
431            handle: self.handle,
432        }
433    }
434
435    pub fn to_bytes(&self) -> [u8; 64] {
436        let mut bytes = [0u8; 64];
437        bytes[..32].copy_from_slice(&self.commitment.to_bytes());
438        bytes[32..].copy_from_slice(&self.handle.to_bytes());
439        bytes
440    }
441
442    pub fn from_bytes(bytes: &[u8]) -> Option<ElGamalCiphertext> {
443        if bytes.len() != 64 {
444            return None;
445        }
446
447        Some(ElGamalCiphertext {
448            commitment: PedersenCommitment::from_bytes(&bytes[..32])?,
449            handle: DecryptHandle::from_bytes(&bytes[32..])?,
450        })
451    }
452
453    /// Decrypts the ciphertext using an ElGamal secret key.
454    ///
455    /// The output of this function is of type `DiscreteLog`. To recover, the originally encrypted
456    /// amount, use `DiscreteLog::decode`.
457    pub fn decrypt(&self, secret: &ElGamalSecretKey) -> DiscreteLog {
458        ElGamal::decrypt(secret, self)
459    }
460
461    /// Decrypts the ciphertext using an ElGamal secret key assuming that the message is a positive
462    /// 32-bit number.
463    ///
464    /// If the originally encrypted amount is not a positive 32-bit number, then the function
465    /// returns `None`.
466    pub fn decrypt_u32(&self, secret: &ElGamalSecretKey) -> Option<u64> {
467        ElGamal::decrypt_u32(secret, self)
468    }
469}
470
471impl fmt::Display for ElGamalCiphertext {
472    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
473        write!(f, "{}", base64::encode(self.to_bytes()))
474    }
475}
476
477impl<'a, 'b> Add<&'b ElGamalCiphertext> for &'a ElGamalCiphertext {
478    type Output = ElGamalCiphertext;
479
480    fn add(self, ciphertext: &'b ElGamalCiphertext) -> ElGamalCiphertext {
481        ElGamalCiphertext {
482            commitment: &self.commitment + &ciphertext.commitment,
483            handle: &self.handle + &ciphertext.handle,
484        }
485    }
486}
487
488define_add_variants!(
489    LHS = ElGamalCiphertext,
490    RHS = ElGamalCiphertext,
491    Output = ElGamalCiphertext
492);
493
494impl<'a, 'b> Sub<&'b ElGamalCiphertext> for &'a ElGamalCiphertext {
495    type Output = ElGamalCiphertext;
496
497    fn sub(self, ciphertext: &'b ElGamalCiphertext) -> ElGamalCiphertext {
498        ElGamalCiphertext {
499            commitment: &self.commitment - &ciphertext.commitment,
500            handle: &self.handle - &ciphertext.handle,
501        }
502    }
503}
504
505define_sub_variants!(
506    LHS = ElGamalCiphertext,
507    RHS = ElGamalCiphertext,
508    Output = ElGamalCiphertext
509);
510
511impl<'a, 'b> Mul<&'b Scalar> for &'a ElGamalCiphertext {
512    type Output = ElGamalCiphertext;
513
514    fn mul(self, scalar: &'b Scalar) -> ElGamalCiphertext {
515        ElGamalCiphertext {
516            commitment: &self.commitment * scalar,
517            handle: &self.handle * scalar,
518        }
519    }
520}
521
522define_mul_variants!(
523    LHS = ElGamalCiphertext,
524    RHS = Scalar,
525    Output = ElGamalCiphertext
526);
527
528impl<'a, 'b> Mul<&'b ElGamalCiphertext> for &'a Scalar {
529    type Output = ElGamalCiphertext;
530
531    fn mul(self, ciphertext: &'b ElGamalCiphertext) -> ElGamalCiphertext {
532        ElGamalCiphertext {
533            commitment: self * &ciphertext.commitment,
534            handle: self * &ciphertext.handle,
535        }
536    }
537}
538
539define_mul_variants!(
540    LHS = Scalar,
541    RHS = ElGamalCiphertext,
542    Output = ElGamalCiphertext
543);
544
545/// Decryption handle for Pedersen commitment.
546#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
547pub struct DecryptHandle(RistrettoPoint);
548impl DecryptHandle {
549    pub fn new(public: &ElGamalPubkey, opening: &PedersenOpening) -> Self {
550        Self(&public.0 * &opening.0)
551    }
552
553    pub fn get_point(&self) -> &RistrettoPoint {
554        &self.0
555    }
556
557    pub fn to_bytes(&self) -> [u8; 32] {
558        self.0.compress().to_bytes()
559    }
560
561    pub fn from_bytes(bytes: &[u8]) -> Option<DecryptHandle> {
562        if bytes.len() != 32 {
563            return None;
564        }
565
566        Some(DecryptHandle(
567            CompressedRistretto::from_slice(bytes).decompress()?,
568        ))
569    }
570}
571
572impl<'a, 'b> Add<&'b DecryptHandle> for &'a DecryptHandle {
573    type Output = DecryptHandle;
574
575    fn add(self, handle: &'b DecryptHandle) -> DecryptHandle {
576        DecryptHandle(&self.0 + &handle.0)
577    }
578}
579
580define_add_variants!(
581    LHS = DecryptHandle,
582    RHS = DecryptHandle,
583    Output = DecryptHandle
584);
585
586impl<'a, 'b> Sub<&'b DecryptHandle> for &'a DecryptHandle {
587    type Output = DecryptHandle;
588
589    fn sub(self, handle: &'b DecryptHandle) -> DecryptHandle {
590        DecryptHandle(&self.0 - &handle.0)
591    }
592}
593
594define_sub_variants!(
595    LHS = DecryptHandle,
596    RHS = DecryptHandle,
597    Output = DecryptHandle
598);
599
600impl<'a, 'b> Mul<&'b Scalar> for &'a DecryptHandle {
601    type Output = DecryptHandle;
602
603    fn mul(self, scalar: &'b Scalar) -> DecryptHandle {
604        DecryptHandle(&self.0 * scalar)
605    }
606}
607
608define_mul_variants!(LHS = DecryptHandle, RHS = Scalar, Output = DecryptHandle);
609
610impl<'a, 'b> Mul<&'b DecryptHandle> for &'a Scalar {
611    type Output = DecryptHandle;
612
613    fn mul(self, handle: &'b DecryptHandle) -> DecryptHandle {
614        DecryptHandle(self * &handle.0)
615    }
616}
617
618define_mul_variants!(LHS = Scalar, RHS = DecryptHandle, Output = DecryptHandle);
619
620#[cfg(test)]
621mod tests {
622    use {
623        super::*,
624        crate::encryption::pedersen::Pedersen,
625        solana_sdk::{signature::Keypair, signer::null_signer::NullSigner},
626    };
627
628    #[test]
629    fn test_encrypt_decrypt_correctness() {
630        let ElGamalKeypair { public, secret } = ElGamalKeypair::new_rand();
631        let amount: u32 = 57;
632        let ciphertext = ElGamal::encrypt(&public, amount);
633
634        let expected_instance = DiscreteLog::new(*G, Scalar::from(amount) * &(*G));
635
636        assert_eq!(expected_instance, ElGamal::decrypt(&secret, &ciphertext));
637        assert_eq!(57_u64, secret.decrypt_u32(&ciphertext).unwrap());
638    }
639
640    #[test]
641    fn test_encrypt_decrypt_correctness_multithreaded() {
642        let ElGamalKeypair { public, secret } = ElGamalKeypair::new_rand();
643        let amount: u32 = 57;
644        let ciphertext = ElGamal::encrypt(&public, amount);
645
646        let mut instance = ElGamal::decrypt(&secret, &ciphertext);
647        instance.num_threads(4).unwrap();
648        assert_eq!(57_u64, instance.decode_u32().unwrap());
649    }
650
651    #[test]
652    fn test_decrypt_handle() {
653        let ElGamalKeypair {
654            public: public_0,
655            secret: secret_0,
656        } = ElGamalKeypair::new_rand();
657        let ElGamalKeypair {
658            public: public_1,
659            secret: secret_1,
660        } = ElGamalKeypair::new_rand();
661
662        let amount: u32 = 77;
663        let (commitment, opening) = Pedersen::new(amount);
664
665        let handle_0 = public_0.decrypt_handle(&opening);
666        let handle_1 = public_1.decrypt_handle(&opening);
667
668        let ciphertext_0 = ElGamalCiphertext {
669            commitment,
670            handle: handle_0,
671        };
672        let ciphertext_1 = ElGamalCiphertext {
673            commitment,
674            handle: handle_1,
675        };
676
677        let expected_instance = DiscreteLog::new(*G, Scalar::from(amount) * &(*G));
678
679        assert_eq!(expected_instance, secret_0.decrypt(&ciphertext_0));
680        assert_eq!(expected_instance, secret_1.decrypt(&ciphertext_1));
681    }
682
683    #[test]
684    fn test_homomorphic_addition() {
685        let ElGamalKeypair { public, secret: _ } = ElGamalKeypair::new_rand();
686        let amount_0: u64 = 57;
687        let amount_1: u64 = 77;
688
689        // Add two ElGamal ciphertexts
690        let opening_0 = PedersenOpening::new_rand();
691        let opening_1 = PedersenOpening::new_rand();
692
693        let ciphertext_0 = ElGamal::encrypt_with(amount_0, &public, &opening_0);
694        let ciphertext_1 = ElGamal::encrypt_with(amount_1, &public, &opening_1);
695
696        let ciphertext_sum =
697            ElGamal::encrypt_with(amount_0 + amount_1, &public, &(&opening_0 + &opening_1));
698
699        assert_eq!(ciphertext_sum, ciphertext_0 + ciphertext_1);
700
701        // Add to ElGamal ciphertext
702        let opening = PedersenOpening::new_rand();
703        let ciphertext = ElGamal::encrypt_with(amount_0, &public, &opening);
704        let ciphertext_sum = ElGamal::encrypt_with(amount_0 + amount_1, &public, &opening);
705
706        assert_eq!(ciphertext_sum, ciphertext.add_amount(amount_1));
707    }
708
709    #[test]
710    fn test_homomorphic_subtraction() {
711        let ElGamalKeypair { public, secret: _ } = ElGamalKeypair::new_rand();
712        let amount_0: u64 = 77;
713        let amount_1: u64 = 55;
714
715        // Subtract two ElGamal ciphertexts
716        let opening_0 = PedersenOpening::new_rand();
717        let opening_1 = PedersenOpening::new_rand();
718
719        let ciphertext_0 = ElGamal::encrypt_with(amount_0, &public, &opening_0);
720        let ciphertext_1 = ElGamal::encrypt_with(amount_1, &public, &opening_1);
721
722        let ciphertext_sub =
723            ElGamal::encrypt_with(amount_0 - amount_1, &public, &(&opening_0 - &opening_1));
724
725        assert_eq!(ciphertext_sub, ciphertext_0 - ciphertext_1);
726
727        // Subtract to ElGamal ciphertext
728        let opening = PedersenOpening::new_rand();
729        let ciphertext = ElGamal::encrypt_with(amount_0, &public, &opening);
730        let ciphertext_sub = ElGamal::encrypt_with(amount_0 - amount_1, &public, &opening);
731
732        assert_eq!(ciphertext_sub, ciphertext.subtract_amount(amount_1));
733    }
734
735    #[test]
736    fn test_homomorphic_multiplication() {
737        let ElGamalKeypair { public, secret: _ } = ElGamalKeypair::new_rand();
738        let amount_0: u64 = 57;
739        let amount_1: u64 = 77;
740
741        let opening = PedersenOpening::new_rand();
742
743        let ciphertext = ElGamal::encrypt_with(amount_0, &public, &opening);
744        let scalar = Scalar::from(amount_1);
745
746        let ciphertext_prod =
747            ElGamal::encrypt_with(amount_0 * amount_1, &public, &(&opening * scalar));
748
749        assert_eq!(ciphertext_prod, ciphertext * scalar);
750        assert_eq!(ciphertext_prod, scalar * ciphertext);
751    }
752
753    #[test]
754    fn test_serde_ciphertext() {
755        let ElGamalKeypair { public, secret: _ } = ElGamalKeypair::new_rand();
756        let amount: u64 = 77;
757        let ciphertext = public.encrypt(amount);
758
759        let encoded = bincode::serialize(&ciphertext).unwrap();
760        let decoded: ElGamalCiphertext = bincode::deserialize(&encoded).unwrap();
761
762        assert_eq!(ciphertext, decoded);
763    }
764
765    #[test]
766    fn test_serde_pubkey() {
767        let ElGamalKeypair { public, secret: _ } = ElGamalKeypair::new_rand();
768
769        let encoded = bincode::serialize(&public).unwrap();
770        let decoded: ElGamalPubkey = bincode::deserialize(&encoded).unwrap();
771
772        assert_eq!(public, decoded);
773    }
774
775    #[test]
776    fn test_serde_secretkey() {
777        let ElGamalKeypair { public: _, secret } = ElGamalKeypair::new_rand();
778
779        let encoded = bincode::serialize(&secret).unwrap();
780        let decoded: ElGamalSecretKey = bincode::deserialize(&encoded).unwrap();
781
782        assert_eq!(secret, decoded);
783    }
784
785    fn tmp_file_path(name: &str) -> String {
786        use std::env;
787        let out_dir = env::var("FARF_DIR").unwrap_or_else(|_| "farf".to_string());
788        let keypair = ElGamalKeypair::new_rand();
789        format!("{}/tmp/{}-{}", out_dir, name, keypair.public)
790    }
791
792    #[test]
793    fn test_write_keypair_file() {
794        let outfile = tmp_file_path("test_write_keypair_file.json");
795        let serialized_keypair = ElGamalKeypair::new_rand()
796            .write_json_file(&outfile)
797            .unwrap();
798        let keypair_vec: Vec<u8> = serde_json::from_str(&serialized_keypair).unwrap();
799        assert!(Path::new(&outfile).exists());
800        assert_eq!(
801            keypair_vec,
802            ElGamalKeypair::read_json_file(&outfile)
803                .unwrap()
804                .to_bytes()
805                .to_vec()
806        );
807
808        #[cfg(unix)]
809        {
810            use std::os::unix::fs::PermissionsExt;
811            assert_eq!(
812                File::open(&outfile)
813                    .expect("open")
814                    .metadata()
815                    .expect("metadata")
816                    .permissions()
817                    .mode()
818                    & 0o777,
819                0o600
820            );
821        }
822        fs::remove_file(&outfile).unwrap();
823    }
824
825    #[test]
826    fn test_write_keypair_file_overwrite_ok() {
827        let outfile = tmp_file_path("test_write_keypair_file_overwrite_ok.json");
828
829        ElGamalKeypair::new_rand()
830            .write_json_file(&outfile)
831            .unwrap();
832        ElGamalKeypair::new_rand()
833            .write_json_file(&outfile)
834            .unwrap();
835    }
836
837    #[test]
838    fn test_write_keypair_file_truncate() {
839        let outfile = tmp_file_path("test_write_keypair_file_truncate.json");
840
841        ElGamalKeypair::new_rand()
842            .write_json_file(&outfile)
843            .unwrap();
844        ElGamalKeypair::read_json_file(&outfile).unwrap();
845
846        // Ensure outfile is truncated
847        {
848            let mut f = File::create(&outfile).unwrap();
849            f.write_all(String::from_utf8([b'a'; 2048].to_vec()).unwrap().as_bytes())
850                .unwrap();
851        }
852        ElGamalKeypair::new_rand()
853            .write_json_file(&outfile)
854            .unwrap();
855        ElGamalKeypair::read_json_file(&outfile).unwrap();
856    }
857
858    #[test]
859    fn test_secret_key_new() {
860        let keypair1 = Keypair::new();
861        let keypair2 = Keypair::new();
862
863        assert_ne!(
864            ElGamalSecretKey::new(&keypair1, &Pubkey::default())
865                .unwrap()
866                .0,
867            ElGamalSecretKey::new(&keypair2, &Pubkey::default())
868                .unwrap()
869                .0,
870        );
871
872        let null_signer = NullSigner::new(&Pubkey::default());
873        assert!(ElGamalSecretKey::new(&null_signer, &Pubkey::default()).is_err());
874    }
875
876    #[test]
877    fn test_decrypt_handle_bytes() {
878        let handle = DecryptHandle(RistrettoPoint::default());
879
880        let encoded = handle.to_bytes();
881        let decoded = DecryptHandle::from_bytes(&encoded).unwrap();
882
883        assert_eq!(handle, decoded);
884    }
885
886    #[test]
887    fn test_serde_decrypt_handle() {
888        let handle = DecryptHandle(RistrettoPoint::default());
889
890        let encoded = bincode::serialize(&handle).unwrap();
891        let decoded: DecryptHandle = bincode::deserialize(&encoded).unwrap();
892
893        assert_eq!(handle, decoded);
894    }
895}