gost-crypto 0.2.0

Pure Rust: GOST 28147-89 block cipher and GOST R 34.11-94 hash (RustCrypto compatible)
Documentation
//! GOST R 34.11-94 hash function (RFC 5831).
//!
//! Implements the [`digest::Update`] and [`digest::FixedOutput`] traits
//! for compatibility with the RustCrypto ecosystem.

use crate::gost28147::Gost28147;
use crate::sbox::{Sbox, SBOX_CRYPTOPRO};
use digest::{HashMarker, Output, OutputSizeUser, Reset, Update};
use digest::typenum::U32;

/// C3 constant from RFC 5831.
const C3: [u8; 32] = [
    0xff, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0xff,
    0xff, 0x00, 0x00, 0xff, 0x00, 0xff, 0xff, 0x00,
    0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff,
    0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00,
];

#[inline]
fn xor32(a: &[u8; 32], b: &[u8; 32]) -> [u8; 32] {
    let mut out = [0u8; 32];
    for i in 0..32 { out[i] = a[i] ^ b[i]; }
    out
}

#[inline]
fn perm_a(x: &[u8; 32]) -> [u8; 32] {
    let mut out = [0u8; 32];
    for i in 0..8 { out[i] = x[16 + i] ^ x[24 + i]; }
    out[8..32].copy_from_slice(&x[0..24]);
    out
}

#[inline]
fn perm_p(x: &[u8; 32]) -> [u8; 32] {
    let mut out = [0u8; 32];
    for i in 0..4usize {
        for j in 0..8usize {
            out[i + 4 * j] = x[8 * i + j];
        }
    }
    out
}

#[inline]
fn chi(y: &[u8; 32]) -> [u8; 32] {
    let mut out = [0u8; 32];
    out[0] = y[30] ^ y[28] ^ y[26] ^ y[24] ^ y[0] ^ y[6];
    out[1] = y[31] ^ y[29] ^ y[27] ^ y[25] ^ y[1] ^ y[7];
    out[2..32].copy_from_slice(&y[0..30]);
    out
}

#[inline]
fn chi_n(y: &[u8; 32], n: usize) -> [u8; 32] {
    let mut x = *y;
    for _ in 0..n { x = chi(&x); }
    x
}

fn add256_be(a: &[u8; 32], b: &[u8; 32]) -> [u8; 32] {
    let mut out = [0u8; 32];
    let mut carry = 0u16;
    for i in (0..32).rev() {
        let sum = a[i] as u16 + b[i] as u16 + carry;
        out[i] = sum as u8;
        carry = sum >> 8;
    }
    out
}

fn step(h: &[u8; 32], m: &[u8; 32], sbox: &Sbox) -> [u8; 32] {
    let zeros = [0u8; 32];

    let mut u = *h;
    let mut v = *m;

    let k1 = { let w = xor32(&u, &v); let mut k = perm_p(&w); k.reverse(); k };
    u = xor32(&perm_a(&u), &zeros);
    v = perm_a(&perm_a(&v));
    let k2 = { let w = xor32(&u, &v); let mut k = perm_p(&w); k.reverse(); k };
    u = xor32(&perm_a(&u), &C3);
    v = perm_a(&perm_a(&v));
    let k3 = { let w = xor32(&u, &v); let mut k = perm_p(&w); k.reverse(); k };
    u = xor32(&perm_a(&u), &zeros);
    v = perm_a(&perm_a(&v));
    let k4 = { let w = xor32(&u, &v); let mut k = perm_p(&w); k.reverse(); k };

    let enc = |key: &[u8; 32], chunk: &[u8; 8]| -> [u8; 8] {
        let mut rev = *chunk;
        rev.reverse();
        let cipher = Gost28147::with_sbox(key, sbox);
        let mut out = cipher.encrypt_block_raw(&rev);
        out.reverse();
        out
    };

    let mut s = [0u8; 32];
    s[24..32].copy_from_slice(&enc(&k1, &h[24..32].try_into().unwrap()));
    s[16..24].copy_from_slice(&enc(&k2, &h[16..24].try_into().unwrap()));
    s[8..16].copy_from_slice( &enc(&k3, &h[8..16].try_into().unwrap()));
    s[0..8].copy_from_slice(  &enc(&k4, &h[0..8].try_into().unwrap()));

    let t1 = chi_n(&s, 12);
    let t2 = xor32(m, &t1);
    let t3 = chi(&t2);
    let t4 = xor32(h, &t3);
    chi_n(&t4, 61)
}

/// GOST R 34.11-94 hash function.
pub struct Gost341194 {
    sbox: Sbox,
    h: [u8; 32],
    checksum: [u8; 32],
    bit_len: u64,
    buf: [u8; 32],
    buf_len: usize,
}

