conundrum 0.1.0

Hard-to-misuse crypto primitives with purpose scoping.
Documentation
use chacha20::cipher::generic_array::{typenum::Unsigned, GenericArray};
use chacha20poly1305::aead::Aead;
use crypto_box::{aead::AeadCore, ChaChaBox, PublicKey, SecretKey};
use litl::{serde::DeserializeError, Litl};
use rand08::rngs::OsRng;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_derive::{Deserialize, Serialize};
use std::{fmt::Debug, marker::PhantomData, ops::Deref};
use thiserror::Error;
use zeroize::Zeroizing;

#[derive(Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename = "Conundrum/EncrPubKey:Curve25519XSalsa20Poly1305")]
pub struct RawAsymmEncrPublicKey(PublicKey);

impl Deref for RawAsymmEncrPublicKey {
    type Target = PublicKey;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl Ord for RawAsymmEncrPublicKey {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        // TODO(security): constant time chack possible?
        self.0.as_bytes().cmp(other.0.as_bytes())
    }
}

impl PartialOrd for RawAsymmEncrPublicKey {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        Some(self.cmp(other))
    }
}

#[derive(Clone, Hash, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct AsymmEncrPublicKey<S> {
    pub_key: RawAsymmEncrPublicKey,
    purpose: S,
}

impl<S> Deref for AsymmEncrPublicKey<S> {
    type Target = RawAsymmEncrPublicKey;
    fn deref(&self) -> &Self::Target {
        &self.pub_key
    }
}

impl<S: Serialize> Debug for AsymmEncrPublicKey<S> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        Litl::from_se(self).fmt(f)
    }
}

#[derive(Serialize, Deserialize)]
#[serde(rename = "Conundrum/AnonAsymmEncrypted")]
pub struct AnonAsymmEncrypted<T, S> {
    ephemeral_pubkey: AsymmEncrPublicKey<S>,
    pub recipient_pubkey: AsymmEncrPublicKey<S>,
    #[serde(with = "serde_bytes")]
    ciphertext: Vec<u8>,
    #[serde(skip)]
    _marker: PhantomData<T>,
}

impl<T, S: Serialize> Debug for AnonAsymmEncrypted<T, S> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        Litl::from_se(self).fmt(f)
    }
}

impl<T, S: Clone> Clone for AnonAsymmEncrypted<T, S> {
    fn clone(&self) -> Self {
        Self {
            ephemeral_pubkey: self.ephemeral_pubkey.clone(),
            recipient_pubkey: self.recipient_pubkey.clone(),
            ciphertext: self.ciphertext.clone(),
            _marker: PhantomData,
        }
    }
}

impl<T, S: PartialEq> PartialEq for AnonAsymmEncrypted<T, S> {
    fn eq(&self, other: &Self) -> bool {
        self.ephemeral_pubkey == other.ephemeral_pubkey
            && self.recipient_pubkey == other.recipient_pubkey
            && self.ciphertext == other.ciphertext
    }
}

impl<T, S: Eq> Eq for AnonAsymmEncrypted<T, S> {}

impl<'de, T: Serialize + Deserialize<'de>, S: Clone + Default> AnonAsymmEncrypted<T, S> {
    pub fn encrypt_for(
        recipient_pubkey: &AsymmEncrPublicKey<S>,
        data: &T,
    ) -> AnonAsymmEncrypted<T, S> {
        let ephemeral_keypair = AsymmEncrSecretKey::new_random();
        let ephemeral_pubkey = ephemeral_keypair.public();
        let nonce = nonce_for(&ephemeral_keypair.public(), recipient_pubkey);
        let chacha_box = ChaChaBox::new(recipient_pubkey, &ephemeral_keypair);

        AnonAsymmEncrypted {
            ephemeral_pubkey,
            recipient_pubkey: recipient_pubkey.clone(),
            ciphertext: chacha_box
                .encrypt(&nonce, Litl::write_from(data).as_slice())
                .expect("Unable to encrypt"),
            _marker: PhantomData,
        }
    }
}

#[derive(Error, Debug)]
pub enum AsymmDecryptionError {
    #[error("Decryption error.")]
    DecryptionError,
    #[error("Error converting from decrypted bytes.")]
    DeserializeError(DeserializeError),
}

type Nonce = GenericArray<u8, <ChaChaBox as AeadCore>::NonceSize>;

fn nonce_for(ephemeral_pubkey: &PublicKey, recipient_pubkey: &PublicKey) -> Nonce {
    let mut hasher = blake3::Hasher::new();
    hasher.update(ephemeral_pubkey.as_bytes());
    hasher.update(recipient_pubkey.as_bytes());
    *Nonce::from_slice(
        &hasher.finalize().as_bytes()[0..<ChaChaBox as AeadCore>::NonceSize::to_usize()],
    )
}

