dkg_pedpop/
encryption.rs

1use core::{ops::Deref, fmt};
2use std::{io, collections::HashMap};
3
4use thiserror::Error;
5
6use zeroize::{Zeroize, Zeroizing};
7use rand_core::{RngCore, CryptoRng};
8
9use chacha20::{
10  cipher::{crypto_common::KeyIvInit, StreamCipher},
11  Key as Cc20Key, Nonce as Cc20Iv, ChaCha20,
12};
13
14use transcript::{Transcript, RecommendedTranscript};
15
16#[cfg(test)]
17use ciphersuite::group::ff::Field;
18use ciphersuite::{group::GroupEncoding, Ciphersuite};
19use multiexp::BatchVerifier;
20
21use schnorr::SchnorrSignature;
22use dleq::DLEqProof;
23
24use dkg::{Participant, ThresholdParams};
25
26mod sealed {
27  use super::*;
28
29  pub trait ReadWrite: Sized {
30    fn read<R: io::Read>(reader: &mut R, params: ThresholdParams) -> io::Result<Self>;
31    fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()>;
32
33    fn serialize(&self) -> Vec<u8> {
34      let mut buf = vec![];
35      self.write(&mut buf).unwrap();
36      buf
37    }
38  }
39
40  pub trait Message: Clone + PartialEq + Eq + fmt::Debug + Zeroize + ReadWrite {}
41  impl<M: Clone + PartialEq + Eq + fmt::Debug + Zeroize + ReadWrite> Message for M {}
42
43  pub trait Encryptable: Clone + AsRef<[u8]> + AsMut<[u8]> + Zeroize + ReadWrite {}
44  impl<E: Clone + AsRef<[u8]> + AsMut<[u8]> + Zeroize + ReadWrite> Encryptable for E {}
45}
46pub(crate) use sealed::*;
47
48/// Wraps a message with a key to use for encryption in the future.
49#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
50pub struct EncryptionKeyMessage<C: Ciphersuite, M: Message> {
51  msg: M,
52  enc_key: C::G,
53}
54
55// Doesn't impl ReadWrite so that doesn't need to be imported
56impl<C: Ciphersuite, M: Message> EncryptionKeyMessage<C, M> {
57  pub fn read<R: io::Read>(reader: &mut R, params: ThresholdParams) -> io::Result<Self> {
58    Ok(Self { msg: M::read(reader, params)?, enc_key: C::read_G(reader)? })
59  }
60
61  pub fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
62    self.msg.write(writer)?;
63    writer.write_all(self.enc_key.to_bytes().as_ref())
64  }
65
66  pub fn serialize(&self) -> Vec<u8> {
67    let mut buf = vec![];
68    self.write(&mut buf).unwrap();
69    buf
70  }
71
72  #[cfg(test)]
73  pub(crate) fn enc_key(&self) -> C::G {
74    self.enc_key
75  }
76}
77
78/// An encrypted message, with a per-message encryption key enabling revealing specific messages
79/// without side effects.
80#[derive(Clone, Zeroize)]
81pub struct EncryptedMessage<C: Ciphersuite, E: Encryptable> {
82  key: C::G,
83  // Also include a proof-of-possession for the key.
84  // If this proof-of-possession wasn't here, Eve could observe Alice encrypt to Bob with key X,
85  // then send Bob a message also claiming to use X.
86  // While Eve's message would fail to meaningfully decrypt, Bob would then use this to create a
87  // blame argument against Eve. When they do, they'd reveal bX, revealing Alice's message to Bob.
88  // This is a massive side effect which could break some protocols, in the worst case.
89  // While Eve can still reuse their own keys, causing Bob to leak all messages by revealing for
90  // any single one, that's effectively Eve revealing themselves, and not considered relevant.
91  pop: SchnorrSignature<C>,
92  msg: Zeroizing<E>,
93}
94
95fn ecdh<C: Ciphersuite>(private: &Zeroizing<C::F>, public: C::G) -> Zeroizing<C::G> {
96  Zeroizing::new(public * private.deref())
97}
98
99// Each ecdh must be distinct. Reuse of an ecdh for multiple ciphers will cause the messages to be
100// leaked.
101fn cipher<C: Ciphersuite>(context: [u8; 32], ecdh: &Zeroizing<C::G>) -> ChaCha20 {
102  // Ideally, we'd box this transcript with ZAlloc, yet that's only possible on nightly
103  // TODO: https://github.com/serai-dex/serai/issues/151
104  let mut transcript = RecommendedTranscript::new(b"DKG Encryption v0.2");
105  transcript.append_message(b"context", context);
106
107  transcript.domain_separate(b"encryption_key");
108
109  let mut ecdh = ecdh.to_bytes();
110  transcript.append_message(b"shared_key", ecdh.as_ref());
111  ecdh.as_mut().zeroize();
112
113  let zeroize = |buf: &mut [u8]| buf.zeroize();
114
115  let mut key = Cc20Key::default();
116  let mut challenge = transcript.challenge(b"key");
117  key.copy_from_slice(&challenge[.. 32]);
118  zeroize(challenge.as_mut());
119
120  // Since the key is single-use, it doesn't matter what we use for the IV
121  // The issue is key + IV reuse. If we never reuse the key, we can't have the opportunity to
122  // reuse a nonce
123  // Use a static IV in acknowledgement of this
124  let mut iv = Cc20Iv::default();
125  // The \0 is to satisfy the length requirement (12), not to be null terminated
126  iv.copy_from_slice(b"DKG IV v0.2\0");
127
128  // ChaCha20 has the same commentary as the transcript regarding ZAlloc
129  // TODO: https://github.com/serai-dex/serai/issues/151
130  let res = ChaCha20::new(&key, &iv);
131  zeroize(key.as_mut());
132  res
133}
134
135fn encrypt<R: RngCore + CryptoRng, C: Ciphersuite, E: Encryptable>(
136  rng: &mut R,
137  context: [u8; 32],
138  from: Participant,
139  to: C::G,
140  mut msg: Zeroizing<E>,
141) -> EncryptedMessage<C, E> {
142  /*
143  The following code could be used to replace the requirement on an RNG here.
144  It's just currently not an issue to require taking in an RNG here.
145  let last = self.last_enc_key.to_bytes();
146  self.last_enc_key = C::hash_to_F(b"encryption_base", last.as_ref());
147  let key = C::hash_to_F(b"encryption_key", last.as_ref());
148  last.as_mut().zeroize();
149  */
150
151  // Generate a new key for this message, satisfying cipher's requirement of distinct keys per
152  // message, and enabling revealing this message without revealing any others
153  let key = Zeroizing::new(C::random_nonzero_F(rng));
154  cipher::<C>(context, &ecdh::<C>(&key, to)).apply_keystream(msg.as_mut().as_mut());
155
156  let pub_key = C::generator() * key.deref();
157  let nonce = Zeroizing::new(C::random_nonzero_F(rng));
158  let pub_nonce = C::generator() * nonce.deref();
159  EncryptedMessage {
160    key: pub_key,
161    pop: SchnorrSignature::sign(
162      &key,
163      nonce,
164      pop_challenge::<C>(context, pub_nonce, pub_key, from, msg.deref().as_ref()),
165    ),
166    msg,
167  }
168}
169
170impl<C: Ciphersuite, E: Encryptable> EncryptedMessage<C, E> {
171  pub fn read<R: io::Read>(reader: &mut R, params: ThresholdParams) -> io::Result<Self> {
172    Ok(Self {
173      key: C::read_G(reader)?,
174      pop: SchnorrSignature::<C>::read(reader)?,
175      msg: Zeroizing::new(E::read(reader, params)?),
176    })
177  }
178
179  pub fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
180    writer.write_all(self.key.to_bytes().as_ref())?;
181    self.pop.write(writer)?;
182    self.msg.write(writer)
183  }
184
185  pub fn serialize(&self) -> Vec<u8> {
186    let mut buf = vec![];
187    self.write(&mut buf).unwrap();
188    buf
189  }
190
191  #[cfg(test)]
192  pub(crate) fn invalidate_pop(&mut self) {
193    self.pop.s += C::F::ONE;
194  }
195
196  #[cfg(test)]
197  pub(crate) fn invalidate_msg<R: RngCore + CryptoRng>(
198    &mut self,
199    rng: &mut R,
200    context: [u8; 32],
201    from: Participant,
202  ) {
203    // Invalidate the message by specifying a new key/Schnorr PoP
204    // This will cause all initial checks to pass, yet a decrypt to gibberish
205    let key = Zeroizing::new(C::random_nonzero_F(rng));
206    let pub_key = C::generator() * key.deref();
207    let nonce = Zeroizing::new(C::random_nonzero_F(rng));
208    let pub_nonce = C::generator() * nonce.deref();
209    self.key = pub_key;
210    self.pop = SchnorrSignature::sign(
211      &key,
212      nonce,
213      pop_challenge::<C>(context, pub_nonce, pub_key, from, self.msg.deref().as_ref()),
214    );
215  }
216
217  // Assumes the encrypted message is a secret share.
218  #[cfg(test)]
219  pub(crate) fn invalidate_share_serialization<R: RngCore + CryptoRng>(
220    &mut self,
221    rng: &mut R,
222    context: [u8; 32],
223    from: Participant,
224    to: C::G,
225  ) {
226    use ciphersuite::group::ff::PrimeField;
227
228    let mut repr = <C::F as PrimeField>::Repr::default();
229    for b in repr.as_mut() {
230      *b = 255;
231    }
232    // Tries to guarantee the above assumption.
233    assert_eq!(repr.as_ref().len(), self.msg.as_ref().len());
234    // Checks that this isn't over a field where this is somehow valid
235    assert!(!bool::from(C::F::from_repr(repr).is_some()));
236
237    self.msg.as_mut().as_mut().copy_from_slice(repr.as_ref());
238    *self = encrypt(rng, context, from, to, self.msg.clone());
239  }
240
241  // Assumes the encrypted message is a secret share.
242  #[cfg(test)]
243  pub(crate) fn invalidate_share_value<R: RngCore + CryptoRng>(
244    &mut self,
245    rng: &mut R,
246    context: [u8; 32],
247    from: Participant,
248    to: C::G,
249  ) {
250    use ciphersuite::group::ff::PrimeField;
251
252    // Assumes the share isn't randomly 1
253    let repr = C::F::ONE.to_repr();
254    self.msg.as_mut().as_mut().copy_from_slice(repr.as_ref());
255    *self = encrypt(rng, context, from, to, self.msg.clone());
256  }
257}
258
259/// A proof that the provided encryption key is a legitimately derived shared key for some message.
260#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
261pub struct EncryptionKeyProof<C: Ciphersuite> {
262  key: Zeroizing<C::G>,
263  dleq: DLEqProof<C::G>,
264}
265
266impl<C: Ciphersuite> EncryptionKeyProof<C> {
267  pub fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
268    Ok(Self { key: Zeroizing::new(C::read_G(reader)?), dleq: DLEqProof::read(reader)? })
269  }
270
271  pub fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
272    writer.write_all(self.key.to_bytes().as_ref())?;
273    self.dleq.write(writer)
274  }
275
276  pub fn serialize(&self) -> Vec<u8> {
277    let mut buf = vec![];
278    self.write(&mut buf).unwrap();
279    buf
280  }
281
282  #[cfg(test)]
283  pub(crate) fn invalidate_key(&mut self) {
284    *self.key += C::generator();
285  }
286
287  #[cfg(test)]
288  pub(crate) fn invalidate_dleq(&mut self) {
289    let mut buf = vec![];
290    self.dleq.write(&mut buf).unwrap();
291    // Adds one to c since this is serialized c, s
292    // Adding one to c will leave a validly serialized c
293    // Adding one to s may leave an invalidly serialized s
294    buf[0] = buf[0].wrapping_add(1);
295    self.dleq = DLEqProof::read::<&[u8]>(&mut buf.as_ref()).unwrap();
296  }
297}
298
299// This doesn't need to take the msg. It just doesn't hurt as an extra layer.
300// This still doesn't mean the DKG offers an authenticated channel. The per-message keys have no
301// root of trust other than their existence in the assumed-to-exist external authenticated channel.
302fn pop_challenge<C: Ciphersuite>(
303  context: [u8; 32],
304  nonce: C::G,
305  key: C::G,
306  sender: Participant,
307  msg: &[u8],
308) -> C::F {
309  let mut transcript = RecommendedTranscript::new(b"DKG Encryption Key Proof of Possession v0.2");
310  transcript.append_message(b"context", context);
311
312  transcript.domain_separate(b"proof_of_possession");
313
314  transcript.append_message(b"nonce", nonce.to_bytes());
315  transcript.append_message(b"key", key.to_bytes());
316  // This is sufficient to prevent the attack this is meant to stop
317  transcript.append_message(b"sender", sender.to_bytes());
318  // This, as written above, doesn't hurt
319  transcript.append_message(b"message", msg);
320  // While this is a PoK and a PoP, it's called a PoP here since the important part is its owner
321  // Elsewhere, where we use the term PoK, the important part is that it isn't some inverse, with
322  // an unknown to anyone discrete log, breaking the system
323  C::hash_to_F(b"DKG-encryption-proof_of_possession", &transcript.challenge(b"schnorr"))
324}
325
326fn encryption_key_transcript(context: [u8; 32]) -> RecommendedTranscript {
327  let mut transcript = RecommendedTranscript::new(b"DKG Encryption Key Correctness Proof v0.2");
328  transcript.append_message(b"context", context);
329  transcript
330}
331
332#[derive(Clone, Copy, PartialEq, Eq, Debug, Error)]
333pub(crate) enum DecryptionError {
334  #[error("accused provided an invalid signature")]
335  InvalidSignature,
336  #[error("accuser provided an invalid decryption key")]
337  InvalidProof,
338}
339
340// A simple box for managing decryption.
341#[derive(Clone, Debug)]
342pub(crate) struct Decryption<C: Ciphersuite> {
343  context: [u8; 32],
344  enc_keys: HashMap<Participant, C::G>,
345}
346
347impl<C: Ciphersuite> Decryption<C> {
348  pub(crate) fn new(context: [u8; 32]) -> Self {
349    Self { context, enc_keys: HashMap::new() }
350  }
351  pub(crate) fn register<M: Message>(
352    &mut self,
353    participant: Participant,
354    msg: EncryptionKeyMessage<C, M>,
355  ) -> M {
356    assert!(
357      !self.enc_keys.contains_key(&participant),
358      "Re-registering encryption key for a participant"
359    );
360    self.enc_keys.insert(participant, msg.enc_key);
361    msg.msg
362  }
363
364  // Given a message, and the intended decryptor, and a proof for its key, decrypt the message.
365  // Returns None if the key was wrong.
366  pub(crate) fn decrypt_with_proof<E: Encryptable>(
367    &self,
368    from: Participant,
369    decryptor: Participant,
370    mut msg: EncryptedMessage<C, E>,
371    // There's no encryption key proof if the accusation is of an invalid signature
372    proof: Option<EncryptionKeyProof<C>>,
373  ) -> Result<Zeroizing<E>, DecryptionError> {
374    if !msg.pop.verify(
375      msg.key,
376      pop_challenge::<C>(self.context, msg.pop.R, msg.key, from, msg.msg.deref().as_ref()),
377    ) {
378      Err(DecryptionError::InvalidSignature)?;
379    }
380
381    if let Some(proof) = proof {
382      // Verify this is the decryption key for this message
383      proof
384        .dleq
385        .verify(
386          &mut encryption_key_transcript(self.context),
387          &[C::generator(), msg.key],
388          &[self.enc_keys[&decryptor], *proof.key],
389        )
390        .map_err(|_| DecryptionError::InvalidProof)?;
391
392      cipher::<C>(self.context, &proof.key).apply_keystream(msg.msg.as_mut().as_mut());
393      Ok(msg.msg)
394    } else {
395      Err(DecryptionError::InvalidProof)
396    }
397  }
398}
399
400// A simple box for managing encryption.
401#[derive(Clone)]
402pub(crate) struct Encryption<C: Ciphersuite> {
403  context: [u8; 32],
404  i: Participant,
405  enc_key: Zeroizing<C::F>,
406  enc_pub_key: C::G,
407  decryption: Decryption<C>,
408}
409
410impl<C: Ciphersuite> fmt::Debug for Encryption<C> {
411  fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
412    fmt
413      .debug_struct("Encryption")
414      .field("context", &self.context)
415      .field("i", &self.i)
416      .field("enc_pub_key", &self.enc_pub_key)
417      .field("decryption", &self.decryption)
418      .finish_non_exhaustive()
419  }
420}
421
422impl<C: Ciphersuite> Zeroize for Encryption<C> {
423  fn zeroize(&mut self) {
424    self.enc_key.zeroize();
425    self.enc_pub_key.zeroize();
426    for (_, mut value) in self.decryption.enc_keys.drain() {
427      value.zeroize();
428    }
429  }
430}
431
432impl<C: Ciphersuite> Encryption<C> {
433  pub(crate) fn new<R: RngCore + CryptoRng>(
434    context: [u8; 32],
435    i: Participant,
436    rng: &mut R,
437  ) -> Self {
438    let enc_key = Zeroizing::new(C::random_nonzero_F(rng));
439    Self {
440      context,
441      i,
442      enc_pub_key: C::generator() * enc_key.deref(),
443      enc_key,
444      decryption: Decryption::new(context),
445    }
446  }
447
448  pub(crate) fn registration<M: Message>(&self, msg: M) -> EncryptionKeyMessage<C, M> {
449    EncryptionKeyMessage { msg, enc_key: self.enc_pub_key }
450  }
451
452  pub(crate) fn register<M: Message>(
453    &mut self,
454    participant: Participant,
455    msg: EncryptionKeyMessage<C, M>,
456  ) -> M {
457    self.decryption.register(participant, msg)
458  }
459
460  pub(crate) fn encrypt<R: RngCore + CryptoRng, E: Encryptable>(
461    &self,
462    rng: &mut R,
463    participant: Participant,
464    msg: Zeroizing<E>,
465  ) -> EncryptedMessage<C, E> {
466    encrypt(rng, self.context, self.i, self.decryption.enc_keys[&participant], msg)
467  }
468
469  pub(crate) fn decrypt<R: RngCore + CryptoRng, I: Copy + Zeroize, E: Encryptable>(
470    &self,
471    rng: &mut R,
472    batch: &mut BatchVerifier<I, C::G>,
473    // Uses a distinct batch ID so if this batch verifier is reused, we know its the PoP aspect
474    // which failed, and therefore to use None for the blame
475    batch_id: I,
476    from: Participant,
477    mut msg: EncryptedMessage<C, E>,
478  ) -> (Zeroizing<E>, EncryptionKeyProof<C>) {
479    msg.pop.batch_verify(
480      rng,
481      batch,
482      batch_id,
483      msg.key,
484      pop_challenge::<C>(self.context, msg.pop.R, msg.key, from, msg.msg.deref().as_ref()),
485    );
486
487    let key = ecdh::<C>(&self.enc_key, msg.key);
488    cipher::<C>(self.context, &key).apply_keystream(msg.msg.as_mut().as_mut());
489    (
490      msg.msg,
491      EncryptionKeyProof {
492        key,
493        dleq: DLEqProof::prove(
494          rng,
495          &mut encryption_key_transcript(self.context),
496          &[C::generator(), msg.key],
497          &self.enc_key,
498        ),
499      },
500    )
501  }
502
503  pub(crate) fn into_decryption(self) -> Decryption<C> {
504    self.decryption
505  }
506}