integrationos_domain/domain/access_key/
encrypted_data.rs

1use crate::{IntegrationOSError, InternalError};
2use aes::cipher::{generic_array::GenericArray, KeyIvInit, StreamCipher};
3use sha2::{Digest, Sha256};
4
5type Aes256Ctr = ctr::Ctr64BE<aes::Aes256>;
6const HASH_LENGTH: usize = 32;
7pub const IV_LENGTH: usize = 16;
8pub const PASSWORD_LENGTH: usize = 32;
9type HashBuf = [u8; HASH_LENGTH];
10
11const HASH_PREFIX: &str = "\x19Event Signed Message:\n";
12
13#[derive(Debug, Clone, Eq, PartialEq)]
14pub struct EncryptedData {
15    data: Vec<u8>,
16}
17
18impl EncryptedData {
19    pub fn new(data: Vec<u8>) -> Self {
20        Self { data }
21    }
22
23    pub fn verify_and_decrypt(
24        &mut self,
25        password: &[u8; PASSWORD_LENGTH],
26    ) -> Result<&[u8], IntegrationOSError> {
27        if !self.verify_hash(password)? {
28            return Err(InternalError::invalid_argument("Hash does not match", None));
29        }
30
31        self.decrypt(password)
32    }
33
34    pub fn encrypt(
35        mut content: Vec<u8>,
36        iv: &[u8; IV_LENGTH],
37        password: &[u8; PASSWORD_LENGTH],
38    ) -> Result<Vec<u8>, IntegrationOSError> {
39        let mut cipher = Aes256Ctr::new(password.into(), iv.into());
40        cipher
41            .try_apply_keystream(&mut content)
42            .map_err(|e| InternalError::io_err(&format!("Could not encode content: {e}"), None))?;
43        let hash = Self::compute_hash(&content, iv, password)?;
44        content.extend(iv);
45        content.extend(hash);
46        Ok(content)
47    }
48
49    fn decrypt(&mut self, password: &[u8; PASSWORD_LENGTH]) -> Result<&[u8], IntegrationOSError> {
50        let mut cipher = Aes256Ctr::new(password.into(), self.get_iv().into());
51        let content = self.get_content_mut();
52        cipher
53            .try_apply_keystream(content)
54            .map_err(|e| InternalError::io_err(&format!("Could not decode content: {e}"), None))?;
55        Ok(content)
56    }
57
58    fn verify_hash(&self, password: &[u8; PASSWORD_LENGTH]) -> Result<bool, IntegrationOSError> {
59        let actual_hash = Self::compute_hash(self.get_content(), self.get_iv(), password)?;
60        Ok(actual_hash == *self.get_hash())
61    }
62
63    pub fn compute_hash(
64        content: &[u8],
65        iv: &[u8],
66        password: &[u8],
67    ) -> Result<HashBuf, IntegrationOSError> {
68        let message_len = content.len() + iv.len() + password.len();
69        let mut hasher = Sha256::new();
70        hasher.update(HASH_PREFIX);
71        hasher.update(message_len.to_string());
72        hasher.update(content);
73        hasher.update(iv);
74        hasher.update(password);
75        let mut actual_hash = [0u8; HASH_LENGTH];
76        hasher.finalize_into(GenericArray::from_mut_slice(&mut actual_hash));
77        Ok(actual_hash)
78    }
79
80    fn get_iv(&self) -> &[u8] {
81        &self.data[self.data.len() - HASH_LENGTH - IV_LENGTH..self.data.len() - HASH_LENGTH]
82    }
83
84    fn get_hash(&self) -> &[u8] {
85        &self.data[self.data.len() - HASH_LENGTH..]
86    }
87
88    fn get_content_mut(&mut self) -> &mut [u8] {
89        let len = self.data.len();
90        &mut self.data[..len - HASH_LENGTH - IV_LENGTH]
91    }
92
93    fn get_content(&self) -> &[u8] {
94        let len = self.data.len();
95        &self.data[..len - HASH_LENGTH - IV_LENGTH]
96    }
97}
98
99#[cfg(test)]
100mod test {
101
102    use base64ct::{Base64UrlUnpadded, Encoding};
103
104    use super::*;
105
106    const ENCRYPTED_DATA: &str = "Q71YUIZydcgSwJQNOUCHhaTMqmIvslIafF5LluORJfJKydMGELHtYe_ydtBIrVuomEvIMurKaAUqlujQ8xzs4LBOxyf_lJ2unwqyFzk1TnCKBMNyRJybyL9RTBp90BExEwf2WwMtU4FDBUhP2bhWmxm7eQAAAAAAAAAAAAAAAAAAAADPLKD188CZczQr7eWGtyipuCZLZKQ2lBKL3S_R-nEgBA";
107    const INVALID_ENCRYPTED_DATA: &str = "h72AgcpeanqdZgXkeblx4zAygpn1r9Kx5wZDvH67K5QC8pGYCwxUvSsGiIWkAIswPUA6OPOEtGNPB4Je9zpppnnYX8YmbhghsBt7ClICnaOcs2a5i0IAzaUphhYkoz1r8BWLSCi9m0gpEw7tH1JhxK-k5My-7TgjfA";
108
109    const PASSWORD: &[u8; PASSWORD_LENGTH] = b"32KFFT_i4UpkJmyPwY2TGzgHpxfXs7zS";
110    const DECRYPTED_DATA: &str = "\n&build-2e76c839f5fd419db6b34682f4cdff1e\u{12}\u{7}default\u{18}\u{1}\"\u{7}webhook*\nmy-webhook2\u{e}event.received:\u{7}foo.barB\u{7}foo.barJ\u{7}foo.bar";
111
112    #[test]
113    fn test_verify_and_decrypt() {
114        let data = Base64UrlUnpadded::decode_vec(ENCRYPTED_DATA).unwrap();
115        let mut data: EncryptedData = EncryptedData::new(data);
116        let decrypted = data.verify_and_decrypt(PASSWORD).unwrap();
117        assert_eq!(decrypted, DECRYPTED_DATA.as_bytes());
118    }
119
120    #[test]
121    fn test_verify_hash() {
122        let data = Base64UrlUnpadded::decode_vec(ENCRYPTED_DATA).unwrap();
123        let data: EncryptedData = EncryptedData::new(data);
124        assert!(data.verify_hash(PASSWORD).unwrap());
125    }
126
127    #[test]
128    fn test_incorrect_hash() {
129        let data = Base64UrlUnpadded::decode_vec(INVALID_ENCRYPTED_DATA).unwrap();
130        let data: EncryptedData = EncryptedData::new(data);
131        assert!(!data.verify_hash(PASSWORD).unwrap());
132    }
133
134    #[test]
135    fn test_decrypt() {
136        let data = Base64UrlUnpadded::decode_vec(ENCRYPTED_DATA).unwrap();
137        let mut data: EncryptedData = EncryptedData::new(data);
138        let decrypted = data.decrypt(PASSWORD).unwrap();
139        assert_eq!(decrypted, DECRYPTED_DATA.as_bytes());
140    }
141
142    #[test]
143    fn test_incorrect_decrypt() {
144        let data = Base64UrlUnpadded::decode_vec(INVALID_ENCRYPTED_DATA).unwrap();
145        let mut data: EncryptedData = EncryptedData::new(data);
146        let decrypted = data.decrypt(PASSWORD).unwrap();
147        let res = std::str::from_utf8(decrypted);
148        assert!(res.is_err());
149    }
150}