mdict-rs 0.1.4

Library-first Rust parser for MDict .mdx and .mdd dictionaries
Documentation
use crate::error::{Error, Result};
use crate::types::Passcode;

const RIPEMD128_R: [usize; 64] = [
    0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 7, 4, 13, 1, 10, 6, 15, 3, 12, 0, 9, 5,
    2, 14, 11, 8, 3, 10, 14, 4, 9, 15, 8, 1, 2, 7, 0, 6, 13, 11, 5, 12, 1, 9, 11, 10, 0, 8, 12, 4,
    13, 3, 7, 15, 14, 5, 6, 2,
];

const RIPEMD128_RP: [usize; 64] = [
    5, 14, 7, 0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12, 6, 11, 3, 7, 0, 13, 5, 10, 14, 15, 8, 12,
    4, 9, 1, 2, 15, 5, 1, 3, 7, 14, 6, 9, 11, 8, 12, 2, 10, 0, 4, 13, 8, 6, 4, 1, 3, 11, 15, 0, 5,
    12, 2, 13, 9, 7, 10, 14,
];

const RIPEMD128_S: [u32; 64] = [
    11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8, 7, 6, 8, 13, 11, 9, 7, 15, 7, 12, 15,
    9, 11, 7, 13, 12, 11, 13, 6, 7, 14, 9, 13, 15, 14, 8, 13, 6, 5, 12, 7, 5, 11, 12, 14, 15, 14,
    15, 9, 8, 9, 14, 5, 6, 8, 6, 5, 12,
];

const RIPEMD128_SP: [u32; 64] = [
    8, 9, 9, 11, 13, 15, 15, 5, 7, 7, 8, 11, 14, 14, 12, 6, 9, 13, 15, 7, 12, 8, 9, 11, 7, 7, 12,
    7, 6, 15, 13, 11, 9, 7, 15, 11, 8, 6, 6, 14, 12, 13, 5, 14, 13, 13, 7, 5, 15, 5, 8, 11, 14, 14,
    6, 14, 6, 9, 12, 9, 12, 5, 15, 8,
];

pub fn decrypt_keyword_index_block(checksum: u32, data: &mut [u8]) {
    let mut key_material = checksum.to_be_bytes().to_vec();
    key_material.extend_from_slice(&[0x95, 0x36, 0x00, 0x00]);
    let key = ripemd128(&key_material);

    let mut previous = 0x36u8;
    for (index, byte) in data.iter_mut().enumerate() {
        let cipher = *byte;
        let swapped = cipher.rotate_left(4);
        *byte = swapped ^ (index as u8) ^ key[index % key.len()] ^ previous;
        previous = cipher;
    }
}

pub fn decrypt_keyword_header_block(data: &mut [u8], passcode: &Passcode) -> Result<()> {
    let key = derive_keyword_header_key(passcode)?;
    salsa20_8_xor(data, &key);
    Ok(())
}

fn derive_keyword_header_key(passcode: &Passcode) -> Result<[u8; 16]> {
    let mut encrypted_hash = decode_hex_16(&passcode.reg_code_hex)?;
    let user_hash = ripemd128(passcode.user_id.as_bytes());
    salsa20_8_xor(&mut encrypted_hash, &user_hash);
    Ok(encrypted_hash)
}

fn decode_hex_16(text: &str) -> Result<[u8; 16]> {
    if text.len() != 32 {
        return Err(Error::InvalidData(
            "registration code must be 32 hex digits".to_owned(),
        ));
    }
    let mut out = [0u8; 16];
    for (index, chunk) in text.as_bytes().chunks_exact(2).enumerate() {
        let high = hex_nibble(chunk[0])?;
        let low = hex_nibble(chunk[1])?;
        out[index] = (high << 4) | low;
    }
    Ok(out)
}

fn hex_nibble(byte: u8) -> Result<u8> {
    match byte {
        b'0'..=b'9' => Ok(byte - b'0'),
        b'a'..=b'f' => Ok(byte - b'a' + 10),
        b'A'..=b'F' => Ok(byte - b'A' + 10),
        _ => Err(Error::InvalidData(
            "registration code contains non-hex bytes".to_owned(),
        )),
    }
}

