Skip to main content

libsignal_service/
attachment_cipher.rs

1use aes::cipher::consts::{U32, U64};
2use aes::cipher::{block_padding::Pkcs7, Array, BlockModeEncrypt};
3use aes::cipher::{BlockModeDecrypt, KeyIvInit};
4use hmac::{Hmac, KeyInit, Mac};
5use sha2::Sha256;
6
7type Aes256CbcEnc = cbc::Encryptor<aes::Aes256>;
8type Aes256CbcDec = cbc::Decryptor<aes::Aes256>;
9
10#[derive(thiserror::Error, Debug, PartialEq, Eq)]
11pub enum AttachmentCipherError {
12    #[error("MAC verification error")]
13    MacError,
14    #[error("Padding verification error")]
15    PaddingError,
16}
17
18/// Encrypts an attachment in place, given the key material.
19///
20/// The Vec will be reused when it has enough space to house the MAC,
21/// otherwise reallocation might happen.
22#[tracing::instrument(skip(iv, key, plaintext))]
23pub fn encrypt_in_place(iv: [u8; 16], key: [u8; 64], plaintext: &mut Vec<u8>) {
24    let key: &Array<u8, U64> = (&key).into();
25    let (aes_half, mac_half) = key.split_ref::<U32>();
26
27    let plaintext_len = plaintext.len();
28    plaintext.reserve(plaintext.len() + 16 + 16);
29
30    // Prepend IV
31    plaintext.extend(&[0u8; 16]);
32    plaintext.copy_within(..plaintext_len, 16);
33    plaintext[0..16].copy_from_slice(&iv);
34
35    // Pad with zeroes for padding
36    plaintext.extend(&[0u8; 16]);
37
38    let cipher = Aes256CbcEnc::new(aes_half, &iv.into());
39
40    let buffer = plaintext;
41    let ciphertext_slice = cipher
42        .encrypt_padded::<Pkcs7>(&mut buffer[16..], plaintext_len)
43        .expect("encrypted ciphertext");
44    let ciphertext_len = ciphertext_slice.len();
45    // Correct length for padding
46    buffer.truncate(16 + ciphertext_len);
47
48    // Compute and append MAC
49    let mut mac = Hmac::<Sha256>::new_from_slice(mac_half)
50        .expect("fixed length key material");
51    mac.update(buffer);
52    buffer.extend(mac.finalize().into_bytes());
53}
54
55/// Decrypts an attachment in place, given the key material.
56///
57/// On error, ciphertext is not changed.
58#[tracing::instrument(skip(key, ciphertext))]
59pub fn decrypt_in_place(
60    key: [u8; 64],
61    ciphertext: &mut Vec<u8>,
62) -> Result<(), AttachmentCipherError> {
63    let key: &Array<u8, U64> = (&key).into();
64    let (aes_half, mac_half) = key.split_ref::<U32>();
65
66    let ciphertext_len = ciphertext.len();
67
68    let (buffer, their_mac) = ciphertext.split_at_mut(ciphertext_len - 32);
69
70    // Compute and append MAC
71    let mut mac = Hmac::<Sha256>::new_from_slice(mac_half)
72        .expect("fixed length key material");
73    mac.update(buffer);
74    mac.verify_slice(their_mac)
75        .map_err(|_| AttachmentCipherError::MacError)?;
76
77    let (iv, buffer) = buffer
78        .split_first_chunk_mut::<16>()
79        .ok_or(AttachmentCipherError::PaddingError)?;
80
81    let cipher = Aes256CbcDec::new(aes_half, (&*iv).into());
82
83    let plaintext_slice = cipher
84        .decrypt_padded::<Pkcs7>(buffer)
85        .map_err(|_| AttachmentCipherError::PaddingError)?;
86
87    let plaintext_len = plaintext_slice.len();
88
89    // Get rid of IV and MAC
90    ciphertext.copy_within(16..(plaintext_len + 16), 0);
91    ciphertext.truncate(plaintext_len);
92
93    Ok(())
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    use rand::prelude::*;
101
102    #[test]
103    fn attachment_encrypt_decrypt() -> Result<(), AttachmentCipherError> {
104        let mut key = [0u8; 64];
105        let mut iv = [0u8; 16];
106        rand::rng().fill_bytes(&mut key);
107        rand::rng().fill_bytes(&mut iv);
108
109        let plaintext = b"Peter Parker";
110        let mut buf = Vec::from(plaintext as &[u8]);
111        encrypt_in_place(iv, key, &mut buf);
112        assert_ne!(&buf, &plaintext);
113        decrypt_in_place(key, &mut buf)?;
114        assert_eq!(&buf, &plaintext);
115        Ok(())
116    }
117
118    #[test]
119    fn attachment_encrypt_decrypt_empty() -> Result<(), AttachmentCipherError> {
120        let mut key = [0u8; 64];
121        let mut iv = [0u8; 16];
122        rand::rng().fill_bytes(&mut key);
123        rand::rng().fill_bytes(&mut iv);
124        let plaintext = b"";
125        let mut buf = Vec::from(plaintext as &[u8]);
126        encrypt_in_place(iv, key, &mut buf);
127        assert_ne!(&buf, &plaintext);
128        decrypt_in_place(key, &mut buf)?;
129        assert_eq!(&buf, &plaintext);
130        Ok(())
131    }
132
133    #[test]
134    fn attachment_encrypt_decrypt_bad_key() {
135        let mut key = [0u8; 64];
136        let mut iv = [0u8; 16];
137        rand::rng().fill_bytes(&mut key);
138        rand::rng().fill_bytes(&mut iv);
139        let plaintext = b"Peter Parker";
140        let mut buf = Vec::from(plaintext as &[u8]);
141        encrypt_in_place(iv, key, &mut buf);
142
143        // Generate bad key
144        rand::rng().fill_bytes(&mut key);
145        assert_eq!(
146            decrypt_in_place(key, &mut buf).unwrap_err(),
147            AttachmentCipherError::MacError
148        );
149        assert_ne!(&buf, &plaintext);
150    }
151
152    #[test]
153    fn know_answer_test_attachment() -> Result<(), AttachmentCipherError> {
154        let mut ciphertext = include!("kat.bin.rs");
155        let key_material = [
156            52, 102, 97, 87, 153, 192, 64, 116, 93, 96, 57, 110, 6, 197, 208,
157            85, 49, 249, 154, 137, 116, 124, 112, 107, 8, 158, 48, 4, 8, 66,
158            173, 5, 28, 16, 199, 226, 234, 38, 69, 167, 163, 34, 107, 164, 15,
159            118, 101, 146, 34, 213, 85, 164, 110, 83, 129, 245, 62, 44, 158,
160            78, 205, 62, 153, 108,
161        ];
162
163        decrypt_in_place(key_material, &mut ciphertext)?;
164        // This 32 is given by the AttachmentPointer
165        ciphertext.truncate(32);
166        assert_eq!(ciphertext, b"test for libsignal-service-rust\n");
167        Ok(())
168    }
169}