impl Gost341194 {
    /// Create with the **CryptoPro** S-box (КриптоПро CSP compatible).
    pub fn new_with_cryptopro() -> Self {
        Self::new_with_sbox(&SBOX_CRYPTOPRO)
    }

    /// Create with a custom S-box.
    pub fn new_with_sbox(sbox: &Sbox) -> Self {
        Self {
            sbox: *sbox,
            h: [0u8; 32],
            checksum: [0u8; 32],
            bit_len: 0,
            buf: [0u8; 32],
            buf_len: 0,
        }
    }

    fn process_block(&mut self, block: &[u8; 32]) {
        let mut rev = *block;
        rev.reverse();
        self.checksum = add256_be(&self.checksum, &rev);
        self.h = step(&self.h, &rev, &self.sbox);
        self.bit_len = self.bit_len.wrapping_add(256);
    }

    /// Compute the final hash. Consumes self.
    pub fn finalize_bytes(self) -> [u8; 32] {
        let mut h = self.h;
        let mut checksum = self.checksum;
        let mut bit_len = self.bit_len;

        if self.buf_len > 0 {
            bit_len = bit_len.wrapping_add((self.buf_len as u64) * 8);
            let mut block = [0u8; 32];
            block[..self.buf_len].copy_from_slice(&self.buf[..self.buf_len]);
            block.reverse();
            checksum = add256_be(&checksum, &block);
            h = step(&h, &block, &self.sbox);
        }

        let mut len_block = [0u8; 32];
        len_block[24..32].copy_from_slice(&bit_len.to_be_bytes());
        h = step(&h, &len_block, &self.sbox);
        h = step(&h, &checksum, &self.sbox);

        h.reverse();
        h
    }
}

impl HashMarker for Gost341194 {}

impl OutputSizeUser for Gost341194 {
    type OutputSize = U32;
}

impl Update for Gost341194 {
    fn update(&mut self, data: &[u8]) {
        let mut offset = 0;
        if self.buf_len > 0 {
            let take = (32 - self.buf_len).min(data.len());
            self.buf[self.buf_len..self.buf_len + take].copy_from_slice(&data[..take]);
            self.buf_len += take;
            offset += take;
            if self.buf_len == 32 {
                let block: [u8; 32] = self.buf;
                self.process_block(&block);
                self.buf_len = 0;
            }
        }
        while offset + 32 <= data.len() {
            let block: [u8; 32] = data[offset..offset + 32].try_into().unwrap();
            self.process_block(&block);
            offset += 32;
        }
        let remaining = data.len() - offset;
        if remaining > 0 {
            self.buf[..remaining].copy_from_slice(&data[offset..]);
            self.buf_len = remaining;
        }
    }
}

impl digest::FixedOutput for Gost341194 {
    fn finalize_into(self, out: &mut Output<Self>) {
        out.copy_from_slice(&self.finalize_bytes());
    }
}

impl Reset for Gost341194 {
    fn reset(&mut self) {
        *self = Self::new_with_sbox(&self.sbox);
    }
}

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

#[cfg(test)]
mod tests {
    extern crate std;
    use super::*;
    use digest::Update;
    use crate::sbox::{SBOX_CRYPTOPRO, SBOX_TEST};

    fn hash(sbox: &Sbox, data: &[u8]) -> [u8; 32] {
        let mut h = Gost341194::new_with_sbox(sbox);
        Update::update(&mut h, data);
        h.finalize_bytes()
    }

    fn from_hex(s: &str) -> [u8; 32] {
        let bytes = (0..s.len())
            .step_by(2)
            .map(|i| u8::from_str_radix(&s[i..i+2], 16).unwrap())
            .collect::<std::vec::Vec<u8>>();
        bytes.try_into().unwrap()
    }

    // ── Positive: known vectors (CryptoPro), verified against gogost ──────────

    #[test]
    fn cryptopro_empty_string() {
        assert_eq!(
            hash(&SBOX_CRYPTOPRO, b""),
            from_hex("981e5f3ca30c841487830f84fb433e13ac1101569b9c13584ac483234cd656c0")
        );
    }

    #[test]
    fn cryptopro_single_char_a() {
        assert_eq!(
            hash(&SBOX_CRYPTOPRO, b"a"),
            from_hex("e74c52dd282183bf37af0079c9f78055715a103f17e3133ceff1aacf2f403011")
        );
    }

    #[test]
    fn cryptopro_abc() {
        assert_eq!(
            hash(&SBOX_CRYPTOPRO, b"abc"),
            from_hex("b285056dbf18d7392d7677369524dd14747459ed8143997e163b2986f92fd42c")
        );
    }

