lib-q-romulus 0.0.4

Romulus-N and Romulus-M AEAD (SKINNY-128-384+), no_std / wasm-friendly
Documentation
//! SKINNY-128-384+ tweakable block cipher (encryption only), Romulus v1.3 profile.
//!
//! 128-bit block, 384-bit tweakey, 40 rounds. Layout matches the reference C
//! implementation (`state[row][col]` with `state[i>>2][i&3]` ↔ linear index `i`).

#![deny(unsafe_code)]

/// Encrypt one block in place. `block` is 16 bytes I/O; `userkey` is 48-byte tweakey.
pub(crate) fn skinny_128_384_plus_enc(block: &mut [u8; 16], userkey: &[u8; 48]) {
    let mut state = [[0u8; 4]; 4];
    let mut key_cells = [[[0u8; 4]; 4]; 3];

    for (i, &byte) in block.iter().enumerate() {
        let r = i >> 2;
        let c = i & 3;
        state[r][c] = byte;
        key_cells[0][r][c] = userkey[i];
        key_cells[1][r][c] = userkey[i + 16];
        key_cells[2][r][c] = userkey[i + 32];
    }

    for rnd in 0..40 {
        sub_cell8(&mut state);
        add_constants(&mut state, rnd);
        add_key(&mut state, &mut key_cells);
        shift_rows(&mut state);
        mix_column(&mut state);
    }

    for (i, byte) in block.iter_mut().enumerate() {
        let r = i >> 2;
        let c = i & 3;
        *byte = state[r][c];
    }
}

const SBOX_8: [u8; 256] = [
    0x65, 0x4C, 0x6A, 0x42, 0x4B, 0x63, 0x43, 0x6B, 0x55, 0x75, 0x5A, 0x7A, 0x53, 0x73, 0x5B, 0x7B,
    0x35, 0x8C, 0x3A, 0x81, 0x89, 0x33, 0x80, 0x3B, 0x95, 0x25, 0x98, 0x2A, 0x90, 0x23, 0x99, 0x2B,
    0xE5, 0xCC, 0xE8, 0xC1, 0xC9, 0xE0, 0xC0, 0xE9, 0xD5, 0xF5, 0xD8, 0xF8, 0xD0, 0xF0, 0xD9, 0xF9,
    0xA5, 0x1C, 0xA8, 0x12, 0x1B, 0xA0, 0x13, 0xA9, 0x05, 0xB5, 0x0A, 0xB8, 0x03, 0xB0, 0x0B, 0xB9,
    0x32, 0x88, 0x3C, 0x85, 0x8D, 0x34, 0x84, 0x3D, 0x91, 0x22, 0x9C, 0x2C, 0x94, 0x24, 0x9D, 0x2D,
    0x62, 0x4A, 0x6C, 0x45, 0x4D, 0x64, 0x44, 0x6D, 0x52, 0x72, 0x5C, 0x7C, 0x54, 0x74, 0x5D, 0x7D,
    0xA1, 0x1A, 0xAC, 0x15, 0x1D, 0xA4, 0x14, 0xAD, 0x02, 0xB1, 0x0C, 0xBC, 0x04, 0xB4, 0x0D, 0xBD,
    0xE1, 0xC8, 0xEC, 0xC5, 0xCD, 0xE4, 0xC4, 0xED, 0xD1, 0xF1, 0xDC, 0xFC, 0xD4, 0xF4, 0xDD, 0xFD,
    0x36, 0x8E, 0x38, 0x82, 0x8B, 0x30, 0x83, 0x39, 0x96, 0x26, 0x9A, 0x28, 0x93, 0x20, 0x9B, 0x29,
    0x66, 0x4E, 0x68, 0x41, 0x49, 0x60, 0x40, 0x69, 0x56, 0x76, 0x58, 0x78, 0x50, 0x70, 0x59, 0x79,
    0xA6, 0x1E, 0xAA, 0x11, 0x19, 0xA3, 0x10, 0xAB, 0x06, 0xB6, 0x08, 0xBA, 0x00, 0xB3, 0x09, 0xBB,
    0xE6, 0xCE, 0xEA, 0xC2, 0xCB, 0xE3, 0xC3, 0xEB, 0xD6, 0xF6, 0xDA, 0xFA, 0xD3, 0xF3, 0xDB, 0xFB,
    0x31, 0x8A, 0x3E, 0x86, 0x8F, 0x37, 0x87, 0x3F, 0x92, 0x21, 0x9E, 0x2E, 0x97, 0x27, 0x9F, 0x2F,
    0x61, 0x48, 0x6E, 0x46, 0x4F, 0x67, 0x47, 0x6F, 0x51, 0x71, 0x5E, 0x7E, 0x57, 0x77, 0x5F, 0x7F,
    0xA2, 0x18, 0xAE, 0x16, 0x1F, 0xA7, 0x17, 0xAF, 0x01, 0xB2, 0x0E, 0xBE, 0x07, 0xB7, 0x0F, 0xBF,
    0xE2, 0xCA, 0xEE, 0xC6, 0xCF, 0xE7, 0xC7, 0xEF, 0xD2, 0xF2, 0xDE, 0xFE, 0xD7, 0xF7, 0xDF, 0xFF,
];

