penumbra_custody/threshold/dkg/encryption.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186
use anyhow::{anyhow, Result};
use chacha20poly1305::{aead::AeadInPlace, aead::NewAead, ChaCha20Poly1305, Key as SymmetricKey};
use rand_core::CryptoRngCore;
/// The number of bytes in a key agreement public key.
const PK_SIZE: usize = 32;
/// The number of bytes in our AEAD's authentication tag.
const TAG_SIZE: usize = 16;
/// The number of bytes in our nonce.
const NONCE_SIZE: usize = 12;
/// Generate a random shared secret.
///
/// I had to write this method because it didn't exist natively. That's probably
/// a good idea. The reason I added it here is because we want to do the trick
/// of using a fake shared secret to make key agreement infallible.
fn random_shared_secret(rng: &mut impl CryptoRngCore) -> decaf377_ka::SharedSecret {
let mut data = [0u8; 32];
rng.fill_bytes(&mut data);
decaf377_ka::SharedSecret(data)
}
/// Perform key agreement in a way that cannot fail.
///
/// Whenever key agreement were to fail, instead we return a random shared secret.
/// This means that if an invalid public key is announced, the ciphertexts encrypted
/// to that key will not be decryptable. This is the same situation as announcing a
/// public key whose corresponding secret key you do not know; this behavior thus seems fine.
fn infallible_key_agreement(
rng: &mut impl CryptoRngCore,
sk: &decaf377_ka::Secret,
pk: &decaf377_ka::Public,
) -> decaf377_ka::SharedSecret {
let fake_secret = random_shared_secret(rng);
sk.key_agreement_with(pk).unwrap_or(fake_secret)
}
/// Derive a symmetric key from the information used to produce a shared secret.
///
/// `pk` is the target public key we're encrypting data to.
/// `epk` is data we add to the ciphertext so that the owner of that public key can decrypt it.
/// `secret` is the result of a key exchange between those two keys.
fn derive_symmetric_key(
pk: &decaf377_ka::Public,
epk: &decaf377_ka::Public,
secret: &decaf377_ka::SharedSecret,
) -> SymmetricKey {
TryInto::<[u8; 32]>::try_into(
&blake2b_simd::Params::new()
.personal(b"dkg-encryption")
.to_state()
.update(&pk.0)
.update(&epk.0)
.update(&secret.0)
.finalize()
.as_array()[..32],
)
.expect("array conversion should not fail")
.into()
}
/// A key to which data can be encrypted.
///
/// This key has a corresponding decryption key which can decrypt the messages encrypted to it.
#[derive(Clone, Copy)]
pub struct EncryptionKey(decaf377_ka::Public);
impl EncryptionKey {
/// Encrypt a message, producing a ciphertext.
pub fn encrypt(&self, rng: &mut impl CryptoRngCore, message: &[u8]) -> Vec<u8> {
let esk = decaf377_ka::Secret::new(rng);
let epk = esk.public();
let secret = infallible_key_agreement(rng, &esk, &self.0);
let key = derive_symmetric_key(&self.0, &epk, &secret);
// ciphertext = EPK || <aead tag> || <encrypted data>
// The tag will also include the EPK as associated data that gets authenticated.
let ciphertext = {
let mut ciphertext = Vec::new();
ciphertext.extend_from_slice(&epk.0);
// Reserve space for the tag
ciphertext.extend_from_slice(&[0u8; TAG_SIZE]);
// Include the message, which will be written over in place
ciphertext.extend_from_slice(message);
let tag = ChaCha20Poly1305::new(&key)
.encrypt_in_place_detached(
&[0u8; NONCE_SIZE].into(),
&epk.0,
&mut ciphertext[PK_SIZE + TAG_SIZE..],
)
.expect("chacha20poly1305 encryption should not fail");
ciphertext[PK_SIZE..PK_SIZE + TAG_SIZE].copy_from_slice(&tag);
ciphertext
};
ciphertext
}
/// Return a view of this value's underlying bytes
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0 .0
}
}
impl TryFrom<&[u8]> for EncryptionKey {
type Error = anyhow::Error;
fn try_from(value: &[u8]) -> std::result::Result<Self, Self::Error> {
let repr: [u8; 32] = value.try_into()?;
Ok(Self(decaf377_ka::Public(repr)))
}
}
/// A key that allows decrypting ciphertexts sent to the corresponding encryption key.
#[derive(Clone)]
pub struct DecryptionKey(decaf377_ka::Secret);
impl From<DecryptionKey> for EncryptionKey {
fn from(value: DecryptionKey) -> Self {
EncryptionKey(value.0.public())
}
}
impl DecryptionKey {
pub fn new(rng: &mut impl CryptoRngCore) -> Self {
Self(decaf377_ka::Secret::new(rng))
}
/// Get the corresponding public encryption key for this decryption key.
///
/// This is a synonym for the From impl.
pub fn public(&self) -> EncryptionKey {
self.clone().into()
}
/// Decrypt a ciphertext, extracitng out the corresponding message.
///
/// This may potentially fail, if the ciphertext is malformed, or was tampered with.
pub fn decrypt(&self, rng: &mut impl CryptoRngCore, ciphertext: &[u8]) -> Result<Vec<u8>> {
if ciphertext.len() < PK_SIZE + TAG_SIZE {
anyhow::bail!("failed to decrypt ciphertext");
}
let (header, message) = ciphertext.split_at(PK_SIZE + TAG_SIZE);
let mut message = message.to_owned();
let epk = decaf377_ka::Public(
header[..PK_SIZE]
.try_into()
.expect("array conversion should not fail"),
);
// Not key committing, but shouldn't be a big concern.
//
// (By this I mean that decryption may still succeed even if the public key gets mangled in
// the ciphertext, but that's alright I guess).
let secret = infallible_key_agreement(rng, &self.0, &epk);
let key = derive_symmetric_key(&self.0.public(), &epk, &secret);
ChaCha20Poly1305::new(&key)
.decrypt_in_place_detached(
&[0u8; NONCE_SIZE].into(),
&header[..PK_SIZE],
&mut message,
header[PK_SIZE..PK_SIZE + TAG_SIZE].into(),
)
.map_err(|_| anyhow!("failed to decrypt ciphertext"))?;
Ok(message)
}
}
#[cfg(test)]
mod test {
use rand_core::OsRng;
use super::*;
#[test]
fn test_encryption_roundtrip() -> Result<()> {
let mut rng = OsRng;
let dk = DecryptionKey::new(&mut rng);
let ek = dk.public();
let msg = "ペンブラが好きです".as_bytes();
let ciphertext = ek.encrypt(&mut rng, msg);
let msg2 = dk.decrypt(&mut rng, &ciphertext)?;
assert_eq!(msg, &msg2);
Ok(())
}
}