Skip to main content

cryptography/ciphers/
grasshopper.rs

1//! Kuznyechik (Grasshopper) block cipher — RFC 7801 / GOST R 34.12-2015.
2//!
3//! 128-bit block, 256-bit key, 10 rounds.
4//! All tables and test vectors from RFC 7801.
5//!
6//! `Grasshopper` keeps the original fast table-driven software path.
7//! `GrasshopperCt` is separate and keeps the same round structure, but removes
8//! secret-indexed tables by using a packed ANF bitset form for the S-box and
9//! direct arithmetic for the linear transform.
10
11// ── GF(2⁸) with primitive polynomial p(x) = x⁸ + x⁷ + x⁶ + x + 1 ──────────
12//
13// Reduction: x⁸ ≡ x⁷ + x⁶ + x + 1  ⟹  modulus byte 0xC3.
14
15#[inline]
16const fn gf_mul2(a: u8) -> u8 {
17    (a << 1) ^ (0xC3 & 0u8.wrapping_sub(a >> 7))
18}
19
20const fn gf_mul_const(mut a: u8, mut b: u8) -> u8 {
21    let mut r = 0u8;
22    while b != 0 {
23        if b & 1 != 0 {
24            r ^= a;
25        }
26        a = gf_mul2(a);
27        b >>= 1;
28    }
29    r
30}
31
32#[inline]
33fn gf_mul(mut a: u8, mut b: u8) -> u8 {
34    let mut r = 0u8;
35    for _ in 0..8 {
36        let mask = 0u8.wrapping_sub(b & 1);
37        r ^= a & mask;
38        a = gf_mul2(a);
39        b >>= 1;
40    }
41    r
42}
43
44// ── S-box and inverse (RFC 7801 §A.1) ────────────────────────────────────────
45//
46// Pi  : forward substitution (256-entry bijection over GF(2⁸))
47// Pi' : inverse substitution
48
49const PI: [u8; 256] = [
50    252, 238, 221, 17, 207, 110, 49, 22, 251, 196, 250, 218, 35, 197, 4, 77, 233, 119, 240, 219,
51    147, 46, 153, 186, 23, 54, 241, 187, 20, 205, 95, 193, 249, 24, 101, 90, 226, 92, 239, 33, 129,
52    28, 60, 66, 139, 1, 142, 79, 5, 132, 2, 174, 227, 106, 143, 160, 6, 11, 237, 152, 127, 212,
53    211, 31, 235, 52, 44, 81, 234, 200, 72, 171, 242, 42, 104, 162, 253, 58, 206, 204, 181, 112,
54    14, 86, 8, 12, 118, 18, 191, 114, 19, 71, 156, 183, 93, 135, 21, 161, 150, 41, 16, 123, 154,
55    199, 243, 145, 120, 111, 157, 158, 178, 177, 50, 117, 25, 61, 255, 53, 138, 126, 109, 84, 198,
56    128, 195, 189, 13, 87, 223, 245, 36, 169, 62, 168, 67, 201, 215, 121, 214, 246, 124, 34, 185,
57    3, 224, 15, 236, 222, 122, 148, 176, 188, 220, 232, 40, 80, 78, 51, 10, 74, 167, 151, 96, 115,
58    30, 0, 98, 68, 26, 184, 56, 130, 100, 159, 38, 65, 173, 69, 70, 146, 39, 94, 85, 47, 140, 163,
59    165, 125, 105, 213, 149, 59, 7, 88, 179, 64, 134, 172, 29, 247, 48, 55, 107, 228, 136, 217,
60    231, 137, 225, 27, 131, 73, 76, 63, 248, 254, 141, 83, 170, 144, 202, 216, 133, 97, 32, 113,
61    103, 164, 45, 43, 9, 91, 203, 155, 37, 208, 190, 229, 108, 82, 89, 166, 116, 210, 230, 244,
62    180, 192, 209, 102, 175, 194, 57, 75, 99, 182,
63];
64
65const PI_INV: [u8; 256] = [
66    165, 45, 50, 143, 14, 48, 56, 192, 84, 230, 158, 57, 85, 126, 82, 145, 100, 3, 87, 90, 28, 96,
67    7, 24, 33, 114, 168, 209, 41, 198, 164, 63, 224, 39, 141, 12, 130, 234, 174, 180, 154, 99, 73,
68    229, 66, 228, 21, 183, 200, 6, 112, 157, 65, 117, 25, 201, 170, 252, 77, 191, 42, 115, 132,
69    213, 195, 175, 43, 134, 167, 177, 178, 91, 70, 211, 159, 253, 212, 15, 156, 47, 155, 67, 239,
70    217, 121, 182, 83, 127, 193, 240, 35, 231, 37, 94, 181, 30, 162, 223, 166, 254, 172, 34, 249,
71    226, 74, 188, 53, 202, 238, 120, 5, 107, 81, 225, 89, 163, 242, 113, 86, 17, 106, 137, 148,
72    101, 140, 187, 119, 60, 123, 40, 171, 210, 49, 222, 196, 95, 204, 207, 118, 44, 184, 216, 46,
73    54, 219, 105, 179, 20, 149, 190, 98, 161, 59, 22, 102, 233, 92, 108, 109, 173, 55, 97, 75, 185,
74    227, 186, 241, 160, 133, 131, 218, 71, 197, 176, 51, 250, 150, 111, 110, 194, 246, 80, 255, 93,
75    169, 142, 23, 27, 151, 125, 236, 88, 247, 31, 251, 124, 9, 13, 122, 103, 69, 135, 220, 232, 79,
76    29, 78, 4, 235, 248, 243, 62, 61, 189, 138, 136, 221, 205, 11, 19, 152, 2, 147, 128, 144, 208,
77    36, 52, 203, 237, 244, 206, 153, 16, 68, 64, 146, 58, 1, 38, 18, 26, 72, 104, 245, 129, 139,
78    199, 214, 32, 10, 8, 0, 76, 215, 116,
79];
80
81/// Packed ANF coefficients for the Grasshopper S-box (forward or inverse).
82///
83/// Two `u128`s per output bit — 256 monomials for 8 input variables.
84/// `GrasshopperCt` evaluates with parity instead of indexing the 256-byte table.
85const fn build_pi_anf(table: &[u8; 256]) -> [[u128; 2]; 8] {
86    crate::ct::build_byte_sbox_anf(table)
87}
88
89const PI_ANF: [[u128; 2]; 8] = build_pi_anf(&PI);
90const PI_INV_ANF: [[u128; 2]; 8] = build_pi_anf(&PI_INV);
91
92// ── L-transform (RFC 7801 §2.2) ───────────────────────────────────────────────
93//
94// l(a₁₅,...,a₀) = 148·a₁₅ ⊕ 32·a₁₄ ⊕ 133·a₁₃ ⊕ 16·a₁₂ ⊕ 194·a₁₁ ⊕ 192·a₁₀
95//               ⊕   1·a₉  ⊕ 251·a₈  ⊕   1·a₇  ⊕ 192·a₆  ⊕ 194·a₅  ⊕  16·a₄
96//               ⊕ 133·a₃  ⊕  32·a₂  ⊕ 148·a₁  ⊕   1·a₀
97//
98// In our byte array, block[0] = a₁₅ and block[15] = a₀.
99//
100// R(block) = [l(block), block[0], …, block[14]]   ← push l to front, drop last
101// L = R¹⁶
102//
103// `Grasshopper` keeps these tables for throughput. `GrasshopperCt` computes the
104// products directly instead so the linear layer does not depend on secret-
105// indexed byte lookups.
106
107const L_COEFF: [u8; 16] = [
108    148, 32, 133, 16, 194, 192, 1, 251, 1, 192, 194, 16, 133, 32, 148, 1,
109];
110
111const fn build_l_tables() -> [[u8; 256]; 16] {
112    let mut t = [[0u8; 256]; 16];
113    let mut i = 0usize;
114    while i < 16 {
115        let mut v = 0u8;
116        loop {
117            t[i][v as usize] = gf_mul_const(L_COEFF[i], v);
118            if v == u8::MAX {
119                break;
120            }
121            v = v.wrapping_add(1);
122        }
123        i += 1;
124    }
125    t
126}
127
128static L_TABLES: [[u8; 256]; 16] = build_l_tables();
129
130// ── Core transforms ───────────────────────────────────────────────────────────
131
132#[inline]
133fn xor_block(a: &mut [u8; 16], b: &[u8; 16]) {
134    for (x, y) in a.iter_mut().zip(b.iter()) {
135        *x ^= y;
136    }
137}
138
139#[inline]
140fn apply_s(block: &mut [u8; 16]) {
141    for b in block.iter_mut() {
142        *b = PI[*b as usize];
143    }
144}
145
146#[inline]
147fn apply_s_inv(block: &mut [u8; 16]) {
148    for b in block.iter_mut() {
149        *b = PI_INV[*b as usize];
150    }
151}
152
153/// l-function: linear combination of 16 bytes over GF(2⁸).
154#[inline]
155fn l_func(block: &[u8; 16]) -> u8 {
156    let mut r = 0u8;
157    for i in 0..16 {
158        r ^= L_TABLES[i][block[i] as usize];
159    }
160    r
161}
162
163/// R step: push l(block) to front, shift right, drop last byte.
164#[inline]
165fn r_step(block: &mut [u8; 16]) {
166    let lc = l_func(block);
167    block.copy_within(0..15, 1); // block[1..=15] ← old block[0..=14]
168    block[0] = lc;
169}
170
171/// R⁻¹ step: shift left, recover and append the missing last byte.
172///
173/// Since l-coefficient for the last input byte (a₀) is 1, the missing byte
174/// equals block[0] XOR l(block[1..16] ∥ 0), with no modular inversion needed.
175#[inline]
176fn r_inv_step(block: &mut [u8; 16]) {
177    // sum = L_COEFF[0..15] · block[1..16]
178    let sum: u8 = (0..15).fold(0u8, |acc, i| acc ^ L_TABLES[i][block[i + 1] as usize]);
179    let new_last = block[0] ^ sum;
180    block.copy_within(1..16, 0); // block[0..=14] ← old block[1..=15]
181    block[15] = new_last;
182}
183
184/// L = R¹⁶.
185fn apply_l(block: &mut [u8; 16]) {
186    for _ in 0..16 {
187        r_step(block);
188    }
189}
190
191/// L⁻¹ = (R⁻¹)¹⁶.
192fn apply_l_inv(block: &mut [u8; 16]) {
193    for _ in 0..16 {
194        r_inv_step(block);
195    }
196}
197
198#[inline]
199fn pi_eval(coeffs: &[[u128; 2]; 8], input: u8) -> u8 {
200    crate::ct::eval_byte_sbox(coeffs, input)
201}
202
203#[inline]
204fn apply_s_ct(block: &mut [u8; 16]) {
205    // Same S layer as `apply_s()`, but each byte is evaluated through the
206    // packed ANF representation instead of indexing the 256-byte table.
207    for b in block.iter_mut() {
208        *b = pi_eval(&PI_ANF, *b);
209    }
210}
211
212#[inline]
213fn apply_s_inv_ct(block: &mut [u8; 16]) {
214    // Inverse S layer using the packed ANF form of `PI_INV`.
215    for b in block.iter_mut() {
216        *b = pi_eval(&PI_INV_ANF, *b);
217    }
218}
219
220#[inline]
221fn l_func_ct(block: &[u8; 16]) -> u8 {
222    // Same linear map as `l_func()`. The Ct path computes the field products
223    // directly instead of indexing `L_TABLES` with secret bytes.
224    let mut r = 0u8;
225    for i in 0..16 {
226        r ^= gf_mul(L_COEFF[i], block[i]);
227    }
228    r
229}
230
231#[inline]
232fn r_step_ct(block: &mut [u8; 16]) {
233    let lc = l_func_ct(block);
234    block.copy_within(0..15, 1);
235    block[0] = lc;
236}
237
238#[inline]
239fn r_inv_step_ct(block: &mut [u8; 16]) {
240    let sum: u8 = (0..15).fold(0u8, |acc, i| acc ^ gf_mul(L_COEFF[i], block[i + 1]));
241    let new_last = block[0] ^ sum;
242    block.copy_within(1..16, 0);
243    block[15] = new_last;
244}
245
246fn apply_l_ct(block: &mut [u8; 16]) {
247    for _ in 0..16 {
248        r_step_ct(block);
249    }
250}
251
252fn apply_l_inv_ct(block: &mut [u8; 16]) {
253    for _ in 0..16 {
254        r_inv_step_ct(block);
255    }
256}
257
258// ── Key schedule ──────────────────────────────────────────────────────────────
259//
260// Round constants C_i = L(Vec₁₂₈(i)), i = 1..32.
261// Vec₁₂₈(i): 128-bit big-endian representation of i (stored as [u8; 16]).
262// For i ≤ 255 this is [0, …, 0, i] with i in byte[15].
263//
264// Feistel step F[C](a₁, a₀) = (L(S(a₁ ⊕ C)) ⊕ a₀, a₁).
265//
266// Key K = k₁ ∥ k₀  (two 128-bit halves).
267// K₁ = k₁, K₂ = k₀; round keys K₃–K₁₀ derived by 4 groups of 8 F steps
268// using constants C₁–C₃₂.
269
270fn round_const(i: u8) -> [u8; 16] {
271    let mut v = [0u8; 16];
272    v[15] = i;
273    apply_l(&mut v);
274    v
275}
276
277fn round_const_ct(i: u8) -> [u8; 16] {
278    let mut v = [0u8; 16];
279    v[15] = i;
280    apply_l_ct(&mut v);
281    v
282}
283
284fn f_step(a1: &mut [u8; 16], a0: &mut [u8; 16], c: &[u8; 16]) {
285    let mut tmp = *a1;
286    xor_block(&mut tmp, c); // X[C]
287    apply_s(&mut tmp); // S
288    apply_l(&mut tmp); // L
289    xor_block(&mut tmp, a0); // XOR a₀
290    *a0 = *a1; // old a₁ becomes new a₀
291    *a1 = tmp; // new a₁
292}
293
294fn f_step_ct(a1: &mut [u8; 16], a0: &mut [u8; 16], c: &[u8; 16]) {
295    let mut tmp = *a1;
296    xor_block(&mut tmp, c);
297    apply_s_ct(&mut tmp);
298    apply_l_ct(&mut tmp);
299    xor_block(&mut tmp, a0);
300    *a0 = *a1;
301    *a1 = tmp;
302}
303
304fn key_schedule(key: &[u8; 32]) -> [[u8; 16]; 10] {
305    let mut rk = [[0u8; 16]; 10];
306    rk[0].copy_from_slice(&key[0..16]); // K₁
307    rk[1].copy_from_slice(&key[16..32]); // K₂
308
309    let mut a1 = rk[0];
310    let mut a0 = rk[1];
311
312    for group in 0usize..4 {
313        for step in 0usize..8 {
314            let ci = u8::try_from(group * 8 + step + 1).expect("round constant index fits in u8"); // 1..=32
315            let c = round_const(ci);
316            f_step(&mut a1, &mut a0, &c);
317        }
318        rk[2 + group * 2] = a1; // K₃, K₅, K₇, K₉
319        rk[3 + group * 2] = a0; // K₄, K₆, K₈, K₁₀
320    }
321
322    rk
323}
324
325fn key_schedule_ct(key: &[u8; 32]) -> [[u8; 16]; 10] {
326    let mut rk = [[0u8; 16]; 10];
327    rk[0].copy_from_slice(&key[0..16]);
328    rk[1].copy_from_slice(&key[16..32]);
329
330    let mut a1 = rk[0];
331    let mut a0 = rk[1];
332
333    for group in 0usize..4 {
334        for step in 0usize..8 {
335            let ci = u8::try_from(group * 8 + step + 1).expect("round constant index fits in u8");
336            let c = round_const_ct(ci);
337            f_step_ct(&mut a1, &mut a0, &c);
338        }
339        rk[2 + group * 2] = a1;
340        rk[3 + group * 2] = a0;
341    }
342
343    rk
344}
345
346// ── Public interface ──────────────────────────────────────────────────────────
347
348/// Kuznyechik (Grasshopper) block cipher — RFC 7801 / GOST R 34.12-2015.
349///
350/// 128-bit block, 256-bit key.  Pure Rust, no unsafe, no heap allocation.
351pub struct Grasshopper {
352    rk: [[u8; 16]; 10],
353}
354
355impl Grasshopper {
356    /// Construct from a 32-byte (256-bit) key.
357    #[must_use]
358    pub fn new(key: &[u8; 32]) -> Self {
359        Grasshopper {
360            rk: key_schedule(key),
361        }
362    }
363
364    /// Construct from a 32-byte key and wipe the provided key buffer.
365    pub fn new_wiping(key: &mut [u8; 32]) -> Self {
366        let out = Self::new(key);
367        crate::ct::zeroize_slice(key.as_mut_slice());
368        out
369    }
370
371    /// Encrypt a 128-bit block (ECB mode).
372    #[must_use]
373    pub fn encrypt_block(&self, block: &[u8; 16]) -> [u8; 16] {
374        let mut s = *block;
375        for i in 0..9 {
376            xor_block(&mut s, &self.rk[i]);
377            apply_s(&mut s);
378            apply_l(&mut s);
379        }
380        xor_block(&mut s, &self.rk[9]);
381        s
382    }
383
384    /// Decrypt a 128-bit block (ECB mode).
385    #[must_use]
386    pub fn decrypt_block(&self, block: &[u8; 16]) -> [u8; 16] {
387        let mut s = *block;
388        xor_block(&mut s, &self.rk[9]);
389        for i in (0..9).rev() {
390            apply_l_inv(&mut s);
391            apply_s_inv(&mut s);
392            xor_block(&mut s, &self.rk[i]);
393        }
394        s
395    }
396}
397
398/// A software-only constant-time Grasshopper path.
399///
400/// `GrasshopperCt` keeps the standard round/key schedule structure, but avoids
401/// the fast path's table-driven S and L layers: the S-box uses the packed ANF
402/// bitset form above and the linear layer uses direct GF(2^8) arithmetic.
403pub struct GrasshopperCt {
404    rk: [[u8; 16]; 10],
405}
406
407impl GrasshopperCt {
408    /// Construct from a 32-byte (256-bit) key.
409    #[must_use]
410    pub fn new(key: &[u8; 32]) -> Self {
411        GrasshopperCt {
412            rk: key_schedule_ct(key),
413        }
414    }
415
416    /// Construct from a 32-byte key and wipe the provided key buffer.
417    pub fn new_wiping(key: &mut [u8; 32]) -> Self {
418        let out = Self::new(key);
419        crate::ct::zeroize_slice(key.as_mut_slice());
420        out
421    }
422
423    /// Encrypt a 128-bit block (ECB mode).
424    #[must_use]
425    pub fn encrypt_block(&self, block: &[u8; 16]) -> [u8; 16] {
426        let mut s = *block;
427        for i in 0..9 {
428            xor_block(&mut s, &self.rk[i]);
429            apply_s_ct(&mut s);
430            apply_l_ct(&mut s);
431        }
432        xor_block(&mut s, &self.rk[9]);
433        s
434    }
435
436    /// Decrypt a 128-bit block (ECB mode).
437    #[must_use]
438    pub fn decrypt_block(&self, block: &[u8; 16]) -> [u8; 16] {
439        let mut s = *block;
440        xor_block(&mut s, &self.rk[9]);
441        for i in (0..9).rev() {
442            apply_l_inv_ct(&mut s);
443            apply_s_inv_ct(&mut s);
444            xor_block(&mut s, &self.rk[i]);
445        }
446        s
447    }
448}
449
450impl crate::BlockCipher for Grasshopper {
451    const BLOCK_LEN: usize = 16;
452    fn encrypt(&self, block: &mut [u8]) {
453        let arr: &[u8; 16] = (&*block).try_into().expect("wrong block length");
454        block.copy_from_slice(&self.encrypt_block(arr));
455    }
456    fn decrypt(&self, block: &mut [u8]) {
457        let arr: &[u8; 16] = (&*block).try_into().expect("wrong block length");
458        block.copy_from_slice(&self.decrypt_block(arr));
459    }
460}
461
462impl crate::BlockCipher for GrasshopperCt {
463    const BLOCK_LEN: usize = 16;
464    fn encrypt(&self, block: &mut [u8]) {
465        let arr: &[u8; 16] = (&*block).try_into().expect("wrong block length");
466        block.copy_from_slice(&self.encrypt_block(arr));
467    }
468    fn decrypt(&self, block: &mut [u8]) {
469        let arr: &[u8; 16] = (&*block).try_into().expect("wrong block length");
470        block.copy_from_slice(&self.decrypt_block(arr));
471    }
472}
473
474impl Drop for Grasshopper {
475    fn drop(&mut self) {
476        // Grasshopper keeps all 10 round keys in memory for repeated use.
477        for rk in &mut self.rk {
478            crate::ct::zeroize_slice(rk.as_mut_slice());
479        }
480    }
481}
482
483impl Drop for GrasshopperCt {
484    fn drop(&mut self) {
485        for rk in &mut self.rk {
486            crate::ct::zeroize_slice(rk.as_mut_slice());
487        }
488    }
489}
490
491// ── Tests (all vectors from RFC 7801) ────────────────────────────────────────
492
493#[cfg(test)]
494mod tests {
495    use super::*;
496
497    fn h16(s: &str) -> [u8; 16] {
498        let b: Vec<u8> = (0..s.len())
499            .step_by(2)
500            .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
501            .collect();
502        b.try_into().unwrap()
503    }
504
505    fn h32(s: &str) -> [u8; 32] {
506        let b: Vec<u8> = (0..s.len())
507            .step_by(2)
508            .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
509            .collect();
510        b.try_into().unwrap()
511    }
512
513    #[test]
514    fn ct_sboxes_match_tables() {
515        for x in 0u16..=255 {
516            let b = u8::try_from(x).expect("table index fits in u8");
517            assert_eq!(pi_eval(&PI_ANF, b), PI[x as usize], "pi {x:02x}");
518            assert_eq!(
519                pi_eval(&PI_INV_ANF, b),
520                PI_INV[x as usize],
521                "pi_inv {x:02x}"
522            );
523        }
524    }
525
526    // ── S-transform (RFC 7801 §A.3) ───────────────────────────────────────────
527
528    #[test]
529    fn s_vectors() {
530        let cases = [
531            (
532                "ffeeddccbbaa99881122334455667700",
533                "b66cd8887d38e8d77765aeea0c9a7efc",
534            ),
535            (
536                "b66cd8887d38e8d77765aeea0c9a7efc",
537                "559d8dd7bd06cbfe7e7b262523280d39",
538            ),
539            (
540                "559d8dd7bd06cbfe7e7b262523280d39",
541                "0c3322fed531e4630d80ef5c5a81c50b",
542            ),
543            (
544                "0c3322fed531e4630d80ef5c5a81c50b",
545                "23ae65633f842d29c5df529c13f5acda",
546            ),
547        ];
548        for (inp, exp) in cases {
549            let mut b = h16(inp);
550            apply_s(&mut b);
551            assert_eq!(b, h16(exp), "S({inp})");
552        }
553    }
554
555    // ── R-transform (RFC 7801 §A.4) ───────────────────────────────────────────
556
557    #[test]
558    fn r_vectors() {
559        let cases = [
560            (
561                "00000000000000000000000000000100",
562                "94000000000000000000000000000001",
563            ),
564            (
565                "94000000000000000000000000000001",
566                "a5940000000000000000000000000000",
567            ),
568            (
569                "a5940000000000000000000000000000",
570                "64a59400000000000000000000000000",
571            ),
572            (
573                "64a59400000000000000000000000000",
574                "0d64a594000000000000000000000000",
575            ),
576        ];
577        for (inp, exp) in cases {
578            let mut b = h16(inp);
579            r_step(&mut b);
580            assert_eq!(b, h16(exp), "R({inp})");
581        }
582    }
583
584    // ── L-transform (RFC 7801 §A.5) ───────────────────────────────────────────
585
586    #[test]
587    fn l_vectors() {
588        let cases = [
589            (
590                "64a59400000000000000000000000000",
591                "d456584dd0e3e84cc3166e4b7fa2890d",
592            ),
593            (
594                "d456584dd0e3e84cc3166e4b7fa2890d",
595                "79d26221b87b584cd42fbc4ffea5de9a",
596            ),
597            (
598                "79d26221b87b584cd42fbc4ffea5de9a",
599                "0e93691a0cfc60408b7b68f66b513c13",
600            ),
601            (
602                "0e93691a0cfc60408b7b68f66b513c13",
603                "e6a8094fee0aa204fd97bcb0b44b8580",
604            ),
605        ];
606        for (inp, exp) in cases {
607            let mut b = h16(inp);
608            apply_l(&mut b);
609            assert_eq!(b, h16(exp), "L({inp})");
610        }
611    }
612
613    // ── Encrypt / Decrypt (RFC 7801 §5.5) ────────────────────────────────────
614
615    #[test]
616    fn encrypt_decrypt_rfc() {
617        let key = h32("8899aabbccddeeff0011223344556677fedcba98765432100123456789abcdef");
618        let pt = h16("1122334455667700ffeeddccbbaa9988");
619        let ct = h16("7f679d90bebc24305a468d42b9d4edcd");
620        let c = Grasshopper::new(&key);
621        assert_eq!(c.encrypt_block(&pt), ct, "encrypt");
622        assert_eq!(c.decrypt_block(&ct), pt, "decrypt");
623    }
624
625    #[test]
626    fn encrypt_decrypt_rfc_ct() {
627        let key = h32("8899aabbccddeeff0011223344556677fedcba98765432100123456789abcdef");
628        let pt = h16("1122334455667700ffeeddccbbaa9988");
629        let ct = h16("7f679d90bebc24305a468d42b9d4edcd");
630        let fast = Grasshopper::new(&key);
631        let slow = GrasshopperCt::new(&key);
632        assert_eq!(slow.encrypt_block(&pt), ct, "encrypt");
633        assert_eq!(slow.decrypt_block(&ct), pt, "decrypt");
634        assert_eq!(
635            slow.encrypt_block(&pt),
636            fast.encrypt_block(&pt),
637            "match fast"
638        );
639    }
640
641    // ── Key schedule (RFC 7801 §5.4) ─────────────────────────────────────────
642
643    #[test]
644    fn key_schedule_vectors() {
645        let key = h32("8899aabbccddeeff0011223344556677fedcba98765432100123456789abcdef");
646        let rk = key_schedule(&key);
647        assert_eq!(rk[0], h16("8899aabbccddeeff0011223344556677"), "K_1");
648        assert_eq!(rk[1], h16("fedcba98765432100123456789abcdef"), "K_2");
649        assert_eq!(rk[2], h16("db31485315694343228d6aef8cc78c44"), "K_3");
650        assert_eq!(rk[3], h16("3d4553d8e9cfec6815ebadc40a9ffd04"), "K_4");
651        assert_eq!(rk[4], h16("57646468c44a5e28d3e59246f429f1ac"), "K_5");
652        assert_eq!(rk[9], h16("72e9dd7416bcf45b755dbaa88e4a4043"), "K_10");
653    }
654
655    // ── Roundtrip ─────────────────────────────────────────────────────────────
656
657    #[test]
658    fn roundtrip() {
659        let key = [0xABu8; 32];
660        let pt = [0x42u8; 16];
661        let c = Grasshopper::new(&key);
662        assert_eq!(c.decrypt_block(&c.encrypt_block(&pt)), pt);
663    }
664
665    // ── Round constants (RFC 7801 §5.4) ──────────────────────────────────────
666
667    #[test]
668    fn round_const_vectors() {
669        assert_eq!(
670            round_const(1),
671            h16("6ea276726c487ab85d27bd10dd849401"),
672            "C_1"
673        );
674        assert_eq!(
675            round_const(2),
676            h16("dc87ece4d890f4b3ba4eb92079cbeb02"),
677            "C_2"
678        );
679        assert_eq!(
680            round_const(3),
681            h16("b2259a96b4d88e0be7690430a44f7f03"),
682            "C_3"
683        );
684        assert_eq!(
685            round_const(4),
686            h16("7bcd1b0b73e32ba5b79cb140f2551504"),
687            "C_4"
688        );
689        assert_eq!(
690            round_const(5),
691            h16("156f6d791fab511deabb0c502fd18105"),
692            "C_5"
693        );
694        assert_eq!(
695            round_const(6),
696            h16("a74af7efab73df160dd208608b9efe06"),
697            "C_6"
698        );
699        assert_eq!(
700            round_const(7),
701            h16("c9e8819dc73ba5ae50f5b570561a6a07"),
702            "C_7"
703        );
704        assert_eq!(
705            round_const(8),
706            h16("f6593616e6055689adfba18027aa2a08"),
707            "C_8"
708        );
709    }
710}