Skip to main content

vector_core/crypto/gcm/
mod.rs

1//! AES-256-GCM with a 16-byte nonce and no associated data: the attachment cipher
2//! (0xChat-compatible). A stitched AES-NI/PCLMULQDQ engine where it has been measured
3//! to win, RustCrypto's `ctr` + `ghash` everywhere else; both produce the same bytes.
4
5#[cfg(target_arch = "x86_64")]
6mod x86;
7
8use aes::cipher::{BlockCipherEncrypt, KeyInit, KeyIvInit, StreamCipher};
9use aes::Aes256;
10use ghash::universal_hash::UniversalHash;
11use ghash::GHash;
12
13pub const TAG_LEN: usize = 16;
14
15// One per message, built and dropped on the stack: boxing the larger variant would cost
16// an allocation per seal.
17#[allow(clippy::large_enum_variant)]
18enum Engine {
19    #[cfg(target_arch = "x86_64")]
20    Stitched(x86::Gcm),
21    Generic { ctr: ctr::Ctr32BE<Aes256>, ghash: GHash, tag_mask: [u8; TAG_LEN] },
22}
23
24/// One GCM message fed in chunks. Every chunk but the last must be a multiple of 16
25/// bytes: GHASH may only pad the final block.
26pub struct Stream {
27    engine: Engine,
28    len: u64,
29    closed: bool,
30}
31
32impl Stream {
33    /// The stitched engine wherever it runs, key setup included: in `crypto_bench sweep`
34    /// it won every size from 0 bytes to 1 MiB against the generic one.
35    pub fn new(key: &[u8; 32], nonce: &[u8; 16]) -> Self {
36        #[cfg(target_arch = "x86_64")]
37        if x86::available() {
38            return Self::stitched(key, nonce);
39        }
40        Self::generic(key, nonce)
41    }
42
43    #[cfg(target_arch = "x86_64")]
44    fn stitched(key: &[u8; 32], nonce: &[u8; 16]) -> Self {
45        // SAFETY: callers establish `x86::instructions_present()`.
46        let gcm = unsafe { x86::Gcm::new(key, nonce) };
47        Stream { engine: Engine::Stitched(gcm), len: 0, closed: false }
48    }
49
50    fn generic(key: &[u8; 32], nonce: &[u8; 16]) -> Self {
51        // H = E_K(0^128); a 16-byte nonce derives J0 through GHASH (SP 800-38D 7.1).
52        let cipher = Aes256::new(&(*key).into());
53        let mut h = ghash::Block::default();
54        cipher.encrypt_block(&mut h);
55        let mut j0_hash = GHash::new(&h);
56        j0_hash.update_padded(nonce);
57        let mut len_block = ghash::Block::default();
58        len_block[8..].copy_from_slice(&128u64.to_be_bytes());
59        j0_hash.update(&[len_block]);
60        let j0 = j0_hash.finalize();
61
62        // Keystream block 0 masks the tag; the payload starts at block 1.
63        let mut ctr = ctr::Ctr32BE::<Aes256>::new(&(*key).into(), &j0);
64        let mut tag_mask = [0u8; TAG_LEN];
65        ctr.apply_keystream(&mut tag_mask);
66        Stream { engine: Engine::Generic { ctr, ghash: GHash::new(&h), tag_mask }, len: 0, closed: false }
67    }
68
69    fn account(&mut self, chunk: &[u8]) {
70        debug_assert!(!self.closed, "a chunk followed a partial one");
71        self.closed = !chunk.len().is_multiple_of(16);
72        self.len += chunk.len() as u64;
73    }
74
75    pub fn encrypt(&mut self, chunk: &mut [u8]) {
76        self.account(chunk);
77        match &mut self.engine {
78            // SAFETY: construction checked the instructions.
79            #[cfg(target_arch = "x86_64")]
80            Engine::Stitched(gcm) => unsafe { gcm.encrypt(chunk) },
81            Engine::Generic { ctr, ghash, .. } => {
82                ctr.apply_keystream(chunk);
83                ghash.update_padded(chunk);
84            }
85        }
86    }
87
88    pub fn decrypt(&mut self, chunk: &mut [u8]) {
89        self.account(chunk);
90        match &mut self.engine {
91            // SAFETY: construction checked the instructions.
92            #[cfg(target_arch = "x86_64")]
93            Engine::Stitched(gcm) => unsafe { gcm.decrypt(chunk) },
94            Engine::Generic { ctr, ghash, .. } => {
95                ghash.update_padded(chunk);
96                ctr.apply_keystream(chunk);
97            }
98        }
99    }
100
101    /// The tag over everything fed so far.
102    pub fn finish(self) -> [u8; TAG_LEN] {
103        match self.engine {
104            // SAFETY: construction checked the instructions.
105            #[cfg(target_arch = "x86_64")]
106            Engine::Stitched(gcm) => unsafe { gcm.finish(self.len) },
107            Engine::Generic { mut ghash, tag_mask, .. } => {
108                let mut lengths = ghash::Block::default();
109                lengths[8..].copy_from_slice(&(self.len * 8).to_be_bytes());
110                ghash.update(&[lengths]);
111                let mut tag = [0u8; TAG_LEN];
112                for (t, (s, m)) in tag.iter_mut().zip(ghash.finalize().iter().zip(tag_mask)) {
113                    *t = s ^ m;
114                }
115                tag
116            }
117        }
118    }
119}
120
121/// Seal `data` in place and return the tag.
122pub fn seal(key: &[u8; 32], nonce: &[u8; 16], data: &mut [u8]) -> [u8; TAG_LEN] {
123    let mut s = Stream::new(key, nonce);
124    s.encrypt(data);
125    s.finish()
126}
127
128/// The tag did not authenticate the ciphertext.
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130pub struct TagMismatch;
131
132/// Open `data` in place. On a bad tag the buffer is wiped, so no unauthenticated
133/// plaintext survives the call.
134pub fn open(key: &[u8; 32], nonce: &[u8; 16], data: &mut [u8], tag: &[u8; TAG_LEN]) -> Result<(), TagMismatch> {
135    let mut s = Stream::new(key, nonce);
136    s.decrypt(data);
137    if tags_equal(&s.finish(), tag) {
138        Ok(())
139    } else {
140        super::wipe(data);
141        Err(TagMismatch)
142    }
143}
144
145/// Every byte compared, whatever the first difference.
146pub fn tags_equal(a: &[u8; TAG_LEN], b: &[u8; TAG_LEN]) -> bool {
147    let diff = a.iter().zip(b).fold(0u8, |acc, (x, y)| acc | (x ^ y));
148    core::hint::black_box(diff) == 0
149}
150
151/// The engines with their choice forced, for measuring where the stitched one pays.
152#[doc(hidden)]
153pub mod bench {
154    use super::*;
155
156    /// Whether this CPU can run the stitched engine at all (VAES or not).
157    pub fn stitched_present() -> bool {
158        #[cfg(target_arch = "x86_64")]
159        {
160            x86::instructions_present()
161        }
162        #[cfg(not(target_arch = "x86_64"))]
163        {
164            false
165        }
166    }
167
168    /// Whether production picks the stitched engine on this CPU.
169    pub fn stitched_in_production() -> bool {
170        #[cfg(target_arch = "x86_64")]
171        {
172            x86::available()
173        }
174        #[cfg(not(target_arch = "x86_64"))]
175        {
176            false
177        }
178    }
179
180    /// A stream on the named engine ("stitched" or "generic"), or None if this CPU lacks it.
181    pub fn stream(engine: &str, key: &[u8; 32], nonce: &[u8; 16]) -> Option<Stream> {
182        match engine {
183            #[cfg(target_arch = "x86_64")]
184            "stitched" if x86::instructions_present() => Some(Stream::stitched(key, nonce)),
185            "generic" => Some(Stream::generic(key, nonce)),
186            _ => None,
187        }
188    }
189}
190
191#[cfg(test)]
192mod tests;