Skip to main content

ghast_protocol/
crypto.rs

1//! AES-128-CFB8 stream cipher used after the login encryption handshake.
2//!
3//! Key and IV are both the 16-byte shared secret (negotiated by `ghast-auth`). Both
4//! directions are independent streams; [`Cfb8Encryptor`] and [`Cfb8Decryptor`] keep the
5//! running state between calls, so data can be processed in arbitrary pieces.
6//!
7//! Encryption is inherently serial in CFB8 (each output byte feeds the next block). The
8//! decryptor exploits that every keystream block depends only on *ciphertext*, which is
9//! all known up front: it batches many AES block encryptions per call so the
10//! hardware AES pipeline (AES-NI, ARMv8 crypto extensions — runtime-detected by the
11//! `aes` crate) stays full. See the `decrypt` benchmark for the speedup over a serial
12//! implementation.
13
14use aes::Aes128;
15use aes::cipher::{BlockCipherEncrypt, KeyInit, KeyIvInit};
16
17/// Bytes decrypted per AES batch.
18const BATCH: usize = 64;
19
20/// Streaming AES-128-CFB8 encryptor.
21pub struct Cfb8Encryptor {
22    inner: cfb8::Encryptor<Aes128>,
23}
24
25impl std::fmt::Debug for Cfb8Encryptor {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        f.write_str("Cfb8Encryptor { .. }")
28    }
29}
30
31impl Cfb8Encryptor {
32    /// Creates an encryptor with key = IV = `secret`.
33    pub fn new(secret: &[u8; 16]) -> Self {
34        Cfb8Encryptor {
35            inner: cfb8::Encryptor::<Aes128>::new(&(*secret).into(), &(*secret).into()),
36        }
37    }
38
39    /// Encrypts `buf` in place, continuing the stream.
40    #[inline]
41    pub fn encrypt(&mut self, buf: &mut [u8]) {
42        self.inner.encrypt(buf);
43    }
44}
45
46/// Streaming AES-128-CFB8 decryptor with batched (pipelined) AES.
47pub struct Cfb8Decryptor {
48    cipher: Aes128,
49    /// The last 16 ciphertext bytes (initially the IV).
50    shift: [u8; 16],
51    /// Scratch blocks for one batch.
52    blocks: Box<[aes::Block; BATCH]>,
53}
54
55impl std::fmt::Debug for Cfb8Decryptor {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        f.write_str("Cfb8Decryptor { .. }")
58    }
59}
60
61impl Cfb8Decryptor {
62    /// Creates a decryptor with key = IV = `secret`.
63    pub fn new(secret: &[u8; 16]) -> Self {
64        Cfb8Decryptor {
65            cipher: Aes128::new(&(*secret).into()),
66            shift: *secret,
67            blocks: Box::new([aes::Block::default(); BATCH]),
68        }
69    }
70
71    /// Decrypts `buf` in place, continuing the stream.
72    pub fn decrypt(&mut self, buf: &mut [u8]) {
73        // `window` = last 16 ciphertext bytes followed by this batch's ciphertext; the
74        // keystream block for byte i is AES(window[i..i + 16]).
75        let mut window = [0u8; 16 + BATCH];
76        for chunk in buf.chunks_mut(BATCH) {
77            let n = chunk.len();
78            window[..16].copy_from_slice(&self.shift);
79            window[16..16 + n].copy_from_slice(chunk);
80            for (i, block) in self.blocks[..n].iter_mut().enumerate() {
81                block.copy_from_slice(&window[i..i + 16]);
82            }
83            self.cipher.encrypt_blocks(&mut self.blocks[..n]);
84            for (b, ks) in chunk.iter_mut().zip(self.blocks[..n].iter()) {
85                *b ^= ks[0];
86            }
87            self.shift.copy_from_slice(&window[n..n + 16]);
88        }
89    }
90}
91
92/// A matched pair of stream ciphers for one connection.
93#[derive(Debug)]
94pub struct CipherPair {
95    /// Encrypts outbound bytes.
96    pub encryptor: Cfb8Encryptor,
97    /// Decrypts inbound bytes.
98    pub decryptor: Cfb8Decryptor,
99}
100
101impl CipherPair {
102    /// Creates both directions from the shared secret.
103    pub fn new(secret: &[u8; 16]) -> Self {
104        CipherPair {
105            encryptor: Cfb8Encryptor::new(secret),
106            decryptor: Cfb8Decryptor::new(secret),
107        }
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    fn hex(s: &str) -> Vec<u8> {
116        (0..s.len())
117            .step_by(2)
118            .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
119            .collect()
120    }
121
122    /// NIST SP 800-38A, F.3.7 / F.3.8 (CFB8-AES128). Uses a distinct IV, so drive the
123    /// cipher types directly instead of through the key = IV constructor.
124    #[test]
125    fn nist_sp800_38a_cfb8() {
126        let key: [u8; 16] = hex("2b7e151628aed2a6abf7158809cf4f3c").try_into().unwrap();
127        let iv: [u8; 16] = hex("000102030405060708090a0b0c0d0e0f").try_into().unwrap();
128        let pt = hex("6bc1bee22e409f96e93d7e117393172aae2d");
129        let ct = hex("3b79424c9c0dd436bace9e0ed4586a4f32b9");
130
131        let mut enc = cfb8::Encryptor::<Aes128>::new(&key.into(), &iv.into());
132        let mut buf = pt.clone();
133        enc.encrypt(&mut buf);
134        assert_eq!(buf, ct);
135
136        let mut dec = Cfb8Decryptor {
137            cipher: Aes128::new(&key.into()),
138            shift: iv,
139            blocks: Box::new([aes::Block::default(); BATCH]),
140        };
141        let mut buf = ct.clone();
142        dec.decrypt(&mut buf);
143        assert_eq!(buf, pt);
144    }
145
146    #[test]
147    fn stream_in_pieces_matches_reference() {
148        let secret = [7u8; 16];
149        let data: Vec<u8> = (0..5000u32)
150            .map(|i| (i.wrapping_mul(2654435761) >> 13) as u8)
151            .collect();
152        let mut ct = data.clone();
153        let mut enc = Cfb8Encryptor::new(&secret);
154        // Encrypt in uneven pieces.
155        let mut off = 0;
156        for step in [1, 15, 16, 17, 63, 64, 65, 200, 1000].iter().cycle() {
157            if off >= ct.len() {
158                break;
159            }
160            let end = (off + step).min(ct.len());
161            enc.encrypt(&mut ct[off..end]);
162            off = end;
163        }
164        // Reference: the serial cfb8 decryptor in one go.
165        let mut reference = ct.clone();
166        cfb8::Decryptor::<Aes128>::new(&secret.into(), &secret.into()).decrypt(&mut reference);
167        assert_eq!(reference, data);
168        // Ours, in different uneven pieces.
169        let mut pt = ct.clone();
170        let mut dec = Cfb8Decryptor::new(&secret);
171        let mut off = 0;
172        for step in [3, 64, 1, 129, 7, 4096].iter().cycle() {
173            if off >= pt.len() {
174                break;
175            }
176            let end = (off + step).min(pt.len());
177            dec.decrypt(&mut pt[off..end]);
178            off = end;
179        }
180        assert_eq!(pt, data);
181    }
182}