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