#[derive(Serialize, Deserialize)]
#[serde(rename = "Conundrum/EncrSecretKey:Curve25519XSalsa20Poly1305")]
pub struct RawAsymmEncrSecretKey(
    #[serde(deserialize_with = "deserialize_secret_key")]
    #[serde(serialize_with = "serialize_secret_key")]
    SecretKey,
);

fn deserialize_secret_key<'de, D>(deserializer: D) -> Result<SecretKey, D::Error>
where
    D: Deserializer<'de>,
{
    let bytes = <[u8; crypto_box::KEY_SIZE]>::deserialize(deserializer)?;
    Ok(SecretKey::from(bytes))
}

fn serialize_secret_key<S>(key: &SecretKey, serializer: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    key.as_bytes().serialize(serializer)
}

impl Deref for RawAsymmEncrSecretKey {
    type Target = SecretKey;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl RawAsymmEncrSecretKey {
    pub fn new_random() -> Self {
        RawAsymmEncrSecretKey(SecretKey::generate(&mut OsRng {}))
    }

    pub fn public(&self) -> RawAsymmEncrPublicKey {
        RawAsymmEncrPublicKey(self.0.public_key())
    }
}

#[derive(Serialize, Deserialize)]
pub struct AsymmEncrSecretKey<S> {
    keypair: RawAsymmEncrSecretKey,
    purpose: S,
}

impl<S: Clone + Default> AsymmEncrSecretKey<S> {
    pub fn new_random() -> AsymmEncrSecretKey<S> {
        AsymmEncrSecretKey {
            keypair: RawAsymmEncrSecretKey::new_random(),
            purpose: S::default(),
        }
    }

    pub fn public(&self) -> AsymmEncrPublicKey<S> {
        AsymmEncrPublicKey {
            pub_key: self.keypair.public(),
            purpose: self.purpose.clone(),
        }
    }

    pub fn decrypt<'de, T: Deserialize<'de>>(
        &self,
        encr: &AnonAsymmEncrypted<T, S>,
    ) -> Result<T, AsymmDecryptionError> {
        let recipient_pubkey = self.public();
        let nonce = nonce_for(&encr.ephemeral_pubkey, &recipient_pubkey);
        let chacha_box = ChaChaBox::new(&encr.ephemeral_pubkey, self);

        let plaintext = Zeroizing::new(
            chacha_box
                .decrypt(&nonce, encr.ciphertext.as_slice())
                .map_err(|_aead_err| AsymmDecryptionError::DecryptionError)?,
        );
        Litl::read_as::<T>(&plaintext).map_err(AsymmDecryptionError::DeserializeError)
    }
}

impl<S> Deref for AsymmEncrSecretKey<S> {
    type Target = RawAsymmEncrSecretKey;
    fn deref(&self) -> &Self::Target {
        &self.keypair
    }
}

#[cfg(test)]
mod test {
    use litl::Litl;
    use serde_derive::{Deserialize, Serialize};

    use crate::{
        asymm_encr::{AnonAsymmEncrypted, AsymmEncrSecretKey},
        purpose,
    };

    #[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug)]
    struct TestData {
        bla: [u8; 4],
    }
    purpose!(TestPurpose);

    #[test]
    fn encryption_roundtrip_works() {
        let data = TestData { bla: [1, 2, 3, 4] };
        let recipient = AsymmEncrSecretKey::<TestPurpose>::new_random();
        let encrypted = AnonAsymmEncrypted::encrypt_for(&recipient.public(), &data);

        println!("{:?}", Litl::from_se(&encrypted));

        let decrypted = recipient.decrypt(&encrypted);

        assert_eq!(decrypted.unwrap(), data);
    }

    #[test]
    fn can_not_decrypt_with_wrong_secret_key() {
        let data = TestData { bla: [1, 2, 3, 4] };
        let recipient = AsymmEncrSecretKey::<TestPurpose>::new_random();
        let encrypted = AnonAsymmEncrypted::encrypt_for(&recipient.public(), &data);

        let fake_recipient = AsymmEncrSecretKey::<TestPurpose>::new_random();
        let decrypted = fake_recipient.decrypt(&encrypted);

        assert!(matches!(decrypted, Err(_),));
    }

    #[test]
    fn can_not_decrypt_invalid_ciphertext() {
        let data = TestData { bla: [1, 2, 3, 4] };
        let recipient = AsymmEncrSecretKey::<TestPurpose>::new_random();
        let mut encrypted = AnonAsymmEncrypted::encrypt_for(&recipient.public(), &data);
        encrypted.ciphertext = vec![0, 0, 0, 1, 6];

        let decrypted = recipient.decrypt(&encrypted);

        assert!(matches!(decrypted, Err(_),));
    }
}