Skip to main content

cryptography/modes/
siv.rs

1//! Synthetic IV (SIV) authenticated encryption (RFC 5297).
2//!
3//! This module implements the AES-SIV construction with CMAC-based S2V and
4//! CTR encryption. The caller supplies separate block-cipher instances for the
5//! CMAC key half and CTR key half.
6
7use crate::BlockCipher;
8
9#[inline]
10fn dbl_block(block: [u8; 16]) -> [u8; 16] {
11    let mut out = [0u8; 16];
12    let mut carry = 0u8;
13    for i in (0..16).rev() {
14        out[i] = (block[i] << 1) | carry;
15        carry = block[i] >> 7;
16    }
17    if carry != 0 {
18        out[15] ^= 0x87;
19    }
20    out
21}
22
23#[inline]
24fn xor_block(a: [u8; 16], b: [u8; 16]) -> [u8; 16] {
25    let mut out = [0u8; 16];
26    for i in 0..16 {
27        out[i] = a[i] ^ b[i];
28    }
29    out
30}
31
32#[inline]
33fn xor_in_place(dst: &mut [u8], src: &[u8]) {
34    for (d, s) in dst.iter_mut().zip(src.iter()) {
35        *d ^= *s;
36    }
37}
38
39#[inline]
40fn rb_for(block_len: usize) -> u8 {
41    match block_len {
42        8 => 0x1b,
43        16 => 0x87,
44        _ => panic!("CMAC only supports 64-bit or 128-bit block ciphers"),
45    }
46}
47
48fn dbl(block: &[u8]) -> Vec<u8> {
49    let mut out = vec![0u8; block.len()];
50    let mut carry = 0u8;
51    for (o, &b) in out.iter_mut().rev().zip(block.iter().rev()) {
52        *o = (b << 1) | carry;
53        carry = b >> 7;
54    }
55    if carry != 0 {
56        let last = out.len() - 1;
57        out[last] ^= rb_for(block.len());
58    }
59    out
60}
61
62fn cmac_compute<C: BlockCipher>(cipher: &C, data: &[u8]) -> [u8; 16] {
63    assert_eq!(C::BLOCK_LEN, 16, "SIV requires a 128-bit block cipher");
64    let blk = C::BLOCK_LEN;
65    let mut l = vec![0u8; blk];
66    cipher.encrypt(&mut l);
67    let k1 = dbl(&l);
68    let k2 = dbl(&k1);
69
70    let n = if data.is_empty() {
71        1
72    } else {
73        data.len().div_ceil(blk)
74    };
75    let last_complete = !data.is_empty() && data.len().is_multiple_of(blk);
76
77    let mut x = vec![0u8; blk];
78    let mut y = vec![0u8; blk];
79
80    for block in data.chunks(blk).take(n.saturating_sub(1)) {
81        y.copy_from_slice(&x);
82        xor_in_place(&mut y, block);
83        cipher.encrypt(&mut y);
84        x.copy_from_slice(&y);
85    }
86
87    let mut m_last = vec![0u8; blk];
88    if last_complete {
89        let start = (n - 1) * blk;
90        m_last.copy_from_slice(&data[start..start + blk]);
91        xor_in_place(&mut m_last, &k1);
92    } else {
93        let start = (n - 1) * blk;
94        let rem = data.len().saturating_sub(start);
95        if rem != 0 {
96            m_last[..rem].copy_from_slice(&data[start..]);
97        }
98        m_last[rem] = 0x80;
99        xor_in_place(&mut m_last, &k2);
100    }
101
102    xor_in_place(&mut m_last, &x);
103    cipher.encrypt(&mut m_last);
104    m_last.try_into().expect("CMAC output is one block")
105}
106
107#[inline]
108fn cmac_block<C: BlockCipher>(cipher: &C, data: &[u8]) -> [u8; 16] {
109    cmac_compute(cipher, data)
110}
111
112fn s2v<C: BlockCipher>(mac_cipher: &C, components: &[&[u8]], plaintext: &[u8]) -> [u8; 16] {
113    // RFC 5297 S2V with plaintext as the final component.
114    let mut d = cmac_block(mac_cipher, &[0u8; 16]);
115    for component in components {
116        d = xor_block(dbl_block(d), cmac_block(mac_cipher, component));
117    }
118
119    let t = if plaintext.len() >= 16 {
120        let mut t = plaintext.to_vec();
121        let start = t.len() - 16;
122        for i in 0..16 {
123            t[start + i] ^= d[i];
124        }
125        t
126    } else {
127        let mut padded = [0u8; 16];
128        padded[..plaintext.len()].copy_from_slice(plaintext);
129        padded[plaintext.len()] = 0x80;
130        let mixed = xor_block(dbl_block(d), padded);
131        mixed.to_vec()
132    };
133
134    cmac_block(mac_cipher, &t)
135}
136
137#[inline]
138fn clear_siv_ctr_bits(counter: &mut [u8; 16]) {
139    // RFC 5297 section 2.6: clear bit 31 and bit 63.
140    counter[8] &= 0x7f;
141    counter[12] &= 0x7f;
142}
143
144#[inline]
145fn increment_be32(block: &mut [u8; 16]) {
146    for b in block[12..].iter_mut().rev() {
147        let (next, carry) = b.overflowing_add(1);
148        *b = next;
149        if !carry {
150            break;
151        }
152    }
153}
154
155fn ctr_apply<C: BlockCipher>(cipher: &C, initial_counter: &[u8; 16], data: &mut [u8]) {
156    let mut counter = *initial_counter;
157    for chunk in data.chunks_mut(16) {
158        let mut stream = counter;
159        cipher.encrypt(&mut stream);
160        for i in 0..chunk.len() {
161            chunk[i] ^= stream[i];
162        }
163        increment_be32(&mut counter);
164    }
165}
166
167/// RFC 5297 SIV construction parameterized by two block-cipher instances.
168pub struct Siv<C> {
169    mac_cipher: C,
170    ctr_cipher: C,
171}
172
173impl<C> Siv<C> {
174    /// Construct from separate CMAC and CTR ciphers.
175    pub fn new(mac_cipher: C, ctr_cipher: C) -> Self {
176        Self {
177            mac_cipher,
178            ctr_cipher,
179        }
180    }
181
182    /// Borrow the CMAC-side cipher.
183    pub fn mac_cipher(&self) -> &C {
184        &self.mac_cipher
185    }
186
187    /// Borrow the CTR-side cipher.
188    pub fn ctr_cipher(&self) -> &C {
189        &self.ctr_cipher
190    }
191}
192
193impl<C: BlockCipher> Siv<C> {
194    /// Encrypt using an explicit ordered vector of associated-data components.
195    pub fn encrypt_with_components(
196        &self,
197        components: &[&[u8]],
198        plaintext: &[u8],
199    ) -> (Vec<u8>, [u8; 16]) {
200        let tag = s2v(&self.mac_cipher, components, plaintext);
201        let mut counter = tag;
202        clear_siv_ctr_bits(&mut counter);
203
204        let mut ciphertext = plaintext.to_vec();
205        ctr_apply(&self.ctr_cipher, &counter, &mut ciphertext);
206        (ciphertext, tag)
207    }
208
209    /// Decrypt/authenticate using an explicit vector of associated-data components.
210    pub fn decrypt_with_components(
211        &self,
212        components: &[&[u8]],
213        ciphertext: &mut [u8],
214        tag: &[u8; 16],
215    ) -> bool {
216        let mut counter = *tag;
217        clear_siv_ctr_bits(&mut counter);
218
219        let mut plaintext = ciphertext.to_vec();
220        ctr_apply(&self.ctr_cipher, &counter, &mut plaintext);
221        let expected = s2v(&self.mac_cipher, components, &plaintext);
222        if crate::ct::constant_time_eq_mask(&expected, tag) != u8::MAX {
223            // SIV must decrypt before authenticating (S2V is over plaintext),
224            // so zeroize the speculative plaintext before dropping on failure.
225            crate::ct::zeroize_slice(&mut plaintext);
226            return false;
227        }
228        ciphertext.copy_from_slice(&plaintext);
229        true
230    }
231
232    /// Encrypt and return `(ciphertext, detached_tag)`.
233    ///
234    /// RFC 5297 treats the `nonce` as an associated-data component.
235    pub fn encrypt(&self, nonce: &[u8], aad: &[u8], plaintext: &[u8]) -> (Vec<u8>, [u8; 16]) {
236        if nonce.is_empty() {
237            self.encrypt_with_components(&[aad], plaintext)
238        } else {
239            self.encrypt_with_components(&[aad, nonce], plaintext)
240        }
241    }
242
243    /// Authenticate and decrypt in place.
244    ///
245    /// Returns `false` and leaves `ciphertext` unchanged on failure.
246    pub fn decrypt(&self, nonce: &[u8], aad: &[u8], ciphertext: &mut [u8], tag: &[u8; 16]) -> bool {
247        if nonce.is_empty() {
248            self.decrypt_with_components(&[aad], ciphertext, tag)
249        } else {
250            self.decrypt_with_components(&[aad, nonce], ciphertext, tag)
251        }
252    }
253}
254
255#[cfg(test)]
256mod tests {
257    use super::Siv;
258    use crate::Aes128;
259
260    fn unhex_ws(input: &str) -> Vec<u8> {
261        let compact: String = input.chars().filter(|c| !c.is_whitespace()).collect();
262        let mut out = Vec::with_capacity(compact.len() / 2);
263        let bytes = compact.as_bytes();
264        let mut i = 0usize;
265        while i + 1 < bytes.len() {
266            let hi = (bytes[i] as char).to_digit(16).expect("hex") as u8;
267            let lo = (bytes[i + 1] as char).to_digit(16).expect("hex") as u8;
268            out.push((hi << 4) | lo);
269            i += 2;
270        }
271        out
272    }
273
274    #[test]
275    fn rfc5297_a1_deterministic_vector() {
276        // RFC 5297 Appendix A.1.
277        let key = <[u8; 32]>::try_from(unhex_ws(
278            "fffefdfc fbfaf9f8 f7f6f5f4 f3f2f1f0
279             f0f1f2f3 f4f5f6f7 f8f9fafb fcfdfeff",
280        ))
281        .expect("key");
282        let k1: [u8; 16] = key[..16].try_into().expect("k1");
283        let k2: [u8; 16] = key[16..].try_into().expect("k2");
284        let aad = unhex_ws("10111213 14151617 18191a1b 1c1d1e1f 20212223 24252627");
285        let nonce: [u8; 0] = [];
286        let plaintext = unhex_ws("11223344 55667788 99aabbcc ddee");
287        let expected_tag =
288            <[u8; 16]>::try_from(unhex_ws("85632d07 c6e8f37f 950acd32 0a2ecc93")).expect("tag");
289        let expected_ct = unhex_ws("40c02b96 90c4dc04 daef7f6a fe5c");
290
291        let siv = Siv::new(Aes128::new(&k1), Aes128::new(&k2));
292        let (ct, tag) = siv.encrypt(&nonce, &aad, &plaintext);
293        assert_eq!(tag, expected_tag);
294        assert_eq!(ct, expected_ct);
295
296        let mut roundtrip = ct.clone();
297        assert!(siv.decrypt(&nonce, &aad, &mut roundtrip, &tag));
298        assert_eq!(roundtrip, plaintext);
299    }
300
301    #[test]
302    fn rfc5297_a2_nonce_based_vector() {
303        // RFC 5297 Appendix A.2.
304        let key = <[u8; 32]>::try_from(unhex_ws(
305            "7f7e7d7c 7b7a7978 77767574 73727170
306             40414243 44454647 48494a4b 4c4d4e4f",
307        ))
308        .expect("key");
309        let k1: [u8; 16] = key[..16].try_into().expect("k1");
310        let k2: [u8; 16] = key[16..].try_into().expect("k2");
311        let ad1 = unhex_ws(
312            "00112233 44556677 8899aabb ccddeeff
313             deaddada deaddada ffeeddcc bbaa9988
314             77665544 33221100",
315        );
316        let ad2 = unhex_ws("10203040 50607080 90a0");
317        let nonce = unhex_ws("09f91102 9d74e35b d84156c5 635688c0");
318        let plaintext = unhex_ws(
319            "74686973 20697320 736f6d65 20706c61
320             696e7465 78742074 6f20656e 63727970
321             74207573 696e6720 5349562d 414553",
322        );
323        let expected_tag =
324            <[u8; 16]>::try_from(unhex_ws("7bdb6e3b 432667eb 06f4d14b ff2fbd0f")).expect("tag");
325        let expected_ct = unhex_ws(
326            "cb900f2f ddbe4043 26601965 c889bf17
327             dba77ceb 094fa663 b7a3f748 ba8af829
328             ea64ad54 4a272e9c 485b62a3 fd5c0d",
329        );
330
331        let siv = Siv::new(Aes128::new(&k1), Aes128::new(&k2));
332        let (ct, tag) = siv.encrypt_with_components(&[&ad1, &ad2, &nonce], &plaintext);
333        assert_eq!(tag, expected_tag);
334        assert_eq!(ct, expected_ct);
335
336        let mut roundtrip = ct.clone();
337        assert!(siv.decrypt_with_components(&[&ad1, &ad2, &nonce], &mut roundtrip, &tag));
338        assert_eq!(roundtrip, plaintext);
339    }
340
341    #[test]
342    fn tamper_rejected_without_plaintext_commit() {
343        let k1 = [0x11u8; 16];
344        let k2 = [0x22u8; 16];
345        let nonce = [0x33u8; 16];
346        let aad = b"aad";
347        let plaintext = b"siv plaintext".to_vec();
348        let siv = Siv::new(Aes128::new(&k1), Aes128::new(&k2));
349        let (mut ct, tag) = siv.encrypt(&nonce, aad, &plaintext);
350
351        ct[0] ^= 1;
352        let snapshot = ct.clone();
353        assert!(!siv.decrypt(&nonce, aad, &mut ct, &tag));
354        assert_eq!(ct, snapshot);
355    }
356}