gost-crypto 0.2.0

Pure Rust: GOST 28147-89 block cipher and GOST R 34.11-94 hash (RustCrypto compatible)
Documentation
//! GOST 28147-89 block cipher (RFC 5830).
//!
//! 64-bit block, 256-bit key. Implements [`cipher::BlockEncrypt`],
//! [`cipher::BlockDecrypt`] and [`cipher::KeyInit`] for RustCrypto compatibility.
//!
//! Use with external mode crates: `cbc`, `cfb-mode`, `ofb`, `cmac`, etc.
//!
//! [`KeyInit::new`] uses the **CryptoPro** S-box. For other S-boxes use
//! [`Gost28147::with_sbox`].

use cipher::{KeyInit, KeySizeUser, consts::{U8, U32}};
use crate::sbox::{Sbox, SBOX_CRYPTOPRO};

/// Encryption key schedule: K0–K7 ×3, then K7–K0.
const SEQ_ENCRYPT: [usize; 32] = [
    0,1,2,3,4,5,6,7,
    0,1,2,3,4,5,6,7,
    0,1,2,3,4,5,6,7,
    7,6,5,4,3,2,1,0,
];

/// Decryption key schedule: K0–K7, then K7–K0 ×3.
const SEQ_DECRYPT: [usize; 32] = [
    0,1,2,3,4,5,6,7,
    7,6,5,4,3,2,1,0,
    7,6,5,4,3,2,1,0,
    7,6,5,4,3,2,1,0,
];

/// GOST 28147-89 block cipher.
///
/// Implements [`cipher::KeyInit`] (CryptoPro S-box), [`cipher::BlockEncrypt`],
/// and [`cipher::BlockDecrypt`].
#[derive(Clone)]
pub struct Gost28147 {
    subkeys: [u32; 8],
    sbox: Sbox,
}

impl Gost28147 {
    /// Construct with an explicit S-box.
    pub fn with_sbox(key: &[u8; 32], sbox: &Sbox) -> Self {
        let mut subkeys = [0u32; 8];
        for i in 0..8 {
            subkeys[i] = u32::from_le_bytes(key[4*i..4*i+4].try_into().unwrap());
        }
        Self { subkeys, sbox: *sbox }
    }

    #[inline]
    fn apply_sbox(&self, val: u32) -> u32 {
        let mut result = 0u32;
        for i in 0..8 {
            let nibble = ((val >> (4 * i)) & 0xF) as usize;
            result |= (self.sbox[i][nibble] as u32) << (4 * i);
        }
        result
    }

    #[inline]
    fn round_fn(&self, n1: u32, k: u32) -> u32 {
        self.apply_sbox(n1.wrapping_add(k)).rotate_left(11)
    }

    fn xcrypt(&self, seq: &[usize; 32], block: &[u8; 8]) -> [u8; 8] {
        let mut n1 = u32::from_le_bytes(block[0..4].try_into().unwrap());
        let mut n2 = u32::from_le_bytes(block[4..8].try_into().unwrap());
        for &ki in seq {
            let t = self.round_fn(n1, self.subkeys[ki]);
            let new_n1 = t ^ n2;
            n2 = n1;
            n1 = new_n1;
        }
        let mut out = [0u8; 8];
        out[0..4].copy_from_slice(&n2.to_le_bytes());
        out[4..8].copy_from_slice(&n1.to_le_bytes());
        out
    }

    /// Encrypt a single 8-byte block (low-level, no trait overhead).
    #[inline]
    pub fn encrypt_block_raw(&self, block: &[u8; 8]) -> [u8; 8] {
        self.xcrypt(&SEQ_ENCRYPT, block)
    }

    /// Decrypt a single 8-byte block (low-level, no trait overhead).
    #[inline]
    pub fn decrypt_block_raw(&self, block: &[u8; 8]) -> [u8; 8] {
        self.xcrypt(&SEQ_DECRYPT, block)
    }
}

// ── RustCrypto cipher traits ──────────────────────────────────────────────────

impl KeySizeUser for Gost28147 {
    type KeySize = U32;
}

/// Uses **CryptoPro** S-box. For other S-boxes use [`Gost28147::with_sbox`].
impl KeyInit for Gost28147 {
    fn new(key: &cipher::Key<Self>) -> Self {
        let bytes: &[u8] = key.as_ref();
        Self::with_sbox(bytes.try_into().unwrap(), &SBOX_CRYPTOPRO)
    }
}

