1use std::borrow::Cow;
19use std::collections::HashMap;
20use std::fmt::Display;
21use std::str::FromStr;
22
23use crate::{error, PushError};
24use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
25use rc_crypto::ece::{self, EcKeyComponents, LocalKeyPair};
26use rc_crypto::ece_crypto::RcCryptoLocalKeyPair;
27use rc_crypto::rand;
28use serde::{Deserialize, Serialize};
29
30pub const SER_AUTH_LENGTH: usize = 16;
31pub type Decrypted = Vec<u8>;
32
33#[derive(Serialize, Deserialize, Clone)]
34pub(crate) enum VersionnedKey<'a> {
35 V1(Cow<'a, KeyV1>),
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
39enum CryptoEncoding {
40 Aesgcm,
41 Aes128gcm,
42}
43
44impl FromStr for CryptoEncoding {
45 type Err = PushError;
46
47 fn from_str(s: &str) -> Result<Self, Self::Err> {
48 Ok(match s.to_lowercase().as_str() {
49 "aesgcm" => Self::Aesgcm,
50 "aes128gcm" => Self::Aes128gcm,
51 _ => {
52 return Err(PushError::CryptoError(format!(
53 "Invalid crypto encoding {}",
54 s
55 )))
56 }
57 })
58 }
59}
60
61impl Display for CryptoEncoding {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 write!(
64 f,
65 "{}",
66 match self {
67 Self::Aesgcm => "aesgcm",
68 Self::Aes128gcm => "aes128gcm",
69 }
70 )
71 }
72}
73
74#[derive(Clone, PartialEq, Serialize, Deserialize)]
75pub struct KeyV1 {
76 pub(crate) p256key: EcKeyComponents,
77 pub(crate) auth: Vec<u8>,
78}
79pub type Key = KeyV1;
80
81impl std::fmt::Debug for KeyV1 {
82 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 f.debug_struct("KeyV1").finish()
84 }
85}
86
87impl Key {
88 pub(crate) fn serialize(&self) -> error::Result<Vec<u8>> {
92 Ok(bincode::serialize(&VersionnedKey::V1(Cow::Borrowed(self)))?)
93 }
94
95 pub(crate) fn deserialize(bytes: &[u8]) -> error::Result<Self> {
96 let versionned = bincode::deserialize(bytes)?;
97 match versionned {
98 VersionnedKey::V1(prv_key) => Ok(prv_key.into_owned()),
99 }
100 }
101
102 pub fn key_pair(&self) -> &EcKeyComponents {
103 &self.p256key
104 }
105
106 pub fn auth_secret(&self) -> &[u8] {
107 &self.auth
108 }
109
110 pub fn private_key(&self) -> &[u8] {
111 self.p256key.private_key()
112 }
113
114 pub fn public_key(&self) -> &[u8] {
115 self.p256key.public_key()
116 }
117}
118
119#[cfg_attr(test, mockall::automock)]
120pub trait Cryptography: Default {
121 fn generate_key() -> error::Result<Key>;
123
124 #[allow(clippy::needless_lifetimes)]
126 fn decrypt<'a>(key: &Key, push_payload: PushPayload<'a>) -> error::Result<Decrypted>;
128
129 fn decrypt_aesgcm(
131 key: &Key,
132 content: &[u8],
133 salt: Option<Vec<u8>>,
134 crypto_key: Option<Vec<u8>>,
135 ) -> error::Result<Decrypted>;
136
137 fn decrypt_aes128gcm(key: &Key, content: &[u8]) -> error::Result<Decrypted>;
139}
140
141#[derive(Default)]
142pub struct Crypto;
143
144pub fn get_random_bytes(size: usize) -> error::Result<Vec<u8>> {
145 let mut bytes = vec![0u8; size];
146 rand::fill(&mut bytes).map_err(|e| {
147 error::PushError::CryptoError(format!("Could not generate random bytes: {:?}", e))
148 })?;
149 Ok(bytes)
150}
151
152fn extract_value(val: &str, target: &str) -> Option<Vec<u8>> {
156 if !val.contains(&format!("{}=", target)) {
157 error::debug!("No sub-value found for {}", target);
158 return None;
159 }
160 let items = val.split([',', ';']);
161 for item in items {
162 let mut kv = item.split('=');
163 if kv.next() == Some(target) {
164 if let Some(val) = kv.next() {
165 return match URL_SAFE_NO_PAD.decode(val) {
166 Ok(v) => Some(v),
167 Err(e) => {
168 error_support::report_error!(
169 "push-base64-decode",
170 "base64 failed for target:{}; {:?}",
171 target,
172 e
173 );
174 None
175 }
176 };
177 }
178 }
179 }
180 None
181}
182
183impl Cryptography for Crypto {
184 fn generate_key() -> error::Result<Key> {
185 rc_crypto::ensure_initialized();
186
187 let key = RcCryptoLocalKeyPair::generate_random()?;
188 let components = key.raw_components()?;
189 let auth = get_random_bytes(SER_AUTH_LENGTH)?;
190 Ok(Key {
191 p256key: components,
192 auth,
193 })
194 }
195
196 fn decrypt(key: &Key, push_payload: PushPayload<'_>) -> error::Result<Decrypted> {
197 rc_crypto::ensure_initialized();
198 let d_salt = extract_value(push_payload.salt, "salt");
200 let d_dh = extract_value(push_payload.dh, "dh");
201 let d_body = URL_SAFE_NO_PAD.decode(push_payload.body)?;
202
203 match CryptoEncoding::from_str(push_payload.encoding)? {
204 CryptoEncoding::Aesgcm => Self::decrypt_aesgcm(key, &d_body, d_salt, d_dh),
205 CryptoEncoding::Aes128gcm => Self::decrypt_aes128gcm(key, &d_body),
206 }
207 }
208
209 fn decrypt_aesgcm(
210 key: &Key,
211 content: &[u8],
212 salt: Option<Vec<u8>>,
213 crypto_key: Option<Vec<u8>>,
214 ) -> error::Result<Decrypted> {
215 let dh = crypto_key
216 .ok_or_else(|| error::PushError::CryptoError("Missing public key".to_string()))?;
217 let salt = salt.ok_or_else(|| error::PushError::CryptoError("Missing salt".to_string()))?;
218 let block = ece::legacy::AesGcmEncryptedBlock::new(&dh, &salt, 4096, content.to_vec())?;
219 Ok(ece::legacy::decrypt_aesgcm(
220 key.key_pair(),
221 key.auth_secret(),
222 &block,
223 )?)
224 }
225
226 fn decrypt_aes128gcm(key: &Key, content: &[u8]) -> error::Result<Vec<u8>> {
227 Ok(ece::decrypt(key.key_pair(), key.auth_secret(), content)?)
228 }
229}
230
231#[derive(Debug, Deserialize)]
232pub struct PushPayload<'a> {
233 pub(crate) channel_id: &'a str,
234 pub(crate) body: &'a str,
235 pub(crate) encoding: &'a str,
236 pub(crate) salt: &'a str,
237 pub(crate) dh: &'a str,
238}
239
240impl<'a> TryFrom<&'a HashMap<String, String>> for PushPayload<'a> {
241 type Error = PushError;
242
243 fn try_from(value: &'a HashMap<String, String>) -> Result<Self, Self::Error> {
244 let channel_id = value
245 .get("chid")
246 .ok_or_else(|| PushError::CryptoError("Invalid Push payload".to_string()))?;
247 let body = value
248 .get("body")
249 .ok_or_else(|| PushError::CryptoError("Invalid Push payload".to_string()))?;
250 let encoding = value.get("con").map(|s| s.as_str()).unwrap_or("aes128gcm");
251 let salt = value.get("enc").map(|s| s.as_str()).unwrap_or("");
252 let dh = value.get("cryptokey").map(|s| s.as_str()).unwrap_or("");
253 Ok(Self {
254 channel_id,
255 body,
256 encoding,
257 salt,
258 dh,
259 })
260 }
261}
262
263#[cfg(test)]
264mod crypto_tests {
265 use super::*;
266 use nss::ensure_initialized;
267
268 fn test_key(priv_key: &str, pub_key: &str, auth: &str) -> Key {
270 let components = EcKeyComponents::new(
271 URL_SAFE_NO_PAD.decode(priv_key).unwrap(),
272 URL_SAFE_NO_PAD.decode(pub_key).unwrap(),
273 );
274 let auth = URL_SAFE_NO_PAD.decode(auth).unwrap();
275 Key {
276 p256key: components,
277 auth,
278 }
279 }
280
281 const PLAINTEXT:&str = "Amidst the mists and coldest frosts I thrust my fists against the\nposts and still demand to see the ghosts.\n\n";
282
283 fn decrypter(ciphertext: &str, encoding: &str, salt: &str, dh: &str) -> error::Result<Vec<u8>> {
284 let priv_key_d = "qJkxxWGVVxy7BKvraNY3hg8Gs-Y8qi0lRaXWJ3R3aJ8";
285 let auth_raw = "LsuUOBKVQRY6-l7_Ajo-Ag";
287 let pub_key_raw = "BBcJdfs1GtMyymFTtty6lIGWRFXrEtJP40Df0gOvRDR4D8CKVgqE6vlYR7tCYksIRdKD1MxDPhQVmKLnzuife50";
289
290 let key = test_key(priv_key_d, pub_key_raw, auth_raw);
291 Crypto::decrypt(
292 &key,
293 PushPayload {
294 channel_id: "channel_id",
295 body: ciphertext,
296 encoding,
297 salt,
298 dh,
299 },
300 )
301 }
302
303 #[test]
304 fn test_decrypt_aesgcm() {
305 ensure_initialized();
306
307 let ciphertext = "BNKu5uTFhjyS-06eECU9-6O61int3Rr7ARbm-xPhFuyDO5sfxVs-HywGaVonvzkarvfvXE9IRT_YNA81Og2uSqDasdMuw\
309 qm1zd0O3f7049IkQep3RJ2pEZTy5DqvI7kwMLDLzea9nroq3EMH5hYhvQtQgtKXeWieEL_3yVDQVg";
310 let dh = "keyid=foo;dh=BMOebOMWSRisAhWpRK9ZPszJC8BL9MiWvLZBoBU6pG6Kh6vUFSW4BHFMh0b83xCg3_7IgfQZXwmVuyu27vwiv5c,otherval=abcde";
312 let salt = "salt=tSf2qu43C9BD0zkvRW5eUg";
313
314 let decrypted = decrypter(ciphertext, "aesgcm", salt, dh).unwrap();
317
318 assert_eq!(String::from_utf8(decrypted).unwrap(), PLAINTEXT.to_string());
319 }
320
321 #[test]
322 fn test_fail_decrypt_aesgcm() {
323 ensure_initialized();
324
325 let ciphertext = "BNKu5uTFhjyS-06eECU9-6O61int3Rr7ARbm-xPhFuyDO5sfxVs-HywGaVonvzkarvfvXE9IRT_\
326 YNA81Og2uSqDasdMuwqm1zd0O3f7049IkQep3RJ2pEZTy5DqvI7kwMLDLzea9nroq3EMH5hYhvQtQgtKXeWieEL_3yVDQVg";
327 let dh = "dh=BMOebOMWSRisAhWpRK9ZPszJC8BL9MiWvLZBoBU6pG6Kh6vUFSW4BHFMh0b83xCg3_7IgfQZXwmVuyu27vwiv5c";
328 let salt = "salt=SomeInvalidSaltValue";
329
330 decrypter(ciphertext, "aesgcm", salt, dh).expect_err("Failed to abort, bad salt");
331 }
332
333 #[test]
334 fn test_decrypt_aes128gcm() {
335 ensure_initialized();
336
337 let ciphertext = "Ek7iQgliMqS9kjFoiVOqRgAAEABBBFirfBtF6XTeHVPABFDveb1iu7uO1XVA_MYJeAo-\
338 4ih8WYUsXSTIYmkKMv5_UB3tZuQI7BQ2EVpYYQfvOCrWZVMRL8fJCuB5wVXcoRoTaFJw\
339 TlJ5hnw6IMSiaMqGVlc8drX7Hzy-ugzzAKRhGPV2x-gdsp58DZh9Ww5vHpHyT1xwVkXz\
340 x3KTyeBZu4gl_zR0Q00li17g0xGsE6Dg3xlkKEmaalgyUyObl6_a8RA6Ko1Rc6RhAy2jdyY1LQbBUnA";
341
342 let decrypted = decrypter(ciphertext, "aes128gcm", "", "").unwrap();
343 assert_eq!(String::from_utf8(decrypted).unwrap(), PLAINTEXT.to_string());
344 }
345}