use crate::error::RspamdError;
use blake2b_simd::blake2b;
use chacha20::cipher::consts::U64;
use chacha20::cipher::{KeyIvInit, StreamCipher};
use chacha20::{cipher::consts::U10, cipher::zeroize::Zeroizing, hchacha, XChaCha20};
use crypto_box::aead::generic_array::{arr, typenum::U32, GenericArray};
use crypto_box::{
aead::{AeadCore, OsRng},
ChaChaBox, SecretKey,
};
use curve25519_dalek::scalar::clamp_integer;
use curve25519_dalek::{MontgomeryPoint, Scalar};
use poly1305::universal_hash::KeyInit;
use poly1305::{Poly1305, Tag};
use rspamd_base32::{decode, encode};
const SHORT_KEY_ID_SIZE: usize = 5;
pub(crate) type RspamdNM = Zeroizing<GenericArray<u8, U32>>;
pub struct RspamdSecretbox {
enc_ctx: XChaCha20,
mac_ctx: Poly1305,
}
pub struct HTTPCryptEncrypted {
pub body: Vec<u8>,
pub peer_key: String, pub shared_key: RspamdNM,
}
impl RspamdSecretbox {
pub fn new(key: RspamdNM, nonce: chacha20::XNonce) -> Self {
let mut chacha = XChaCha20::new_from_slices(key.as_slice(), nonce.as_slice()).unwrap();
let mut mac_key: GenericArray<u8, U64> = GenericArray::default();
chacha.apply_keystream(mac_key.as_mut());
let poly = Poly1305::new_from_slice(mac_key.split_at(32).0).unwrap();
RspamdSecretbox {
enc_ctx: chacha,
mac_ctx: poly,
}
}
pub fn encrypt_in_place(mut self, data: &mut [u8]) -> Tag {
self.enc_ctx.apply_keystream(data);
self.mac_ctx.compute_unpadded(data)
}
pub fn decrypt_in_place(&mut self, data: &mut [u8], tag: &Tag) -> Result<usize, RspamdError> {
let computed = self.mac_ctx.clone().compute_unpadded(data);
if computed != *tag {
return Err(RspamdError::EncryptionError(
"Authentication failed".to_string(),
));
}
self.enc_ctx.apply_keystream(&mut data[..]);
Ok(computed.len())
}
}
pub fn make_key_header(remote_pk: &str, local_pk: &str) -> Result<String, RspamdError> {
let remote_pk = decode(remote_pk)
.map_err(|_| RspamdError::EncryptionError("Base32 decode failed".to_string()))?;
let hash = blake2b(remote_pk.as_slice());
let hash_b32 = encode(&hash.as_bytes()[0..SHORT_KEY_ID_SIZE]);
Ok(format!("{}={}", hash_b32.as_str(), local_pk))
}
pub(crate) fn rspamd_x25519_scalarmult(
remote_pk: &[u8],
local_sk: &SecretKey,
) -> Result<Zeroizing<MontgomeryPoint>, RspamdError> {
let remote_pk: [u8; 32] = decode(remote_pk)
.map_err(|_| RspamdError::EncryptionError("Base32 decode failed".to_string()))?
.as_slice()
.try_into()
.unwrap();
let e = Scalar::from_bytes_mod_order(clamp_integer(local_sk.to_bytes()));
let p = MontgomeryPoint(remote_pk);
Ok(Zeroizing::new(e * p))
}
pub(crate) fn rspamd_x25519_ecdh(
point: Zeroizing<MontgomeryPoint>,
) -> Zeroizing<GenericArray<u8, U32>> {
let n0 =
arr![u8; 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8,];
Zeroizing::new(hchacha::<U10>(&point.to_bytes().into(), &n0))
}
fn encrypt_inplace(
plaintext: &[u8],
recipient_public_key: &[u8],
local_sk: &SecretKey,
) -> Result<(Vec<u8>, RspamdNM), RspamdError> {
let mut dest = Vec::with_capacity(plaintext.len() + 24 + poly1305::BLOCK_SIZE);
let ec_point = rspamd_x25519_scalarmult(recipient_public_key, local_sk)?;
let nm = rspamd_x25519_ecdh(ec_point);
let nonce = ChaChaBox::generate_nonce(&mut OsRng);
let cbox = RspamdSecretbox::new(nm.clone(), nonce);
dest.extend_from_slice(nonce.as_slice());
dest.extend_from_slice(Tag::default().as_slice());
let offset = dest.len();
dest.extend_from_slice(plaintext);
let tag = cbox.encrypt_in_place(&mut dest.as_mut_slice()[offset..]);
let tag_dest = &mut <Vec<u8> as AsMut<Vec<u8>>>::as_mut(&mut dest)
[nonce.len()..(nonce.len() + poly1305::BLOCK_SIZE)];
tag_dest.copy_from_slice(tag.as_slice());
Ok((dest, nm))
}
pub fn httpcrypt_encrypt<T, HN, HV>(
url: &str,
body: &[u8],
headers: T,
peer_key: &[u8],
) -> Result<HTTPCryptEncrypted, RspamdError>
where
T: IntoIterator<Item = (HN, HV)>,
HN: AsRef<[u8]>,
HV: AsRef<[u8]>,
{
let local_sk = SecretKey::generate(&mut OsRng);
let local_pk = local_sk.public_key();
let extra_size = std::mem::size_of::<<ChaChaBox as AeadCore>::NonceSize>()
+ std::mem::size_of::<<ChaChaBox as AeadCore>::TagSize>();
let mut dest = Vec::with_capacity(body.len() + 128 + extra_size);
dest.extend_from_slice(b"POST ");
dest.extend_from_slice(url.as_bytes());
dest.extend_from_slice(b" HTTP/1.1\n");
for (k, v) in headers {
dest.extend_from_slice(k.as_ref());
dest.extend_from_slice(b": ");
dest.extend_from_slice(v.as_ref());
dest.push(b'\n');
}
dest.extend_from_slice(format!("Content-Length: {}\n\n", body.len()).as_bytes());
dest.extend_from_slice(body.as_ref());
let (encrypted, nm) = encrypt_inplace(dest.as_slice(), peer_key, &local_sk)?;
Ok(HTTPCryptEncrypted {
body: encrypted,
peer_key: rspamd_base32::encode(local_pk.as_ref()),
shared_key: nm,
})
}
pub fn httpcrypt_decrypt(body: &mut [u8], nm: RspamdNM) -> Result<usize, RspamdError> {
if body.len() < 24 + poly1305::BLOCK_SIZE {
return Err(RspamdError::EncryptionError(
"Invalid body size".to_string(),
));
}
let (nonce, remain) = body.split_at_mut(24);
let (tag, decrypted_dest) = remain.split_at_mut(poly1305::BLOCK_SIZE);
let tag = Tag::from_slice(tag);
let mut offset = nonce.len();
let mut sbox = RspamdSecretbox::new(nm, *chacha20::XNonce::from_slice(nonce));
offset += sbox.decrypt_in_place(decrypted_dest, tag)?;
Ok(offset)
}
#[cfg(test)]
mod tests {
use crate::protocol::encryption::*;
const EXPECTED_POINT: [u8; 32] = [
95, 76, 225, 188, 0, 26, 146, 94, 70, 249, 90, 189, 35, 51, 1, 42, 9, 37, 94, 254, 204, 55,
198, 91, 180, 90, 46, 217, 140, 226, 211, 90,
];
#[cfg(test)]
#[test]
fn test_scalarmult() {
use crypto_box::SecretKey;
let sk = SecretKey::from_slice(&[0u8; 32]).unwrap();
let pk = "k4nz984k36xmcynm1hr9kdbn6jhcxf4ggbrb1quay7f88rpm9kay";
let point = rspamd_x25519_scalarmult(pk.as_bytes(), &sk).unwrap();
assert_eq!(point.to_bytes().as_slice(), EXPECTED_POINT);
}
#[cfg(test)]
#[test]
fn test_ecdh() {
const EXPECTED_NM: [u8; 32] = [
61, 109, 220, 195, 100, 174, 127, 237, 148, 122, 154, 61, 165, 83, 93, 105, 127, 166,
153, 112, 103, 224, 2, 200, 136, 243, 73, 51, 8, 163, 150, 7,
];
let point = Zeroizing::new(MontgomeryPoint(EXPECTED_POINT));
let nm = rspamd_x25519_ecdh(point);
assert_eq!(nm.as_slice(), &EXPECTED_NM);
}
}