Skip to main content

cryptography/ciphers/
magma.rs

1//! Magma (GOST R 34.12-2015) block cipher — RFC 8891.
2//!
3//! 64-bit block, 256-bit key, 32 Feistel rounds.
4//! All tables and test vectors from RFC 8891.
5//!
6//! `Magma` keeps the original fast table lookups. `MagmaCt` is separate and
7//! replaces only the eight 4-bit substitutions with fixed boolean circuits,
8//! which is enough to remove secret-indexed lookups from the round function
9//! while keeping the implementation compact.
10
11// ── S-boxes (RFC 8891 §4.1) ────────────────────────────────────────────────
12//
13// Eight 4-bit bijections Pi'_0 .. Pi'_7.
14// Pi'_i processes nibble i of the 32-bit word (nibble 0 = bits [3:0]).
15
16const PI: [[u8; 16]; 8] = [
17    [12, 4, 6, 2, 10, 5, 11, 9, 14, 8, 13, 7, 0, 3, 15, 1], // Pi'_0
18    [6, 8, 2, 3, 9, 10, 5, 12, 1, 14, 4, 7, 11, 13, 0, 15], // Pi'_1
19    [11, 3, 5, 8, 2, 15, 10, 13, 14, 1, 7, 4, 12, 9, 6, 0], // Pi'_2
20    [12, 8, 2, 1, 13, 4, 15, 6, 7, 0, 10, 5, 3, 14, 9, 11], // Pi'_3
21    [7, 15, 5, 10, 8, 1, 6, 13, 0, 9, 3, 14, 11, 4, 2, 12], // Pi'_4
22    [5, 13, 15, 6, 9, 2, 12, 10, 11, 7, 8, 1, 4, 3, 14, 0], // Pi'_5
23    [8, 14, 2, 5, 6, 9, 1, 12, 15, 4, 11, 0, 13, 10, 3, 7], // Pi'_6
24    [1, 7, 14, 13, 0, 5, 8, 3, 4, 15, 10, 6, 9, 12, 11, 2], // Pi'_7
25];
26
27// ── Core transforms ────────────────────────────────────────────────────────
28
29/// t(v): apply 8 independent 4-bit S-boxes to a 32-bit word.
30/// Nibble i (bits [4i+3 : 4i]) is passed through Pi'_i.
31#[inline]
32fn t(v: u32) -> u32 {
33    let mut r = 0u32;
34    for (i, sbox) in PI.iter().enumerate() {
35        let nibble = ((v >> (4 * i)) & 0xf) as usize;
36        let sub = sbox[nibble];
37        r |= u32::from(sub) << (4 * i);
38    }
39    r
40}
41
42/// Compute all monomials up to degree 3 of a 4-bit input.
43///
44/// Magma's S-boxes have Boolean degree ≤ 3, so these monomials span all ANF
45/// terms that appear.  The `pi*_ct` functions XOR the terms with coefficient 1.
46#[inline]
47fn nibble_monomials(nibble: u8) -> ([u8; 4], [u8; 6], [u8; 4]) {
48    let bits = [
49        nibble & 1,
50        (nibble >> 1) & 1,
51        (nibble >> 2) & 1,
52        (nibble >> 3) & 1,
53    ];
54    let pairs = [
55        bits[0] & bits[1],
56        bits[0] & bits[2],
57        bits[0] & bits[3],
58        bits[1] & bits[2],
59        bits[1] & bits[3],
60        bits[2] & bits[3],
61    ];
62    let triples = [
63        pairs[0] & bits[2],
64        pairs[0] & bits[3],
65        pairs[1] & bits[3],
66        pairs[3] & bits[3],
67    ];
68    (bits, pairs, triples)
69}
70
71// Each `pi*_ct` is the ANF of one RFC 8891 S-box written over the four input
72// bits.  Pairs/triples from `nibble_monomials` are the product terms; a bare
73// `1` in a `b*` expression is the ANF constant term.
74#[inline]
75fn pi0_ct(nibble: u8) -> u8 {
76    let (bits, pairs, triples) = nibble_monomials(nibble);
77
78    let b0 = pairs[1] ^ pairs[3] ^ triples[0] ^ pairs[4] ^ triples[3];
79    let b1 = bits[1] ^ bits[2] ^ pairs[1] ^ pairs[3] ^ bits[3] ^ pairs[2] ^ triples[2] ^ triples[3];
80    let b2 = 1 ^ pairs[0] ^ bits[2] ^ pairs[1] ^ pairs[2] ^ triples[3];
81    let b3 = 1 ^ bits[0] ^ bits[1] ^ pairs[0] ^ pairs[3] ^ pairs[2] ^ pairs[4] ^ pairs[5];
82
83    b0 | (b1 << 1) | (b2 << 2) | (b3 << 3)
84}
85
86#[inline]
87fn pi1_ct(nibble: u8) -> u8 {
88    let (bits, pairs, triples) = nibble_monomials(nibble);
89
90    let b0 = pairs[0]
91        ^ bits[2]
92        ^ pairs[1]
93        ^ triples[0]
94        ^ bits[3]
95        ^ pairs[2]
96        ^ pairs[4]
97        ^ triples[1]
98        ^ pairs[5];
99    let b1 = 1 ^ bits[0] ^ pairs[0] ^ bits[2] ^ bits[3] ^ triples[1] ^ triples[3];
100    let b2 = 1
101        ^ bits[0]
102        ^ bits[1]
103        ^ pairs[0]
104        ^ bits[2]
105        ^ pairs[1]
106        ^ triples[0]
107        ^ bits[3]
108        ^ pairs[5]
109        ^ triples[2]
110        ^ triples[3];
111    let b3 = bits[0] ^ pairs[0] ^ bits[2] ^ pairs[1] ^ pairs[3];
112
113    b0 | (b1 << 1) | (b2 << 2) | (b3 << 3)
114}
115
116#[inline]
117fn pi2_ct(nibble: u8) -> u8 {
118    let (bits, pairs, triples) = nibble_monomials(nibble);
119
120    let b0 = 1
121        ^ pairs[0]
122        ^ bits[2]
123        ^ pairs[1]
124        ^ triples[0]
125        ^ bits[3]
126        ^ pairs[2]
127        ^ pairs[4]
128        ^ triples[1]
129        ^ pairs[5]
130        ^ triples[2]
131        ^ triples[3];
132    let b1 = 1 ^ bits[1] ^ pairs[3] ^ triples[0] ^ pairs[2] ^ pairs[4] ^ pairs[5] ^ triples[2];
133    let b2 = bits[1]
134        ^ pairs[0]
135        ^ pairs[1]
136        ^ pairs[3]
137        ^ triples[0]
138        ^ bits[3]
139        ^ pairs[2]
140        ^ pairs[4]
141        ^ triples[2]
142        ^ triples[3];
143    let b3 = 1 ^ bits[0] ^ bits[1] ^ bits[2] ^ triples[0] ^ triples[1] ^ pairs[5] ^ triples[2];
144
145    b0 | (b1 << 1) | (b2 << 2) | (b3 << 3)
146}
147
148#[inline]
149fn pi3_ct(nibble: u8) -> u8 {
150    let (bits, pairs, triples) = nibble_monomials(nibble);
151
152    let b0 = pairs[0]
153        ^ bits[2]
154        ^ pairs[1]
155        ^ triples[0]
156        ^ bits[3]
157        ^ pairs[2]
158        ^ pairs[4]
159        ^ triples[1]
160        ^ pairs[5]
161        ^ triples[2]
162        ^ triples[3];
163    let b1 = bits[1]
164        ^ pairs[0]
165        ^ triples[0]
166        ^ bits[3]
167        ^ pairs[2]
168        ^ pairs[4]
169        ^ triples[1]
170        ^ triples[2]
171        ^ triples[3];
172    let b2 = 1
173        ^ bits[0]
174        ^ bits[1]
175        ^ pairs[0]
176        ^ pairs[1]
177        ^ pairs[3]
178        ^ triples[0]
179        ^ triples[1]
180        ^ pairs[5]
181        ^ triples[2];
182    let b3 = 1 ^ bits[1] ^ pairs[1] ^ pairs[3] ^ bits[3] ^ triples[1] ^ triples[3];
183
184    b0 | (b1 << 1) | (b2 << 2) | (b3 << 3)
185}
186
187#[inline]
188fn pi4_ct(nibble: u8) -> u8 {
189    let (bits, pairs, triples) = nibble_monomials(nibble);
190
191    let b0 = 1
192        ^ pairs[0]
193        ^ bits[2]
194        ^ pairs[1]
195        ^ triples[0]
196        ^ bits[3]
197        ^ pairs[2]
198        ^ pairs[4]
199        ^ triples[1]
200        ^ triples[2];
201    let b1 = 1 ^ bits[1] ^ pairs[0] ^ bits[2] ^ bits[3] ^ triples[1] ^ triples[2] ^ triples[3];
202    let b2 = 1
203        ^ pairs[0]
204        ^ bits[2]
205        ^ pairs[3]
206        ^ triples[0]
207        ^ bits[3]
208        ^ pairs[5]
209        ^ triples[2]
210        ^ triples[3];
211    let b3 = bits[0] ^ bits[2] ^ pairs[3];
212
213    b0 | (b1 << 1) | (b2 << 2) | (b3 << 3)
214}
215
216#[inline]
217fn pi5_ct(nibble: u8) -> u8 {
218    let (bits, pairs, triples) = nibble_monomials(nibble);
219
220    let b0 = 1 ^ pairs[0] ^ pairs[1] ^ pairs[3] ^ pairs[4] ^ pairs[5];
221    let b1 = bits[1] ^ pairs[1] ^ pairs[3] ^ bits[3] ^ pairs[5] ^ triples[3];
222    let b2 = 1 ^ bits[2] ^ pairs[3] ^ triples[0] ^ bits[3] ^ pairs[2] ^ triples[1] ^ triples[3];
223    let b3 = bits[0] ^ bits[1] ^ bits[2] ^ pairs[3] ^ triples[0] ^ bits[3] ^ pairs[4] ^ triples[2];
224
225    b0 | (b1 << 1) | (b2 << 2) | (b3 << 3)
226}
227
228#[inline]
229fn pi6_ct(nibble: u8) -> u8 {
230    let (bits, pairs, triples) = nibble_monomials(nibble);
231
232    let b0 = pairs[0]
233        ^ pairs[1]
234        ^ pairs[3]
235        ^ triples[0]
236        ^ bits[3]
237        ^ pairs[2]
238        ^ triples[1]
239        ^ triples[2]
240        ^ triples[3];
241    let b1 = bits[0] ^ bits[1] ^ bits[2] ^ triples[0] ^ bits[3] ^ pairs[4] ^ triples[3];
242    let b2 = bits[0]
243        ^ bits[2]
244        ^ pairs[3]
245        ^ bits[3]
246        ^ pairs[2]
247        ^ pairs[4]
248        ^ pairs[5]
249        ^ triples[2]
250        ^ triples[3];
251    let b3 = 1 ^ bits[1] ^ bits[2] ^ pairs[1] ^ pairs[3] ^ pairs[2] ^ pairs[4] ^ pairs[5];
252
253    b0 | (b1 << 1) | (b2 << 2) | (b3 << 3)
254}
255
256#[inline]
257fn pi7_ct(nibble: u8) -> u8 {
258    let (bits, pairs, triples) = nibble_monomials(nibble);
259
260    let b0 = 1
261        ^ bits[1]
262        ^ pairs[0]
263        ^ bits[2]
264        ^ pairs[1]
265        ^ pairs[3]
266        ^ triples[0]
267        ^ bits[3]
268        ^ pairs[2]
269        ^ pairs[4]
270        ^ triples[2]
271        ^ triples[3];
272    let b1 = bits[0] ^ bits[1] ^ pairs[1] ^ pairs[3] ^ triples[0] ^ triples[1] ^ triples[3];
273    let b2 = bits[0] ^ bits[1] ^ pairs[0] ^ pairs[3] ^ bits[3] ^ pairs[2] ^ pairs[5] ^ triples[2];
274    let b3 = bits[1] ^ triples[0] ^ pairs[2] ^ pairs[5] ^ triples[2] ^ triples[3];
275
276    b0 | (b1 << 1) | (b2 << 2) | (b3 << 3)
277}
278
279#[cfg(test)]
280#[inline]
281fn pi_ct(box_idx: usize, nibble: u8) -> u8 {
282    match box_idx {
283        0 => pi0_ct(nibble),
284        1 => pi1_ct(nibble),
285        2 => pi2_ct(nibble),
286        3 => pi3_ct(nibble),
287        4 => pi4_ct(nibble),
288        5 => pi5_ct(nibble),
289        6 => pi6_ct(nibble),
290        7 => pi7_ct(nibble),
291        _ => unreachable!(),
292    }
293}
294
295/// Constant-time variant of `t(v)`.
296///
297/// This is the same eight-nibble substitution as `t()`, but each nibble is
298/// routed through its explicit boolean circuit instead of indexing `PI`.
299#[inline]
300fn t_ct(v: u32) -> u32 {
301    let n0 = (v & 0x0000_000f) as u8;
302    let n1 = ((v >> 4) & 0x0000_000f) as u8;
303    let n2 = ((v >> 8) & 0x0000_000f) as u8;
304    let n3 = ((v >> 12) & 0x0000_000f) as u8;
305    let n4 = ((v >> 16) & 0x0000_000f) as u8;
306    let n5 = ((v >> 20) & 0x0000_000f) as u8;
307    let n6 = ((v >> 24) & 0x0000_000f) as u8;
308    let n7 = ((v >> 28) & 0x0000_000f) as u8;
309
310    u32::from(pi0_ct(n0))
311        | (u32::from(pi1_ct(n1)) << 4)
312        | (u32::from(pi2_ct(n2)) << 8)
313        | (u32::from(pi3_ct(n3)) << 12)
314        | (u32::from(pi4_ct(n4)) << 16)
315        | (u32::from(pi5_ct(n5)) << 20)
316        | (u32::from(pi6_ct(n6)) << 24)
317        | (u32::from(pi7_ct(n7)) << 28)
318}
319
320/// g[k](a): wrapping-add key, substitute, rotate left 11 bits.
321#[inline]
322fn g(k: u32, a: u32) -> u32 {
323    t(a.wrapping_add(k)).rotate_left(11)
324}
325
326#[inline]
327fn g_ct(k: u32, a: u32) -> u32 {
328    // Same Feistel round function as `g()`: the only change is the Ct
329    // substitution inside `t_ct`.
330    t_ct(a.wrapping_add(k)).rotate_left(11)
331}
332
333// ── Key schedule (RFC 8891 §4.3) ───────────────────────────────────────────
334//
335// 256-bit key K split into 8 × 32-bit subkeys (big-endian bytes):
336//   k[i] = K_{i+1} = k_{255-32i} .. k_{224-32i}
337//
338// Encryption round key sequence (32 keys):
339//   k[0..8]  (×3 = rounds 1–24)  then  k[7..0]  (×1 = rounds 25–32)
340//
341// Decryption round key sequence = encryption sequence reversed:
342//   k[0..8]  (×1)  then  k[7..0]  (×3)
343
344fn build_round_keys(key: &[u8; 32]) -> ([u32; 32], [u32; 32]) {
345    let mut k = [0u32; 8];
346    for i in 0..8 {
347        k[i] = u32::from_be_bytes(key[4 * i..4 * i + 4].try_into().unwrap());
348    }
349
350    let mut enc = [0u32; 32];
351    for i in 0..24 {
352        enc[i] = k[i % 8];
353    } // rounds 1–24: forward three times
354    for i in 0..8 {
355        enc[24 + i] = k[7 - i];
356    } // rounds 25–32: reversed once
357
358    let mut dec = enc;
359    dec.reverse();
360
361    (enc, dec)
362}
363
364// ── Feistel core ───────────────────────────────────────────────────────────
365//
366// Block is split as a₁ || a₀  (a₁ = upper 32 bits, a₀ = lower 32 bits).
367//
368// Rounds 1–31: G[k](a₁, a₀) = (a₀,  g[k](a₀) ⊕ a₁)  — apply then swap
369// Round 32:   G*[k](a₁, a₀) = (g[k](a₀) ⊕ a₁) || a₀  — apply, no swap
370
371fn magma_core(block: [u8; 8], rk: &[u32; 32]) -> [u8; 8] {
372    let mut a1 = u32::from_be_bytes(block[0..4].try_into().unwrap()); // upper
373    let mut a0 = u32::from_be_bytes(block[4..8].try_into().unwrap()); // lower
374
375    for &round_key in rk.iter().take(31) {
376        let tmp = g(round_key, a0) ^ a1;
377        a1 = a0;
378        a0 = tmp;
379    }
380
381    // G* (final round — no swap)
382    let c1 = g(rk[31], a0) ^ a1; // upper half of output
383    let c0 = a0; // lower half of output
384
385    let mut out = [0u8; 8];
386    out[0..4].copy_from_slice(&c1.to_be_bytes());
387    out[4..8].copy_from_slice(&c0.to_be_bytes());
388    out
389}
390
391fn magma_core_ct(block: [u8; 8], rk: &[u32; 32]) -> [u8; 8] {
392    let mut a1 = u32::from_be_bytes(block[0..4].try_into().unwrap());
393    let mut a0 = u32::from_be_bytes(block[4..8].try_into().unwrap());
394
395    for &round_key in rk.iter().take(31) {
396        let tmp = g_ct(round_key, a0) ^ a1;
397        a1 = a0;
398        a0 = tmp;
399    }
400
401    let c1 = g_ct(rk[31], a0) ^ a1;
402    let c0 = a0;
403
404    let mut out = [0u8; 8];
405    out[0..4].copy_from_slice(&c1.to_be_bytes());
406    out[4..8].copy_from_slice(&c0.to_be_bytes());
407    out
408}
409
410// ── Public interface ───────────────────────────────────────────────────────
411
412/// Magma block cipher — RFC 8891 / GOST R 34.12-2015.
413///
414/// 64-bit block, 256-bit key.  Pure Rust, no unsafe, no heap allocation.
415pub struct Magma {
416    enc_rk: [u32; 32],
417    dec_rk: [u32; 32],
418}
419
420impl Magma {
421    /// Construct from a 32-byte (256-bit) key.
422    #[must_use]
423    pub fn new(key: &[u8; 32]) -> Self {
424        let (enc_rk, dec_rk) = build_round_keys(key);
425        Magma { enc_rk, dec_rk }
426    }
427
428    /// Construct from a 32-byte key and wipe the provided key buffer.
429    pub fn new_wiping(key: &mut [u8; 32]) -> Self {
430        let out = Self::new(key);
431        crate::ct::zeroize_slice(key.as_mut_slice());
432        out
433    }
434
435    /// Encrypt a 64-bit block (ECB mode).
436    #[must_use]
437    pub fn encrypt_block(&self, block: &[u8; 8]) -> [u8; 8] {
438        magma_core(*block, &self.enc_rk)
439    }
440
441    /// Decrypt a 64-bit block (ECB mode).
442    #[must_use]
443    pub fn decrypt_block(&self, block: &[u8; 8]) -> [u8; 8] {
444        magma_core(*block, &self.dec_rk)
445    }
446}
447
448/// A software-only constant-time Magma path.
449///
450/// `MagmaCt` keeps Magma's original structure and key schedule, but replaces
451/// the nibble S-box lookups with the fixed boolean circuits above so the round
452/// function no longer indexes memory with secret-derived values.
453pub struct MagmaCt {
454    enc_rk: [u32; 32],
455    dec_rk: [u32; 32],
456}
457
458impl MagmaCt {
459    /// Construct from a 32-byte (256-bit) key.
460    #[must_use]
461    pub fn new(key: &[u8; 32]) -> Self {
462        let (enc_rk, dec_rk) = build_round_keys(key);
463        MagmaCt { enc_rk, dec_rk }
464    }
465
466    /// Construct from a 32-byte key and wipe the provided key buffer.
467    pub fn new_wiping(key: &mut [u8; 32]) -> Self {
468        let out = Self::new(key);
469        crate::ct::zeroize_slice(key.as_mut_slice());
470        out
471    }
472
473    /// Encrypt a 64-bit block (ECB mode).
474    #[must_use]
475    pub fn encrypt_block(&self, block: &[u8; 8]) -> [u8; 8] {
476        magma_core_ct(*block, &self.enc_rk)
477    }
478
479    /// Decrypt a 64-bit block (ECB mode).
480    #[must_use]
481    pub fn decrypt_block(&self, block: &[u8; 8]) -> [u8; 8] {
482        magma_core_ct(*block, &self.dec_rk)
483    }
484}
485
486impl crate::BlockCipher for Magma {
487    const BLOCK_LEN: usize = 8;
488    fn encrypt(&self, block: &mut [u8]) {
489        let arr: &[u8; 8] = (&*block).try_into().expect("wrong block length");
490        block.copy_from_slice(&self.encrypt_block(arr));
491    }
492    fn decrypt(&self, block: &mut [u8]) {
493        let arr: &[u8; 8] = (&*block).try_into().expect("wrong block length");
494        block.copy_from_slice(&self.decrypt_block(arr));
495    }
496}
497
498impl crate::BlockCipher for MagmaCt {
499    const BLOCK_LEN: usize = 8;
500    fn encrypt(&self, block: &mut [u8]) {
501        let arr: &[u8; 8] = (&*block).try_into().expect("wrong block length");
502        block.copy_from_slice(&self.encrypt_block(arr));
503    }
504    fn decrypt(&self, block: &mut [u8]) {
505        let arr: &[u8; 8] = (&*block).try_into().expect("wrong block length");
506        block.copy_from_slice(&self.decrypt_block(arr));
507    }
508}
509
510impl Drop for Magma {
511    fn drop(&mut self) {
512        // Magma caches both round-key orders; wipe them when the instance dies.
513        crate::ct::zeroize_slice(self.enc_rk.as_mut_slice());
514        crate::ct::zeroize_slice(self.dec_rk.as_mut_slice());
515    }
516}
517
518impl Drop for MagmaCt {
519    fn drop(&mut self) {
520        crate::ct::zeroize_slice(self.enc_rk.as_mut_slice());
521        crate::ct::zeroize_slice(self.dec_rk.as_mut_slice());
522    }
523}
524
525// ── Tests (vectors from RFC 8891) ─────────────────────────────────────────
526
527#[cfg(test)]
528mod tests {
529    use super::*;
530
531    fn h8(s: &str) -> [u8; 8] {
532        let b: Vec<u8> = (0..s.len())
533            .step_by(2)
534            .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
535            .collect();
536        b.try_into().unwrap()
537    }
538
539    fn h32(s: &str) -> [u8; 32] {
540        let b: Vec<u8> = (0..s.len())
541            .step_by(2)
542            .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
543            .collect();
544        b.try_into().unwrap()
545    }
546
547    // ── Encrypt / Decrypt (RFC 8891 §A.3) ────────────────────────────────
548
549    #[test]
550    fn encrypt_decrypt_rfc() {
551        let key = h32("ffeeddccbbaa99887766554433221100f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff");
552        let pt = h8("fedcba9876543210");
553        let ct = h8("4ee901e5c2d8ca3d");
554        let m = Magma::new(&key);
555        assert_eq!(m.encrypt_block(&pt), ct, "encrypt");
556        assert_eq!(m.decrypt_block(&ct), pt, "decrypt");
557    }
558
559    // ── Roundtrip ─────────────────────────────────────────────────────────
560
561    #[test]
562    fn roundtrip() {
563        let key = [0x42u8; 32];
564        let pt = [0xABu8; 8];
565        let m = Magma::new(&key);
566        assert_eq!(m.decrypt_block(&m.encrypt_block(&pt)), pt);
567    }
568
569    #[test]
570    fn ct_sboxes_match_tables() {
571        for (box_idx, table) in PI.iter().enumerate() {
572            for nibble in 0u8..16 {
573                assert_eq!(pi_ct(box_idx, nibble), table[nibble as usize]);
574            }
575        }
576    }
577
578    #[test]
579    fn encrypt_decrypt_rfc_ct() {
580        let key = h32("ffeeddccbbaa99887766554433221100f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff");
581        let pt = h8("fedcba9876543210");
582        let ct = h8("4ee901e5c2d8ca3d");
583        let fast = Magma::new(&key);
584        let slow = MagmaCt::new(&key);
585        assert_eq!(slow.encrypt_block(&pt), ct, "encrypt");
586        assert_eq!(slow.decrypt_block(&ct), pt, "decrypt");
587        assert_eq!(
588            slow.encrypt_block(&pt),
589            fast.encrypt_block(&pt),
590            "match fast"
591        );
592    }
593}