Skip to main content

firebird_wire/auth/
wirecrypt.rs

1//! Cifras de criptografia do wire negociadas após o SRP.
2//!
3//! O Firebird chaveia a cifra simétrica com a chave de sessão SRP `K`. Cada
4//! direção usa uma instância de cifra independente inicializada com a mesma
5//! chave, então o cliente mantém uma cifra de leitura e outra de escrita separadas.
6//!
7//! Estão implementados `Arc4` (RC4) e `ChaCha`/`ChaCha64` (ChaCha20), os três
8//! plugins padrão de `WireCryptPlugin` do FB5.
9//!
10//! - **Arc4**: chaveado direto com a chave de sessão SRP `K`; mesma chave nas
11//!   duas direções.
12//! - **ChaCha / ChaCha64**: a chave é `SHA-256(K)` (32 bytes); o *nonce* é
13//!   anunciado pelo servidor no buffer de troca de chaves do handshake, logo
14//!   após o nome do plugin (`"ChaCha\0"` + 12 bytes, ou `"ChaCha64\0"` + 8
15//!   bytes). Contador inicial 0; mesma chave+nonce nas duas direções. ChaCha usa
16//!   nonce de 96 bits + contador de 32 bits (estilo IETF/RFC 8439); ChaCha64 usa
17//!   nonce de 64 bits + contador de 64 bits (estilo DJB original).
18
19use crate::wire::stream::Cipher;
20use sha2::{Digest, Sha256};
21
22/// O plugin de criptografia do wire a negociar.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum WireCryptPlugin {
25    /// Cifra de fluxo RC4 (`Arc4`).
26    Arc4,
27    /// ChaCha20 com nonce de 96 bits e contador de 32 bits (IETF).
28    ChaCha,
29    /// ChaCha20 com nonce de 64 bits e contador de 64 bits (DJB original).
30    ChaCha64,
31}
32
33impl WireCryptPlugin {
34    /// Nome textual que o servidor Firebird espera no `op_crypt`.
35    pub fn name(self) -> &'static str {
36        match self {
37            WireCryptPlugin::Arc4 => "Arc4",
38            WireCryptPlugin::ChaCha => "ChaCha",
39            WireCryptPlugin::ChaCha64 => "ChaCha64",
40        }
41    }
42}
43
44/// Cifra de fluxo RC4 clássica.
45#[derive(Clone)]
46pub struct Rc4 {
47    s: [u8; 256],
48    i: u8,
49    j: u8,
50}
51
52impl Rc4 {
53    /// Algoritmo de escalonamento de chave.
54    pub fn new(key: &[u8]) -> Self {
55        assert!(!key.is_empty(), "RC4 key must be non-empty");
56        let mut s = [0u8; 256];
57        for (i, b) in s.iter_mut().enumerate() {
58            *b = i as u8;
59        }
60        let mut j: u8 = 0;
61        for i in 0..256 {
62            j = j.wrapping_add(s[i]).wrapping_add(key[i % key.len()]);
63            s.swap(i, j as usize);
64        }
65        Rc4 { s, i: 0, j: 0 }
66    }
67
68    #[inline]
69    fn next_byte(&mut self) -> u8 {
70        self.i = self.i.wrapping_add(1);
71        self.j = self.j.wrapping_add(self.s[self.i as usize]);
72        self.s.swap(self.i as usize, self.j as usize);
73        let idx = self.s[self.i as usize].wrapping_add(self.s[self.j as usize]);
74        self.s[idx as usize]
75    }
76}
77
78impl Cipher for Rc4 {
79    fn process(&mut self, data: &mut [u8]) {
80        for b in data.iter_mut() {
81            *b ^= self.next_byte();
82        }
83    }
84}
85
86/// Cifra de fluxo ChaCha20 (RFC 8439 e a variante DJB de 64 bits).
87///
88/// Mantém o estado de 16 palavras de 32 bits e um bloco de keystream de 64
89/// bytes consumido byte a byte; ao esgotar, o contador avança e um novo bloco é
90/// gerado. Como cifra de fluxo, [`Cipher::process`] faz XOR in-place.
91#[derive(Clone)]
92pub struct ChaCha20 {
93    state: [u32; 16],
94    block: [u8; 64],
95    pos: usize,
96    /// Contador de 64 bits (ChaCha64) em vez de 32 bits (ChaCha/IETF).
97    wide_counter: bool,
98}
99
100const CHACHA_CONST: [u32; 4] = [0x6170_7865, 0x3320_646e, 0x7962_2d32, 0x6b20_6574];
101
102impl ChaCha20 {
103    /// Cria a cifra a partir de uma chave de 32 bytes e de um nonce. Um nonce de
104    /// 12 bytes seleciona o layout IETF (contador de 32 bits); um de 8 bytes, o
105    /// layout DJB (contador de 64 bits). O contador inicial é 0.
106    pub fn new(key: &[u8], nonce: &[u8]) -> Self {
107        assert_eq!(key.len(), 32, "ChaCha20 key must be 32 bytes");
108        let mut state = [0u32; 16];
109        state[0..4].copy_from_slice(&CHACHA_CONST);
110        for i in 0..8 {
111            state[4 + i] = u32::from_le_bytes(key[i * 4..i * 4 + 4].try_into().unwrap());
112        }
113        let wide_counter = match nonce.len() {
114            12 => {
115                // contador (palavra 12) = 0; nonce nas palavras 13..16.
116                for i in 0..3 {
117                    state[13 + i] = u32::from_le_bytes(nonce[i * 4..i * 4 + 4].try_into().unwrap());
118                }
119                false
120            }
121            8 => {
122                // contador de 64 bits (palavras 12,13) = 0; nonce em 14,15.
123                state[14] = u32::from_le_bytes(nonce[0..4].try_into().unwrap());
124                state[15] = u32::from_le_bytes(nonce[4..8].try_into().unwrap());
125                true
126            }
127            other => panic!("ChaCha nonce must be 12 or 8 bytes, got {other}"),
128        };
129        let mut c = ChaCha20 {
130            state,
131            block: [0; 64],
132            pos: 64,
133            wide_counter,
134        };
135        c.refill();
136        c
137    }
138
139    #[inline]
140    fn quarter_round(x: &mut [u32; 16], a: usize, b: usize, c: usize, d: usize) {
141        x[a] = x[a].wrapping_add(x[b]);
142        x[d] = (x[d] ^ x[a]).rotate_left(16);
143        x[c] = x[c].wrapping_add(x[d]);
144        x[b] = (x[b] ^ x[c]).rotate_left(12);
145        x[a] = x[a].wrapping_add(x[b]);
146        x[d] = (x[d] ^ x[a]).rotate_left(8);
147        x[c] = x[c].wrapping_add(x[d]);
148        x[b] = (x[b] ^ x[c]).rotate_left(7);
149    }
150
151    /// Gera o bloco de keystream do contador atual e avança o contador.
152    fn refill(&mut self) {
153        let mut x = self.state;
154        for _ in 0..10 {
155            // rodadas de coluna
156            Self::quarter_round(&mut x, 0, 4, 8, 12);
157            Self::quarter_round(&mut x, 1, 5, 9, 13);
158            Self::quarter_round(&mut x, 2, 6, 10, 14);
159            Self::quarter_round(&mut x, 3, 7, 11, 15);
160            // rodadas diagonais
161            Self::quarter_round(&mut x, 0, 5, 10, 15);
162            Self::quarter_round(&mut x, 1, 6, 11, 12);
163            Self::quarter_round(&mut x, 2, 7, 8, 13);
164            Self::quarter_round(&mut x, 3, 4, 9, 14);
165        }
166        for (i, w) in x.iter_mut().enumerate() {
167            *w = w.wrapping_add(self.state[i]);
168            self.block[i * 4..i * 4 + 4].copy_from_slice(&w.to_le_bytes());
169        }
170        self.pos = 0;
171        // avança o contador
172        let (c0, carry) = self.state[12].overflowing_add(1);
173        self.state[12] = c0;
174        if self.wide_counter && carry {
175            self.state[13] = self.state[13].wrapping_add(1);
176        }
177    }
178}
179
180impl Cipher for ChaCha20 {
181    fn process(&mut self, data: &mut [u8]) {
182        for b in data.iter_mut() {
183            if self.pos == 64 {
184                self.refill();
185            }
186            *b ^= self.block[self.pos];
187            self.pos += 1;
188        }
189    }
190}
191
192/// Deriva a chave de 32 bytes do ChaCha a partir da chave de sessão SRP.
193fn chacha_key(session_key: &[u8]) -> [u8; 32] {
194    let mut h = Sha256::new();
195    h.update(session_key);
196    h.finalize().into()
197}
198
199/// Constrói o par de cifras de leitura/escrita para o plugin negociado.
200///
201/// `key` é a chave de sessão SRP (`K`); `nonce` é o nonce anunciado pelo
202/// servidor (vazio/ignorado para Arc4). As duas direções usam a mesma chave (e
203/// nonce), como faz o fbclient.
204pub fn make_ciphers(
205    plugin: WireCryptPlugin,
206    key: &[u8],
207    nonce: &[u8],
208) -> (Box<dyn Cipher>, Box<dyn Cipher>) {
209    match plugin {
210        WireCryptPlugin::Arc4 => (Box::new(Rc4::new(key)), Box::new(Rc4::new(key))),
211        WireCryptPlugin::ChaCha | WireCryptPlugin::ChaCha64 => {
212            let k = chacha_key(key);
213            (
214                Box::new(ChaCha20::new(&k, nonce)),
215                Box::new(ChaCha20::new(&k, nonce)),
216            )
217        }
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    // Vetor de teste da RFC 6229: chave "Key", início do keystream.
226    #[test]
227    fn rc4_known_answer() {
228        // Texto plano "Plaintext" com chave "Key" -> BBF316E8D940AF0AD3
229        let mut c = Rc4::new(b"Key");
230        let mut data = b"Plaintext".to_vec();
231        c.process(&mut data);
232        assert_eq!(data, hex_to_vec("BBF316E8D940AF0AD3"));
233    }
234
235    #[test]
236    fn rc4_roundtrip() {
237        let key = b"firebird-session-key";
238        let mut enc = Rc4::new(key);
239        let mut dec = Rc4::new(key);
240        let mut buf = b"op_attach payload \x00\x01\x02".to_vec();
241        let orig = buf.clone();
242        enc.process(&mut buf);
243        assert_ne!(buf, orig);
244        dec.process(&mut buf);
245        assert_eq!(buf, orig);
246    }
247
248    fn hex_to_vec(s: &str) -> Vec<u8> {
249        let s: String = s.chars().filter(|c| !c.is_whitespace()).collect();
250        (0..s.len())
251            .step_by(2)
252            .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
253            .collect()
254    }
255
256    /// Vetor de resposta conhecida do bloco ChaCha20, RFC 8439 §2.3.2.
257    /// Chave 00..1f, nonce 00:00:00:09:00:00:00:4a:00:00:00:00, contador 1.
258    /// Nossa cifra começa no contador 0, então o 2º bloco do keystream
259    /// (bytes 64..128) corresponde ao bloco de contador 1 da RFC.
260    #[test]
261    fn chacha20_rfc8439_block() {
262        let key: Vec<u8> = (0u8..32).collect();
263        let nonce = hex_to_vec("000000090000004a00000000");
264        let expected = hex_to_vec(
265            "10f1e7e4d13b5915500fdd1fa32071c4
266             c7d1f4c733c0680304 22aa9ac3d46c4e
267             d2826446079faa0914c2d705d98b02a2
268             b5129cd1de164eb9cbd083e8a2503c4e",
269        );
270        let mut c = ChaCha20::new(&key, &nonce);
271        let mut buf = vec![0u8; 128];
272        c.process(&mut buf);
273        assert_eq!(&buf[64..128], &expected[..]);
274    }
275
276    #[test]
277    fn chacha20_roundtrip_both_variants() {
278        let key = [0x42u8; 32];
279        for nonce in [vec![7u8; 12], vec![9u8; 8]] {
280            let mut enc = ChaCha20::new(&key, &nonce);
281            let mut dec = ChaCha20::new(&key, &nonce);
282            let orig = b"op_attach + segredo \x00\x01\x02 atravessa varios blocos ".repeat(4);
283            let mut buf = orig.clone();
284            enc.process(&mut buf);
285            assert_ne!(buf, orig);
286            dec.process(&mut buf);
287            assert_eq!(buf, orig);
288        }
289    }
290}