mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-11 13:39:25 +00:00
Clean and document the DKG library's encryption
Encryption used to be inlined into FROST. When writing the documentation, I realized it was decently hard to review. It also was antagonistic to other hosted DKG algorithms by not allowing code re-use. Encryption is now a standalone module, providing clear boundaries and reusability. Additionally, the DKG protocol itself used to use the ciphersuite's specified hash function (with an HKDF to prevent length extension attacks). Now, RecommendedTranscript is used to achieve much more robust transcripting and remove the HKDF dependency. This does add Blake2 into all consumers yet is preferred for its security properties and ease of review.
This commit is contained in:
181
crypto/dkg/src/encryption.rs
Normal file
181
crypto/dkg/src/encryption.rs
Normal file
@@ -0,0 +1,181 @@
|
||||
use core::{hash::Hash, fmt::Debug};
|
||||
use std::{
|
||||
ops::Deref,
|
||||
io::{self, Read, Write},
|
||||
collections::HashMap,
|
||||
};
|
||||
|
||||
use zeroize::{Zeroize, Zeroizing};
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use chacha20::{
|
||||
cipher::{crypto_common::KeyIvInit, StreamCipher},
|
||||
Key as Cc20Key, Nonce as Cc20Iv, ChaCha20,
|
||||
};
|
||||
|
||||
use group::GroupEncoding;
|
||||
|
||||
use ciphersuite::Ciphersuite;
|
||||
|
||||
use transcript::{Transcript, RecommendedTranscript};
|
||||
|
||||
use crate::ThresholdParams;
|
||||
|
||||
pub trait ReadWrite: Sized {
|
||||
fn read<R: Read>(reader: &mut R, params: ThresholdParams) -> io::Result<Self>;
|
||||
fn write<W: Write>(&self, writer: &mut W) -> io::Result<()>;
|
||||
|
||||
fn serialize(&self) -> Vec<u8> {
|
||||
let mut buf = vec![];
|
||||
self.write(&mut buf).unwrap();
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Message: Clone + PartialEq + Eq + Debug + Zeroize + ReadWrite {}
|
||||
impl<M: Clone + PartialEq + Eq + Debug + Zeroize + ReadWrite> Message for M {}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
||||
pub struct EncryptionKeyMessage<C: Ciphersuite, M: Message> {
|
||||
msg: M,
|
||||
enc_key: C::G,
|
||||
}
|
||||
|
||||
// Doesn't impl ReadWrite so that doesn't need to be imported
|
||||
impl<C: Ciphersuite, M: Message> EncryptionKeyMessage<C, M> {
|
||||
pub fn read<R: Read>(reader: &mut R, params: ThresholdParams) -> io::Result<Self> {
|
||||
Ok(Self { msg: M::read(reader, params)?, enc_key: C::read_G(reader)? })
|
||||
}
|
||||
|
||||
pub fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
|
||||
self.msg.write(writer)?;
|
||||
writer.write_all(self.enc_key.to_bytes().as_ref())
|
||||
}
|
||||
|
||||
pub fn serialize(&self) -> Vec<u8> {
|
||||
let mut buf = vec![];
|
||||
self.write(&mut buf).unwrap();
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Encryptable: Clone + AsMut<[u8]> + Zeroize + ReadWrite {}
|
||||
impl<E: Clone + AsMut<[u8]> + Zeroize + ReadWrite> Encryptable for E {}
|
||||
#[derive(Clone, Zeroize)]
|
||||
pub struct EncryptedMessage<E: Encryptable>(Zeroizing<E>);
|
||||
|
||||
impl<E: Encryptable> EncryptedMessage<E> {
|
||||
pub fn read<R: Read>(reader: &mut R, params: ThresholdParams) -> io::Result<Self> {
|
||||
Ok(Self(Zeroizing::new(E::read(reader, params)?)))
|
||||
}
|
||||
|
||||
pub fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
|
||||
self.0.write(writer)
|
||||
}
|
||||
|
||||
pub fn serialize(&self) -> Vec<u8> {
|
||||
let mut buf = vec![];
|
||||
self.write(&mut buf).unwrap();
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Encryption<Id: Eq + Hash, C: Ciphersuite> {
|
||||
dst: &'static [u8],
|
||||
enc_key: Zeroizing<C::F>,
|
||||
enc_pub_key: C::G,
|
||||
enc_keys: HashMap<Id, C::G>,
|
||||
}
|
||||
|
||||
impl<Id: Eq + Hash, C: Ciphersuite> Zeroize for Encryption<Id, C> {
|
||||
fn zeroize(&mut self) {
|
||||
self.enc_key.zeroize();
|
||||
self.enc_pub_key.zeroize();
|
||||
for (_, value) in self.enc_keys.drain() {
|
||||
value.zeroize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Id: Eq + Hash, C: Ciphersuite> Encryption<Id, C> {
|
||||
pub(crate) fn new<R: RngCore + CryptoRng>(dst: &'static [u8], rng: &mut R) -> Self {
|
||||
let enc_key = Zeroizing::new(C::random_nonzero_F(rng));
|
||||
Self { dst, enc_pub_key: C::generator() * enc_key.deref(), enc_key, enc_keys: HashMap::new() }
|
||||
}
|
||||
|
||||
pub(crate) fn registration<M: Message>(&self, msg: M) -> EncryptionKeyMessage<C, M> {
|
||||
EncryptionKeyMessage { msg, enc_key: self.enc_pub_key }
|
||||
}
|
||||
|
||||
pub(crate) fn register<M: Message>(
|
||||
&mut self,
|
||||
participant: Id,
|
||||
msg: EncryptionKeyMessage<C, M>,
|
||||
) -> M {
|
||||
if self.enc_keys.contains_key(&participant) {
|
||||
panic!("Re-registering encryption key for a participant");
|
||||
}
|
||||
self.enc_keys.insert(participant, msg.enc_key);
|
||||
msg.msg
|
||||
}
|
||||
|
||||
fn cipher(&self, participant: Id, encrypt: bool) -> ChaCha20 {
|
||||
// Ideally, we'd box this transcript with ZAlloc, yet that's only possible on nightly
|
||||
// TODO
|
||||
let mut transcript = RecommendedTranscript::new(b"DKG Encryption v0");
|
||||
transcript.domain_separate(self.dst);
|
||||
|
||||
let other = self.enc_keys[&participant];
|
||||
if encrypt {
|
||||
transcript.append_message(b"sender", self.enc_pub_key.to_bytes());
|
||||
transcript.append_message(b"receiver", other.to_bytes());
|
||||
} else {
|
||||
transcript.append_message(b"sender", other.to_bytes());
|
||||
transcript.append_message(b"receiver", self.enc_pub_key.to_bytes());
|
||||
}
|
||||
|
||||
let mut shared = Zeroizing::new(other * self.enc_key.deref()).deref().to_bytes();
|
||||
transcript.append_message(b"shared_key", shared.as_ref());
|
||||
shared.as_mut().zeroize();
|
||||
|
||||
let zeroize = |buf: &mut [u8]| buf.zeroize();
|
||||
|
||||
let mut key = Cc20Key::default();
|
||||
let mut challenge = transcript.challenge(b"key");
|
||||
key.copy_from_slice(&challenge[.. 32]);
|
||||
zeroize(challenge.as_mut());
|
||||
|
||||
// The RecommendedTranscript isn't vulnerable to length extension attacks, yet if it was,
|
||||
// it'd make sense to clone it (and fork it) just to hedge against that
|
||||
let mut iv = Cc20Iv::default();
|
||||
let mut challenge = transcript.challenge(b"iv");
|
||||
iv.copy_from_slice(&challenge[.. 12]);
|
||||
zeroize(challenge.as_mut());
|
||||
|
||||
// Same commentary as the transcript regarding ZAlloc
|
||||
// TODO
|
||||
let res = ChaCha20::new(&key, &iv);
|
||||
zeroize(key.as_mut());
|
||||
zeroize(iv.as_mut());
|
||||
res
|
||||
}
|
||||
|
||||
pub(crate) fn encrypt<E: Encryptable>(
|
||||
&self,
|
||||
participant: Id,
|
||||
mut msg: Zeroizing<E>,
|
||||
) -> EncryptedMessage<E> {
|
||||
self.cipher(participant, true).apply_keystream(msg.as_mut().as_mut());
|
||||
EncryptedMessage(msg)
|
||||
}
|
||||
|
||||
pub(crate) fn decrypt<E: Encryptable>(
|
||||
&self,
|
||||
participant: Id,
|
||||
mut msg: EncryptedMessage<E>,
|
||||
) -> Zeroizing<E> {
|
||||
self.cipher(participant, false).apply_keystream(msg.0.as_mut().as_mut());
|
||||
msg.0
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user