    #[test]
    fn cryptopro_message_digest() {
        assert_eq!(
            hash(&SBOX_CRYPTOPRO, b"message digest"),
            from_hex("bc6041dd2aa401ebfa6e9886734174febdb4729aa972d60f549ac39b29721ba0")
        );
    }

    #[test]
    fn cryptopro_quick_brown_fox() {
        assert_eq!(
            hash(&SBOX_CRYPTOPRO, b"The quick brown fox jumps over the lazy dog"),
            from_hex("9004294a361a508c586fe53d1f1b02746765e71b765472786e4770d565830a76")
        );
    }

    // ── Positive: known vectors (TestParamSet) ────────────────────────────────

    #[test]
    fn testparam_empty_string() {
        assert_eq!(
            hash(&SBOX_TEST, b""),
            from_hex("ce85b99cc46752fffee35cab9a7b0278abb4c2d2055cff685af4912c49490f8d")
        );
    }

    #[test]
    fn testparam_single_char_a() {
        assert_eq!(
            hash(&SBOX_TEST, b"a"),
            from_hex("d42c539e367c66e9c88a801f6649349c21871b4344c6a573f849fdce62f314dd")
        );
    }

    #[test]
    fn testparam_abc() {
        assert_eq!(
            hash(&SBOX_TEST, b"abc"),
            from_hex("f3134348c44fb1b2a277729e2285ebb5cb5e0f29c975bc753b70497c06a4d51d")
        );
    }

    // ── Positive: multiblock input (> 32 bytes) ───────────────────────────────

    #[test]
    fn cryptopro_long_input_consistent() {
        // Hash of 64 zero bytes via two separate 32-byte blocks
        let h1 = hash(&SBOX_CRYPTOPRO, &[0u8; 64]);
        // Same via incremental update
        let mut hasher = Gost341194::new_with_sbox(&SBOX_CRYPTOPRO);
        Update::update(&mut hasher, &[0u8; 32]);
        Update::update(&mut hasher, &[0u8; 32]);
        let h2 = hasher.finalize_bytes();
        assert_eq!(h1, h2);
    }

    // ── Positive: incremental updates match single-pass ──────────────────────

    #[test]
    fn incremental_equals_single_pass() {
        let data = b"The quick brown fox jumps over the lazy dog";
        let h_single = hash(&SBOX_CRYPTOPRO, data);

        let mut hasher = Gost341194::new_with_sbox(&SBOX_CRYPTOPRO);
        for chunk in data.chunks(7) {
            Update::update(&mut hasher, chunk);
        }
        let h_incremental = hasher.finalize_bytes();
        assert_eq!(h_single, h_incremental);
    }

    // ── Positive: Reset restores initial state ────────────────────────────────

    #[test]
    fn reset_restores_state() {
        use digest::Reset;
        let mut h = Gost341194::new_with_sbox(&SBOX_CRYPTOPRO);
        Update::update(&mut h, b"garbage data");
        h.reset();
        Update::update(&mut h, b"abc");
        let after_reset = h.finalize_bytes();
        assert_eq!(after_reset, hash(&SBOX_CRYPTOPRO, b"abc"));
    }

    // ── Negative: different inputs → different hashes ────────────────────────

    #[test]
    fn different_inputs_produce_different_hashes() {
        assert_ne!(hash(&SBOX_CRYPTOPRO, b"hello"), hash(&SBOX_CRYPTOPRO, b"world"));
        assert_ne!(hash(&SBOX_CRYPTOPRO, b"a"),     hash(&SBOX_CRYPTOPRO, b"b"));
        assert_ne!(hash(&SBOX_CRYPTOPRO, b""),      hash(&SBOX_CRYPTOPRO, b" "));
    }

    // ── Negative: same input, different S-boxes → different hashes ───────────

    #[test]
    fn different_sboxes_produce_different_hashes() {
        let data = b"test input";
        assert_ne!(hash(&SBOX_CRYPTOPRO, data), hash(&SBOX_TEST, data));
    }

    // ── Negative: hash is deterministic ──────────────────────────────────────

    #[test]
    fn hash_is_deterministic() {
        let data = b"determinism check";
        assert_eq!(hash(&SBOX_CRYPTOPRO, data), hash(&SBOX_CRYPTOPRO, data));
    }

    // ── Negative: appending data changes the hash ─────────────────────────────

    #[test]
    fn appending_byte_changes_hash() {
        let h1 = hash(&SBOX_CRYPTOPRO, b"hello");
        let h2 = hash(&SBOX_CRYPTOPRO, b"hello!");
        assert_ne!(h1, h2);
    }
}