Skip to main content

group_threshold_cryptography_pre_release/
ciphertext.rs

1use std::ops::Mul;
2
3use ark_ec::{pairing::Pairing, AffineRepr};
4use ark_ff::{One, UniformRand};
5use ark_serialize::CanonicalSerialize;
6use chacha20poly1305::{
7    aead::{generic_array::GenericArray, Aead, KeyInit, Payload},
8    ChaCha20Poly1305,
9};
10use ferveo_common::serialization;
11use serde::{Deserialize, Serialize};
12use serde_with::serde_as;
13use sha2::{digest::Digest, Sha256};
14use zeroize::ZeroizeOnDrop;
15
16use crate::{htp_bls12381_g2, Error, Result, SecretBox, SharedSecret};
17
18#[serde_as]
19#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
20pub struct Ciphertext<E: Pairing> {
21    // U
22    #[serde_as(as = "serialization::SerdeAs")]
23    pub commitment: E::G1Affine,
24
25    // W
26    #[serde_as(as = "serialization::SerdeAs")]
27    pub auth_tag: E::G2Affine,
28
29    // V
30    #[serde(with = "serde_bytes")]
31    pub ciphertext: Vec<u8>,
32}
33
34impl<E: Pairing> Ciphertext<E> {
35    pub fn check(&self, aad: &[u8], g_inv: &E::G1Prepared) -> Result<bool> {
36        self.header()?.check(aad, g_inv)
37    }
38
39    pub fn ciphertext_hash(&self) -> [u8; 32] {
40        sha256(&self.ciphertext)
41    }
42
43    pub fn header(&self) -> Result<CiphertextHeader<E>> {
44        Ok(CiphertextHeader {
45            commitment: self.commitment,
46            auth_tag: self.auth_tag,
47            ciphertext_hash: self.ciphertext_hash(),
48        })
49    }
50    pub fn payload(&self) -> Vec<u8> {
51        self.ciphertext.clone()
52    }
53}
54
55#[serde_as]
56#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
57pub struct CiphertextHeader<E: Pairing> {
58    #[serde_as(as = "serialization::SerdeAs")]
59    pub commitment: E::G1Affine,
60    #[serde_as(as = "serialization::SerdeAs")]
61    pub auth_tag: E::G2Affine,
62    pub ciphertext_hash: [u8; 32],
63}
64
65impl<E: Pairing> CiphertextHeader<E> {
66    pub fn check(&self, aad: &[u8], g_inv: &E::G1Prepared) -> Result<bool> {
67        // Implements a variant of the check in section 4.4.2 of the Ferveo paper:
68        // 'TPKE.CheckCiphertextValidity(U,W,aad)'
69        // See: https://eprint.iacr.org/2022/898.pdf
70        // See: https://nikkolasg.github.io/ferveo/tpke.html#to-validate-ciphertext-for-ind-cca2-security
71
72        // H_G2(U, sym_ctxt_digest, aad)
73        let hash_g2 = E::G2Prepared::from(construct_tag_hash::<E>(
74            self.commitment,
75            &self.ciphertext_hash,
76            aad,
77        )?);
78
79        let is_ciphertext_valid = E::multi_pairing(
80            // e(U, H_G2(U, sym_ctxt_digest, aad)) == e(G, W) ==>
81            // e(U, H_G2(U, sym_ctxt_digest, aad)) * e(G_inv, W) == 1
82            [self.commitment.into(), g_inv.to_owned()],
83            [hash_g2, self.auth_tag.into()],
84        )
85        .0 == E::TargetField::one();
86
87        if is_ciphertext_valid {
88            Ok(true)
89        } else {
90            Err(Error::CiphertextVerificationFailed)
91        }
92    }
93}
94
95pub fn encrypt<E: Pairing>(
96    message: SecretBox<Vec<u8>>,
97    aad: &[u8],
98    pubkey: &E::G1Affine,
99    rng: &mut impl rand::Rng,
100) -> Result<Ciphertext<E>> {
101    // r
102    let rand_element = E::ScalarField::rand(rng);
103    // g
104    let g_gen = E::G1Affine::generator();
105    // h
106    let h_gen = E::G2Affine::generator();
107
108    let ry_prep = E::G1Prepared::from(pubkey.mul(rand_element).into());
109    // s
110    let product = E::pairing(ry_prep, h_gen).0;
111    // u
112    let commitment = g_gen.mul(rand_element).into();
113
114    let nonce = Nonce::from_commitment::<E>(commitment)?;
115    let shared_secret = SharedSecret::<E>(product);
116
117    let payload = Payload {
118        msg: message.as_secret().as_ref(),
119        aad,
120    };
121    let ciphertext = shared_secret_to_chacha(&shared_secret)?
122        .encrypt(&nonce.0, payload)
123        .map_err(Error::SymmetricEncryptionError)?
124        .to_vec();
125    let ciphertext_hash = sha256(&ciphertext);
126
127    // w
128    let auth_tag = construct_tag_hash::<E>(commitment, &ciphertext_hash, aad)?
129        .mul(rand_element)
130        .into();
131
132    // TODO: Consider adding aad to the Ciphertext struct
133    Ok(Ciphertext::<E> {
134        commitment,
135        ciphertext,
136        auth_tag,
137    })
138}
139
140pub fn decrypt_symmetric<E: Pairing>(
141    ciphertext: &Ciphertext<E>,
142    aad: &[u8],
143    private_key: &E::G2Affine,
144    g_inv: &E::G1Prepared,
145) -> Result<Vec<u8>> {
146    ciphertext.check(aad, g_inv)?;
147    let shared_secret = E::pairing(
148        E::G1Prepared::from(ciphertext.commitment),
149        E::G2Prepared::from(*private_key),
150    )
151    .0;
152    let shared_secret = SharedSecret(shared_secret);
153    decrypt_with_shared_secret_unchecked(ciphertext, aad, &shared_secret)
154}
155
156fn decrypt_with_shared_secret_unchecked<E: Pairing>(
157    ciphertext: &Ciphertext<E>,
158    aad: &[u8],
159    shared_secret: &SharedSecret<E>,
160) -> Result<Vec<u8>> {
161    let nonce = Nonce::from_commitment::<E>(ciphertext.commitment)?;
162    let ctxt = ciphertext.ciphertext.to_vec();
163    let payload = Payload {
164        msg: ctxt.as_ref(),
165        aad,
166    };
167    let plaintext = shared_secret_to_chacha(shared_secret)?
168        .decrypt(&nonce.0, payload)
169        .map_err(|_| Error::CiphertextVerificationFailed)?
170        .to_vec();
171
172    Ok(plaintext)
173}
174
175pub fn decrypt_with_shared_secret<E: Pairing>(
176    ciphertext: &Ciphertext<E>,
177    aad: &[u8],
178    shared_secret: &SharedSecret<E>,
179    g_inv: &E::G1Prepared,
180) -> Result<Vec<u8>> {
181    ciphertext.check(aad, g_inv)?;
182    decrypt_with_shared_secret_unchecked(ciphertext, aad, shared_secret)
183}
184
185fn sha256(input: &[u8]) -> [u8; 32] {
186    let mut hasher = Sha256::new();
187    hasher.update(input);
188    let result = hasher.finalize();
189    result.into()
190}
191
192pub fn shared_secret_to_chacha<E: Pairing>(
193    shared_secret: &SharedSecret<E>,
194) -> Result<ChaCha20Poly1305> {
195    let mut prf_key = SecretBox::new(Vec::new());
196    shared_secret
197        .0
198        .serialize_compressed(prf_key.as_mut_secret())?;
199    Ok(ChaCha20Poly1305::new(GenericArray::from_slice(&sha256(
200        prf_key.as_secret(),
201    ))))
202}
203
204/// Wrapper around the Nonce implementation from the `chacha20poly1305` crate.
205/// This wrapper implements `ZeroizeOnDrop` to ensure that the key is zeroed when the
206/// `Nonce` struct is dropped.
207#[derive(ZeroizeOnDrop)]
208pub struct Nonce(pub(crate) chacha20poly1305::Nonce);
209
210impl Nonce {
211    pub fn from_commitment<E: Pairing>(
212        commitment: E::G1Affine,
213    ) -> Result<Self> {
214        let mut commitment_bytes = Vec::new();
215        commitment.serialize_compressed(&mut commitment_bytes)?;
216        let commitment_hash = sha256(&commitment_bytes);
217        Ok(Nonce(*chacha20poly1305::Nonce::from_slice(
218            &commitment_hash[..12],
219        )))
220    }
221}
222
223fn hash_to_g2<T: ark_serialize::CanonicalDeserialize>(
224    message: &[u8],
225) -> Result<T> {
226    let point = htp_bls12381_g2(message);
227    let mut point_ser: Vec<u8> = Vec::new();
228    point.serialize_compressed(&mut point_ser)?;
229    T::deserialize_compressed(&point_ser[..]).map_err(Error::ArkSerializeError)
230}
231
232fn construct_tag_hash<E: Pairing>(
233    commitment: E::G1Affine,
234    ciphertext_hash: &[u8],
235    aad: &[u8],
236) -> Result<E::G2Affine> {
237    let mut hash_input = Vec::<u8>::new();
238    commitment.serialize_compressed(&mut hash_input)?;
239    hash_input.extend_from_slice(ciphertext_hash);
240    hash_input.extend_from_slice(aad);
241    hash_to_g2(&hash_input)
242}
243
244#[cfg(test)]
245mod tests {
246    use ark_std::test_rng;
247
248    use crate::{test_common::*, *};
249
250    type E = ark_bls12_381::Bls12_381;
251
252    #[test]
253    fn symmetric_encryption() {
254        let rng = &mut test_rng();
255        let shares_num = 16;
256        let threshold = shares_num * 2 / 3;
257        let msg = "my-msg".as_bytes().to_vec();
258        let aad: &[u8] = "my-aad".as_bytes();
259
260        let (pubkey, privkey, contexts) =
261            setup_fast::<E>(threshold, shares_num, rng);
262        let g_inv = &contexts[0].setup_params.g_inv;
263
264        let ciphertext =
265            encrypt::<E>(SecretBox::new(msg.clone()), aad, &pubkey, rng)
266                .unwrap();
267
268        let plaintext =
269            decrypt_symmetric(&ciphertext, aad, &privkey, g_inv).unwrap();
270
271        assert_eq!(msg, plaintext);
272
273        let bad: &[u8] = "bad-aad".as_bytes();
274
275        assert!(decrypt_symmetric(&ciphertext, bad, &privkey, g_inv).is_err());
276    }
277
278    #[test]
279    fn ciphertext_validity_check() {
280        let rng = &mut test_rng();
281        let shares_num = 16;
282        let threshold = shares_num * 2 / 3;
283        let msg = "my-msg".as_bytes().to_vec();
284        let aad: &[u8] = "my-aad".as_bytes();
285        let (pubkey, _, contexts) = setup_fast::<E>(threshold, shares_num, rng);
286        let g_inv = contexts[0].setup_params.g_inv.clone();
287        let mut ciphertext =
288            encrypt::<E>(SecretBox::new(msg), aad, &pubkey, rng).unwrap();
289
290        // So far, the ciphertext is valid
291        assert!(ciphertext.check(aad, &g_inv).is_ok());
292
293        // Malformed the ciphertext
294        ciphertext.ciphertext[0] += 1;
295        assert!(ciphertext.check(aad, &g_inv).is_err());
296
297        // Malformed the AAD
298        let aad = "bad aad".as_bytes();
299        assert!(ciphertext.check(aad, &g_inv).is_err());
300    }
301}