cipher::impl_simple_block_encdec!(
    <> Gost28147, U8, state, block,
    encrypt: {
        let pt: [u8; 8] = block.get_in().as_slice().try_into().unwrap();
        let ct = state.encrypt_block_raw(&pt);
        block.get_out().copy_from_slice(&ct);
    }
    decrypt: {
        let ct: [u8; 8] = block.get_in().as_slice().try_into().unwrap();
        let pt = state.decrypt_block_raw(&ct);
        block.get_out().copy_from_slice(&pt);
    }
);

// ── Tests ─────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    extern crate std;
    use super::*;
    use cipher::{BlockDecrypt, BlockEncrypt, KeyInit};
    use crate::sbox::{SBOX_CRYPTOPRO, SBOX_TEST};

    // Key used for all vector tests (matches gogost test suite)
    // hex: 0123456789ABCDEF FEDCBA9876543210 0123456789ABCDEF FEDCBA9876543210
    const KEY1: [u8; 32] = [
        0x01,0x23,0x45,0x67,0x89,0xAB,0xCD,0xEF,
        0xFE,0xDC,0xBA,0x98,0x76,0x54,0x32,0x10,
        0x01,0x23,0x45,0x67,0x89,0xAB,0xCD,0xEF,
        0xFE,0xDC,0xBA,0x98,0x76,0x54,0x32,0x10,
    ];

    const KEY2: [u8; 32] = [0xFF; 32]; // all-FF key
    const KEY3: [u8; 32] = [0x00; 32]; // all-zero key

    // ── Positive: known encrypt vectors (SBOX_TEST = GostR341194TestParamSet) ──

    #[test]
    fn test_param_encrypt_zeros() {
        // Vector verified against gogost SboxIdGostR341194TestParamSet
        let c = Gost28147::with_sbox(&KEY1, &SBOX_TEST);
        assert_eq!(
            c.encrypt_block_raw(&[0x00;8]),
            [0xDB,0x52,0x68,0xBC,0x9D,0x83,0x2A,0xA7]
        );
    }

    #[test]
    fn test_param_encrypt_ordered() {
        let c = Gost28147::with_sbox(&KEY1, &SBOX_TEST);
        assert_eq!(
            c.encrypt_block_raw(&[0x01,0x23,0x45,0x67,0x89,0xAB,0xCD,0xEF]),
            [0xD5,0xC1,0xEE,0x12,0x31,0xD2,0xC7,0x98]
        );
    }

    // ── Positive: known encrypt vectors (CryptoProParamSet) ──────────────────

    #[test]
    fn cryptopro_encrypt_zeros() {
        let c = Gost28147::with_sbox(&KEY1, &SBOX_CRYPTOPRO);
        assert_eq!(
            c.encrypt_block_raw(&[0x00;8]),
            [0x7C,0x43,0x32,0x42,0x6A,0x32,0x4D,0xB8]
        );
    }

    #[test]
    fn cryptopro_encrypt_ordered() {
        let c = Gost28147::with_sbox(&KEY1, &SBOX_CRYPTOPRO);
        assert_eq!(
            c.encrypt_block_raw(&[0x01,0x23,0x45,0x67,0x89,0xAB,0xCD,0xEF]),
            [0x1A,0x27,0x35,0x04,0x3D,0x46,0x10,0x5B]
        );
    }

    #[test]
    fn cryptopro_encrypt_all_ff() {
        let c = Gost28147::with_sbox(&KEY1, &SBOX_CRYPTOPRO);
        assert_eq!(
            c.encrypt_block_raw(&[0xFF;8]),
            [0x43,0x6A,0xDD,0x97,0xC6,0xD3,0xDC,0xAE]
        );
    }

    #[test]
    fn cryptopro_encrypt_zero_key() {
        let c = Gost28147::with_sbox(&KEY3, &SBOX_CRYPTOPRO);
        assert_eq!(
            c.encrypt_block_raw(&[0x00;8]),
            [0x8D,0x99,0x78,0x5F,0x93,0x60,0x3F,0xD9]
        );
    }

    // ── Positive: encrypt → decrypt roundtrip ────────────────────────────────

    #[test]
    fn roundtrip_test_param() {
        let c = Gost28147::with_sbox(&KEY1, &SBOX_TEST);
        for pt in [
            [0x00u8;8],
            [0x01,0x23,0x45,0x67,0x89,0xAB,0xCD,0xEF],
            [0xFF;8],
            [0xDE,0xAD,0xBE,0xEF,0xCA,0xFE,0xBA,0xBE],
        ] {
            assert_eq!(c.decrypt_block_raw(&c.encrypt_block_raw(&pt)), pt,
                "roundtrip failed for {pt:02X?}");
        }
    }

    #[test]
    fn roundtrip_cryptopro() {
        let c = Gost28147::with_sbox(&KEY1, &SBOX_CRYPTOPRO);
        for pt in [
            [0x00u8;8],
            [0x01,0x23,0x45,0x67,0x89,0xAB,0xCD,0xEF],
            [0xFF;8],
            [0xDE,0xAD,0xBE,0xEF,0xCA,0xFE,0xBA,0xBE],
        ] {
            assert_eq!(c.decrypt_block_raw(&c.encrypt_block_raw(&pt)), pt);
        }
    }

    // ── Positive: RustCrypto trait API ────────────────────────────────────────

    #[test]
    fn trait_encrypt_decrypt_roundtrip() {
        let c = Gost28147::new(&KEY1.into());
        let pt: [u8; 8] = [0xAA,0xBB,0xCC,0xDD,0x11,0x22,0x33,0x44];
        let mut buf = cipher::Block::<Gost28147>::clone_from_slice(&pt);
        c.encrypt_block(&mut buf);
        assert_ne!(buf.as_slice(), &pt, "ciphertext must differ from plaintext");
        c.decrypt_block(&mut buf);
        assert_eq!(buf.as_slice(), &pt, "decryption must restore plaintext");
    }

    // ── Negative: ciphertext ≠ plaintext ─────────────────────────────────────

    #[test]
    fn ciphertext_differs_from_plaintext() {
        let c = Gost28147::with_sbox(&KEY1, &SBOX_CRYPTOPRO);
        let pt = [0x42u8; 8];
        assert_ne!(c.encrypt_block_raw(&pt), pt);
    }

    // ── Negative: different keys → different ciphertexts ─────────────────────

    #[test]
    fn different_keys_produce_different_ciphertexts() {
        let pt = [0x01,0x23,0x45,0x67,0x89,0xAB,0xCD,0xEFu8];
        let ct1 = Gost28147::with_sbox(&KEY1, &SBOX_CRYPTOPRO).encrypt_block_raw(&pt);
        let ct2 = Gost28147::with_sbox(&KEY2, &SBOX_CRYPTOPRO).encrypt_block_raw(&pt);
        let ct3 = Gost28147::with_sbox(&KEY3, &SBOX_CRYPTOPRO).encrypt_block_raw(&pt);
        assert_ne!(ct1, ct2);
        assert_ne!(ct1, ct3);
        assert_ne!(ct2, ct3);
    }

    // ── Negative: different S-boxes → different ciphertexts ──────────────────

    #[test]
    fn different_sboxes_produce_different_ciphertexts() {
        let pt = [0x01,0x23,0x45,0x67,0x89,0xAB,0xCD,0xEFu8];
        let ct_test   = Gost28147::with_sbox(&KEY1, &SBOX_TEST).encrypt_block_raw(&pt);
        let ct_crypto = Gost28147::with_sbox(&KEY1, &SBOX_CRYPTOPRO).encrypt_block_raw(&pt);
        assert_ne!(ct_test, ct_crypto);
    }

    // ── Negative: wrong key decryption produces garbage ───────────────────────

    #[test]
    fn wrong_key_decryption_gives_wrong_plaintext() {
        let pt = [0x00u8; 8];
        let ct = Gost28147::with_sbox(&KEY1, &SBOX_CRYPTOPRO).encrypt_block_raw(&pt);
        // Vector from gogost: wrong_key_dec = 36CC46767CE95A4C
        let wrong_dec = Gost28147::with_sbox(&KEY2, &SBOX_CRYPTOPRO).decrypt_block_raw(&ct);
        assert_eq!(wrong_dec, [0x36,0xCC,0x46,0x76,0x7C,0xE9,0x5A,0x4C]);
        assert_ne!(wrong_dec, pt);
    }

    // ── Negative: decrypt without prior encrypt gives non-original plaintext ──

    #[test]
    fn decrypt_random_ciphertext_not_all_zeros() {
        let c = Gost28147::with_sbox(&KEY1, &SBOX_CRYPTOPRO);
        let garbage = [0xDE,0xAD,0xBE,0xEF,0xCA,0xFE,0xBA,0xBEu8];
        let dec = c.decrypt_block_raw(&garbage);
        // Decrypting arbitrary bytes with any key is deterministic but not zero
        assert_ne!(dec, [0u8; 8]);
    }
}