pub fn ripemd128(bytes: &[u8]) -> [u8; 16] {
    let mut padded = bytes.to_vec();
    let bit_len = (bytes.len() as u64) * 8;
    padded.push(0x80);
    while padded.len() % 64 != 56 {
        padded.push(0);
    }
    padded.extend_from_slice(&bit_len.to_le_bytes());

    let mut h0 = 0x67452301u32;
    let mut h1 = 0xefcdab89u32;
    let mut h2 = 0x98badcfeu32;
    let mut h3 = 0x10325476u32;

    for chunk in padded.chunks_exact(64) {
        let mut words = [0u32; 16];
        for (idx, word) in words.iter_mut().enumerate() {
            let start = idx * 4;
            *word = u32::from_le_bytes([
                chunk[start],
                chunk[start + 1],
                chunk[start + 2],
                chunk[start + 3],
            ]);
        }

        let (mut a, mut b, mut c, mut d) = (h0, h1, h2, h3);
        let (mut aa, mut bb, mut cc, mut dd) = (h0, h1, h2, h3);

        for idx in 0..64 {
            let tmp = a
                .wrapping_add(f(idx, b, c, d))
                .wrapping_add(words[RIPEMD128_R[idx]])
                .wrapping_add(k(idx))
                .rotate_left(RIPEMD128_S[idx]);
            a = d;
            d = c;
            c = b;
            b = tmp;

            let tmp_p = aa
                .wrapping_add(fp(idx, bb, cc, dd))
                .wrapping_add(words[RIPEMD128_RP[idx]])
                .wrapping_add(kp(idx))
                .rotate_left(RIPEMD128_SP[idx]);
            aa = dd;
            dd = cc;
            cc = bb;
            bb = tmp_p;
        }

        let tmp = h1.wrapping_add(c).wrapping_add(dd);
        h1 = h2.wrapping_add(d).wrapping_add(aa);
        h2 = h3.wrapping_add(a).wrapping_add(bb);
        h3 = h0.wrapping_add(b).wrapping_add(cc);
        h0 = tmp;
    }

    let mut out = [0u8; 16];
    out[0..4].copy_from_slice(&h0.to_le_bytes());
    out[4..8].copy_from_slice(&h1.to_le_bytes());
    out[8..12].copy_from_slice(&h2.to_le_bytes());
    out[12..16].copy_from_slice(&h3.to_le_bytes());
    out
}

fn f(idx: usize, x: u32, y: u32, z: u32) -> u32 {
    match idx {
        0..=15 => x ^ y ^ z,
        16..=31 => (x & y) | (!x & z),
        32..=47 => (x | !y) ^ z,
        _ => (x & z) | (y & !z),
    }
}

fn fp(idx: usize, x: u32, y: u32, z: u32) -> u32 {
    match idx {
        0..=15 => (x & z) | (y & !z),
        16..=31 => (x | !y) ^ z,
        32..=47 => (x & y) | (!x & z),
        _ => x ^ y ^ z,
    }
}

fn k(idx: usize) -> u32 {
    match idx {
        0..=15 => 0x00000000,
        16..=31 => 0x5a827999,
        32..=47 => 0x6ed9eba1,
        _ => 0x8f1bbcdc,
    }
}

fn kp(idx: usize) -> u32 {
    match idx {
        0..=15 => 0x50a28be6,
        16..=31 => 0x5c4dd124,
        32..=47 => 0x6d703ef3,
        _ => 0x00000000,
    }
}

pub fn salsa20_8_xor(data: &mut [u8], key: &[u8; 16]) {
    for (counter, chunk) in data.chunks_mut(64).enumerate() {
        let keystream = salsa20_8_block(key, counter as u64);
        for (dst, src) in chunk.iter_mut().zip(keystream.iter()) {
            *dst ^= *src;
        }
    }
}

