#![allow(unused)]
use chacha20poly1305::{ChaCha20Poly1305, Key as Chacha20Key, KeyInit, Nonce, aead::Aead};
use hkdf::Hkdf;
use rand::thread_rng;
use sha2::Sha512;
use thiserror::Error;
use x25519_dalek::{EphemeralSecret, SharedSecret};
use zeroize::{Zeroize, ZeroizeOnDrop};
pub use self::messages::{InitialMessage, Message, MessageDecodeError};
use crate::Curve25519PublicKey;
mod messages;
const MATRIX_QR_LOGIN_INFO_PREFIX: &str = "MATRIX_QR_CODE_LOGIN";
#[derive(Debug, Error)]
pub enum Error {
#[error("At least one of the keys did not have contributory behaviour")]
NonContributoryKey,
#[error("Failed decrypting the message")]
Decryption,
}
struct EciesNonce {
inner: u128,
}
impl EciesNonce {
const fn new() -> Self {
Self { inner: 0 }
}
fn get(&mut self) -> Nonce {
let current = self.inner;
let (new_nonce, _) = self.inner.overflowing_add(1);
self.inner = new_nonce;
let mut nonce = [0u8; 12];
nonce.copy_from_slice(¤t.to_le_bytes()[..12]);
#[allow(clippy::expect_used)]
#[allow(deprecated)]
Nonce::from_exact_iter(nonce)
.expect("We should be able to construct the correct nonce from a 12 byte slice")
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CheckCode {
bytes: [u8; 2],
}
impl CheckCode {
pub const fn as_bytes(&self) -> &[u8; 2] {
&self.bytes
}
pub const fn to_digit(&self) -> u8 {
let first = (self.bytes[0] % 10) * 10;
let second = self.bytes[1] % 10;
first + second
}
}
#[derive(Debug)]
pub struct InboundCreationResult {
pub ecies: EstablishedEcies,
pub message: Vec<u8>,
}
#[derive(Debug)]
pub struct OutboundCreationResult {
pub ecies: EstablishedEcies,
pub message: InitialMessage,
}
pub struct Ecies {
secret_key: EphemeralSecret,
application_info_prefix: String,
}
#[derive(Debug, Clone, Copy)]
enum Role {
Initiator,
Recipient,
}
impl Ecies {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self::with_info(MATRIX_QR_LOGIN_INFO_PREFIX)
}
pub fn with_info(info: &str) -> Self {
let rng = thread_rng();
let secret_key = EphemeralSecret::random_from_rng(rng);
let application_info_prefix = info.to_owned();
Self { secret_key, application_info_prefix }
}
pub fn establish_outbound_channel(
self,
their_public_key: Curve25519PublicKey,
initial_plaintext: &[u8],
) -> Result<OutboundCreationResult, Error> {
let our_public_key = self.public_key();
let shared_secret = self.secret_key.diffie_hellman(&their_public_key.inner);
if shared_secret.was_contributory() {
let mut ecies = EstablishedEcies::new(
&shared_secret,
our_public_key,
their_public_key,
&self.application_info_prefix,
Role::Initiator,
);
let message = ecies.encrypt(initial_plaintext);
let message =
InitialMessage { public_key: our_public_key, ciphertext: message.ciphertext };
Ok(OutboundCreationResult { ecies, message })
} else {
Err(Error::NonContributoryKey)
}
}
pub fn establish_inbound_channel(
self,
message: &InitialMessage,
) -> Result<InboundCreationResult, Error> {
let our_public_key = self.public_key();
let shared_secret = self.secret_key.diffie_hellman(&message.public_key.inner);
if shared_secret.was_contributory() {
let mut ecies = EstablishedEcies::new(
&shared_secret,
our_public_key,
message.public_key,
&self.application_info_prefix,
Role::Recipient,
);
let nonce = ecies.decryption_nonce.get();
let message = ecies.decrypt_helper(&nonce, &message.ciphertext)?;
Ok(InboundCreationResult { ecies, message })
} else {
Err(Error::NonContributoryKey)
}
}
pub fn public_key(&self) -> Curve25519PublicKey {
Curve25519PublicKey::from(&self.secret_key)
}
}
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct EstablishedEcies {
#[zeroize(skip)]
our_public_key: Curve25519PublicKey,
#[zeroize(skip)]
their_public_key: Curve25519PublicKey,
#[zeroize(skip)]
encryption_nonce: EciesNonce,
#[zeroize(skip)]
decryption_nonce: EciesNonce,
encryption_key: Box<[u8; 32]>,
decryption_key: Box<[u8; 32]>,
#[zeroize(skip)]
check_code: CheckCode,
#[zeroize(skip)]
role: Role,
}
impl std::fmt::Debug for EstablishedEcies {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EstablishedEcies")
.field("our_public_key", &self.our_public_key)
.field("their_public_key", &self.their_public_key)
.field("check_code", &self.check_code)
.field("role", &self.role)
.finish()
}
}
impl EstablishedEcies {
fn create_check_code(
shared_secret: &SharedSecret,
our_public_key: Curve25519PublicKey,
their_public_key: Curve25519PublicKey,
info: &str,
role: Role,
) -> CheckCode {
let mut bytes = [0u8; 2];
let kdf: Hkdf<Sha512> = Hkdf::new(None, shared_secret.as_bytes());
let info = Self::get_check_code_info(info, role, our_public_key, their_public_key);
#[allow(clippy::expect_used)]
kdf.expand(info.as_bytes(), bytes.as_mut_slice()).expect(
"We should be able to expand the 32-byte long shared secret into a 32 byte key.",
);
CheckCode { bytes }
}
fn create_key(info: &str, shared_secret: &SharedSecret) -> Box<[u8; 32]> {
let mut key = Box::new([0u8; 32]);
let kdf: Hkdf<Sha512> = Hkdf::new(None, shared_secret.as_bytes());
#[allow(clippy::expect_used)]
kdf.expand(info.as_bytes(), key.as_mut_slice()).expect(
"We should be able to expand the 32-byte long shared secret into a 32 byte key.",
);
key
}
fn create_encryption_key(
shared_secret: &SharedSecret,
our_public_key: Curve25519PublicKey,
their_public_key: Curve25519PublicKey,
app_info: &str,
role: Role,
) -> Box<[u8; 32]> {
let info = Self::get_encryption_key_info(app_info, role, our_public_key, their_public_key);
Self::create_key(&info, shared_secret)
}
fn create_decryption_key(
shared_secret: &SharedSecret,
our_public_key: Curve25519PublicKey,
their_public_key: Curve25519PublicKey,
app_info: &str,
role: Role,
) -> Box<[u8; 32]> {
let info = Self::get_decryption_key_info(app_info, role, our_public_key, their_public_key);
Self::create_key(&info, shared_secret)
}
fn new(
shared_secret: &SharedSecret,
our_public_key: Curve25519PublicKey,
their_public_key: Curve25519PublicKey,
app_info: &str,
role: Role,
) -> Self {
let (encryption_nonce, decryption_nonce) = (EciesNonce::new(), EciesNonce::new());
let encryption_key = Self::create_encryption_key(
shared_secret,
our_public_key,
their_public_key,
app_info,
role,
);
let decryption_key = Self::create_decryption_key(
shared_secret,
our_public_key,
their_public_key,
app_info,
role,
);
let check_code = Self::create_check_code(
shared_secret,
our_public_key,
their_public_key,
app_info,
role,
);
Self {
encryption_key,
decryption_key,
encryption_nonce,
decryption_nonce,
our_public_key,
their_public_key,
check_code,
role,
}
}
pub const fn public_key(&self) -> Curve25519PublicKey {
self.our_public_key
}
pub const fn check_code(&self) -> &CheckCode {
&self.check_code
}
fn encryption_key(&self) -> &Chacha20Key {
#[allow(deprecated)]
Chacha20Key::from_slice(self.encryption_key.as_slice())
}
fn decryption_key(&self) -> &Chacha20Key {
#[allow(deprecated)]
Chacha20Key::from_slice(self.decryption_key.as_slice())
}
pub fn encrypt(&mut self, plaintext: &[u8]) -> Message {
let nonce = self.encryption_nonce.get();
let cipher = ChaCha20Poly1305::new(self.encryption_key());
#[allow(clippy::expect_used)]
let ciphertext = cipher.encrypt(&nonce, plaintext).expect(
"We should always be able to encrypt a message since we provide the correct nonce",
);
Message { ciphertext }
}
pub fn decrypt(&mut self, message: &Message) -> Result<Vec<u8>, Error> {
let nonce = self.decryption_nonce.get();
self.decrypt_helper(&nonce, &message.ciphertext)
}
fn decrypt_helper(&self, nonce: &Nonce, ciphertext: &[u8]) -> Result<Vec<u8>, Error> {
let cipher = ChaCha20Poly1305::new(self.decryption_key());
let plaintext = cipher.decrypt(nonce, ciphertext).map_err(|_| Error::Decryption)?;
Ok(plaintext)
}
fn get_check_code_info(
app_info: &str,
role: Role,
our_public_key: Curve25519PublicKey,
their_public_key: Curve25519PublicKey,
) -> String {
let partial_info = format!("{app_info}_CHECKCODE");
Self::construct_info_string(&partial_info, role, our_public_key, their_public_key)
}
fn get_encryption_key_info(
app_info: &str,
role: Role,
our_public_key: Curve25519PublicKey,
their_public_key: Curve25519PublicKey,
) -> String {
let partial_info = match role {
Role::Initiator => format!("{app_info}_ENCKEY_S"),
Role::Recipient => format!("{app_info}_ENCKEY_G"),
};
Self::construct_info_string(&partial_info, role, our_public_key, their_public_key)
}
fn get_decryption_key_info(
app_info: &str,
role: Role,
our_public_key: Curve25519PublicKey,
their_public_key: Curve25519PublicKey,
) -> String {
let partial_info = match role {
Role::Initiator => format!("{app_info}_ENCKEY_G"),
Role::Recipient => format!("{app_info}_ENCKEY_S"),
};
Self::construct_info_string(&partial_info, role, our_public_key, their_public_key)
}
fn construct_info_string(
partial_info: &str,
role: Role,
our_public_key: Curve25519PublicKey,
their_public_key: Curve25519PublicKey,
) -> String {
match role {
Role::Recipient => {
format!(
"{partial_info}|{}|{}",
our_public_key.to_base64(),
their_public_key.to_base64(),
)
}
Role::Initiator => {
format!(
"{partial_info}|{}|{}",
their_public_key.to_base64(),
our_public_key.to_base64(),
)
}
}
}
}
#[cfg(test)]
mod test {
use insta::assert_debug_snapshot;
use proptest::prelude::*;
use super::*;
#[test]
fn channel_creation() {
let plaintext = b"It's a secret to everybody";
let alice = Ecies::new();
let bob = Ecies::new();
let OutboundCreationResult { ecies: mut alice, message } = alice
.establish_outbound_channel(bob.public_key(), plaintext)
.expect("We should be able to create an outbound channel");
let InboundCreationResult { ecies: mut bob, message } = bob
.establish_inbound_channel(&message)
.expect("We should be able to create an inbound channel");
assert_eq!(
message, plaintext,
"The decrypted plaintext should match our initial plaintext"
);
assert_eq!(alice.check_code(), bob.check_code());
assert_eq!(alice.check_code().to_digit(), bob.check_code().to_digit());
let message = bob.encrypt(b"Another plaintext");
let decrypted =
alice.decrypt(&message).expect("We should be able to decrypt the second message");
assert_eq!(decrypted, b"Another plaintext");
}
#[test]
fn invalid_check_code() {
let plaintext = b"It's a secret to everybody";
let alice = Ecies::new();
let bob = Ecies::new();
let malory = Ecies::new();
let OutboundCreationResult { mut message, .. } = alice
.establish_outbound_channel(bob.public_key(), plaintext)
.expect("We should be able to create an outbound channel");
message.public_key = malory.public_key();
bob.establish_inbound_channel(&message).expect_err(
"The decryption should fail since Malory inserted the \
wrong public key into the message",
);
}
#[test]
fn nonce() {
let mut nonce = EciesNonce::new();
assert_eq!(nonce.inner, 0, "The nonce should start the counter from 0");
let first = nonce.get();
assert_eq!(
nonce.inner, 1,
"After the first nonce is returned, the counter should have been incremented"
);
#[allow(deprecated)]
let first = first.as_slice();
assert_eq!(first, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
let second = nonce.get();
assert_eq!(
nonce.inner, 2,
"After the first nonce is returned, the counter should have been incremented"
);
#[allow(deprecated)]
let second = second.as_slice();
assert_eq!(second, [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
}
#[test]
fn check_code() {
let check_code = CheckCode { bytes: [0x0, 0x0] };
let digit = check_code.to_digit();
assert_eq!(digit, 0, "Two zero bytes should generate a 0 digit");
assert_eq!(
check_code.as_bytes(),
&[0x0, 0x0],
"CheckCode::as_bytes() should return the exact bytes we generated."
);
let check_code = CheckCode { bytes: [0x9, 0x9] };
let digit = check_code.to_digit();
assert_eq!(
check_code.as_bytes(),
&[0x9, 0x9],
"CheckCode::as_bytes() should return the exact bytes we generated."
);
assert_eq!(digit, 99);
let check_code = CheckCode { bytes: [0xff, 0xff] };
let digit = check_code.to_digit();
assert_eq!(
check_code.as_bytes(),
&[0xff, 0xff],
"CheckCode::as_bytes() should return the exact bytes we generated."
);
assert_eq!(digit, 55, "u8::MAX should generate 55");
}
#[test]
fn test_info_construction() {
use crate::types::Curve25519Keypair;
let app_info = "foobar";
let our_public_key = Curve25519Keypair::new().public_key;
let their_public_key = Curve25519Keypair::new().public_key;
let check_code_info1 = EstablishedEcies::get_check_code_info(
app_info,
Role::Initiator,
our_public_key,
their_public_key,
);
assert_eq!(
check_code_info1,
format!("foobar_CHECKCODE|{their_public_key}|{our_public_key}")
);
let check_code_info2 = EstablishedEcies::get_check_code_info(
app_info,
Role::Recipient,
our_public_key,
their_public_key,
);
assert_eq!(
check_code_info2,
format!("foobar_CHECKCODE|{our_public_key}|{their_public_key}")
);
}
#[test]
fn snapshot_debug() {
let key = Curve25519PublicKey::from_bytes([0; 32]);
let alice = Ecies::new();
let bob = Ecies::new();
let OutboundCreationResult { mut ecies, .. } = alice
.establish_outbound_channel(bob.public_key(), b"")
.expect("We should be able to establish a Ecies channel");
ecies.our_public_key = key;
ecies.their_public_key = key;
ecies.check_code = CheckCode { bytes: [0, 1] };
assert_debug_snapshot!(ecies);
}
proptest! {
#[test]
fn check_code_proptest(bytes in prop::array::uniform2(0u8..) ) {
let check_code = CheckCode {
bytes
};
let digit = check_code.to_digit();
prop_assert!(
(0..=99).contains(&digit),
"The digit should be in the 0-99 range"
);
}
}
}