const P: [u8; 16] = [0, 1, 2, 3, 7, 4, 5, 6, 10, 11, 8, 9, 13, 14, 15, 12];

const TWEAKEY_P: [u8; 16] = [9, 15, 8, 13, 10, 14, 12, 11, 0, 1, 2, 3, 4, 5, 6, 7];

const RC: [u8; 40] = [
    0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3E, 0x3D, 0x3B, 0x37, 0x2F, 0x1E, 0x3C, 0x39, 0x33, 0x27, 0x0E,
    0x1D, 0x3A, 0x35, 0x2B, 0x16, 0x2C, 0x18, 0x30, 0x21, 0x02, 0x05, 0x0B, 0x17, 0x2E, 0x1C, 0x38,
    0x31, 0x23, 0x06, 0x0D, 0x1B, 0x36, 0x2D, 0x1A,
];

fn sub_cell8(state: &mut [[u8; 4]; 4]) {
    for row in state.iter_mut() {
        for cell in row.iter_mut() {
            *cell = SBOX_8[usize::from(*cell)];
        }
    }
}

fn add_constants(state: &mut [[u8; 4]; 4], r: usize) {
    state[0][0] ^= RC[r] & 0xF;
    state[1][0] ^= (RC[r] >> 4) & 0x3;
    state[2][0] ^= 0x2;
}

fn add_key(state: &mut [[u8; 4]; 4], key_cells: &mut [[[u8; 4]; 4]; 3]) {
    let mut key_cells_tmp = [[[0u8; 4]; 4]; 3];

    for i in 0..=1 {
        for (j, cell) in state[i].iter_mut().enumerate() {
            *cell ^= key_cells[0][i][j] ^ key_cells[1][i][j] ^ key_cells[2][i][j];
        }
    }

    for (k, tmp_plane) in key_cells_tmp.iter_mut().enumerate() {
        for i in 0..4 {
            for j in 0..4 {
                let pos = TWEAKEY_P[j + 4 * i];
                tmp_plane[i][j] = key_cells[k][usize::from(pos >> 2)][usize::from(pos & 3)];
            }
        }
    }

    for (k, plane) in key_cells_tmp.iter_mut().enumerate() {
        for row in &mut plane[..2] {
            for cell in row.iter_mut() {
                let v = *cell;
                *cell = if k == 1 {
                    ((v << 1) & 0xFE) ^ ((v >> 7) & 0x01) ^ ((v >> 5) & 0x01)
                } else if k == 2 {
                    ((v >> 1) & 0x7F) ^ ((v << 7) & 0x80) ^ ((v << 1) & 0x80)
                } else {
                    v
                };
            }
        }
    }

    *key_cells = key_cells_tmp;
}

fn shift_rows(state: &mut [[u8; 4]; 4]) {
    let mut state_tmp = [[0u8; 4]; 4];
    for i in 0..4 {
        for j in 0..4 {
            let pos = P[j + 4 * i];
            state_tmp[i][j] = state[usize::from(pos >> 2)][usize::from(pos & 3)];
        }
    }
    *state = state_tmp;
}

fn mix_column(state: &mut [[u8; 4]; 4]) {
    // Column-wise updates; index is clearer than juggling four mutable row references.
    #[allow(clippy::needless_range_loop)]
    for j in 0..4 {
        state[1][j] ^= state[2][j];
        state[2][j] ^= state[0][j];
        state[3][j] ^= state[2][j];

        let temp = state[3][j];
        state[3][j] = state[2][j];
        state[2][j] = state[1][j];
        state[1][j] = state[0][j];
        state[0][j] = temp;
    }
}