fn salsa20_8_block(key: &[u8; 16], counter: u64) -> [u8; 64] {
    let constants = *b"expand 16-byte k";
    let k0 = u32::from_le_bytes([key[0], key[1], key[2], key[3]]);
    let k1 = u32::from_le_bytes([key[4], key[5], key[6], key[7]]);
    let k2 = u32::from_le_bytes([key[8], key[9], key[10], key[11]]);
    let k3 = u32::from_le_bytes([key[12], key[13], key[14], key[15]]);

    let state = [
        u32::from_le_bytes([constants[0], constants[1], constants[2], constants[3]]),
        k0,
        k1,
        k2,
        k3,
        u32::from_le_bytes([constants[4], constants[5], constants[6], constants[7]]),
        0,
        0,
        counter as u32,
        (counter >> 32) as u32,
        u32::from_le_bytes([constants[8], constants[9], constants[10], constants[11]]),
        k0,
        k1,
        k2,
        k3,
        u32::from_le_bytes([constants[12], constants[13], constants[14], constants[15]]),
    ];

    let mut working = state;
    for _ in 0..4 {
        working = double_round(working);
    }
    for (dst, src) in working.iter_mut().zip(state.iter()) {
        *dst = dst.wrapping_add(*src);
    }

    let mut out = [0u8; 64];
    for (idx, word) in working.iter().enumerate() {
        out[idx * 4..idx * 4 + 4].copy_from_slice(&word.to_le_bytes());
    }
    out
}

fn double_round(state: [u32; 16]) -> [u32; 16] {
    row_round(column_round(state))
}

fn column_round(mut state: [u32; 16]) -> [u32; 16] {
    quarter_round(&mut state, 0, 4, 8, 12);
    quarter_round(&mut state, 5, 9, 13, 1);
    quarter_round(&mut state, 10, 14, 2, 6);
    quarter_round(&mut state, 15, 3, 7, 11);
    state
}

fn row_round(mut state: [u32; 16]) -> [u32; 16] {
    quarter_round(&mut state, 0, 1, 2, 3);
    quarter_round(&mut state, 5, 6, 7, 4);
    quarter_round(&mut state, 10, 11, 8, 9);
    quarter_round(&mut state, 15, 12, 13, 14);
    state
}

fn quarter_round(state: &mut [u32; 16], a: usize, b: usize, c: usize, d: usize) {
    state[b] ^= state[a].wrapping_add(state[d]).rotate_left(7);
    state[c] ^= state[b].wrapping_add(state[a]).rotate_left(9);
    state[d] ^= state[c].wrapping_add(state[b]).rotate_left(13);
    state[a] ^= state[d].wrapping_add(state[c]).rotate_left(18);
}

#[cfg(test)]
fn encrypt_keyword_index_block(checksum: u32, data: &mut [u8]) {
    let mut key_material = checksum.to_be_bytes().to_vec();
    key_material.extend_from_slice(&[0x95, 0x36, 0x00, 0x00]);
    let key = ripemd128(&key_material);

    let mut previous = 0x36u8;
    for (index, byte) in data.iter_mut().enumerate() {
        let plain = *byte;
        *byte = (plain ^ (index as u8) ^ key[index % key.len()] ^ previous).rotate_left(4);
        previous = *byte;
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn hex(bytes: &[u8]) -> String {
        bytes.iter().map(|byte| format!("{byte:02x}")).collect()
    }

    #[test]
    fn ripemd128_matches_known_vectors() {
        assert_eq!(hex(&ripemd128(b"")), "cdf26213a150dc3ecb610f18f6b38b46");
        assert_eq!(hex(&ripemd128(b"a")), "86be7afa339d0fc7cfc785e72f578d33");
    }

    #[test]
    fn keyword_index_cipher_round_trips() {
        let checksum = 0x12345678;
        let mut payload = b"example payload".to_vec();
        encrypt_keyword_index_block(checksum, &mut payload);
        decrypt_keyword_index_block(checksum, &mut payload);
        assert_eq!(payload, b"example payload");
    }

    #[test]
    fn salsa20_8_is_symmetric() {
        let key = [0x11u8; 16];
        let mut data = b"hello encrypted header".to_vec();
        salsa20_8_xor(&mut data, &key);
        salsa20_8_xor(&mut data, &key);
        assert_eq!(data, b"hello encrypted header");
    }
}