1use crypto_box::{SecretKey, aead::{OsRng, AeadCore}, ChaChaBox};
8use crate::error::RspamdError;
9use rspamd_base32::{decode, encode};
10use blake2b_simd::blake2b;
11use chacha20::{cipher::consts::U10, hchacha, cipher::zeroize::Zeroizing, XChaCha20};
12use chacha20::cipher::consts::U64;
13use chacha20::cipher::{KeyIvInit, StreamCipher};
14use poly1305::{Poly1305, Tag};
15use crypto_box::aead::generic_array::{arr, GenericArray, typenum::U32};
16use curve25519_dalek::{MontgomeryPoint, Scalar};
17use curve25519_dalek::scalar::clamp_integer;
18use poly1305::universal_hash::{KeyInit};
19
20const SHORT_KEY_ID_SIZE : usize = 5;
22
23pub(crate) type RspamdNM = Zeroizing<GenericArray<u8, U32>>;
24
25pub struct RspamdSecretbox {
26 enc_ctx: XChaCha20,
27 mac_ctx: Poly1305,
28}
29
30pub struct HTTPCryptEncrypted {
31 pub body: Vec<u8>,
32 pub peer_key: String, pub shared_key: RspamdNM,
34}
35
36impl RspamdSecretbox {
37 pub fn new(key: RspamdNM, nonce: chacha20::XNonce) -> Self {
39 let mut chacha = XChaCha20::new_from_slices(key.as_slice(),
41 nonce.as_slice()).unwrap();
42 let mut mac_key : GenericArray<u8, U64> = GenericArray::default();
43 chacha.apply_keystream(mac_key.as_mut());
44 let poly = Poly1305::new_from_slice(mac_key.split_at(32).0).unwrap();
45 RspamdSecretbox {
46 enc_ctx: chacha,
47 mac_ctx: poly,
48 }
49 }
50
51 pub fn encrypt_in_place(mut self, data: &mut [u8]) -> Tag {
53 self.enc_ctx.apply_keystream(data);
55 self.mac_ctx.compute_unpadded(data)
56 }
57
58 pub fn decrypt_in_place(&mut self, data: &mut [u8], tag: &Tag) -> Result<usize, RspamdError> {
60 let computed = self.mac_ctx.clone().compute_unpadded(data);
61 if computed != *tag {
62 return Err(RspamdError::EncryptionError("Authentication failed".to_string()));
63 }
64 self.enc_ctx.apply_keystream(&mut data[..]);
65
66 Ok(computed.len())
67 }
68}
69
70pub fn make_key_header(remote_pk: &str, local_pk: &str) -> Result<String, RspamdError> {
71 let remote_pk = decode(remote_pk)
72 .map_err(|_| RspamdError::EncryptionError("Base32 decode failed".to_string()))?;
73 let hash = blake2b(remote_pk.as_slice());
74 let hash_b32 = encode(&hash.as_bytes()[0..SHORT_KEY_ID_SIZE]);
75 Ok(format!("{}={}", hash_b32.as_str(), local_pk))
76}
77
78pub(crate) fn rspamd_x25519_scalarmult(remote_pk: &[u8], local_sk: &SecretKey) -> Result<Zeroizing<MontgomeryPoint>, RspamdError> {
80 let remote_pk: [u8; 32] = decode(remote_pk)
81 .map_err(|_| RspamdError::EncryptionError("Base32 decode failed".to_string()))?
82 .as_slice().try_into().unwrap();
83 let e = Scalar::from_bytes_mod_order(clamp_integer(local_sk.to_bytes()));
85 let p = MontgomeryPoint(remote_pk);
86 Ok(Zeroizing::new(e * p))
87}
88
89pub(crate) fn rspamd_x25519_ecdh(point: Zeroizing<MontgomeryPoint>) -> Zeroizing<GenericArray<u8, U32>> {
92 let n0 = arr![u8; 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8,];
93 Zeroizing::new(hchacha::<U10>(&point.to_bytes().into(), &n0))
94}
95
96fn encrypt_inplace(
98 plaintext: &[u8],
99 recipient_public_key: &[u8],
100 local_sk: &SecretKey,
101) -> Result<(Vec<u8>, RspamdNM), RspamdError> {
102 let mut dest = Vec::with_capacity(plaintext.len() +
103 24 +
104 poly1305::BLOCK_SIZE);
105 let ec_point = rspamd_x25519_scalarmult(recipient_public_key, local_sk)?;
106 let nm = rspamd_x25519_ecdh(ec_point);
107
108 let nonce = ChaChaBox::generate_nonce(&mut OsRng);
109 let cbox = RspamdSecretbox::new(nm.clone(), nonce);
110 dest.extend_from_slice(nonce.as_slice());
111 dest.extend_from_slice(Tag::default().as_slice());
113 let offset = dest.len();
114 dest.extend_from_slice(plaintext);
115 let tag = cbox.encrypt_in_place(&mut dest.as_mut_slice()[offset..]);
116 let tag_dest = &mut <Vec<u8> as AsMut<Vec<u8>>>::as_mut(&mut dest)[nonce.len()..(nonce.len() + poly1305::BLOCK_SIZE)];
117 tag_dest.copy_from_slice(tag.as_slice());
118 Ok((dest, nm))
119}
120
121
122pub fn httpcrypt_encrypt<T, HN, HV>(url: &str, body: &[u8], headers: T, peer_key: &[u8]) -> Result<HTTPCryptEncrypted, RspamdError>
123where T: IntoIterator<Item = (HN, HV)>,
124 HN: AsRef<[u8]>,
125 HV: AsRef<[u8]>
126{
127 let local_sk = SecretKey::generate(&mut OsRng);
128 let local_pk = local_sk.public_key();
129 let extra_size = std::mem::size_of::<<ChaChaBox as AeadCore>::NonceSize>() + std::mem::size_of::<<ChaChaBox as AeadCore>::TagSize>();
130 let mut dest = Vec::with_capacity(body.len() + 128 + extra_size);
131
132 dest.extend_from_slice(b"POST ");
134 dest.extend_from_slice(url.as_bytes());
135 dest.extend_from_slice(b" HTTP/1.1\n");
136 for (k, v) in headers {
137 dest.extend_from_slice(k.as_ref());
138 dest.extend_from_slice(b": ");
139 dest.extend_from_slice(v.as_ref());
140 dest.push(b'\n');
141 }
142 dest.extend_from_slice(format!("Content-Length: {}\n\n", body.len()).as_bytes());
143 dest.extend_from_slice(body.as_ref());
144
145 let (encrypted, nm) = encrypt_inplace(dest.as_slice(), peer_key, &local_sk)?;
146
147 Ok(HTTPCryptEncrypted {
148 body: encrypted,
149 peer_key: rspamd_base32::encode(local_pk.as_ref()),
150 shared_key: nm,
151 })
152}
153
154pub fn httpcrypt_decrypt(body: &mut [u8], nm: RspamdNM) -> Result<usize, RspamdError> {
156 if body.len() < 24 + poly1305::BLOCK_SIZE {
157 return Err(RspamdError::EncryptionError("Invalid body size".to_string()));
158 }
159
160 let (nonce, remain) = body.split_at_mut(24);
161 let (tag, decrypted_dest) = remain.split_at_mut(poly1305::BLOCK_SIZE);
162 let tag = Tag::from_slice(tag);
163 let mut offset = nonce.len();
164 let mut sbox = RspamdSecretbox::new(nm, *chacha20::XNonce::from_slice(nonce));
165 offset += sbox.decrypt_in_place(decrypted_dest, tag)?;
166 Ok(offset)
167}
168
169#[cfg(test)]
170mod tests {
171 use crate::protocol::encryption::*;
172 const EXPECTED_POINT : [u8; 32] = [95, 76, 225, 188, 0, 26, 146, 94, 70, 249,
173 90, 189, 35, 51, 1, 42, 9, 37, 94, 254, 204, 55, 198, 91, 180, 90,
174 46, 217, 140, 226, 211, 90];
175
176 #[cfg(test)]
177 #[test]
178 fn test_scalarmult() {
179 use crypto_box::{SecretKey};
180 let sk = SecretKey::from_slice(&[0u8; 32]).unwrap();
181 let pk = "k4nz984k36xmcynm1hr9kdbn6jhcxf4ggbrb1quay7f88rpm9kay";
182 let point = rspamd_x25519_scalarmult(pk.as_bytes(), &sk).unwrap();
183 assert_eq!(point.to_bytes().as_slice(), EXPECTED_POINT);
184 }
185
186 #[cfg(test)]
187 #[test]
188 fn test_ecdh() {
189 const EXPECTED_NM : [u8; 32] = [61, 109, 220, 195, 100, 174, 127, 237, 148,
190 122, 154, 61, 165, 83, 93, 105, 127, 166, 153, 112, 103, 224, 2, 200,
191 136, 243, 73, 51, 8, 163, 150, 7];
192 let point = Zeroizing::new(MontgomeryPoint(EXPECTED_POINT));
193 let nm = rspamd_x25519_ecdh(point);
194 assert_eq!(nm.as_slice(), &EXPECTED_NM);
195 }
196}