diff --git a/Cargo.toml b/Cargo.toml index 11e1f13cc..708fa5da2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ version = "0.1.3" # *** Internal Dependencies *** bouncycastle = { path = "./" } bouncycastle-aes = { path = "./crypto/aes" } +bouncycastle-aria = { path = "./crypto/aria" } bouncycastle-ascon = { path = "./crypto/ascon" } bouncycastle-base64 = { path = "./crypto/base64" } bouncycastle-cipher = { path = "./crypto/cipher" } @@ -48,6 +49,7 @@ edition.workspace = true [dependencies] bouncycastle-aes.workspace = true +bouncycastle-aria.workspace = true bouncycastle-ascon.workspace = true bouncycastle-base64.workspace = true bouncycastle-cipher.workspace = true diff --git a/cli/src/aria_cbc_cmd.rs b/cli/src/aria_cbc_cmd.rs new file mode 100644 index 000000000..2a4aa716b --- /dev/null +++ b/cli/src/aria_cbc_cmd.rs @@ -0,0 +1,83 @@ +//! ARIA-CBC encryption and decryption, streaming stdin to stdout. +//! +//! Only the cipher wiring lives here: the IV convention, key loading, stdin framing and +//! block-alignment enforcement are all in [`crate::helpers::block_mode_helpers`], shared with the `aes*-cbc`, +//! `aes*-cfb` and `aes*-ecb` commands. See that module for the +//! command-line contract. +//! +//! `aria128-cbc` / `aria192-cbc` / `aria256-cbc` are the same command as `aes*-cbc` over the ARIA +//! permutation (RFC 5794; 16-, 24- or 32-byte key, 16-byte block -- the `id-aria*-cbc` algorithms of +//! RFC 5794 Appendix B), so every remark there applies unchanged. CBC (NIST SP 800-38A Sec 6.2) +//! provides confidentiality only: neither the ciphertext nor the IV is authenticated. Do not decrypt +//! data you have not authenticated separately. + +use crate::helpers::block_mode_helpers::{ + CipherDirection, decrypt_stream, encrypt_stream, load_key_object, start_encryptor, +}; +use bouncycastle::aria::{ARIA128Key, ARIA192Key, ARIA256Key, BLOCK_LEN}; +use bouncycastle::cipher::modes::CBC; +use bouncycastle::cipher::padding::NoPadding; +use bouncycastle::core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey}; + +/// Names the mode in error messages. +const MODE: &str = "CBC"; + +pub(crate) fn aria128_cbc_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::<_, 16>( + action, + &load_key_object::>, 16>(key, key_file, "ARIA-128"), + output_hex, + ); +} + +pub(crate) fn aria192_cbc_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::<_, 24>( + action, + &load_key_object::>, 24>(key, key_file, "ARIA-192"), + output_hex, + ); +} + +pub(crate) fn aria256_cbc_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::<_, 32>( + action, + &load_key_object::>, 32>(key, key_file, "ARIA-256"), + output_hex, + ); +} + +/// Dispatches to the shared streaming loops under the key's CBC mode. `NoPadding` keeps the +/// command's contract: whole blocks only, with the IV in band. +fn run(action: &CipherDirection, key: &K, output_hex: bool) +where + K: SymmetricEncryptionKey + + SymmetricDecryptionKey, +{ + match action { + CipherDirection::Encrypt => { + let (enc, generated_iv) = start_encryptor(None, |rng| key.create_encryptor(rng)); + encrypt_stream::<_, BLOCK_LEN>(enc, generated_iv.as_ref(), output_hex, Some(MODE)) + } + CipherDirection::Decrypt => decrypt_stream::<_, BLOCK_LEN>( + None, + |iv| key.create_decryptor(iv), + output_hex, + Some(MODE), + ), + } +} diff --git a/cli/src/aria_cfb8_cmd.rs b/cli/src/aria_cfb8_cmd.rs new file mode 100644 index 000000000..10f474367 --- /dev/null +++ b/cli/src/aria_cfb8_cmd.rs @@ -0,0 +1,75 @@ +//! ARIA-CFB8 encryption and decryption, streaming stdin to stdout. +//! +//! Only the cipher wiring lives here: the IV convention, key loading and stdin framing are in +//! [`crate::helpers::stream_mode_helpers`] (and [`crate::helpers::block_mode_helpers`] for the key loader), shared with the +//! `aes*-cfb8` commands. See those modules for the command-line contract. +//! +//! `aria128-cfb8` / `aria192-cfb8` / `aria256-cfb8` are the same command as `aes*-cfb8` +//! over the ARIA permutation (RFC 5794; 16-, 24- or 32-byte key, 16-byte block), so every +//! remark there applies unchanged. The segment size is one byte (NIST SP 800-38A Sec 6.3 with +//! `s = 8`): a DIFFERENT, NON-INTEROPERABLE mode from the CFB128 of `aria*-cfb`, costing a full +//! ARIA call per byte, sixteen times the work. Prefer `aria*-cfb` unless a byte-granular +//! self-synchronising stream is required or the format demands CFB8. +//! +//! # Warning +//! +//! CFB8 provides confidentiality only. It does not detect tampering, and neither the ciphertext nor +//! the IV is authenticated. Do not decrypt data you have not authenticated separately. + +use crate::helpers::block_mode_helpers::{CipherDirection, load_key_object}; +use crate::helpers::stream_mode_helpers::run_stream_mode; +use bouncycastle::aria::{ARIA128Key, ARIA192Key, ARIA256Key, BLOCK_LEN}; +use bouncycastle::cipher::modes::CFB8; +use bouncycastle::core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey}; +use bouncycastle::core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; + +pub(crate) fn aria128_cfb8_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::<_, 16>( + action, + &load_key_object::, 16>(key, key_file, "ARIA-128"), + output_hex, + ); +} + +pub(crate) fn aria192_cfb8_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::<_, 24>( + action, + &load_key_object::, 24>(key, key_file, "ARIA-192"), + output_hex, + ); +} + +pub(crate) fn aria256_cfb8_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::<_, 32>( + action, + &load_key_object::, 32>(key, key_file, "ARIA-256"), + output_hex, + ); +} + +/// Dispatches to the shared streaming loops with `Cfb8` filled in as the mode; the init data +/// travels in band, as the command's contract says. +fn run<'k, K, const KEY_LEN: usize>(action: &CipherDirection, key: &'k K, output_hex: bool) +where + K: SymmetricEncryptionKey + + SymmetricDecryptionKey, + K::Encryptor<'k>: StreamCipherEncryptor, + K::Decryptor<'k>: StreamCipherDecryptor, +{ + run_stream_mode::(action, key, None, output_hex) +} diff --git a/cli/src/aria_cfb_cmd.rs b/cli/src/aria_cfb_cmd.rs new file mode 100644 index 000000000..2101e3630 --- /dev/null +++ b/cli/src/aria_cfb_cmd.rs @@ -0,0 +1,80 @@ +//! ARIA-CFB128 encryption and decryption, streaming stdin to stdout. +//! +//! Only the cipher wiring lives here: the IV convention, key loading and stdin framing are in +//! [`crate::helpers::stream_mode_helpers`] (and [`crate::helpers::block_mode_helpers`] for the key loader), shared with the +//! `aes*-cfb` commands. See those modules for the command-line contract. +//! +//! `aria128-cfb` / `aria192-cfb` / `aria256-cfb` are the same command as `aes*-cfb` +//! over the ARIA permutation (RFC 5794; 16-, 24- or 32-byte key, 16-byte block), so every +//! remark there applies unchanged. The segment size is the full block, i.e. **CFB128** +//! (NIST SP 800-38A Sec 6.3 with `s = b`); the `s = 8` variant is a different, non-interoperable +//! mode and has its own commands, `aria*-cfb8`. +//! +//! CFB is a stream cipher, so unlike `aria*-cbc` these commands accept input of any length and +//! pad nothing; the ciphertext is exactly as long as the plaintext. +//! +//! # Warning +//! +//! CFB provides confidentiality only. It does not detect tampering, and neither the ciphertext nor +//! the IV is authenticated. Flipping a ciphertext bit flips the *same* bit of the plaintext in the +//! *same* block (SP 800-38A Appendix D, Table D.2), at the cost of randomising the next one, so an +//! attacker edits the block they aimed at. Do not decrypt data you have not authenticated +//! separately. + +use crate::helpers::block_mode_helpers::{CipherDirection, load_key_object}; +use crate::helpers::stream_mode_helpers::run_stream_mode; +use bouncycastle::aria::{ARIA128Key, ARIA192Key, ARIA256Key, BLOCK_LEN}; +use bouncycastle::cipher::modes::CFB; +use bouncycastle::core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey}; +use bouncycastle::core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; + +pub(crate) fn aria128_cfb_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::<_, 16>( + action, + &load_key_object::, 16>(key, key_file, "ARIA-128"), + output_hex, + ); +} + +pub(crate) fn aria192_cfb_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::<_, 24>( + action, + &load_key_object::, 24>(key, key_file, "ARIA-192"), + output_hex, + ); +} + +pub(crate) fn aria256_cfb_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::<_, 32>( + action, + &load_key_object::, 32>(key, key_file, "ARIA-256"), + output_hex, + ); +} + +/// Dispatches to the shared streaming loops with `Cfb` filled in as the mode; the init data +/// travels in band, as the command's contract says. +fn run<'k, K, const KEY_LEN: usize>(action: &CipherDirection, key: &'k K, output_hex: bool) +where + K: SymmetricEncryptionKey + + SymmetricDecryptionKey, + K::Encryptor<'k>: StreamCipherEncryptor, + K::Decryptor<'k>: StreamCipherDecryptor, +{ + run_stream_mode::(action, key, None, output_hex) +} diff --git a/cli/src/aria_ctr_cmd.rs b/cli/src/aria_ctr_cmd.rs new file mode 100644 index 000000000..f7375eaa3 --- /dev/null +++ b/cli/src/aria_ctr_cmd.rs @@ -0,0 +1,84 @@ +//! ARIA-CTR encryption and decryption, streaming stdin to stdout. +//! +//! Only the cipher wiring lives here: the nonce convention, key loading and stdin framing are in +//! [`crate::helpers::stream_mode_helpers`] (and [`crate::helpers::block_mode_helpers`] for the key loader), shared with the +//! `aes*-ctr` commands. See those modules for the command-line contract. +//! +//! `aria128-ctr` / `aria192-ctr` / `aria256-ctr` are the same command as `aes*-ctr` over +//! the ARIA permutation (RFC 5794; 16-, 24- or 32-byte key, 16-byte block), so every remark +//! there applies unchanged, including the split of the counter block: the nonce is **12 bytes** and +//! the counter the remaining 4, giving 2^32 blocks -- 64 GiB -- in a single message. That is +//! NIST SP 800-38A Appendix B.2's construction, and it is the one KISA's published ARIA-CTR +//! vectors use: their counter block is all zeros and increments from there, which is what a +//! twelve-byte zero nonce and a counter starting at zero produce. +//! +//! `encrypt` writes that 12-byte nonce as the first bytes of its output and `decrypt` reads it back, +//! exactly as the other modes do with their IVs; note that it is 12 bytes here, not 16. +//! +//! # Warning +//! +//! CTR provides confidentiality only and is the most malleable mode here: flipping any ciphertext +//! bit flips exactly the corresponding plaintext bit and affects nothing else (SP 800-38A +//! Appendix D, Table D.2), so an attacker can edit the plaintext at will with no garbling to give +//! it away. A repeated nonce under one key is fatal rather than merely unwise -- the same keystream +//! twice leaks the XOR of the two messages -- which is why the nonce is drawn from the OS-backed +//! DRBG and there is no way to supply one. Do not decrypt data you have not authenticated +//! separately. + +use crate::helpers::block_mode_helpers::{CipherDirection, load_key_object}; +use crate::helpers::stream_mode_helpers::run_stream_mode; +use bouncycastle::aria::{ARIA128Key, ARIA192Key, ARIA256Key, CTR_NONCE_LEN}; +use bouncycastle::cipher::modes::CTR; +use bouncycastle::core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey}; +use bouncycastle::core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; + +pub(crate) fn aria128_ctr_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::<_, 16>( + action, + &load_key_object::, 16>(key, key_file, "ARIA-128"), + output_hex, + ); +} + +pub(crate) fn aria192_ctr_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::<_, 24>( + action, + &load_key_object::, 24>(key, key_file, "ARIA-192"), + output_hex, + ); +} + +pub(crate) fn aria256_ctr_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::<_, 32>( + action, + &load_key_object::, 32>(key, key_file, "ARIA-256"), + output_hex, + ); +} + +/// Dispatches to the shared streaming loops with `Ctr` filled in as the mode; the init data +/// travels in band, as the command's contract says. +fn run<'k, K, const KEY_LEN: usize>(action: &CipherDirection, key: &'k K, output_hex: bool) +where + K: SymmetricEncryptionKey + + SymmetricDecryptionKey, + K::Encryptor<'k>: StreamCipherEncryptor, + K::Decryptor<'k>: StreamCipherDecryptor, +{ + run_stream_mode::(action, key, None, output_hex) +} diff --git a/cli/src/main.rs b/cli/src/main.rs index 1377e4b33..5b2d51e4b 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -5,6 +5,10 @@ mod aes_cfb_cmd; mod aes_ctr_cmd; mod aes_ecb_cmd; mod aes_gcm_cmd; +mod aria_cbc_cmd; +mod aria_cfb8_cmd; +mod aria_cfb_cmd; +mod aria_ctr_cmd; mod ascon_cmd; mod encoders_cmd; mod helpers; @@ -1696,6 +1700,284 @@ enum Subcommands { x: bool, }, + /// ARIA-128 in CBC mode (RFC 5794 block cipher; NIST SP 800-38A Sec 6.2 mode), streaming + /// stdin to stdout. + /// + /// See `aes128-cbc` for the IV convention, block-alignment requirement and warnings; the key + /// and block are both 16 bytes, as for AES-128. + ARIA128_CBC { + direction: CipherDirection, + + /// The 16-byte ARIA key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the 16-byte ARIA key, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + /// ARIA-192 in CBC mode (RFC 5794 block cipher; NIST SP 800-38A Sec 6.2 mode), streaming + /// stdin to stdout. + /// + /// See `aes128-cbc` for the IV convention, block-alignment requirement and warnings; only the + /// key length differs. + ARIA192_CBC { + direction: CipherDirection, + + /// The 24-byte ARIA key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the 24-byte ARIA key, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + /// ARIA-256 in CBC mode (RFC 5794 block cipher; NIST SP 800-38A Sec 6.2 mode), streaming + /// stdin to stdout. + /// + /// See `aes128-cbc` for the IV convention, block-alignment requirement and warnings; only the + /// key length differs. + ARIA256_CBC { + direction: CipherDirection, + + /// The 32-byte ARIA key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the 32-byte ARIA key, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + /// ARIA-128 in CFB128 mode (RFC 5794 block cipher; NIST SP 800-38A Sec 6.3 mode), streaming + /// stdin to stdout. + /// + /// The segment size is the full block, i.e. CFB128. The 8-bit variant is a different, + /// non-interoperable mode; use `aria128-cfb8` for that. + /// + /// See `aes128-cfb` for the IV convention, input-length rule and warnings; the key and block + /// are both 16 bytes, as for AES-128. + ARIA128_CFB { + direction: CipherDirection, + + /// The 16-byte ARIA key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the 16-byte ARIA key, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + /// ARIA-192 in CFB128 mode (RFC 5794 block cipher; NIST SP 800-38A Sec 6.3 mode), streaming + /// stdin to stdout. + /// + /// See `aria128-cfb` for the IV convention, input-length rule and warnings; only the key length + /// differs. + ARIA192_CFB { + direction: CipherDirection, + + /// The 24-byte ARIA key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the 24-byte ARIA key, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + /// ARIA-256 in CFB128 mode (RFC 5794 block cipher; NIST SP 800-38A Sec 6.3 mode), streaming + /// stdin to stdout. + /// + /// See `aria128-cfb` for the IV convention, input-length rule and warnings; only the key length + /// differs. + ARIA256_CFB { + direction: CipherDirection, + + /// The 32-byte ARIA key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the 32-byte ARIA key, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + /// ARIA-128 in CFB8 mode (RFC 5794 block cipher; NIST SP 800-38A Sec 6.3 mode, s = 8), + /// streaming stdin to stdout. + /// + /// The segment size is one byte. This is a DIFFERENT, NON-INTEROPERABLE mode from the CFB128 of + /// `aria128-cfb`: the two ciphertexts agree only on their first byte. It also costs one ARIA + /// call per byte, sixteen times the work, so prefer `aria128-cfb` unless a byte-granular + /// self-synchronising stream is required or the format demands CFB8. + /// + /// See `aes128-cfb8` for the IV convention, input-length rule and warnings; the key and block + /// are both 16 bytes, as for AES-128. + ARIA128_CFB8 { + direction: CipherDirection, + + /// The 16-byte ARIA key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the 16-byte ARIA key, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + /// ARIA-192 in CFB8 mode (RFC 5794 block cipher; NIST SP 800-38A Sec 6.3 mode, s = 8), + /// streaming stdin to stdout. + /// + /// See `aria128-cfb8` for the IV convention, input-length rule and warnings; only the key + /// length differs. + ARIA192_CFB8 { + direction: CipherDirection, + + /// The 24-byte ARIA key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the 24-byte ARIA key, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + /// ARIA-256 in CFB8 mode (RFC 5794 block cipher; NIST SP 800-38A Sec 6.3 mode, s = 8), + /// streaming stdin to stdout. + /// + /// See `aria128-cfb8` for the IV convention, input-length rule and warnings; only the key + /// length differs. + ARIA256_CFB8 { + direction: CipherDirection, + + /// The 32-byte ARIA key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the 32-byte ARIA key, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + /// ARIA-128 in CTR mode (RFC 5794 block cipher; NIST SP 800-38A Sec 6.5 mode), streaming stdin + /// to stdout. + /// + /// The counter block is a 12-byte nonce followed by a 4-byte counter starting at zero, so one + /// message can be up to 2^32 blocks (64 GiB); past that the command errors rather than + /// repeating keystream. On `encrypt` the nonce is written as the FIRST 12 BYTES of the output + /// and on `decrypt` it is read back from there -- 12, not the 16 the other modes write. + /// + /// See `aes128-ctr` for the nonce convention, input-length rule and warnings, including why a + /// repeated nonce is fatal and why CTR is the most malleable mode here. + ARIA128_CTR { + direction: CipherDirection, + + /// The 16-byte ARIA key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the 16-byte ARIA key, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + /// ARIA-192 in CTR mode (RFC 5794 block cipher; NIST SP 800-38A Sec 6.5 mode), streaming stdin + /// to stdout. + /// + /// See `aria128-ctr` for the nonce convention, input-length rule and warnings; only the key + /// length differs. + ARIA192_CTR { + direction: CipherDirection, + + /// The 24-byte ARIA key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the 24-byte ARIA key, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + /// ARIA-256 in CTR mode (RFC 5794 block cipher; NIST SP 800-38A Sec 6.5 mode), streaming stdin + /// to stdout. + /// + /// See `aria128-ctr` for the nonce convention, input-length rule and warnings; only the key + /// length differs. + ARIA256_CTR { + direction: CipherDirection, + + /// The 32-byte ARIA key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the 32-byte ARIA key, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + /// Perform TupleHash128 (NIST SP 800-185 Sec 5) over a tuple of strings. The tuple is given /// by repeated --element flags, each in hex; with none, stdin is hashed as a single element. /// The boundaries between elements are part of the hash. @@ -2033,6 +2315,42 @@ fn run() { Some(Subcommands::SM3 { x }) => { sm3_cmd::sm3_cmd(*x); } + Some(Subcommands::ARIA128_CBC { direction, key, key_file, x }) => { + aria_cbc_cmd::aria128_cbc_cmd(direction, key, key_file, *x); + } + Some(Subcommands::ARIA192_CBC { direction, key, key_file, x }) => { + aria_cbc_cmd::aria192_cbc_cmd(direction, key, key_file, *x); + } + Some(Subcommands::ARIA256_CBC { direction, key, key_file, x }) => { + aria_cbc_cmd::aria256_cbc_cmd(direction, key, key_file, *x); + } + Some(Subcommands::ARIA128_CFB { direction, key, key_file, x }) => { + aria_cfb_cmd::aria128_cfb_cmd(direction, key, key_file, *x); + } + Some(Subcommands::ARIA192_CFB { direction, key, key_file, x }) => { + aria_cfb_cmd::aria192_cfb_cmd(direction, key, key_file, *x); + } + Some(Subcommands::ARIA256_CFB { direction, key, key_file, x }) => { + aria_cfb_cmd::aria256_cfb_cmd(direction, key, key_file, *x); + } + Some(Subcommands::ARIA128_CFB8 { direction, key, key_file, x }) => { + aria_cfb8_cmd::aria128_cfb8_cmd(direction, key, key_file, *x); + } + Some(Subcommands::ARIA192_CFB8 { direction, key, key_file, x }) => { + aria_cfb8_cmd::aria192_cfb8_cmd(direction, key, key_file, *x); + } + Some(Subcommands::ARIA256_CFB8 { direction, key, key_file, x }) => { + aria_cfb8_cmd::aria256_cfb8_cmd(direction, key, key_file, *x); + } + Some(Subcommands::ARIA128_CTR { direction, key, key_file, x }) => { + aria_ctr_cmd::aria128_ctr_cmd(direction, key, key_file, *x); + } + Some(Subcommands::ARIA192_CTR { direction, key, key_file, x }) => { + aria_ctr_cmd::aria192_ctr_cmd(direction, key, key_file, *x); + } + Some(Subcommands::ARIA256_CTR { direction, key, key_file, x }) => { + aria_ctr_cmd::aria256_ctr_cmd(direction, key, key_file, *x); + } Some(Subcommands::TUPLEHASH128 { length, elements, customization, x }) => { sha3_cmd::tuplehash_cmd(128, *length, elements, customization, *x); } diff --git a/cli/tests/aria_cbc_cli_tests.rs b/cli/tests/aria_cbc_cli_tests.rs new file mode 100644 index 000000000..2d9ff0fd2 --- /dev/null +++ b/cli/tests/aria_cbc_cli_tests.rs @@ -0,0 +1,167 @@ +//! Tests for the `aria128-cbc`, `aria192-cbc` and `aria256-cbc` subcommands. +//! +//! They share their key loader, streaming loops and error paths with the `aes*-cbc` commands +//! (`cli/src/cbc_cmd.rs`), and `aes_cbc_cli_tests.rs` covers those exhaustively. This file pins +//! what is specific to ARIA: the known-answer vectors, the three key lengths, and that the +//! commands exist and round-trip. +//! +//! Vectors are the `ARIA-*-CBC` entries of OpenSSL's `evpciph_aria.txt` (OpenSSL 3.6.2), which +//! are KISA's published ARIA test vectors: a 10-block message under each key length; see +//! `crypto/aria/tests/kisa_vectors_tests.rs`. + +use std::io::{ErrorKind, Write}; +use std::process::{Command, Output, Stdio}; + +/// The path to the binary under test, resolved by cargo. +const BC_RUST: &str = env!("CARGO_BIN_EXE_bc-rust"); + +/// The IV shared by the three CBC vectors. +const IV: &str = "0f1e2d3c4b5a69788796a5b4c3d2e1f0"; + +/// The 160-byte plaintext shared by all three vectors. +const PLAINTEXT: &str = "11111111aaaaaaaa11111111bbbbbbbb11111111cccccccc11111111dddddddd22222222aaaaaaaa22222222bbbbbbbb22222222cccccccc22222222dddddddd33333333aaaaaaaa33333333bbbbbbbb33333333cccccccc33333333dddddddd44444444aaaaaaaa44444444bbbbbbbb44444444cccccccc44444444dddddddd55555555aaaaaaaa55555555bbbbbbbb55555555cccccccc55555555dddddddd"; + +const KEY_128: &str = "00112233445566778899aabbccddeeff"; +const CT_128: &str = "49d61860b14909109cef0d22a9268134fadf9fb23151e9645fba75018bdb1538b53334634bbf7d4cd4b5377033060c155fe3948ca75de1031e1d85619e0ad61eb419a866b3c2dbfd10a4ed18b22149f75897f0b8668b0c1c542c687778835fb7cd46e45f85eaa7072437dd9fa6793d6f8d4ccefc4eb1ac641ac1bd30b18c6d64c49bca137eb21c2e04da62712ca2b4f540c57112c38791852cfac7a5d19ed83a"; + +const KEY_192: &str = "00112233445566778899aabbccddeeff0011223344556677"; +const CT_192: &str = "afe6cf23974b533c672a826264ea785f4e4f7f780dc7f3f1e0962b80902386d514e9c3e77259de92dd1102ffab086c1ea52a71260db5920a83295c25320e421147ca45d532f327b856ea947cd2196ae2e040826548b4c891b0ed0ca6e714dbc4631998d548110d666b3d54c2a091955c6f05beb4f62309368696c9791fc4c551564a2637f194346ec45fbca6c72a5b4612e208d531d6c34cc5c64eac6bd0cf8c"; + +const KEY_256: &str = "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"; +const CT_256: &str = "523a8a806ae621f155fdd28dbc34e1ab7b9b42432ad8b2efb96e23b13f0a6e52f36185d50ad002c5f601bee5493f118b243ee2e313642bffc3902e7b2efd9a12fa682edd2d23c8b9c5f043c18b17c1ec4b5867918270fbec1027c19ed6af833da5d620994668ca22f599791d292dd6273b2959082aafb7a996167cce1eec5f0cfd15f610d87e2dda9ba68ce1260ca54b222491418374294e7909b1e8551cd8de"; + +/// Runs `bc-rust ` with `stdin_bytes` on stdin and returns the completed output. +fn run(args: &[&str], stdin_bytes: &[u8]) -> Output { + let mut child = Command::new(BC_RUST) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("failed to spawn bc-rust"); + + // The error-path tests hand a rejected key to a command that `exit`s before it ever reads + // stdin, so this write races the child's exit and sometimes loses -- reliably so on a loaded + // CI runner. That is an expected outcome, not a harness failure: `wait_with_output` still + // returns the exit status and stderr, which is all those tests assert on. Any other write + // error is a real problem and still panics. + match child.stdin.as_mut().expect("stdin piped").write_all(stdin_bytes) { + Ok(()) => {} + Err(e) if e.kind() == ErrorKind::BrokenPipe => {} + Err(e) => panic!("failed to write to stdin: {e}"), + } + + child.wait_with_output().expect("failed to wait for bc-rust") +} + +/// Runs a command that is expected to succeed, returning stdout. +fn run_ok(args: &[&str], stdin_bytes: &[u8]) -> Vec { + let out = run(args, stdin_bytes); + assert!( + out.status.success(), + "expected success from {args:?}, got {:?}\nstderr: {}", + out.status, + String::from_utf8_lossy(&out.stderr) + ); + out.stdout +} + +/// Runs a command that is expected to fail, returning stderr as a string. +fn run_err(args: &[&str], stdin_bytes: &[u8]) -> String { + let out = run(args, stdin_bytes); + assert!( + !out.status.success(), + "expected failure from {args:?}, but it succeeded\nstdout: {:?}", + String::from_utf8_lossy(&out.stdout) + ); + String::from_utf8_lossy(&out.stderr).into_owned() +} + +fn unhex(s: &str) -> Vec { + assert!(s.len().is_multiple_of(2), "hex string must have even length"); + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("valid hex")) + .collect() +} + +fn tohex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{b:02x}")).collect() +} + +const VARIANTS: [(&str, &str, &str); 3] = [ + ("aria128-cbc", KEY_128, CT_128), + ("aria192-cbc", KEY_192, CT_192), + ("aria256-cbc", KEY_256, CT_256), +]; + +/// `decrypt` reproduces the plaintext when handed the IV followed by the ciphertext, for all +/// three key lengths. +/// +/// This is the direction that can be pinned exactly: `encrypt` picks its own IV, so it cannot be +/// asked to reproduce a published ciphertext. `encrypt` is covered by the round trip below and, at +/// the library level, by `crypto/aria/tests/kisa_vectors_tests.rs`. +#[test] +fn decrypt_matches_the_kisa_vectors() { + for (cmd, key, ct) in VARIANTS { + let input = unhex(&format!("{IV}{ct}")); + let out = run_ok(&[cmd, "decrypt", "--key", key], &input); + assert_eq!(tohex(&out), PLAINTEXT, "{cmd} decrypt should give the plaintext"); + } +} + +/// The same with `-x`: identical answer in hex plus a trailing newline. +#[test] +fn hex_output_matches_binary_output() { + let input = unhex(&format!("{IV}{CT_256}")); + let out = run_ok(&["aria256-cbc", "decrypt", "--key", KEY_256, "-x"], &input); + assert_eq!(String::from_utf8_lossy(&out), format!("{PLAINTEXT}\n")); +} + +/// `encrypt | decrypt` composes: the IV rides in the first block, and the plaintext comes back. +#[test] +fn encrypt_then_decrypt_round_trips() { + let plaintext: Vec = (0..4096u32).map(|i| (i.wrapping_mul(31) >> 3) as u8).collect(); + + for (cmd, key, _) in VARIANTS { + let ciphertext = run_ok(&[cmd, "encrypt", "--key", key], &plaintext); + assert_eq!( + ciphertext.len(), + 16 + plaintext.len(), + "{cmd}: IV block plus one block per input block" + ); + assert_ne!(&ciphertext[16..], &plaintext[..]); + + let recovered = run_ok(&[cmd, "decrypt", "--key", key], &ciphertext); + assert_eq!(recovered, plaintext, "{cmd}"); + } +} + +/// Each command wants exactly its key length. The 256-bit key to the 128-bit command, and the +/// 128-bit key to the 192-bit command, are errors naming the algorithm. +#[test] +fn a_key_of_the_wrong_length_is_rejected() { + let err = run_err(&["aria128-cbc", "encrypt", "--key", KEY_256], &[0u8; 16]); + assert!(err.contains("ARIA-128 needs a 16-byte key, got 32 bytes"), "stderr was: {err}"); + let err = run_err(&["aria192-cbc", "encrypt", "--key", KEY_128], &[0u8; 16]); + assert!(err.contains("ARIA-192 needs a 24-byte key, got 16 bytes"), "stderr was: {err}"); + let err = run_err(&["aria256-cbc", "encrypt", "--key", KEY_192], &[0u8; 16]); + assert!(err.contains("ARIA-256 needs a 32-byte key, got 24 bytes"), "stderr was: {err}"); +} + +/// The harness above must survive a write that loses the race with the child's exit. This pins it +/// deterministically: the key is rejected so `aria128-cbc` exits before reading a byte, and +/// the payload is far larger than any pipe buffer, so the write is certain to get EPIPE rather +/// than merely likely to. +#[test] +fn a_large_payload_on_an_error_path_does_not_break_the_harness() { + let err = run_err(&["aria128-cbc", "encrypt", "--key", KEY_256], &vec![0u8; 4 * 1024 * 1024]); + assert!(err.contains("ARIA-128 needs a 16-byte key"), "stderr was: {err}"); +} + +/// Unaligned input is rejected, as for the AES commands. +#[test] +fn unaligned_input_is_rejected() { + let err = run_err(&["aria128-cbc", "encrypt", "--key", KEY_128], &[0u8; 17]); + assert!(err.contains("whole number of 16-byte blocks"), "stderr was: {err}"); +} diff --git a/cli/tests/aria_cfb8_cli_tests.rs b/cli/tests/aria_cfb8_cli_tests.rs new file mode 100644 index 000000000..6c6306f26 --- /dev/null +++ b/cli/tests/aria_cfb8_cli_tests.rs @@ -0,0 +1,232 @@ +//! Tests for the `aria128-cfb8`, `aria192-cfb8` and `aria256-cfb8` subcommands. +//! +//! They share their key loader, streaming loops and error paths with the `aes*-cfb8` commands +//! (`cli/src/stream_mode_cmd.rs`), and `aes_cfb8_cli_tests.rs` covers those exhaustively. This +//! file pins what is specific to ARIA: the known-answer vectors, the three key lengths, and +//! that the commands exist and round-trip at any length. +//! +//! Vectors are the `ARIA-*-CFB8` entries of OpenSSL's `evpciph_aria.txt` (OpenSSL 3.6.2), +//! which are KISA's published ARIA test vectors; see `crypto/aria/tests/stream_mode_tests.rs`. + +use std::io::{ErrorKind, Write}; +use std::process::{Command, Output, Stdio}; + +/// The path to the binary under test, resolved by cargo. +const BC_RUST: &str = env!("CARGO_BIN_EXE_bc-rust"); + +/// The IV the CFB128 and CFB8 vectors share -- the same one the CBC vectors use. +const IV: &str = "0f1e2d3c4b5a69788796a5b4c3d2e1f0"; + +/// The 160-byte plaintext shared by all nine KISA mode vectors. +const PLAINTEXT: &str = "11111111aaaaaaaa11111111bbbbbbbb11111111cccccccc11111111dddddddd22222222aaaaaaaa22222222bbbbbbbb22222222cccccccc22222222dddddddd33333333aaaaaaaa33333333bbbbbbbb33333333cccccccc33333333dddddddd44444444aaaaaaaa44444444bbbbbbbb44444444cccccccc44444444dddddddd55555555aaaaaaaa55555555bbbbbbbb55555555cccccccc55555555dddddddd"; + +const KEY_128: &str = "00112233445566778899aabbccddeeff"; +const KEY_192: &str = "00112233445566778899aabbccddeeff0011223344556677"; +const KEY_256: &str = "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"; + +/// `ARIA-128-CFB8` ciphertext (CFB8). +const CT_128: &str = concat!( + "373c8f6a965599ec785cc8f8149f6c81", + "b632ccb8e0c6eb6a9707ae52c59257a4", + "1f94701c1096933127a90195ed0c8e98", + "690547572423bb45c3d70e4a18ee56b9", + "67c10e000ba4df5fba7c404134a343d8", + "375d04b151d161ef83417fe1748447d3", + "0a6723c406733df7d18aa39a20752d23", + "81942e244811bb97f72eae446b1815aa", + "690cd1b1adcbd007c0088ecdc91cb2e2", + "caf0e11e72459878137eea64ac62a9a1", +); + +/// `ARIA-192-CFB8` ciphertext (CFB8). +const CT_192: &str = concat!( + "411d3b4f57f705aa4d13c46e2cf426af", + "7c8c916ed7923d889f0047bbf11471b6", + "d54f8757ef519339105be3cb69babb97", + "6a57d5631fc23cc3051fe9d36e8b8e27", + "a2b2c0c4d31928ccbf30ea8239b46ba1", + "b77f6198e7ecd2ce27b35958148e826f", + "06aaf385bd30362ff141583e7c1d8924", + "d44d36a1133094074631e18adafa9d2e", + "55de98f6895c89d4266ebd33f3d4be51", + "53a96fa12132ece2e81e66e55baa7ade", +); + +/// `ARIA-256-CFB8` ciphertext (CFB8). +const CT_256: &str = concat!( + "26baa33651e1f66434fec88ef27fd2b9", + "a79e246dd89a3ffa00e8bdb37155433e", + "6c24bd0b87d9a85baa9f485ccb984f5e", + "c24d6a3ef5e3c81396177f039cf580df", + "db55d6e1c47a28921dfe369e12fd357b", + "289ad3a5544e1c1bd616d454db9c5f91", + "f603373f29d5b2ed1b4b51de80f28537", + "bbd43d5e3b5dd071dc91153cbbe732df", + "c325821b06ed8acaae656dcf2da9f13e", + "4f29db671476f1e644ff06d9b67d6bd4", +); + +/// Runs `bc-rust ` with `stdin_bytes` on stdin and returns the completed output. +fn run(args: &[&str], stdin_bytes: &[u8]) -> Output { + let mut child = Command::new(BC_RUST) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("failed to spawn bc-rust"); + + // The error-path tests hand a rejected key to a command that `exit`s before it ever reads + // stdin, so this write races the child's exit and sometimes loses -- reliably so on a loaded + // CI runner. That is an expected outcome, not a harness failure: `wait_with_output` still + // returns the exit status and stderr, which is all those tests assert on. Any other write + // error is a real problem and still panics. + match child.stdin.as_mut().expect("stdin piped").write_all(stdin_bytes) { + Ok(()) => {} + Err(e) if e.kind() == ErrorKind::BrokenPipe => {} + Err(e) => panic!("failed to write to stdin: {e}"), + } + + child.wait_with_output().expect("failed to wait for bc-rust") +} + +/// Runs a command that is expected to succeed, returning stdout. +fn run_ok(args: &[&str], stdin_bytes: &[u8]) -> Vec { + let out = run(args, stdin_bytes); + assert!( + out.status.success(), + "expected success from {args:?}, got {:?}\nstderr: {}", + out.status, + String::from_utf8_lossy(&out.stderr) + ); + out.stdout +} + +/// Runs a command that is expected to fail, returning stderr as a string. +fn run_err(args: &[&str], stdin_bytes: &[u8]) -> String { + let out = run(args, stdin_bytes); + assert!( + !out.status.success(), + "expected failure from {args:?}, but it succeeded\nstdout: {:?}", + String::from_utf8_lossy(&out.stdout) + ); + String::from_utf8_lossy(&out.stderr).into_owned() +} + +fn unhex(s: &str) -> Vec { + assert!(s.len().is_multiple_of(2), "hex string must have even length"); + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("valid hex")) + .collect() +} + +fn tohex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{b:02x}")).collect() +} + +/// Deterministic pseudo-random bytes, so the tests do not depend on an RNG. +fn pseudo_random(len: usize, seed: u32) -> Vec { + let mut state = seed.wrapping_mul(2_654_435_761).wrapping_add(1); + (0..len) + .map(|_| { + state ^= state << 13; + state ^= state >> 17; + state ^= state << 5; + (state >> 24) as u8 + }) + .collect() +} + +const VARIANTS: [(&str, &str, &str); 3] = [ + ("aria128-cfb8", KEY_128, CT_128), + ("aria192-cfb8", KEY_192, CT_192), + ("aria256-cfb8", KEY_256, CT_256), +]; + +/// `decrypt` reproduces the plaintext when handed the IV followed by the ciphertext, for +/// all three key lengths. +/// +/// This is the direction that can be pinned exactly: `encrypt` picks its own IV, so it +/// cannot be asked to reproduce a published ciphertext. `encrypt` is covered by the round trip +/// below and, at the library level, by `crypto/aria/tests/stream_mode_tests.rs`. +#[test] +fn decrypt_matches_the_kisa_vectors() { + for (cmd, key, ct) in VARIANTS { + let input = unhex(&format!("{}{ct}", IV)); + let out = run_ok(&[cmd, "decrypt", "--key", key], &input); + assert_eq!(tohex(&out), PLAINTEXT, "{cmd} decrypt should give the plaintext"); + } +} + +/// The same with `-x`: identical answer in hex plus a trailing newline. +#[test] +fn hex_output_matches_binary_output() { + let input = unhex(&format!("{}{}", IV, CT_256)); + let out = run_ok(&["aria256-cfb8", "decrypt", "--key", KEY_256, "-x"], &input); + assert_eq!(String::from_utf8_lossy(&out), format!("{PLAINTEXT}\n")); +} + +/// `encrypt | decrypt` composes: the init data rides at the front, and the plaintext comes back. +#[test] +fn encrypt_then_decrypt_round_trips() { + let plaintext = pseudo_random(4096, 0xC0FFEE); + + for (cmd, key, _) in VARIANTS { + let ciphertext = run_ok(&[cmd, "encrypt", "--key", key], &plaintext); + assert_eq!( + ciphertext.len(), + 16 + plaintext.len(), + "{cmd}: init data plus an equal-length body" + ); + assert_ne!(&ciphertext[16..], &plaintext[..]); + + let recovered = run_ok(&[cmd, "decrypt", "--key", key], &ciphertext); + assert_eq!(recovered, plaintext, "{cmd}"); + } +} + +/// Unlike `aria*-cbc`, any length is accepted and nothing is padded: this is a stream cipher. +#[test] +fn any_input_length_is_accepted() { + for len in [0usize, 1, 15, 17, 33] { + let plaintext = pseudo_random(len, len as u32); + let ciphertext = run_ok(&["aria128-cfb8", "encrypt", "--key", KEY_128], &plaintext); + assert_eq!(ciphertext.len(), 16 + len, "len {len}: init data plus an equal-length body"); + let recovered = run_ok(&["aria128-cfb8", "decrypt", "--key", KEY_128], &ciphertext); + assert_eq!(recovered, plaintext, "len {len}: round trip"); + } +} + +/// Each command wants exactly its key length. The 256-bit key to the 128-bit command, and so on, +/// are errors naming the algorithm. +#[test] +fn a_key_of_the_wrong_length_is_rejected() { + let err = run_err(&["aria128-cfb8", "encrypt", "--key", KEY_256], &[0u8; 16]); + assert!(err.contains("ARIA-128 needs a 16-byte key, got 32 bytes"), "stderr was: {err}"); + let err = run_err(&["aria192-cfb8", "encrypt", "--key", KEY_128], &[0u8; 16]); + assert!(err.contains("ARIA-192 needs a 24-byte key, got 16 bytes"), "stderr was: {err}"); + let err = run_err(&["aria256-cfb8", "encrypt", "--key", KEY_192], &[0u8; 16]); + assert!(err.contains("ARIA-256 needs a 32-byte key, got 24 bytes"), "stderr was: {err}"); +} + +/// The harness above must survive a write that loses the race with the child's exit. This pins it +/// deterministically: the key is rejected so `aria128-cfb8` exits before reading a byte, and +/// the payload is far larger than any pipe buffer, so the write is certain to get EPIPE rather +/// than merely likely to. It guards the `run` helper that every test in this file -- and, copy +/// for copy, the sibling cfb and ctr suites -- depends on. +#[test] +fn a_large_payload_on_an_error_path_does_not_break_the_harness() { + let err = run_err(&["aria128-cfb8", "encrypt", "--key", KEY_256], &vec![0u8; 4 * 1024 * 1024]); + assert!(err.contains("ARIA-128 needs a 16-byte key"), "stderr was: {err}"); +} + +/// The subcommands are discoverable. +#[test] +fn the_subcommands_are_listed_in_help() { + let out = run_ok(&["--help"], &[]); + let help = String::from_utf8_lossy(&out); + for cmd in ["aria128-cfb8", "aria192-cfb8", "aria256-cfb8"] { + assert!(help.contains(cmd), "`--help` should list {cmd}"); + } +} diff --git a/cli/tests/aria_cfb_cli_tests.rs b/cli/tests/aria_cfb_cli_tests.rs new file mode 100644 index 000000000..b2314f4af --- /dev/null +++ b/cli/tests/aria_cfb_cli_tests.rs @@ -0,0 +1,221 @@ +//! Tests for the `aria128-cfb`, `aria192-cfb` and `aria256-cfb` subcommands. +//! +//! They share their key loader, streaming loops and error paths with the `aes*-cfb` commands +//! (`cli/src/stream_mode_cmd.rs`), and `aes_cfb_cli_tests.rs` covers those exhaustively. This +//! file pins what is specific to ARIA: the known-answer vectors, the three key lengths, and +//! that the commands exist and round-trip at any length. +//! +//! Vectors are the `ARIA-*-CFB` entries of OpenSSL's `evpciph_aria.txt` (OpenSSL 3.6.2), +//! which are KISA's published ARIA test vectors; see `crypto/aria/tests/stream_mode_tests.rs`. + +use std::io::{ErrorKind, Write}; +use std::process::{Command, Output, Stdio}; + +/// The path to the binary under test, resolved by cargo. +const BC_RUST: &str = env!("CARGO_BIN_EXE_bc-rust"); + +/// The IV the CFB128 and CFB8 vectors share -- the same one the CBC vectors use. +const IV: &str = "0f1e2d3c4b5a69788796a5b4c3d2e1f0"; + +/// The 160-byte plaintext shared by all nine KISA mode vectors. +const PLAINTEXT: &str = "11111111aaaaaaaa11111111bbbbbbbb11111111cccccccc11111111dddddddd22222222aaaaaaaa22222222bbbbbbbb22222222cccccccc22222222dddddddd33333333aaaaaaaa33333333bbbbbbbb33333333cccccccc33333333dddddddd44444444aaaaaaaa44444444bbbbbbbb44444444cccccccc44444444dddddddd55555555aaaaaaaa55555555bbbbbbbb55555555cccccccc55555555dddddddd"; + +const KEY_128: &str = "00112233445566778899aabbccddeeff"; +const KEY_192: &str = "00112233445566778899aabbccddeeff0011223344556677"; +const KEY_256: &str = "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"; + +/// `ARIA-128-CFB` ciphertext (CFB128). +const CT_128: &str = concat!( + "3720e53ba7d615383406b09f0a05a200", + "c07c21e6370f413a5d132500a6828501", + "7c61b434c7b7ca9685a51071861e4d4b", + "b873b599b479e2d573dddeafba89f812", + "ac6a9e44d554078eb3be94839db4b33d", + "a3f59c063123a7ef6f20e10579fa4fd2", + "39100ca73b52d4fcafeadee73f139f78", + "f9b7614c2b3b9dbe010f87db06a89a94", + "35f79ce8121431371f4e87b984e0230c", + "22a6dacb32fc42dcc6accef33285bf11", +); + +/// `ARIA-192-CFB` ciphertext (CFB128). +const CT_192: &str = concat!( + "4171f7192bf4495494d2736129640f5c", + "4d87a9a213664c9448477c6ecc201359", + "8d9766952dd8c3868f17e36ef66fd84b", + "fa45d1593d2d6ee3ea2115047d710d4f", + "b66187caa3a315b3c8ea2d313962edcf", + "e5a3e2028d5ba9a09fd5c65c19d3440e", + "477f0cab0628ec6902c73ee02f1afee9", + "f80115be7b9df82d1e28228e28581a20", + "560e195cbb9e2b327bf56fd2d0ae5502", + "e42c13e9b4015d4da42dc859252e7da4", +); + +/// `ARIA-256-CFB` ciphertext (CFB128). +const CT_256: &str = concat!( + "26834705b0f2c0e2588d4a7f09009635", + "f28bb93d8c31f870ec1e0bdb082b66fa", + "402dd9c202be300c4517d196b14d4ce1", + "1dce97f7aaba54341b0d872cc9b63753", + "a3e8556a14be6f7b3e27e3cfc39caf80", + "f2a355aa50dc83c09c7b11828694f8e4", + "aa726c528976b53f2c877f4991a3a8d2", + "8adb63bd751846ffb2350265e179d499", + "0753ae8485ff9b4133ddad5875b84a90", + "cbcfa62a045d726df71b6bda0eeca0be", +); + +/// Runs `bc-rust ` with `stdin_bytes` on stdin and returns the completed output. +fn run(args: &[&str], stdin_bytes: &[u8]) -> Output { + let mut child = Command::new(BC_RUST) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("failed to spawn bc-rust"); + + // The error-path tests hand a rejected key to a command that `exit`s before it ever reads + // stdin, so this write races the child's exit and sometimes loses -- reliably so on a loaded + // CI runner. That is an expected outcome, not a harness failure: `wait_with_output` still + // returns the exit status and stderr, which is all those tests assert on. Any other write + // error is a real problem and still panics. + match child.stdin.as_mut().expect("stdin piped").write_all(stdin_bytes) { + Ok(()) => {} + Err(e) if e.kind() == ErrorKind::BrokenPipe => {} + Err(e) => panic!("failed to write to stdin: {e}"), + } + + child.wait_with_output().expect("failed to wait for bc-rust") +} + +/// Runs a command that is expected to succeed, returning stdout. +fn run_ok(args: &[&str], stdin_bytes: &[u8]) -> Vec { + let out = run(args, stdin_bytes); + assert!( + out.status.success(), + "expected success from {args:?}, got {:?}\nstderr: {}", + out.status, + String::from_utf8_lossy(&out.stderr) + ); + out.stdout +} + +/// Runs a command that is expected to fail, returning stderr as a string. +fn run_err(args: &[&str], stdin_bytes: &[u8]) -> String { + let out = run(args, stdin_bytes); + assert!( + !out.status.success(), + "expected failure from {args:?}, but it succeeded\nstdout: {:?}", + String::from_utf8_lossy(&out.stdout) + ); + String::from_utf8_lossy(&out.stderr).into_owned() +} + +fn unhex(s: &str) -> Vec { + assert!(s.len().is_multiple_of(2), "hex string must have even length"); + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("valid hex")) + .collect() +} + +fn tohex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{b:02x}")).collect() +} + +/// Deterministic pseudo-random bytes, so the tests do not depend on an RNG. +fn pseudo_random(len: usize, seed: u32) -> Vec { + let mut state = seed.wrapping_mul(2_654_435_761).wrapping_add(1); + (0..len) + .map(|_| { + state ^= state << 13; + state ^= state >> 17; + state ^= state << 5; + (state >> 24) as u8 + }) + .collect() +} + +const VARIANTS: [(&str, &str, &str); 3] = [ + ("aria128-cfb", KEY_128, CT_128), + ("aria192-cfb", KEY_192, CT_192), + ("aria256-cfb", KEY_256, CT_256), +]; + +/// `decrypt` reproduces the plaintext when handed the IV followed by the ciphertext, for +/// all three key lengths. +/// +/// This is the direction that can be pinned exactly: `encrypt` picks its own IV, so it +/// cannot be asked to reproduce a published ciphertext. `encrypt` is covered by the round trip +/// below and, at the library level, by `crypto/aria/tests/stream_mode_tests.rs`. +#[test] +fn decrypt_matches_the_kisa_vectors() { + for (cmd, key, ct) in VARIANTS { + let input = unhex(&format!("{}{ct}", IV)); + let out = run_ok(&[cmd, "decrypt", "--key", key], &input); + assert_eq!(tohex(&out), PLAINTEXT, "{cmd} decrypt should give the plaintext"); + } +} + +/// The same with `-x`: identical answer in hex plus a trailing newline. +#[test] +fn hex_output_matches_binary_output() { + let input = unhex(&format!("{}{}", IV, CT_256)); + let out = run_ok(&["aria256-cfb", "decrypt", "--key", KEY_256, "-x"], &input); + assert_eq!(String::from_utf8_lossy(&out), format!("{PLAINTEXT}\n")); +} + +/// `encrypt | decrypt` composes: the init data rides at the front, and the plaintext comes back. +#[test] +fn encrypt_then_decrypt_round_trips() { + let plaintext = pseudo_random(4096, 0xC0FFEE); + + for (cmd, key, _) in VARIANTS { + let ciphertext = run_ok(&[cmd, "encrypt", "--key", key], &plaintext); + assert_eq!( + ciphertext.len(), + 16 + plaintext.len(), + "{cmd}: init data plus an equal-length body" + ); + assert_ne!(&ciphertext[16..], &plaintext[..]); + + let recovered = run_ok(&[cmd, "decrypt", "--key", key], &ciphertext); + assert_eq!(recovered, plaintext, "{cmd}"); + } +} + +/// Unlike `aria*-cbc`, any length is accepted and nothing is padded: this is a stream cipher. +#[test] +fn any_input_length_is_accepted() { + for len in [0usize, 1, 15, 17, 33] { + let plaintext = pseudo_random(len, len as u32); + let ciphertext = run_ok(&["aria128-cfb", "encrypt", "--key", KEY_128], &plaintext); + assert_eq!(ciphertext.len(), 16 + len, "len {len}: init data plus an equal-length body"); + let recovered = run_ok(&["aria128-cfb", "decrypt", "--key", KEY_128], &ciphertext); + assert_eq!(recovered, plaintext, "len {len}: round trip"); + } +} + +/// Each command wants exactly its key length. The 256-bit key to the 128-bit command, and so on, +/// are errors naming the algorithm. +#[test] +fn a_key_of_the_wrong_length_is_rejected() { + let err = run_err(&["aria128-cfb", "encrypt", "--key", KEY_256], &[0u8; 16]); + assert!(err.contains("ARIA-128 needs a 16-byte key, got 32 bytes"), "stderr was: {err}"); + let err = run_err(&["aria192-cfb", "encrypt", "--key", KEY_128], &[0u8; 16]); + assert!(err.contains("ARIA-192 needs a 24-byte key, got 16 bytes"), "stderr was: {err}"); + let err = run_err(&["aria256-cfb", "encrypt", "--key", KEY_192], &[0u8; 16]); + assert!(err.contains("ARIA-256 needs a 32-byte key, got 24 bytes"), "stderr was: {err}"); +} + +/// The subcommands are discoverable. +#[test] +fn the_subcommands_are_listed_in_help() { + let out = run_ok(&["--help"], &[]); + let help = String::from_utf8_lossy(&out); + for cmd in ["aria128-cfb", "aria192-cfb", "aria256-cfb"] { + assert!(help.contains(cmd), "`--help` should list {cmd}"); + } +} diff --git a/cli/tests/aria_ctr_cli_tests.rs b/cli/tests/aria_ctr_cli_tests.rs new file mode 100644 index 000000000..0901b299e --- /dev/null +++ b/cli/tests/aria_ctr_cli_tests.rs @@ -0,0 +1,253 @@ +//! Tests for the `aria128-ctr`, `aria192-ctr` and `aria256-ctr` subcommands. +//! +//! They share their key loader, streaming loops and error paths with the `aes*-ctr` commands +//! (`cli/src/stream_mode_cmd.rs`), and `aes_ctr_cli_tests.rs` covers those exhaustively. This +//! file pins what is specific to ARIA: the known-answer vectors, the three key lengths, and +//! that the commands exist and round-trip at any length. +//! +//! Vectors are the `ARIA-*-CTR` entries of OpenSSL's `evpciph_aria.txt` (OpenSSL 3.6.2), +//! which are KISA's published ARIA test vectors; see `crypto/aria/tests/stream_mode_tests.rs`. + +use std::io::{ErrorKind, Write}; +use std::process::{Command, Output, Stdio}; + +/// The path to the binary under test, resolved by cargo. +const BC_RUST: &str = env!("CARGO_BIN_EXE_bc-rust"); + +/// The nonce the CTR vectors imply: their counter block is all zeros, and these commands split the +/// leading 12 bytes of it off as the nonce. +const NONCE: &str = "000000000000000000000000"; + +/// The 160-byte plaintext shared by all nine KISA mode vectors. +const PLAINTEXT: &str = "11111111aaaaaaaa11111111bbbbbbbb11111111cccccccc11111111dddddddd22222222aaaaaaaa22222222bbbbbbbb22222222cccccccc22222222dddddddd33333333aaaaaaaa33333333bbbbbbbb33333333cccccccc33333333dddddddd44444444aaaaaaaa44444444bbbbbbbb44444444cccccccc44444444dddddddd55555555aaaaaaaa55555555bbbbbbbb55555555cccccccc55555555dddddddd"; + +const KEY_128: &str = "00112233445566778899aabbccddeeff"; +const KEY_192: &str = "00112233445566778899aabbccddeeff0011223344556677"; +const KEY_256: &str = "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"; + +/// `ARIA-128-CTR` ciphertext (CTR). +const CT_128: &str = concat!( + "ac5d7de805a0bf1c57c854501af60fa1", + "1497e2a34519dea1569e91e5b5ccae2f", + "f3bfa1bf975f4571f48be191613546c3", + "911163c085f871f0e7ae5f2a085b8185", + "1c2a3ddf20ecb8fa51901aec8ee4ba32", + "a35dab67bb72cd9140ad188a967ac0fb", + "bdfa94ea6cce47dcf8525ab5a814cfeb", + "2bb60ee2b126e2d9d847c1a9e96f9019", + "e3e6a7fe40d3829afb73db1cc245646a", + "ddb62d9b907baaafbe46a73dbc131d3d", +); + +/// `ARIA-192-CTR` ciphertext (CTR). +const CT_192: &str = concat!( + "08625ca8fe569c19ba7af3760a6ed1ce", + "f4d199263e999dde14082dbba7560b79", + "a4c6b456b8707dce751f9854f18893df", + "db3f4e5afa539733e6f1e70b98ba3789", + "1f8f81e95df8efc26c7ce043504cb189", + "58b865e4e316cd2aa1c97f31bf23dc04", + "6ef326b95a692a191ba0f2a41c5fe9ae", + "070f236ff7078e703b42666caafbdd20", + "bad74ac4c20c0f46c7ca24c151716575", + "c947da16c90cfe1bf217a41cfebe7531", +); + +/// `ARIA-256-CTR` ciphertext (CTR). +const CT_256: &str = concat!( + "30026c329666141721178b99c0a1f1b2", + "f06940253f7b3089e2a30ea86aa3c88f", + "5940f05ad7ee41d71347bb7261e348f1", + "8360473fdf7d4e7723bffb4411cc13f6", + "cdd89f3bc7b9c768145022c7a74f14d7", + "c305cd012a10f16050c23f1ae5c23f45", + "998d13fbaa041e51619577e077276489", + "6a5d4516d8ffceb3bf7e05f613edd9a6", + "0cdcedaff9cfcaf4e00d445a54334f73", + "ab2cad944e51d266548e61c6eb0aa1cd", +); + +/// Runs `bc-rust ` with `stdin_bytes` on stdin and returns the completed output. +fn run(args: &[&str], stdin_bytes: &[u8]) -> Output { + let mut child = Command::new(BC_RUST) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("failed to spawn bc-rust"); + + // The error-path tests hand a rejected key to a command that `exit`s before it ever reads + // stdin, so this write races the child's exit and sometimes loses -- reliably so on a loaded + // CI runner. That is an expected outcome, not a harness failure: `wait_with_output` still + // returns the exit status and stderr, which is all those tests assert on. Any other write + // error is a real problem and still panics. + match child.stdin.as_mut().expect("stdin piped").write_all(stdin_bytes) { + Ok(()) => {} + Err(e) if e.kind() == ErrorKind::BrokenPipe => {} + Err(e) => panic!("failed to write to stdin: {e}"), + } + + child.wait_with_output().expect("failed to wait for bc-rust") +} + +/// Runs a command that is expected to succeed, returning stdout. +fn run_ok(args: &[&str], stdin_bytes: &[u8]) -> Vec { + let out = run(args, stdin_bytes); + assert!( + out.status.success(), + "expected success from {args:?}, got {:?}\nstderr: {}", + out.status, + String::from_utf8_lossy(&out.stderr) + ); + out.stdout +} + +/// Runs a command that is expected to fail, returning stderr as a string. +fn run_err(args: &[&str], stdin_bytes: &[u8]) -> String { + let out = run(args, stdin_bytes); + assert!( + !out.status.success(), + "expected failure from {args:?}, but it succeeded\nstdout: {:?}", + String::from_utf8_lossy(&out.stdout) + ); + String::from_utf8_lossy(&out.stderr).into_owned() +} + +fn unhex(s: &str) -> Vec { + assert!(s.len().is_multiple_of(2), "hex string must have even length"); + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("valid hex")) + .collect() +} + +fn tohex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{b:02x}")).collect() +} + +/// Deterministic pseudo-random bytes, so the tests do not depend on an RNG. +fn pseudo_random(len: usize, seed: u32) -> Vec { + let mut state = seed.wrapping_mul(2_654_435_761).wrapping_add(1); + (0..len) + .map(|_| { + state ^= state << 13; + state ^= state >> 17; + state ^= state << 5; + (state >> 24) as u8 + }) + .collect() +} + +const VARIANTS: [(&str, &str, &str); 3] = [ + ("aria128-ctr", KEY_128, CT_128), + ("aria192-ctr", KEY_192, CT_192), + ("aria256-ctr", KEY_256, CT_256), +]; + +/// `decrypt` reproduces the plaintext when handed the NONCE followed by the ciphertext, for +/// all three key lengths. +/// +/// This is the direction that can be pinned exactly: `encrypt` picks its own NONCE, so it +/// cannot be asked to reproduce a published ciphertext. `encrypt` is covered by the round trip +/// below and, at the library level, by `crypto/aria/tests/stream_mode_tests.rs`. +#[test] +fn decrypt_matches_the_kisa_vectors() { + for (cmd, key, ct) in VARIANTS { + let input = unhex(&format!("{}{ct}", NONCE)); + let out = run_ok(&[cmd, "decrypt", "--key", key], &input); + assert_eq!(tohex(&out), PLAINTEXT, "{cmd} decrypt should give the plaintext"); + } +} + +/// The same with `-x`: identical answer in hex plus a trailing newline. +#[test] +fn hex_output_matches_binary_output() { + let input = unhex(&format!("{}{}", NONCE, CT_256)); + let out = run_ok(&["aria256-ctr", "decrypt", "--key", KEY_256, "-x"], &input); + assert_eq!(String::from_utf8_lossy(&out), format!("{PLAINTEXT}\n")); +} + +/// `encrypt | decrypt` composes: the init data rides at the front, and the plaintext comes back. +#[test] +fn encrypt_then_decrypt_round_trips() { + let plaintext = pseudo_random(4096, 0xC0FFEE); + + for (cmd, key, _) in VARIANTS { + let ciphertext = run_ok(&[cmd, "encrypt", "--key", key], &plaintext); + assert_eq!( + ciphertext.len(), + 12 + plaintext.len(), + "{cmd}: init data plus an equal-length body" + ); + assert_ne!(&ciphertext[12..], &plaintext[..]); + + let recovered = run_ok(&[cmd, "decrypt", "--key", key], &ciphertext); + assert_eq!(recovered, plaintext, "{cmd}"); + } +} + +/// Unlike `aria*-cbc`, any length is accepted and nothing is padded: this is a stream cipher. +#[test] +fn any_input_length_is_accepted() { + for len in [0usize, 1, 15, 17, 33] { + let plaintext = pseudo_random(len, len as u32); + let ciphertext = run_ok(&["aria128-ctr", "encrypt", "--key", KEY_128], &plaintext); + assert_eq!(ciphertext.len(), 12 + len, "len {len}: init data plus an equal-length body"); + let recovered = run_ok(&["aria128-ctr", "decrypt", "--key", KEY_128], &ciphertext); + assert_eq!(recovered, plaintext, "len {len}: round trip"); + } +} + +/// CTR writes a **12-byte** nonce where the other ARIA modes write a 16-byte IV, so the output is +/// 12 bytes longer than the input rather than 16. Getting this wrong would shift every byte of the +/// payload. +#[test] +fn the_nonce_is_twelve_bytes_not_sixteen() { + let plaintext = pseudo_random(64, 7); + let out = run_ok(&["aria128-ctr", "encrypt", "--key", KEY_128], &plaintext); + assert_eq!(out.len(), plaintext.len() + 12, "a 12-byte nonce plus the ciphertext"); + + let via_cfb = run_ok(&["aria128-cfb", "encrypt", "--key", KEY_128], &plaintext); + assert_eq!(via_cfb.len(), plaintext.len() + 16, "aria128-cfb writes a 16-byte IV"); + + let back = run_ok(&["aria128-ctr", "decrypt", "--key", KEY_128], &out); + assert_eq!(back, plaintext); +} + +/// A fresh nonce per invocation. For CTR this is the whole security argument: a repeated nonce +/// under one key repeats the keystream and leaks the XOR of the two messages. +#[test] +fn each_invocation_uses_a_fresh_nonce() { + let plaintext = pseudo_random(64, 11); + let mut seen = std::collections::BTreeSet::new(); + + for _ in 0..8 { + let ciphertext = run_ok(&["aria128-ctr", "encrypt", "--key", KEY_128], &plaintext); + assert!(seen.insert(ciphertext[..12].to_vec()), "the CLI reused a nonce"); + let recovered = run_ok(&["aria128-ctr", "decrypt", "--key", KEY_128], &ciphertext); + assert_eq!(recovered, plaintext); + } +} + +/// Each command wants exactly its key length. The 256-bit key to the 128-bit command, and so on, +/// are errors naming the algorithm. +#[test] +fn a_key_of_the_wrong_length_is_rejected() { + let err = run_err(&["aria128-ctr", "encrypt", "--key", KEY_256], &[0u8; 16]); + assert!(err.contains("ARIA-128 needs a 16-byte key, got 32 bytes"), "stderr was: {err}"); + let err = run_err(&["aria192-ctr", "encrypt", "--key", KEY_128], &[0u8; 16]); + assert!(err.contains("ARIA-192 needs a 24-byte key, got 16 bytes"), "stderr was: {err}"); + let err = run_err(&["aria256-ctr", "encrypt", "--key", KEY_192], &[0u8; 16]); + assert!(err.contains("ARIA-256 needs a 32-byte key, got 24 bytes"), "stderr was: {err}"); +} + +/// The subcommands are discoverable. +#[test] +fn the_subcommands_are_listed_in_help() { + let out = run_ok(&["--help"], &[]); + let help = String::from_utf8_lossy(&out); + for cmd in ["aria128-ctr", "aria192-ctr", "aria256-ctr"] { + assert!(help.contains(cmd), "`--help` should list {cmd}"); + } +} diff --git a/crypto/aria/Cargo.toml b/crypto/aria/Cargo.toml new file mode 100644 index 000000000..d1b55fcb8 --- /dev/null +++ b/crypto/aria/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "bouncycastle-aria" +version.workspace = true +edition.workspace = true + +[dependencies] +bouncycastle-core.workspace = true +bouncycastle-utils.workspace = true +# Only for the ARIA-CBC type aliases in `cbc.rs`; the engine itself does not use it. +bouncycastle-cipher.workspace = true +# Only for the padded CBC aliases in `cbc.rs`; the engine itself does not use it. + +[dev-dependencies] +bouncycastle-core-test-framework.workspace = true +bouncycastle-hex.workspace = true +bouncycastle-rng.workspace = true +criterion.workspace = true +serde_json = "1.0" + +[[bench]] +name = "aria_benches" +harness = false diff --git a/crypto/aria/benches/aria_benches.rs b/crypto/aria/benches/aria_benches.rs new file mode 100644 index 000000000..903b5ac3d --- /dev/null +++ b/crypto/aria/benches/aria_benches.rs @@ -0,0 +1,134 @@ +//! Criterion benchmarks for the bit-sliced ARIA engine. +//! +//! The comparison that matters is `encrypt_block` against `encrypt_2blocks` and `encrypt_4blocks` +//! over the same number of bytes. The S-box circuits always process four lanes, so a single-block +//! call does four blocks' worth of work and the four-block path (what the CBC mode's decryption +//! uses) should be close to four times the throughput; the two-block path sits at half. Decryption costs the same as encryption (the same rounds with +//! the round keys in the decryption order). ARIA-192 sits between the other two in rounds, so only +//! the 12-round ARIA-128 and the 16-round ARIA-256 are benchmarked. + +use bouncycastle_aria::hazmat::{ARIA128Internal, ARIA256Internal}; +use bouncycastle_aria::{BLOCK_LEN, LANES}; +use bouncycastle_core::hazmat::{BlockPermutation, ElectronicCodeBook}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::RNG; +use bouncycastle_rng as rng; +use criterion::{Criterion, Throughput, criterion_group, criterion_main}; +use std::hint::black_box; + +/// 16 KiB of data, i.e. 1024 ARIA blocks. +const NUM_BLOCKS: usize = 1024; +const DATA_LEN: usize = NUM_BLOCKS * BLOCK_LEN; + +fn random_blocks() -> Vec<[u8; BLOCK_LEN]> { + let mut blocks = vec![[0u8; BLOCK_LEN]; NUM_BLOCKS]; + let mut generator = rng::DefaultRNG::default(); + for block in blocks.iter_mut() { + generator.next_bytes_out(block).unwrap(); + } + blocks +} + +fn key() -> KeyMaterial { + let mut bytes = [0u8; KEY_LEN]; + rng::DefaultRNG::default().next_bytes_out(&mut bytes).unwrap(); + KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).unwrap() +} + +fn bench_key_expansion(c: &mut Criterion) { + let mut group = c.benchmark_group("aria::key expansion"); + + let key128 = key::<16>(); + group.bench_function("ARIA128Internal::new()", |b| { + b.iter(|| black_box(ARIA128Internal::new(black_box(&key128)).unwrap())) + }); + let key256 = key::<32>(); + group.bench_function("ARIA256Internal::new()", |b| { + b.iter(|| black_box(ARIA256Internal::new(black_box(&key256)).unwrap())) + }); + + group.finish(); +} + +fn bench_aria128(c: &mut Criterion) { + let aria = ARIA128Internal::new(&key::<16>()).unwrap(); + let blocks = random_blocks(); + + let mut group = c.benchmark_group("aria::ARIA128Internal"); + group.throughput(Throughput::Bytes(DATA_LEN as u64)); + + group.bench_function("16KiB -- .encrypt_block() x1024", |b| { + b.iter(|| { + let mut buf = blocks.clone(); + for block in buf.iter_mut() { + aria.encrypt_block(black_box(block)); + } + black_box(&buf); + }) + }); + + group.bench_function("16KiB -- .encrypt_2blocks() x512", |b| { + b.iter(|| { + let mut buf = blocks.clone(); + for pair in buf.as_chunks_mut::<2>().0 { + aria.encrypt_2blocks(black_box(pair)); + } + black_box(&buf); + }) + }); + + group.bench_function("16KiB -- .encrypt_4blocks() x256", |b| { + b.iter(|| { + let mut buf = blocks.clone(); + for four in buf.as_chunks_mut::().0 { + aria.encrypt_4blocks(black_box(four)); + } + black_box(&buf); + }) + }); + + group.bench_function("16KiB -- .decrypt_block() x1024", |b| { + b.iter(|| { + let mut buf = blocks.clone(); + for block in buf.iter_mut() { + aria.decrypt_block(black_box(block)); + } + black_box(&buf); + }) + }); + + group.finish(); +} + +fn bench_aria256(c: &mut Criterion) { + let aria = ARIA256Internal::new(&key::<32>()).unwrap(); + let blocks = random_blocks(); + + let mut group = c.benchmark_group("aria::ARIA256Internal"); + group.throughput(Throughput::Bytes(DATA_LEN as u64)); + + group.bench_function("16KiB -- .encrypt_block() x1024", |b| { + b.iter(|| { + let mut buf = blocks.clone(); + for block in buf.iter_mut() { + aria.encrypt_block(black_box(block)); + } + black_box(&buf); + }) + }); + + group.bench_function("16KiB -- .encrypt_4blocks() x256", |b| { + b.iter(|| { + let mut buf = blocks.clone(); + for four in buf.as_chunks_mut::().0 { + aria.encrypt_4blocks(black_box(four)); + } + black_box(&buf); + }) + }); + + group.finish(); +} + +criterion_group!(benches, bench_key_expansion, bench_aria128, bench_aria256); +criterion_main!(benches); diff --git a/crypto/aria/src/bitslice.rs b/crypto/aria/src/bitslice.rs new file mode 100644 index 000000000..7ac1d0573 --- /dev/null +++ b/crypto/aria/src/bitslice.rs @@ -0,0 +1,106 @@ +//! Bit-plane transposition for the constant-time S-boxes. +//! +//! # Layout +//! +//! A substitution layer applies one of the four S-boxes to each byte of the state (RFC 5794 +//! Sec 2.4.2). The four bytes of a block that share an S-box form one 32-bit *class word* (see +//! [`crate::round`]). To evaluate an S-box as a Boolean circuit, the class words of four +//! independent blocks are split into 16-bit halves, eight words in all, and transposed so that +//! plane `q[k]` holds bit `k` of every one of the 16 bytes: +//! +//! ```text +//! before ortho: q[w] bit (8L + k) == bit k of byte L of half-word w +//! after ortho: q[k] bit (8L + w) == bit k of byte L of half-word w +//! ``` +//! +//! That is, within each byte-lane `L` of the eight half-words, the 8x8 bit matrix indexed by +//! (half-word, bit-within-lane) is transposed. Which byte of which word is "byte `L` of half-word +//! `w`" does not matter to an S-box, which is applied to every byte independently; only that the +//! same byte comes back to the same place, which [`ortho`] being its own inverse guarantees. +//! +//! `test_layout_matches_the_documented_table` pins the identity above exhaustively. +//! +//! # Why 16-bit planes +//! +//! Eight `u32` planes would hold one class word from each of eight blocks and double the +//! throughput on a 32- or 64-bit machine, at the cost of doubling every buffer in the round: the +//! working state and the planes. This crate takes the smaller working set -- that is what +//! "lowmemory" means here -- and processes four blocks per pass. +//! +//! # Provenance +//! +//! The three-stage masked-swap transpose is the one `bouncycastle-aes` and +//! `bouncycastle-camellia` use on `u32` words and `bouncycastle-sm4` on +//! `u16`: it transposes within each byte-lane, so the number of byte-lanes per word does not enter +//! into it. All of them are translated from BearSSL `src/symcipher/aes_ct.c` (`br_aes_ct_ortho`) +//! by Thomas Pornin, MIT licensed. It is duplicated rather than shared because it is each crate's +//! private detail and none should depend on another. + +/// The eight bit-planes holding the two 16-bit halves of one word from each of four blocks. See +/// the module docs. +pub(crate) type Planes = [u16; 8]; + +/// Transposes bytes into bit-planes, and back -- it is its own inverse. +/// +/// Three stages of masked swaps exchange bit-fields of width 1, 2 and 4 between pairs of words, +/// which together transpose the 8x8 bit matrix inside each byte-lane. +pub(crate) fn ortho(q: &mut Planes) { + /// One masked swap: exchanges the `cl`-selected fields of `y` into `x` and the `ch`-selected + /// fields of `x` into `y`, moving them by `s` bit positions. + /// + /// `cl` and `ch` are complementary, and `s` is exactly the field width, so in each returned + /// word the two combined operands occupy disjoint bits: `(x & cl)` and `(y & cl) << s` cannot + /// both be set in the same position. `|` and `^` therefore compute the same function here, + /// which is why `cargo mutants` reports the `| -> ^` mutants in this function as surviving -- + /// they are equivalent programs. `test_ortho_is_an_involution` and + /// `test_layout_matches_the_documented_table` are what actually pin this code. + #[inline(always)] + fn swap(cl: u16, ch: u16, s: u32, x: u16, y: u16) -> (u16, u16) { + ((x & cl) | ((y & cl) << s), ((x & ch) >> s) | (y & ch)) + } + + // Stage 1: swap single bits between adjacent words (0x55 = even bits, 0xAA = odd bits). + for (a, b) in [(0, 1), (2, 3), (4, 5), (6, 7)] { + (q[a], q[b]) = swap(0x5555, 0xAAAA, 1, q[a], q[b]); + } + // Stage 2: swap 2-bit fields between words two apart. + for (a, b) in [(0, 2), (1, 3), (4, 6), (5, 7)] { + (q[a], q[b]) = swap(0x3333, 0xCCCC, 2, q[a], q[b]); + } + // Stage 3: swap nibbles between words four apart. + for (a, b) in [(0, 4), (1, 5), (2, 6), (3, 7)] { + (q[a], q[b]) = swap(0x0F0F, 0xF0F0, 4, q[a], q[b]); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_ortho_is_an_involution() { + let mut q: Planes = core::array::from_fn(|i| 0x0123u16.wrapping_mul(i as u16 * 37 + 3)); + let original = q; + ortho(&mut q); + assert_ne!(q, original, "ortho must actually move bits"); + ortho(&mut q); + assert_eq!(q, original); + } + + #[test] + fn test_layout_matches_the_documented_table() { + // A single set bit at (word w, byte-lane L, bit k) must land at (plane k, lane L, bit w). + for w in 0..8 { + for lane in 0..2 { + for k in 0..8 { + let mut q: Planes = [0; 8]; + q[w] = 1 << (8 * lane + k); + ortho(&mut q); + let mut expected: Planes = [0; 8]; + expected[k] = 1 << (8 * lane + w); + assert_eq!(q, expected, "word {w}, lane {lane}, bit {k}"); + } + } + } + } +} diff --git a/crypto/aria/src/cbc.rs b/crypto/aria/src/cbc.rs new file mode 100644 index 000000000..93e24fe4e --- /dev/null +++ b/crypto/aria/src/cbc.rs @@ -0,0 +1,121 @@ +//! Type aliases for ARIA (RFC 5794, KS X 1213-1) in CBC mode (NIST SP 800-38A Sec 6.2), with padding. +//! +//! See `bouncycastle_cipher::modes::cbc` for the construction. The aliases name the padded +//! workers, which accept input of any size; `NoPadding` accepts only whole blocks but goes through +//! the same adapter. The workers come from a key: [`ARIA128Key>`](crate::ARIA128Key) +//! (or `CBC`) hands them out through the [`SymmetricEncryptionKey`] / +//! [`SymmetricDecryptionKey`] pair, and `ARIA128Key>` hands out the unpadded `Cbc` +//! itself through the block pair. +//! +//! CBC is defined only on whole blocks (SP 800-38A Sec 5.2), so CBC on data of any other length is +//! always CBC *plus a padding scheme*, and the scheme changes the ciphertext and must be agreed by +//! both ends. Naming it in the key's mode makes that choice explicit and makes a mismatched pair a +//! compile error. +//! +//! ``` +//! use bouncycastle_aria::ARIA128Key; +//! use bouncycastle_cipher::modes::CBC; +//! use bouncycastle_cipher::padding::PKCS7; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey}; +//! use bouncycastle_rng::HashDRBG_SHA512; +//! +//! let mut rng = HashDRBG_SHA512::new_from_os(); +//! let material = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! let key = ARIA128Key::>::from_key_material(&material).expect("a valid ARIA-128 key"); +//! let message = b"a message of no particular length"; +//! +//! // The IV is drawn from `rng` and returned; there is no API for supplying one. +//! let (iv, ciphertext) = key.encrypt(&mut rng, message).expect("encryption"); +//! let recovered = key.decrypt(&iv, &ciphertext).expect("decryption"); +//! assert_eq!(recovered, message); +//! ``` +//! +//! With [`NoPadding`] nothing is added, and a message that is not a whole number of blocks is an +//! error rather than something silently padded: +//! +//! ``` +//! use bouncycastle_aria::ARIA128Key; +//! use bouncycastle_cipher::modes::CBC; +//! use bouncycastle_cipher::padding::NoPadding; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricEncryptionKey}; +//! use bouncycastle_rng::HashDRBG_SHA512; +//! +//! let mut rng = HashDRBG_SHA512::new_from_os(); +//! let material = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); +//! let key = ARIA128Key::>::from_key_material(&material).unwrap(); +//! +//! let mut out = [0u8; 16]; +//! let (written, _iv) = key.encrypt_out(&mut rng, &[0u8; 16], &mut out).expect("aligned"); +//! assert_eq!(written, 16); +//! assert!(key.encrypt_out(&mut rng, b"hello", &mut out).is_err()); +//! ``` + +use crate::BLOCK_LEN; +use crate::hazmat::{ARIA128Internal, ARIA192Internal, ARIA256Internal}; +use bouncycastle_cipher::Direction; +use bouncycastle_cipher::modes::Cbc; +use bouncycastle_cipher::padding::{PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor}; +use bouncycastle_cipher::{Decrypting, Encrypting}; + +// Imports needed for docs +#[allow(unused_imports)] +use bouncycastle_cipher::padding::{NoPadding, PKCS7}; +#[allow(unused_imports)] +use bouncycastle_core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey}; +// end of imports needed for docs + +/// ARIA-128 in CBC mode with a padding scheme: what an [`ARIA128Key`](crate::ARIA128Key) under +/// `CBC` hands out. `Dir` is [`Encrypting`] or [`Decrypting`] and `Pad` is [`PKCS7`] or +/// [`NoPadding`]; the wrong direction is a compile error, not a runtime check. +#[allow(non_camel_case_types)] +pub type ARIA_CBC_128<'k, Dir, Pad> = ::Select< + PaddedBlockCipherEncryptor< + Cbc<'k, ARIA128Internal, Encrypting, BLOCK_LEN>, + Pad, + BLOCK_LEN, + BLOCK_LEN, + >, + PaddedBlockCipherDecryptor< + Cbc<'k, ARIA128Internal, Decrypting, BLOCK_LEN>, + Pad, + BLOCK_LEN, + BLOCK_LEN, + >, +>; + +/// ARIA-192 in CBC mode with a padding scheme. See [`ARIA_CBC_128`]. +#[allow(non_camel_case_types)] +pub type ARIA_CBC_192<'k, Dir, Pad> = ::Select< + PaddedBlockCipherEncryptor< + Cbc<'k, ARIA192Internal, Encrypting, BLOCK_LEN>, + Pad, + BLOCK_LEN, + BLOCK_LEN, + >, + PaddedBlockCipherDecryptor< + Cbc<'k, ARIA192Internal, Decrypting, BLOCK_LEN>, + Pad, + BLOCK_LEN, + BLOCK_LEN, + >, +>; + +/// ARIA-256 in CBC mode with a padding scheme. See [`ARIA_CBC_128`]. +#[allow(non_camel_case_types)] +pub type ARIA_CBC_256<'k, Dir, Pad> = ::Select< + PaddedBlockCipherEncryptor< + Cbc<'k, ARIA256Internal, Encrypting, BLOCK_LEN>, + Pad, + BLOCK_LEN, + BLOCK_LEN, + >, + PaddedBlockCipherDecryptor< + Cbc<'k, ARIA256Internal, Decrypting, BLOCK_LEN>, + Pad, + BLOCK_LEN, + BLOCK_LEN, + >, +>; diff --git a/crypto/aria/src/cfb.rs b/crypto/aria/src/cfb.rs new file mode 100644 index 000000000..e5c51516b --- /dev/null +++ b/crypto/aria/src/cfb.rs @@ -0,0 +1,67 @@ +//! Type aliases for ARIA (RFC 5794, KS X 1213-1) in CFB mode (NIST SP 800-38A Sec 6.3). +//! +//! See `bouncycastle_cipher::modes::cfb` for the construction. The aliases are stream ciphers: +//! the data is a `&mut [u8]` of any length, encrypted or decrypted in place. They come from a key, +//! [`ARIA128Key`](crate::ARIA128Key) and friends, through the [`SymmetricEncryptionKey`] / +//! [`SymmetricDecryptionKey`] pair; the IV is one block, drawn from the caller's RNG by encryption +//! and returned, and there is no API for supplying one. +//! +//! The segment size is the full block, so these are **CFB128**. SP 800-38A's `s = 8` variant is a +//! different, non-interoperable mode with its own aliases -- [`ARIA_CFB8_128`](crate::ARIA_CFB8_128) +//! and friends -- and `s = 1` is not implemented; see the `bouncycastle_cipher::modes::Cfb` docs. +//! +//! ``` +//! use bouncycastle_aria::ARIA128Key; +//! use bouncycastle_cipher::modes::CFB; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey}; +//! use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; +//! use bouncycastle_rng::HashDRBG_SHA512; +//! +//! let mut rng = HashDRBG_SHA512::new_from_os(); +//! let material = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! let key = ARIA128Key::::from_key_material(&material).expect("a valid ARIA-128 key"); +//! +//! // 47 bytes: a stream cipher does not need a whole number of blocks. +//! let message = [0u8; 47]; +//! let mut data = message; +//! let (_, iv) = key.encrypt_inplace(&mut rng, &mut data).unwrap(); +//! assert_ne!(data, message); +//! key.decrypt_inplace(&iv, &mut data).unwrap(); +//! assert_eq!(data, message); +//! +//! // Streaming, at any byte boundary: +//! let (mut enc, iv) = key.create_encryptor(&mut rng).unwrap(); +//! let mut first = [0u8; 5]; +//! let mut rest = [1u8; 30]; +//! enc.do_encrypt_inplace(&mut first).unwrap(); +//! enc.do_encrypt_inplace(&mut rest).unwrap(); +//! let mut dec = key.create_decryptor(&iv); +//! dec.do_decrypt_inplace(&mut first).unwrap(); +//! dec.do_decrypt_inplace(&mut rest).unwrap(); +//! assert_eq!(first, [0u8; 5]); +//! assert_eq!(rest, [1u8; 30]); +//! ``` + +use crate::BLOCK_LEN; +use crate::hazmat::{ARIA128Internal, ARIA192Internal, ARIA256Internal}; +use bouncycastle_cipher::modes::Cfb; + +// Imports needed for docs +#[allow(unused_imports)] +use bouncycastle_core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey}; +// end of imports needed for docs + +/// ARIA-128 in CFB128 mode. `Dir` is [`bouncycastle_cipher::Encrypting`] or +/// [`bouncycastle_cipher::Decrypting`]; the wrong direction is a compile error, not a runtime check. +#[allow(non_camel_case_types)] +pub type ARIA_CFB_128<'k, Dir> = Cfb<'k, ARIA128Internal, Dir, BLOCK_LEN>; + +/// ARIA-192 in CFB128 mode. See [`ARIA_CFB_128`]. +#[allow(non_camel_case_types)] +pub type ARIA_CFB_192<'k, Dir> = Cfb<'k, ARIA192Internal, Dir, BLOCK_LEN>; + +/// ARIA-256 in CFB128 mode. See [`ARIA_CFB_128`]. +#[allow(non_camel_case_types)] +pub type ARIA_CFB_256<'k, Dir> = Cfb<'k, ARIA256Internal, Dir, BLOCK_LEN>; diff --git a/crypto/aria/src/cfb8.rs b/crypto/aria/src/cfb8.rs new file mode 100644 index 000000000..eac9317b7 --- /dev/null +++ b/crypto/aria/src/cfb8.rs @@ -0,0 +1,62 @@ +//! Type aliases for ARIA (RFC 5794, KS X 1213-1) in CFB8 mode (NIST SP 800-38A Sec 6.3, `s = 8`). +//! +//! See `bouncycastle_cipher::modes::cfb8` for the construction. The aliases are stream ciphers +//! with a one-byte segment, and come from a key, [`ARIA128Key`](crate::ARIA128Key) and +//! friends, through the [`SymmetricEncryptionKey`] / [`SymmetricDecryptionKey`] pair; the IV is +//! one block, drawn from the caller's RNG by encryption and returned. +//! +//! CFB8 is a **different, non-interoperable mode** from CFB128, not a variant of it: their +//! ciphertexts differ from the second byte, and it costs a full ARIA call per byte, sixteen times +//! the work of [`ARIA_CFB_128`](crate::ARIA_CFB_128). See the `bouncycastle_cipher::modes::Cfb8` +//! docs for when that is the right trade. +//! +//! ``` +//! use bouncycastle_aria::ARIA128Key; +//! use bouncycastle_cipher::modes::{CFB, CFB8}; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey}; +//! use bouncycastle_rng::HashDRBG_SHA512; +//! +//! let mut rng = HashDRBG_SHA512::new_from_os(); +//! let material = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! let cfb8_key = ARIA128Key::::from_key_material(&material).expect("a valid ARIA-128 key"); +//! +//! // 5 bytes: CFB8's segment is one byte, so any length at all is fine. +//! let message = *b"hello"; +//! let mut data = message; +//! let (_, iv) = cfb8_key.encrypt_inplace(&mut rng, &mut data).unwrap(); +//! assert_ne!(data, message); +//! cfb8_key.decrypt_inplace(&iv, &mut data).unwrap(); +//! assert_eq!(data, message); +//! +//! // CFB8 and CFB128 are not interchangeable: same key material, same IV, different ciphertext. +//! let cfb_key = ARIA128Key::::from_key_material(&material).unwrap(); +//! let mut as_cfb8 = message; +//! let (_, iv) = cfb8_key.encrypt_inplace(&mut rng, &mut as_cfb8).unwrap(); +//! let mut as_cfb128 = as_cfb8; +//! cfb_key.decrypt_inplace(&iv, &mut as_cfb128).unwrap(); +//! assert_ne!(as_cfb128, message); +//! ``` + +use crate::BLOCK_LEN; +use crate::hazmat::{ARIA128Internal, ARIA192Internal, ARIA256Internal}; +use bouncycastle_cipher::modes::Cfb8; + +// Imports needed for docs +#[allow(unused_imports)] +use bouncycastle_core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey}; +// end of imports needed for docs + +/// ARIA-128 in CFB8 mode. `Dir` is [`bouncycastle_cipher::Encrypting`] or +/// [`bouncycastle_cipher::Decrypting`]; the wrong direction is a compile error, not a runtime check. +#[allow(non_camel_case_types)] +pub type ARIA_CFB8_128<'k, Dir> = Cfb8<'k, ARIA128Internal, Dir, BLOCK_LEN>; + +/// ARIA-192 in CFB8 mode. See [`ARIA_CFB8_128`]. +#[allow(non_camel_case_types)] +pub type ARIA_CFB8_192<'k, Dir> = Cfb8<'k, ARIA192Internal, Dir, BLOCK_LEN>; + +/// ARIA-256 in CFB8 mode. See [`ARIA_CFB8_128`]. +#[allow(non_camel_case_types)] +pub type ARIA_CFB8_256<'k, Dir> = Cfb8<'k, ARIA256Internal, Dir, BLOCK_LEN>; diff --git a/crypto/aria/src/ctr.rs b/crypto/aria/src/ctr.rs new file mode 100644 index 000000000..eed6a48ce --- /dev/null +++ b/crypto/aria/src/ctr.rs @@ -0,0 +1,75 @@ +//! Type aliases for ARIA (RFC 5794, KS X 1213-1) in CTR mode (NIST SP 800-38A Sec 6.5). +//! +//! See `bouncycastle_cipher::modes::ctr` for the construction. The aliases are stream ciphers: +//! the data is a `&mut [u8]` of any length, encrypted or decrypted in place. They come from a key, +//! [`ARIA128Key`](crate::ARIA128Key) and friends, through the [`SymmetricEncryptionKey`] / +//! [`SymmetricDecryptionKey`] pair; the nonce is drawn from the caller's RNG by encryption and +//! returned, and there is no API for supplying one. +//! +//! # The nonce length is 12, so the counter is 4 +//! +//! `Ctr` splits the counter block into a nonce and a counter by the length of its init data, and +//! these aliases, like `CTR`'s defaults, choose a **12-byte nonce**, leaving the 4-byte counter +//! that is the mode's maximum. That allows 2^32 blocks -- 64 GiB -- in one message, and past it +//! the mode errors rather than repeating keystream. A shorter message limit in exchange for more +//! nonce bits is available by naming `CTR<13>`, `CTR<14>` or `CTR<15>` as the key's mode. +//! +//! ``` +//! use bouncycastle_aria::ARIA128Key; +//! use bouncycastle_cipher::modes::CTR; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey}; +//! use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; +//! use bouncycastle_rng::HashDRBG_SHA512; +//! +//! let mut rng = HashDRBG_SHA512::new_from_os(); +//! let material = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! let key = ARIA128Key::::from_key_material(&material).expect("a valid ARIA-128 key"); +//! +//! // 47 bytes: a stream cipher does not need a whole number of blocks. +//! let message = [0u8; 47]; +//! let mut data = message; +//! let (_, nonce) = key.encrypt_inplace(&mut rng, &mut data).unwrap(); +//! assert_eq!(nonce.len(), 12); +//! assert_ne!(data, message); +//! key.decrypt_inplace(&nonce, &mut data).unwrap(); +//! assert_eq!(data, message); +//! +//! // Streaming, at any byte boundary: +//! let (mut enc, nonce) = key.create_encryptor(&mut rng).unwrap(); +//! let mut first = [0u8; 5]; +//! let mut rest = [1u8; 30]; +//! enc.do_encrypt_inplace(&mut first).unwrap(); +//! enc.do_encrypt_inplace(&mut rest).unwrap(); +//! let mut dec = key.create_decryptor(&nonce); +//! dec.do_decrypt_inplace(&mut first).unwrap(); +//! dec.do_decrypt_inplace(&mut rest).unwrap(); +//! assert_eq!(first, [0u8; 5]); +//! assert_eq!(rest, [1u8; 30]); +//! ``` + +use crate::BLOCK_LEN; +use crate::hazmat::{ARIA128Internal, ARIA192Internal, ARIA256Internal}; +use bouncycastle_cipher::modes::Ctr; + +// Imports needed for docs +#[allow(unused_imports)] +use bouncycastle_core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey}; +// end of imports needed for docs + +/// The nonce length these aliases use, leaving a 4-byte counter. +pub const CTR_NONCE_LEN: usize = 12; + +/// ARIA-128 in CTR mode with a 12-byte nonce. `Dir` is [`bouncycastle_cipher::Encrypting`] or +/// [`bouncycastle_cipher::Decrypting`]; the wrong direction is a compile error, not a runtime check. +#[allow(non_camel_case_types)] +pub type ARIA_CTR_128<'k, Dir> = Ctr<'k, ARIA128Internal, Dir, BLOCK_LEN, CTR_NONCE_LEN>; + +/// ARIA-192 in CTR mode with a 12-byte nonce. See [`ARIA_CTR_128`]. +#[allow(non_camel_case_types)] +pub type ARIA_CTR_192<'k, Dir> = Ctr<'k, ARIA192Internal, Dir, BLOCK_LEN, CTR_NONCE_LEN>; + +/// ARIA-256 in CTR mode with a 12-byte nonce. See [`ARIA_CTR_128`]. +#[allow(non_camel_case_types)] +pub type ARIA_CTR_256<'k, Dir> = Ctr<'k, ARIA256Internal, Dir, BLOCK_LEN, CTR_NONCE_LEN>; diff --git a/crypto/aria/src/hazmat/aria.rs b/crypto/aria/src/hazmat/aria.rs new file mode 100644 index 000000000..b94e7d2e0 --- /dev/null +++ b/crypto/aria/src/hazmat/aria.rs @@ -0,0 +1,447 @@ +//! The public engine types, and the encryption and decryption entry points (RFC 5794 Sec 2.3). + +use crate::round::{RoundKey, diffuse, rounds}; +use crate::schedule::{ARIA128Params, ARIA192Params, ARIA256Params, ARIAParams, ek, expand}; +use crate::{BLOCK_LEN, LANES}; +use bouncycastle_cipher::modes::ModeNames; +use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; +use bouncycastle_core::hazmat::{BlockPermutation, ElectronicCodeBook}; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::Algorithm; +use bouncycastle_utils::secret::Secret; + +/// One ARIA block. +pub type Block = [u8; BLOCK_LEN]; + +/// The ARIA keyed permutation, parameterised by key length, constant-time: the engine under +/// [`ARIAKey`](crate::ARIAKey). +/// +/// Use the aliases [`ARIA128Internal`], [`ARIA192Internal`] and [`ARIA256Internal`] rather than +/// naming this directly. `P` is sealed to the three parameter sets of RFC 5794, so no fourth +/// instantiation exists. +/// +/// The only state is the encryption round keys, held in a [`Secret`] so that they are zeroized on +/// drop and redacted from `Debug`. There is no direction flag and no initialisation state: +/// decryption uses the same rounds with the decryption round keys of Sec 2.2 (`dk1 = ek{n+1}`, +/// `dk{i} = A(ek{n+2-i})`, `dk{n+1} = ek1`), which are derived from the stored encryption keys as +/// each round needs them, so both directions work from one stored schedule and a constructed +/// value is always ready to use -- there is no `init()` or `reset()`, unlike an engine that lays +/// the keys out differently depending on the direction it is initialised for. +#[derive(Clone)] +pub struct ARIAInternal { + schedule: Secret, +} + +// Written out rather than derived so that `P`, a marker type, need not be `PartialEq`. `Secret`'s +// equality inspects every byte of the schedule, so the time taken does not say where two differ. +impl PartialEq for ARIAInternal

{ + fn eq(&self, other: &Self) -> bool { + self.schedule == other.schedule + } +} + +impl Eq for ARIAInternal

{} + +/// ARIA-128: 16-byte key, 12 rounds (RFC 5794 Sec 2.3.1.1). +#[allow(non_camel_case_types)] +pub type ARIA128Internal = ARIAInternal; +/// ARIA-192: 24-byte key, 14 rounds (RFC 5794 Sec 2.3.1.2). +#[allow(non_camel_case_types)] +pub type ARIA192Internal = ARIAInternal; +/// ARIA-256: 32-byte key, 16 rounds (RFC 5794 Sec 2.3.1.3). +#[allow(non_camel_case_types)] +pub type ARIA256Internal = ARIAInternal; + +impl ARIAInternal

{ + /// Checks a key is fit to use before it is expanded. + /// + /// The key must be tagged [`KeyType::SymmetricCipherKey`], must be exactly `P::KEY_LEN` bytes + /// of the buffer, and must carry a [`SecurityStrength`] at least equal to its own length -- + /// which is what a key of this length from a correctly-instantiated RNG or KDF will have. + /// The checks exist to catch a key that arrived from somewhere it should not have: a seed + /// reused as a cipher key, or a 32-byte buffer holding material only derived at the 128-bit + /// strength. + /// + /// Takes `&dyn KeyMaterialTrait` so the three constructors, whose `KeyMaterial` capacities + /// differ, can share one implementation. + fn validate(key: &dyn KeyMaterialTrait) -> Result<(), SymmetricCipherError> { + if key.key_type() != KeyType::SymmetricCipherKey { + return Err(KeyMaterialError::InvalidKeyType( + "ARIA requires a key of type KeyType::SymmetricCipherKey.", + ) + .into()); + } + if key.key_len() != P::KEY_LEN { + return Err(KeyMaterialError::InvalidLength.into()); + } + if key.security_strength() < SecurityStrength::from_bytes(P::KEY_LEN) { + return Err(KeyMaterialError::SecurityStrength( + "The provided key has a lower security strength than the ARIA key length implies.", + ) + .into()); + } + Ok(()) + } + + /// Encrypts four independent blocks in place (Sec 2.3.1): the encryption round keys + /// `ek1 .. ek{n+1}` in order. + /// + /// This is the natural unit of work. Each S-box circuit substitutes 16 bytes per pass and a + /// substitution layer sends four bytes of each block through each of the four S-boxes, so four + /// blocks fill the four passes exactly; fewer blocks cost the same. Modes whose blocks are + /// independent -- CTR, and the decryption direction of CBC and CFB -- reach it through + /// the `ElectronicCodeBook` four-block batch; CBC encryption cannot, since its blocks + /// are serially dependent. + /// + /// Infallible: a constructed [`ARIAInternal`] is always usable and every input length is fixed. + pub fn encrypt_4blocks(&self, blocks: &mut [Block; LANES]) { + let s = self.schedule.as_ref(); + rounds(blocks, P::ROUNDS, |i| ek(s, i)); + } + + /// Decrypts four independent blocks in place (Sec 2.3.2): "the same as the encryption process + /// except that encryption round keys are replaced by decryption round keys", with Sec 2.2's + /// `dk1 = ek{n+1}`, `dk{i} = A(ek{n+2-i})` for `i = 2 .. n`, and `dk{n+1} = ek1`, each derived + /// from the stored encryption keys as the round needs it. See [`ARIAInternal::encrypt_4blocks`]. + pub fn decrypt_4blocks(&self, blocks: &mut [Block; LANES]) { + let s = self.schedule.as_ref(); + let n = P::ROUNDS; + rounds(blocks, n, |i| -> RoundKey { + if i == 1 { + ek(s, n + 1) + } else if i == n + 1 { + ek(s, 1) + } else { + let mut k = ek(s, n + 2 - i); + diffuse(&mut k); + k + } + }); + } + + /// Encrypts one block in place. + /// + /// The circuits always process four lanes, so a single-block call puts the block in every + /// lane and discards three results: it does four blocks' worth of work. Use + /// [`ARIAInternal::encrypt_4blocks`] where independent blocks are + /// available. + /// + /// Filling the spare lanes with copies costs exactly what zeros would, and buys a free + /// self-check: all four lanes must agree, which `debug_assert` verifies. It is not a security + /// property; the spare lanes are never returned either way. + pub(crate) fn encrypt_block(&self, block: &mut Block) { + let mut lanes = [*block; LANES]; + self.encrypt_4blocks(&mut lanes); + debug_assert!(lanes.iter().all(|b| *b == lanes[0]), "all lanes must agree"); + *block = lanes[0]; + } + + /// Decrypts one block in place. See [`ElectronicCodeBook::encrypt_block`] for the four-lane caveat. + pub(crate) fn decrypt_block(&self, block: &mut Block) { + let mut lanes = [*block; LANES]; + self.decrypt_4blocks(&mut lanes); + debug_assert!(lanes.iter().all(|b| *b == lanes[0]), "all lanes must agree"); + *block = lanes[0]; + } + + /// Encrypts two blocks in place, in lanes 0 and 1; the other two lanes carry copies of the + /// first and are discarded. Two blocks for the price of four, but twice as good as two + /// [`ElectronicCodeBook::encrypt_block`] calls, which is why the trait method uses it. + pub(crate) fn encrypt_2blocks(&self, blocks: &mut [Block; 2]) { + let mut lanes = [blocks[0]; LANES]; + lanes[1] = blocks[1]; + self.encrypt_4blocks(&mut lanes); + *blocks = [lanes[0], lanes[1]]; + } + + /// Decrypts two blocks in place. See [`ElectronicCodeBook::encrypt_2blocks`]. + pub(crate) fn decrypt_2blocks(&self, blocks: &mut [Block; 2]) { + let mut lanes = [blocks[0]; LANES]; + lanes[1] = blocks[1]; + self.decrypt_4blocks(&mut lanes); + *blocks = [lanes[0], lanes[1]]; + } +} + +impl ARIA128Internal { + /// Expands a 16-byte key into the ARIA-128 round keys (Sec 2.2). + /// + /// # Errors + /// * [`KeyMaterialError::InvalidKeyType`] if the key is not [`KeyType::SymmetricCipherKey`]. + /// * [`KeyMaterialError::InvalidLength`] if the key is not 16 bytes long. + /// * [`KeyMaterialError::SecurityStrength`] if the key carries a strength below 128 bits. + pub(crate) fn new(key: &KeyMaterial<16>) -> Result { + Self::validate(key)?; + Ok(Self { schedule: expand::(key.ref_to_bytes()) }) + } +} + +impl ARIA192Internal { + /// Expands a 24-byte key into the ARIA-192 round keys. See [`ElectronicCodeBook::new`] for the error + /// cases. + pub(crate) fn new(key: &KeyMaterial<24>) -> Result { + Self::validate(key)?; + Ok(Self { schedule: expand::(key.ref_to_bytes()) }) + } +} + +impl ARIA256Internal { + /// Expands a 32-byte key into the ARIA-256 round keys. See [`ElectronicCodeBook::new`] for the error + /// cases. + pub(crate) fn new(key: &KeyMaterial<32>) -> Result { + Self::validate(key)?; + Ok(Self { schedule: expand::(key.ref_to_bytes()) }) + } +} + +impl Algorithm for ARIA128Internal { + const ALG_NAME: &'static str = ARIA128Params::ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; +} + +impl Algorithm for ARIA192Internal { + const ALG_NAME: &'static str = ARIA192Params::ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_192bit; +} + +impl Algorithm for ARIA256Internal { + const ALG_NAME: &'static str = ARIA256Params::ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_256bit; +} + +// The three `ElectronicCodeBook` / `BlockPermutation` impl pairs are one-line delegations to the +// inherent methods above, +// written out longhand rather than generated because `cargo mutants` cannot see into macro bodies. +// +// Each fills the lanes in its pair and four-block methods: two blocks in two of the four lanes +// cost one set of circuit passes per round where two single-block calls would cost two, and four +// blocks are one full pass where two pair calls would be two. + +impl ElectronicCodeBook<16, BLOCK_LEN> for ARIA128Internal { + fn new(key: &KeyMaterial<16>) -> Result { + ARIA128Internal::new(key) + } +} + +impl BlockPermutation for ARIA128Internal { + fn encrypt_block(&self, block: &mut Block) { + ARIAInternal::encrypt_block(self, block) + } + fn decrypt_block(&self, block: &mut Block) { + ARIAInternal::decrypt_block(self, block) + } + fn encrypt_2blocks(&self, blocks: &mut [Block; 2]) { + ARIAInternal::encrypt_2blocks(self, blocks) + } + fn decrypt_2blocks(&self, blocks: &mut [Block; 2]) { + ARIAInternal::decrypt_2blocks(self, blocks) + } + fn encrypt_4blocks(&self, blocks: &mut [Block; LANES]) { + ARIAInternal::encrypt_4blocks(self, blocks) + } + fn decrypt_4blocks(&self, blocks: &mut [Block; LANES]) { + ARIAInternal::decrypt_4blocks(self, blocks) + } +} + +impl ElectronicCodeBook<24, BLOCK_LEN> for ARIA192Internal { + fn new(key: &KeyMaterial<24>) -> Result { + ARIA192Internal::new(key) + } +} + +impl BlockPermutation for ARIA192Internal { + fn encrypt_block(&self, block: &mut Block) { + ARIAInternal::encrypt_block(self, block) + } + fn decrypt_block(&self, block: &mut Block) { + ARIAInternal::decrypt_block(self, block) + } + fn encrypt_2blocks(&self, blocks: &mut [Block; 2]) { + ARIAInternal::encrypt_2blocks(self, blocks) + } + fn decrypt_2blocks(&self, blocks: &mut [Block; 2]) { + ARIAInternal::decrypt_2blocks(self, blocks) + } + fn encrypt_4blocks(&self, blocks: &mut [Block; LANES]) { + ARIAInternal::encrypt_4blocks(self, blocks) + } + fn decrypt_4blocks(&self, blocks: &mut [Block; LANES]) { + ARIAInternal::decrypt_4blocks(self, blocks) + } +} + +impl ElectronicCodeBook<32, BLOCK_LEN> for ARIA256Internal { + fn new(key: &KeyMaterial<32>) -> Result { + ARIA256Internal::new(key) + } +} + +impl BlockPermutation for ARIA256Internal { + fn encrypt_block(&self, block: &mut Block) { + ARIAInternal::encrypt_block(self, block) + } + fn decrypt_block(&self, block: &mut Block) { + ARIAInternal::decrypt_block(self, block) + } + fn encrypt_2blocks(&self, blocks: &mut [Block; 2]) { + ARIAInternal::encrypt_2blocks(self, blocks) + } + fn decrypt_2blocks(&self, blocks: &mut [Block; 2]) { + ARIAInternal::decrypt_2blocks(self, blocks) + } + fn encrypt_4blocks(&self, blocks: &mut [Block; LANES]) { + ARIAInternal::encrypt_4blocks(self, blocks) + } + fn decrypt_4blocks(&self, blocks: &mut [Block; LANES]) { + ARIAInternal::decrypt_4blocks(self, blocks) + } +} + +// RFC 5794 Appendix B assigns object identifiers to ARIA in every one of these modes, so each is +// named, although the keys hand out workers for the four aliased modes (CBC, CFB, CFB8, CTR) only. +impl ModeNames for ARIA128Internal { + const CBC_ALG_NAME: &'static str = "ARIA_CBC_128"; + const CCM_ALG_NAME: &'static str = "ARIA_CCM_128"; + const CFB_ALG_NAME: &'static str = "ARIA_CFB_128"; + const CFB8_ALG_NAME: &'static str = "ARIA_CFB8_128"; + const CMAC_ALG_NAME: &'static str = "ARIA_CMAC_128"; + const CTR_ALG_NAME: &'static str = "ARIA_CTR_128"; + const ECB_ALG_NAME: &'static str = "ARIA_ECB_128"; + const GCM_ALG_NAME: &'static str = "ARIA_GCM_128"; + const GMAC_ALG_NAME: &'static str = "ARIA_GMAC_128"; +} + +impl ModeNames for ARIA192Internal { + const CBC_ALG_NAME: &'static str = "ARIA_CBC_192"; + const CCM_ALG_NAME: &'static str = "ARIA_CCM_192"; + const CFB_ALG_NAME: &'static str = "ARIA_CFB_192"; + const CFB8_ALG_NAME: &'static str = "ARIA_CFB8_192"; + const CMAC_ALG_NAME: &'static str = "ARIA_CMAC_192"; + const CTR_ALG_NAME: &'static str = "ARIA_CTR_192"; + const ECB_ALG_NAME: &'static str = "ARIA_ECB_192"; + const GCM_ALG_NAME: &'static str = "ARIA_GCM_192"; + const GMAC_ALG_NAME: &'static str = "ARIA_GMAC_192"; +} + +impl ModeNames for ARIA256Internal { + const CBC_ALG_NAME: &'static str = "ARIA_CBC_256"; + const CCM_ALG_NAME: &'static str = "ARIA_CCM_256"; + const CFB_ALG_NAME: &'static str = "ARIA_CFB_256"; + const CFB8_ALG_NAME: &'static str = "ARIA_CFB8_256"; + const CMAC_ALG_NAME: &'static str = "ARIA_CMAC_256"; + const CTR_ALG_NAME: &'static str = "ARIA_CTR_256"; + const ECB_ALG_NAME: &'static str = "ARIA_ECB_256"; + const GCM_ALG_NAME: &'static str = "ARIA_GCM_256"; + const GMAC_ALG_NAME: &'static str = "ARIA_GMAC_256"; +} + +impl core::fmt::Debug for ARIAInternal

{ + /// Prints the algorithm name only. The round keys are secret and are never formatted. + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str(P::ALG_NAME) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_engine_sizes_are_exactly_the_round_keys() { + // The "Memory Usage" table in the crate docs quotes these: 13, 15 or 17 round keys of 16 + // bytes and nothing else -- no direction flag, no round counter, no initialised marker. + assert_eq!(size_of::(), 208); + assert_eq!(size_of::(), 240); + assert_eq!(size_of::(), 272); + } + + #[test] + fn test_alg_names_and_strengths() { + assert_eq!(::ALG_NAME, "ARIA-128"); + assert_eq!(::ALG_NAME, "ARIA-192"); + assert_eq!(::ALG_NAME, "ARIA-256"); + assert_eq!( + ::MAX_SECURITY_STRENGTH, + SecurityStrength::from_bytes(ARIA128Params::KEY_LEN) + ); + assert_eq!( + ::MAX_SECURITY_STRENGTH, + SecurityStrength::from_bytes(ARIA192Params::KEY_LEN) + ); + assert_eq!( + ::MAX_SECURITY_STRENGTH, + SecurityStrength::from_bytes(ARIA256Params::KEY_LEN) + ); + } + + #[test] + fn test_lanes_constant() { + // The docs and the two-block override both assume four lanes: four 4-byte class words + // fill the 16 byte positions of the u16 planes. + assert_eq!(LANES, 4); + assert_eq!(LANES * 4, 16); + } + + #[test] + fn test_decryption_key_order_is_section_2_2() { + // dk1 = ek{n+1}, dk{i} = A(ek{n+2-i}) for i in 2..=n, dk{n+1} = ek1: check the index map + // the closure in decrypt_4blocks implements, for n = 12. + let n = 12; + let src = |i: usize| { + if i == 1 { + n + 1 + } else if i == n + 1 { + 1 + } else { + n + 2 - i + } + }; + assert_eq!(src(1), 13); + assert_eq!(src(2), 12); + assert_eq!(src(12), 2); + assert_eq!(src(13), 1); + } + + /// Appendix A.1, 128-bit key: the intermediate round values `P1 .. P11` and the ciphertext, + /// through a literal one-round-at-a-time transcription of Sec 2.3.1.1, which the four-lane + /// `rounds` must agree with. + #[test] + fn test_appendix_a_1_intermediate_values() { + use crate::round::{fe1, fo1}; + let key_bytes: [u8; 16] = core::array::from_fn(|i| i as u8); + let key = + KeyMaterial::<16>::from_bytes_as_type(&key_bytes, KeyType::SymmetricCipherKey).unwrap(); + let aria = ARIA128Internal::new(&key).unwrap(); + let s = aria.schedule.as_ref(); + let to_state = |v: u128| -> RoundKey { + let b = v.to_be_bytes(); + let (w, _) = b.as_chunks::<4>(); + core::array::from_fn(|i| u32::from_be_bytes(w[i])) + }; + let expected: [u128; 11] = [ + 0x7fc7f12befd0a0791de87fa96b469f52, + 0xac8de17e49f7c5117618993162b189e9, + 0xc3e8d59ec2e62d5249ca2741653cb7dd, + 0x5d4aebb165e141ff759f669e1e85cc45, + 0x7806e469f68874c5004b5f4a046bbcfa, + 0x110f93c9a630cdd51f97d2202413345a, + 0xe054428ef088fef97928241cd3be499e, + 0x5734f38ea1ca3ddd102e71f95e1d5f97, + 0x4903325be3e500cccd52fba4354a39ae, + 0xcb8c508e2c4f87880639dc896d25ec9d, + 0xe7e0d2457ed73d23d481424095afdca0, + ]; + let mut p = to_state(0x00112233445566778899aabbccddeeff); + for (i, want) in expected.iter().enumerate() { + let round = i + 1; + p = if round % 2 == 1 { fo1(p, &ek(s, round)) } else { fe1(p, &ek(s, round)) }; + assert_eq!(p, to_state(*want), "P{round}"); + } + // Round 12: C = SL2(P11 ^ ek12) ^ ek13, via the engine on the original plaintext. + let mut block = 0x00112233445566778899aabbccddeeffu128.to_be_bytes(); + aria.encrypt_block(&mut block); + assert_eq!(block, 0xd718fbd6ab644c739da95f3be6451778u128.to_be_bytes()); + } +} diff --git a/crypto/aria/src/hazmat/mod.rs b/crypto/aria/src/hazmat/mod.rs new file mode 100644 index 000000000..4593a1991 --- /dev/null +++ b/crypto/aria/src/hazmat/mod.rs @@ -0,0 +1,10 @@ +//! The raw ARIA permutation, [`ARIAInternal`] and its three key-length aliases; see +//! [`bouncycastle_core::hazmat`] for what the path means and the supported uses. +//! +//! [`ARIA128Internal`] and its siblings transform exactly one block and are the primitive under the modes +//! in this crate, not a cipher for data; use [`ARIA_CBC_128`](crate::ARIA_CBC_128) and the other +//! mode aliases at the crate root. + +pub(crate) mod aria; + +pub use aria::{ARIA128Internal, ARIA192Internal, ARIA256Internal, ARIAInternal}; diff --git a/crypto/aria/src/key.rs b/crypto/aria/src/key.rs new file mode 100644 index 000000000..4566632fe --- /dev/null +++ b/crypto/aria/src/key.rs @@ -0,0 +1,251 @@ +//! ARIA key objects: a key bound to one ARIA variant and one mode, validated and expanded once, +//! from which the mode's encryptors and decryptors are made. +//! +//! [`ARIA128Key`], [`ARIA192Key`] and [`ARIA256Key`] are one type each per mode `M`, as +//! `AES128Key` and friends are in `bouncycastle-aes`: the key holds the mode's per-key state, +//! the workers it hands out borrow it, and which role pair a key carries is decided by its mode: +//! the symmetric pair for the stream modes and the padded block modes (`CFB`, `CFB8`, `CTR`, +//! `CBC`), the block pair for `CBC`. Those are the four modes this crate aliases +//! as `ARIA_*`; a key under any other mode has no role here. +//! +//! # Usage Examples +//! +//! ``` +//! use bouncycastle_aria::ARIA256Key; +//! use bouncycastle_cipher::modes::CBC; +//! use bouncycastle_cipher::padding::PKCS7; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey}; +//! use bouncycastle_rng::HashDRBG_SHA512; +//! +//! let mut rng = HashDRBG_SHA512::new_from_os(); // one RNG, held across messages +//! let material = KeyMaterial::<32>::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey) +//! .expect("a 32-byte symmetric cipher key"); +//! let key = ARIA256Key::>::from_key_material(&material).expect("a valid ARIA-256 key"); +//! +//! let (iv, ciphertext) = key.encrypt(&mut rng, b"a message of no particular length").unwrap(); +//! assert_eq!(key.decrypt(&iv, &ciphertext).unwrap(), b"a message of no particular length"); +//! ``` +//! +//! # Memory Usage +//! +//! A key is its mode's per-key state: the expanded schedule, 208, 240 or 272 bytes for ARIA-128, +//! -192 and -256; the four aliased modes keep nothing else per key. The cost is paid per key, not +//! per message. + +use crate::hazmat::{ARIA128Internal, ARIA192Internal, ARIA256Internal, ARIAInternal}; +use crate::schedule::{ARIA128Params, ARIA192Params, ARIA256Params, ARIAParams}; +use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; +use bouncycastle_core::hazmat::{ + BlockDecryptMode, BlockEncryptMode, Mode, ModeKey, SymmetricDecryptMode, SymmetricEncryptMode, + do_hazardous_operations, +}; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::symmetric_key::{ + BlockDecryptionKey, BlockEncryptionKey, FromKeyMaterial, SymmetricDecryptionKey, + SymmetricEncryptionKey, +}; +use bouncycastle_core::traits::RNG; + +/// An ARIA key for the mode `M`, parameterised by key length. Use the aliases [`ARIA128Key`], +/// [`ARIA192Key`] and [`ARIA256Key`] rather than naming this directly. +/// +/// The only state is the mode's per-key state, which holds the expanded schedule in a `Secret` +/// and so is zeroized on drop; the key has no `Debug` and no way to read the material back. +pub struct ARIAKey>> { + state: M::KeyState, +} + +/// An ARIA-128 key for the mode `M`: 16 bytes of material, expanded to a 12-round schedule. +#[allow(non_camel_case_types)] +pub type ARIA128Key = ARIAKey; +/// An ARIA-192 key for the mode `M`: 24 bytes of material, expanded to a 14-round schedule. +#[allow(non_camel_case_types)] +pub type ARIA192Key = ARIAKey; +/// An ARIA-256 key for the mode `M`: 32 bytes of material, expanded to a 16-round schedule. +#[allow(non_camel_case_types)] +pub type ARIA256Key = ARIAKey; + +// Written out rather than derived so that `M`, a marker type, need not be `Clone`. +impl>> Clone for ARIAKey { + fn clone(&self) -> Self { + Self { state: self.state.clone() } + } +} + +// Likewise for equality: two keys are equal when their mode states are, which every state over +// the engine decides in constant time, so the comparison follows the material without exposing it. +impl>> PartialEq for ARIAKey +where + M::KeyState: PartialEq, +{ + fn eq(&self, other: &Self) -> bool { + self.state == other.state + } +} + +impl>> Eq for ARIAKey where M::KeyState: Eq {} + +/// The key-type check the mode asks for (`Mode::KEY_TYPE`), ahead of the permutation's own +/// validation. +fn check_key_type( + key: &dyn KeyMaterialTrait, + expected: KeyType, +) -> Result<(), SymmetricCipherError> { + if key.key_type() != expected { + return Err(KeyMaterialError::InvalidKeyType( + "The key is not of the KeyType this mode requires.", + ) + .into()); + } + Ok(()) +} + +/// The permutation accepts only a `SymmetricCipherKey`, so a MAC mode's key -- already checked to +/// be the `MACKey` the mode asks for -- is handed to it as a copy retagged at the same strength, +/// used for nothing else. The strength check in the permutation's constructor still applies. +fn as_cipher_key( + key: &KeyMaterial, +) -> Result, SymmetricCipherError> { + let mut cipher_key = + KeyMaterial::::from_bytes_as_type(key.ref_to_bytes(), KeyType::SymmetricCipherKey)?; + do_hazardous_operations(&mut cipher_key, |k| { + k.set_key_type(KeyType::SymmetricCipherKey)?; + k.set_security_strength(key.security_strength()) + })?; + Ok(cipher_key) +} + +// The constructors are implemented per key length rather than once over `P`, because stable Rust +// cannot name `KeyMaterial<{ P::KEY_LEN }>` in a generic signature; the fixed-capacity argument +// is what makes a wrong-length key a compile error rather than a runtime one. + +impl> FromKeyMaterial<16> for ARIA128Key { + const KEY_TYPE: KeyType = M::KEY_TYPE; + + /// Validates and expands a 16-byte key. + /// + /// # Errors + /// * [`KeyMaterialError::InvalidKeyType`] if the key is not of [`Self::KEY_TYPE`]: the + /// `SymmetricCipherKey` a cipher mode asks for, or the `MACKey` a MAC mode does. + /// * [`KeyMaterialError::InvalidLength`] if the key is not 16 bytes long. + /// * [`KeyMaterialError::SecurityStrength`] if the key carries a strength below 128 bits. + fn from_key_material(key: &KeyMaterial<16>) -> Result { + check_key_type(key, M::KEY_TYPE)?; + let perm = match M::KEY_TYPE { + KeyType::SymmetricCipherKey => ARIA128Internal::new(key)?, + _ => ARIA128Internal::new(&as_cipher_key(key)?)?, + }; + Ok(Self { state: M::key_state(perm) }) + } +} + +impl> FromKeyMaterial<24> for ARIA192Key { + const KEY_TYPE: KeyType = M::KEY_TYPE; + + /// Validates and expands a 24-byte key; the errors are those of the 16-byte form, at 24 + /// bytes and 192 bits. + fn from_key_material(key: &KeyMaterial<24>) -> Result { + check_key_type(key, M::KEY_TYPE)?; + let perm = match M::KEY_TYPE { + KeyType::SymmetricCipherKey => ARIA192Internal::new(key)?, + _ => ARIA192Internal::new(&as_cipher_key(key)?)?, + }; + Ok(Self { state: M::key_state(perm) }) + } +} + +impl> FromKeyMaterial<32> for ARIA256Key { + const KEY_TYPE: KeyType = M::KEY_TYPE; + + /// Validates and expands a 32-byte key; the errors are those of the 16-byte form, at 32 + /// bytes and 256 bits. + fn from_key_material(key: &KeyMaterial<32>) -> Result { + check_key_type(key, M::KEY_TYPE)?; + let perm = match M::KEY_TYPE { + KeyType::SymmetricCipherKey => ARIA256Internal::new(key)?, + _ => ARIA256Internal::new(&as_cipher_key(key)?)?, + }; + Ok(Self { state: M::key_state(perm) }) + } +} + +impl>> ModeKey, M> for ARIAKey { + fn state(&self) -> &M::KeyState { + &self.state + } +} + +// The role impls forward to the mode traits `M` implements over this permutation: the symmetric +// pair and the block pair, which between them cover the four modes this crate aliases. A key +// under any other mode has no role here. + +impl + SymmetricEncryptionKey for ARIAKey +where + P: ARIAParams, + M: SymmetricEncryptMode, KEY_LEN, INIT_DATA_LEN, FINAL_LEN>, +{ + type Encryptor<'k> + = M::Encryptor<'k> + where + Self: 'k; + + fn create_encryptor<'k>( + &'k self, + rng: &mut dyn RNG, + ) -> Result<(Self::Encryptor<'k>, [u8; INIT_DATA_LEN]), SymmetricCipherError> { + M::encrypt_init(&self.state, rng) + } +} + +impl + SymmetricDecryptionKey for ARIAKey +where + P: ARIAParams, + M: SymmetricDecryptMode, KEY_LEN, INIT_DATA_LEN, FINAL_LEN>, +{ + type Decryptor<'k> + = M::Decryptor<'k> + where + Self: 'k; + + fn create_decryptor<'k>(&'k self, init_data: &[u8; INIT_DATA_LEN]) -> Self::Decryptor<'k> { + M::decrypt_init(&self.state, init_data) + } +} + +impl + BlockEncryptionKey for ARIAKey +where + P: ARIAParams, + M: BlockEncryptMode, KEY_LEN, INIT_DATA_LEN, BLOCK_LEN>, +{ + type Encryptor<'k> + = M::Encryptor<'k> + where + Self: 'k; + + fn create_encryptor<'k>( + &'k self, + rng: &mut dyn RNG, + ) -> Result<(Self::Encryptor<'k>, [u8; INIT_DATA_LEN]), SymmetricCipherError> { + M::encrypt_init(&self.state, rng) + } +} + +impl + BlockDecryptionKey for ARIAKey +where + P: ARIAParams, + M: BlockDecryptMode, KEY_LEN, INIT_DATA_LEN, BLOCK_LEN>, +{ + type Decryptor<'k> + = M::Decryptor<'k> + where + Self: 'k; + + fn create_decryptor<'k>(&'k self, init_data: &[u8; INIT_DATA_LEN]) -> Self::Decryptor<'k> { + M::decrypt_init(&self.state, init_data) + } +} diff --git a/crypto/aria/src/lib.rs b/crypto/aria/src/lib.rs new file mode 100644 index 000000000..c30999185 --- /dev/null +++ b/crypto/aria/src/lib.rs @@ -0,0 +1,312 @@ +//! A constant-time, table-free, low-memory ARIA block cipher engine (RFC 5794), ported from +//! Bouncy Castle Java's `ARIAEngine`. +//! +//! This crate provides ARIA -- a 128-bit block cipher with 128-, 192- and 256-bit keys, the Korean +//! standard block cipher (KS X 1213:2004) described in RFC 5794, which is the specification every +//! comment in this crate cites -- under the library's key objects: [`ARIA128Key`], +//! [`ARIA192Key`] and [`ARIA256Key`], a key bound to one mode `M`, validated and expanded +//! once, handing out that mode's encryptors and decryptors. The engines underneath, +//! [`ARIA128Internal`] and friends, evaluate the four S-boxes as Boolean circuits over bit-planes +//! rather than looking them up in tables, so they are constant-time and carry no tables at all; +//! the planes are 16 bits wide so that the working set stays small, which is what "lowmemory" +//! means here. See [Design](#design). +//! +//! An engine is a *permutation*, not a cipher you can encrypt data with, which is why it lives +//! under [`hazmat`]. See [Security Considerations](#security-considerations). +//! +//! # Usage Examples +//! +//! ## Modes of operation +//! +//! A key is built once, under a mode from `bouncycastle_cipher::modes`, and used for as many +//! messages as needed: `CBC` for CBC (NIST SP 800-38A Sec 6.2) with PKCS#7 padding, `CFB` +//! for CFB128 (Sec 6.3), `CFB8` for the `s = 8` segment size, a different and non-interoperable +//! mode costing one ARIA call per byte, and `CTR` for CTR (Sec 6.5) with a 12-byte nonce and a +//! 4-byte counter. The aliases [`ARIA_CBC_128`], [`ARIA_CFB_128`], [`ARIA_CFB8_128`], +//! [`ARIA_CTR_128`] and their `_192` / `_256` siblings name the workers the keys hand out. +//! +//! CBC is a block cipher, so it is defined only on whole blocks and the key's mode carries a +//! padding scheme to bridge the difference; the CFB modes and CTR are stream ciphers and take any +//! length with no padding at all. See the `bouncycastle_cipher::modes` crate docs for the +//! comparison. +//! +//! ``` +//! use bouncycastle_aria::ARIA256Key; +//! use bouncycastle_cipher::modes::CBC; +//! use bouncycastle_cipher::padding::PKCS7; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey}; +//! use bouncycastle_rng::HashDRBG_SHA512; +//! +//! let mut rng = HashDRBG_SHA512::new_from_os(); // one RNG, held across messages +//! let material = KeyMaterial::<32>::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey) +//! .expect("a 32-byte symmetric cipher key"); +//! let key = ARIA256Key::>::from_key_material(&material).expect("a valid ARIA-256 key"); +//! +//! // Any length: PKCS#7 pads it out to whole blocks, so 50 bytes is as good as 48. +//! let plaintext = [0x5Au8; 50]; +//! +//! // The IV is drawn from `rng` and returned; there is no API for supplying one. +//! let (iv, ciphertext) = key.encrypt(&mut rng, &plaintext).expect("encryption"); +//! assert_eq!(ciphertext.len(), 64, "50 bytes padded out to four blocks"); +//! +//! let recovered = key.decrypt(&iv, &ciphertext).expect("decryption"); +//! assert_eq!(recovered, plaintext); +//! ``` +//! +//! The stream modes take any length and return the initialisation data the same way: +//! +//! ``` +//! use bouncycastle_aria::ARIA256Key; +//! use bouncycastle_cipher::modes::CTR; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey}; +//! use bouncycastle_rng::HashDRBG_SHA512; +//! +//! let mut rng = HashDRBG_SHA512::new_from_os(); +//! let material = KeyMaterial::<32>::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey) +//! .expect("a 32-byte symmetric cipher key"); +//! let key = ARIA256Key::::from_key_material(&material).expect("a valid ARIA-256 key"); +//! +//! // 50 bytes, and the ciphertext is 50 bytes: no padding anywhere. +//! let plaintext = [0x5Au8; 50]; +//! let mut data = plaintext; +//! let (_, nonce) = key.encrypt_inplace(&mut rng, &mut data).expect("encryption"); +//! assert_ne!(data, plaintext); +//! +//! key.decrypt_inplace(&nonce, &mut data).expect("decryption"); +//! assert_eq!(data, plaintext); +//! ``` +//! +//! For the block-aligned API -- whole blocks in place, through the block role pair -- make the +//! key under `CBC`; the padded aliases wrap the same worker. +//! +//! ## The raw permutation +//! +//! The engine transforms one block, and `ElectronicCodeBook::new` on it is the one-shot: +//! +//! ``` +//! use bouncycastle_aria::hazmat::ARIA128Internal; +//! use bouncycastle_core::hazmat::{BlockPermutation, ElectronicCodeBook}; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! +//! // RFC 5794 Appendix A.1, "128-Bit Key". +//! let key = KeyMaterial::<16>::from_bytes_as_type( +//! &[0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, +//! 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f], +//! KeyType::SymmetricCipherKey, +//! ).expect("a 16-byte symmetric cipher key"); +//! +//! let aria = ARIA128Internal::new(&key).expect("a valid ARIA-128 key"); +//! +//! let mut block = [0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, +//! 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]; +//! aria.encrypt_block(&mut block); +//! assert_eq!(block, [0xd7, 0x18, 0xfb, 0xd6, 0xab, 0x64, 0x4c, 0x73, +//! 0x9d, 0xa9, 0x5f, 0x3b, 0xe6, 0x45, 0x17, 0x78]); +//! +//! // The same value decrypts, from the same round keys -- there is no separate decryptor. +//! aria.decrypt_block(&mut block); +//! assert_eq!(block, [0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, +//! 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]); +//! ``` +//! +//! The 192- and 256-bit variants are [`ARIA192Internal`] and [`ARIA256Internal`], constructed +//! from a `KeyMaterial<24>` and a `KeyMaterial<32>`; a key of the wrong length does not compile. +//! +//! ## Four blocks at a time +//! +//! Each S-box circuit substitutes 16 bytes per pass and a substitution layer sends four bytes of +//! every block through each of the four S-boxes, so four independent blocks cost the same as one. +//! Where a caller has four, [`ARIAInternal::encrypt_4blocks`] is four times the throughput of four +//! [`BlockPermutation::encrypt_block`](bouncycastle_core::hazmat::BlockPermutation::encrypt_block) calls, and it is also the +//! four-block batch [`BlockPermutation::encrypt_4blocks`](bouncycastle_core::hazmat::BlockPermutation::encrypt_4blocks) offers to modes: +//! +//! ``` +//! use bouncycastle_aria::hazmat::ARIA256Internal; +//! use bouncycastle_aria::LANES; +//! use bouncycastle_core::hazmat::{BlockPermutation, ElectronicCodeBook}; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! +//! let key = KeyMaterial::<32>::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey) +//! .expect("a 32-byte symmetric cipher key"); +//! let aria = ARIA256Internal::new(&key).expect("a valid ARIA-256 key"); +//! +//! let mut blocks: [[u8; 16]; LANES] = core::array::from_fn(|i| [i as u8; 16]); +//! let original = blocks; +//! aria.encrypt_4blocks(&mut blocks); +//! aria.decrypt_4blocks(&mut blocks); +//! assert_eq!(blocks, original); +//! ``` +//! +//! # Design +//! +//! ARIA is a substitution-permutation network of 12, 14 or 16 rounds (Sec 2.3). A round XORs a +//! 128-bit round key into the state, puts each of the 16 bytes through one of four 8-bit S-boxes +//! -- `SB1`, `SB2` and their inverses `SB3`, `SB4`, assigned by byte position and alternating +//! between two patterns in odd and even rounds (Sec 2.4.2) -- and mixes the bytes with the +//! diffusion layer `A`, a 16x16 binary matrix in which every output byte is the XOR of seven input +//! bytes (Sec 2.4.3). The last round substitutes without diffusing and adds an extra round key. The +//! key schedule (Sec 2.2) runs three rounds of the same functions over the key with fixed +//! constants derived from `1/pi`, then takes every round key as an XOR of two rotated results. +//! Decryption is the same network with the round keys reversed and passed through `A`. +//! +//! ## Why not a lookup table +//! +//! Sec 2.4.2 presents the S-boxes as tables, and a straightforward implementation stores the four +//! 256-byte tables. A table indexed by a byte of the state is indexed by secret data, so on any +//! CPU with a data cache the memory access pattern, and hence the timing, depends on the key and +//! the data. +//! That is the classic cache-timing attack on table-driven block ciphers, and it is not fixable +//! while the lookup remains -- in the cipher or in the key schedule, whose three `FO`/`FE` calls go +//! through the S-boxes too. +//! +//! ## The S-boxes as circuits +//! +//! This crate has no table outside its tests. `SB1` is the AES S-box, and every one of the four +//! S-boxes is inversion in GF(2^8) between two affine maps -- for `SB2` a fact **found by +//! exhaustive search** against the Sec 2.4.2 table, not taken from recall: of the 256 possible +//! input constants exactly one admits such a decomposition, and it is unique up to the inverse's +//! own symmetries. So the 62-gate non-linear section of the Boyar-Peralta AES circuit does the +//! inversion in all four, with generated affine top and bottom layers pinned by exhaustive +//! 256-input tests; `SB1` uses the Boyar-Peralta program verbatim. Each S-box is a circuit of +//! roughly 120 gates; the `sbox` module docs give the exact counts and the derivation. +//! +//! A substitution layer sends four bytes of each block through each S-box -- the four bytes of one +//! *class word*, the column of the 4x4 byte matrix -- and each circuit's eight `u16` planes hold 16 +//! byte positions, so the engine works on **four blocks at once**: each round transposes every +//! block's rows into class words, runs each of the four circuits once on that class word from all +//! four blocks, transposes back, and diffuses per block. The blocks never mix. The diffusion layer +//! is computed in the word-level form the 32-bit implementations use (a per-word byte parity, six +//! word XORs, three fixed byte permutations, six word XORs again), a decomposition verified against +//! the sixteen equations of Sec 2.4.3 rather than recalled; see the `round` module. +//! +//! Beyond the S-boxes, this crate stores one schedule that serves both directions (a +//! direction-aware engine typically lays the keys out differently per direction at +//! initialisation; here the decryption keys of Sec 2.2 are derived from the stored encryption +//! keys as each round needs them); the block methods are infallible (the run-time buffer and +//! initialisation checks are compile-time facts here); and the constructors require a key tagged +//! as a symmetric cipher key of at least the strength its length implies, as every cipher in this +//! workspace does. +//! +//! ## Why "lowmemory" +//! +//! Two things. First, no tables: a straightforward implementation carries four 256-byte S-boxes +//! indexed by secret data (and OpenSSL's `aria.c` 4 KiB of combined tables); here the S-boxes are +//! code. +//! Second, the working set. Eight `u32` planes would take eight blocks per pass and double the +//! throughput, but every per-call buffer -- the block state, the planes -- would double with them. +//! Four lanes over `u16` planes keep the whole per-call working state near 100 bytes. +//! +//! # Memory Usage +//! +//! No heap allocation and no lookup tables. The persistent state is the encryption round keys of +//! Sec 2.2, one more than the number of rounds, 16 bytes each, held once in the key; a worker adds +//! the mode's own state and a reference to the key: +//! +//! | Type | Key | Rounds | Round keys (persistent) | Tables | +//! |---|---|---|---|---| +//! | [`ARIA128Internal`] | 16 B | 12 | 13 x 16 = 208 B | 0 B | +//! | [`ARIA192Internal`] | 24 B | 14 | 15 x 16 = 240 B | 0 B | +//! | [`ARIA256Internal`] | 32 B | 16 | 17 x 16 = 272 B | 0 B | +//! +//! Per-call stack usage is the four-block working state -- four words per block, 64 bytes -- plus +//! the four class words being substituted (16 bytes), the eight `u16` planes (16 bytes), one round +//! key (16 bytes) and the circuits' temporaries, most of which the compiler keeps in registers. +//! Measure with `cargo run --release -p mem_usage_benches --bin bench_aria_mem_usage`. +//! +//! For comparison, a straightforward implementation carries 1 KiB of S-box tables on top of a +//! round-key array of the same size as here (plus 256 bytes of diffusion masks). +//! +//! # Security Considerations +//! +//! ## A block permutation is not a cipher +//! +//! [`ARIA128Internal`] and its siblings transform exactly 16 bytes. Using one directly on data means ECB, +//! which is not confidential: identical plaintext blocks produce identical ciphertext blocks, so +//! structure in the plaintext survives encryption. **Do not do it.** Use a mode of operation, and +//! prefer an authenticated one so that ciphertext tampering is detected. RFC 5794 Appendix B +//! assigns object identifiers to ARIA in ECB, CBC, CFB, OFB, CTR, CMAC, OCB2, GCM, CCM and key-wrap +//! modes; the ECB one exists for completeness, not as a recommendation. +//! That is why the permutation lives under [`hazmat`]; [`bouncycastle_core::hazmat`] lists the +//! supported uses. +//! +//! ## Constant-time properties +//! +//! By construction there is no secret-dependent memory access and no secret-dependent branch, in +//! the cipher *or* in the key schedule: the three `FO`/`FE` calls of Sec 2.2 go through the same +//! circuits as the rounds. The only branches are the round loop and the lane loops, which count +//! over public constants, and the odd/even test on the public round counter. +//! +//! Caveats worth stating plainly: +//! +//! * The Rust compiler makes no guarantee it will preserve this. The code is written so that the +//! natural code generation is straight-line, and `#![forbid(unsafe_code)]` rules out the usual +//! ways of forcing the issue, but the property is not contractual. +//! * The four-block working state is not scrubbed after a call. Only the round keys (and, during +//! expansion, `KL`, `KR` and `W0 .. W3`) are wrapped in `Secret`, and so only they are guaranteed +//! to be zeroized on drop. +//! * Constant-time execution says nothing about power or electromagnetic side channels. +//! +//! ## Security strength +//! +//! RFC 5794 Sec 3 reports ARIA "designed to be resistant to all known attacks on block ciphers", +//! with an independent analysis by the COSIC group finding no flaw. Each variant's +//! `MAX_SECURITY_STRENGTH` is its key length: 128, 192 or 256 bits. +//! +//! # Provenance +//! +//! * **Source engine.** The constants, byte-array `A`, `SL1`/`SL2`, `FO`/`FE` and key schedule +//! this crate reproduces, and the S-box tables the circuits are verified against, come from +//! Bouncy Castle Java's `ARIAEngine`. A direct transcription of it, including its fused +//! multiply-broadcast round, lives in the tests (`tests/common/mod.rs`) and this engine is +//! checked against it on thousands of inputs. +//! * **Normative reference: RFC 5794**, "A Description of the ARIA Encryption Algorithm" (Lee, +//! Lee, Kim, Kwon, Kim; March 2010). Every function cites its section. The S-box tables, the +//! diffusion equations and the constants were extracted mechanically from the text of the RFC. +//! * **The non-linear section of the S-box circuits** is from the 113-gate straight-line program +//! `SLP_AES_113.txt` in Peralta's circuit collection, described in J. Boyar and R. Peralta, "A +//! new combinational logic minimization technique with applications to cryptology", +//! , as transcribed in `bouncycastle-aes`. +//! * **The bit-plane transpose** is translated from BearSSL's `aes_ct` by Thomas Pornin (MIT +//! licence), as in the AES, SM4 and Camellia crates. +//! * Verified against RFC 5794 Appendix A -- the three ciphertexts, and for the 128-bit key every +//! round key `ek1 .. ek13` and every intermediate value `P1 .. P11`; the ECB, CBC, CFB128, CFB8 +//! and CTR vectors of OpenSSL's `evpciph_aria.txt`, which are KISA's published ARIA test vectors +//! (10 blocks under each key length); the vectors and invariants of `tests/bc_java_tests.rs`; and the +//! transcription of the source engine on thousands of keys and blocks in every lane, both +//! directions. + +#![no_std] +#![forbid(unsafe_code)] +#![forbid(missing_docs)] + +mod bitslice; +mod cbc; +mod cfb; +mod cfb8; +mod ctr; +pub mod hazmat; +mod key; +mod round; +mod sbox; +mod schedule; + +/// The ARIA block length in bytes: 16 (RFC 5794 Sec 1.1, "encrypts 128-bit blocks"). +pub const BLOCK_LEN: usize = 16; + +/// The number of blocks the bit-sliced S-box circuits substitute at once. See +/// [`ARIAInternal::encrypt_4blocks`]. +pub const LANES: usize = 4; + +pub use cbc::{ARIA_CBC_128, ARIA_CBC_192, ARIA_CBC_256}; +pub use cfb::{ARIA_CFB_128, ARIA_CFB_192, ARIA_CFB_256}; +pub use cfb8::{ARIA_CFB8_128, ARIA_CFB8_192, ARIA_CFB8_256}; +pub use ctr::{ARIA_CTR_128, ARIA_CTR_192, ARIA_CTR_256, CTR_NONCE_LEN}; +pub use key::{ARIA128Key, ARIA192Key, ARIA256Key, ARIAKey}; +pub use schedule::ARIAParams; + +// Imports needed for docs +#[allow(unused_imports)] +use hazmat::{ARIA128Internal, ARIA192Internal, ARIA256Internal, ARIAInternal}; +// end of imports needed for docs diff --git a/crypto/aria/src/round.rs b/crypto/aria/src/round.rs new file mode 100644 index 000000000..18197f5f1 --- /dev/null +++ b/crypto/aria/src/round.rs @@ -0,0 +1,403 @@ +//! The components of ARIA (RFC 5794 Sec 2.4) -- the substitution layers `SL1` / `SL2`, the +//! diffusion layer `A`, the round functions `FO` / `FE` -- and the data randomizing part (Sec 2.3) +//! that runs them, on four blocks at once. +//! +//! # State layout +//! +//! A 16-byte block `x0 || x1 || ... || x15` is held as four big-endian 32-bit *row words* +//! `t[i] = x{4i} || x{4i+1} || x{4i+2} || x{4i+3}`. The substitution layers assign S-boxes by byte +//! position modulo four (Sec 2.4.2: `SB1, SB2, SB3, SB4` repeating in `SL1`, `SB3, SB4, SB1, SB2` +//! in `SL2`), so the bytes that share an S-box are one *column* of the 4x4 byte matrix -- +//! `x{j} || x{j+4} || x{j+8} || x{j+12}`, the *class word* `j`. [`transpose`] converts between the +//! two views so that each S-box circuit can be run once on a whole class word from each block. + +use crate::LANES; +use crate::bitslice::{Planes, ortho}; +use crate::hazmat::aria::Block; +use crate::sbox::{sb1, sb2, sb3, sb4}; + +/// One block as four big-endian 32-bit words. See the module docs. +pub(crate) type State = [u32; 4]; + +/// A round key, in the same layout as [`State`]. +pub(crate) type RoundKey = State; + +/// Transposes the 4x4 byte matrix of a block: row words `t[i] = x{4i} .. x{4i+3}` become class +/// words `c[j] = x{j} || x{j+4} || x{j+8} || x{j+12}`, and back -- it is its own inverse. +/// +/// Two stages of masked swaps: 16-bit halves between words two apart, then bytes between +/// adjacent words. `test_transpose_is_the_byte_transpose` pins it against a byte-by-byte +/// rearrangement. The `|`s join disjoint bit ranges, so `|` and `^` compute the same function +/// here (surviving `cargo mutants` equivalences, not gaps). +pub(crate) fn transpose(t: &mut State) { + let [t0, t1, t2, t3] = *t; + // Stage 1: (x0 x1 x2 x3),(x8 x9 x10 x11) -> (x0 x1 x8 x9),(x2 x3 x10 x11); likewise t1, t3. + let a0 = (t0 & 0xFFFF_0000) | (t2 >> 16); + let a2 = (t0 << 16) | (t2 & 0x0000_FFFF); + let a1 = (t1 & 0xFFFF_0000) | (t3 >> 16); + let a3 = (t1 << 16) | (t3 & 0x0000_FFFF); + // Stage 2: (x0 x1 x8 x9),(x4 x5 x12 x13) -> (x0 x4 x8 x12),(x1 x5 x9 x13); likewise a2, a3. + t[0] = (a0 & 0xFF00_FF00) | ((a1 >> 8) & 0x00FF_00FF); + t[1] = ((a0 << 8) & 0xFF00_FF00) | (a1 & 0x00FF_00FF); + t[2] = (a2 & 0xFF00_FF00) | ((a3 >> 8) & 0x00FF_00FF); + t[3] = ((a2 << 8) & 0xFF00_FF00) | (a3 & 0x00FF_00FF); +} + +/// Applies one S-box to all four bytes of one class word from each of the four blocks. +/// +/// Each word is split into its two 16-bit halves -- the high halves in plane words `0 .. 4`, the +/// low halves in `4 .. 8` -- the eight half-words are transposed into bit-planes, every byte +/// position is substituted by one pass of the circuit, the planes are transposed back and the +/// halves rejoined. The byte order is irrelevant to the S-box, which substitutes each byte +/// independently; [`ortho`] being its own inverse returns each byte to its place. +fn substitute(words: &mut [u32; LANES], sbox: fn(&mut Planes)) { + let mut q: Planes = [0; 8]; + for b in 0..LANES { + q[b] = (words[b] >> 16) as u16; + q[LANES + b] = words[b] as u16; + } + ortho(&mut q); + sbox(&mut q); + ortho(&mut q); + for b in 0..LANES { + // Disjoint bit ranges: `|` and `^` agree (a surviving `cargo mutants` equivalence). + words[b] = ((q[b] as u32) << 16) | q[LANES + b] as u32; + } +} + +/// A substitution layer on four blocks: class word `j` of every block goes through `sboxes[j]`. +fn substitution_layer(s: &mut [State; LANES], sboxes: [fn(&mut Planes); 4]) { + for st in s.iter_mut() { + transpose(st); + } + for (j, sbox) in sboxes.into_iter().enumerate() { + let mut class: [u32; LANES] = core::array::from_fn(|b| s[b][j]); + substitute(&mut class, sbox); + for b in 0..LANES { + s[b][j] = class[b]; + } + } + for st in s.iter_mut() { + transpose(st); + } +} + +/// `SL1` (Sec 2.4.2): `y{4k} = SB1(x{4k}), y{4k+1} = SB2(x{4k+1}), y{4k+2} = SB3(x{4k+2}), +/// y{4k+3} = SB4(x{4k+3})`, on four blocks. +pub(crate) fn sl1(s: &mut [State; LANES]) { + substitution_layer(s, [sb1, sb2, sb3, sb4]); +} + +/// `SL2` (Sec 2.4.2): `y{4k} = SB3(x{4k}), y{4k+1} = SB4(x{4k+1}), y{4k+2} = SB1(x{4k+2}), +/// y{4k+3} = SB2(x{4k+3})`, on four blocks. `SL2` is the inverse of `SL1`. +pub(crate) fn sl2(s: &mut [State; LANES]) { + substitution_layer(s, [sb3, sb4, sb1, sb2]); +} + +/// The diffusion layer `A` (Sec 2.4.3) on one block. +/// +/// Sec 2.4.3 gives `A` as sixteen equations, each output byte the XOR of seven input bytes. This +/// computes the same 16x16 binary matrix in three word-level steps, the way the 32-bit +/// implementations of ARIA (OpenSSL's `aria.c`, the designers' reference code) do: +/// +/// 1. within each row word, replace every byte by the XOR of the *other three* bytes of the +/// word -- `b ^ (b0 ^ b1 ^ b2 ^ b3)` for each byte `b`; +/// 2. six word XORs: `t1 ^= t2; t2 ^= t3; t0 ^= t1; t3 ^= t1; t2 ^= t0; t1 ^= t2`; +/// 3. byte permutations: swap adjacent bytes in `t1` (`abcd -> badc`), rotate `t2` by 16 bits +/// (`abcd -> cdab`), reverse `t3` (`abcd -> dcba`); +/// 4. the six word XORs of step 2 again. +/// +/// The decomposition was **verified, not recalled**: the generator that produced this crate's +/// S-box circuits also checked this sequence against the sixteen equations of Sec 2.4.3 on every +/// one of the 128 unit vectors (`A` is linear, so that determines it) and on 1000 random blocks, +/// and `test_diffusion_matches_the_rfc_equations` repeats the check in Rust. `A` is an involution +/// (Sec 2.4.3, "x = A(A(x))"), which `test_diffusion_is_an_involution` checks. +/// +/// The multiplication by `0x0101_0101` in step 1 broadcasts the parity byte to all four byte +/// positions; it cannot carry, since the operand is below 256. +pub(crate) fn diffuse(t: &mut State) { + // 1. b_i ^= b0 ^ b1 ^ b2 ^ b3, per word. + for w in t.iter_mut() { + let mut parity = *w ^ (*w >> 16); + parity ^= parity >> 8; + *w ^= (parity & 0xFF).wrapping_mul(0x0101_0101); + } + // 2. + word_mix(t); + // 3. badc, cdab, dcba. (The `|` joins disjoint byte positions, so `|` and `^` agree here -- a + // surviving `cargo mutants` equivalence, like those in `transpose`.) + t[1] = ((t[1] << 8) & 0xFF00_FF00) | ((t[1] >> 8) & 0x00FF_00FF); + t[2] = t[2].rotate_right(16); + t[3] = t[3].swap_bytes(); + // 4. + word_mix(t); +} + +/// The six word XORs of [`diffuse`]. +#[inline(always)] +fn word_mix(t: &mut State) { + t[1] ^= t[2]; + t[2] ^= t[3]; + t[0] ^= t[1]; + t[3] ^= t[1]; + t[2] ^= t[0]; + t[1] ^= t[2]; +} + +/// `D ^ RK`: the key addition inside `FO`, `FE` and the final round, on one block. +#[inline(always)] +fn add_key(s: &mut State, rk: &RoundKey) { + for (w, k) in s.iter_mut().zip(rk.iter()) { + *w ^= k; + } +} + +/// `FO(D, RK) = A(SL1(D ^ RK))` (Sec 2.4.1) on four blocks. +pub(crate) fn fo(s: &mut [State; LANES], rk: &RoundKey) { + for st in s.iter_mut() { + add_key(st, rk); + } + sl1(s); + for st in s.iter_mut() { + diffuse(st); + } +} + +/// `FE(D, RK) = A(SL2(D ^ RK))` (Sec 2.4.1) on four blocks. +pub(crate) fn fe(s: &mut [State; LANES], rk: &RoundKey) { + for st in s.iter_mut() { + add_key(st, rk); + } + sl2(s); + for st in s.iter_mut() { + diffuse(st); + } +} + +/// `FO` on one 128-bit value, for the key schedule (Sec 2.2, `W1 = FO(W0, CK1) ^ KR` and +/// `W3 = FO(W2, CK3) ^ W1`): the value goes in every lane and lane 0 comes back. +pub(crate) fn fo1(d: State, rk: &RoundKey) -> State { + let mut s = [d; LANES]; + fo(&mut s, rk); + s[0] +} + +/// `FE` on one 128-bit value, for the key schedule (`W2 = FE(W1, CK2) ^ W0`). +pub(crate) fn fe1(d: State, rk: &RoundKey) -> State { + let mut s = [d; LANES]; + fe(&mut s, rk); + s[0] +} + +/// The data randomizing part (Sec 2.3.1 for encryption, Sec 2.3.2 for decryption) on four blocks +/// at once, with `num_rounds` rounds (12, 14 or 16) and `key(i)` supplying round key `i` in +/// `1 ..= num_rounds + 1`: `ek_i` for encryption, `dk_i` for decryption (see +/// [`crate::ARIA::decrypt_4blocks`]). +/// +/// Line by line against Sec 2.3.1.1 (the 192- and 256-bit versions only add rounds): +/// +/// * `P1 = FO(P, ek1)` -- round 1, and every odd round `i`: `P_i = FO(P_{i-1}, ek_i)`; +/// * `P2 = FE(P1, ek2)` -- and every even round before the last: `P_i = FE(P_{i-1}, ek_i)`; +/// * `C = SL2(P_{n-1} ^ ek_n) ^ ek_{n+1}` -- the last round (`n` is even) substitutes without +/// diffusing and adds the extra round key. +/// +/// The RFC writes the rounds out longhand; this is the same sequence as a loop over a public round +/// counter. The four blocks are processed together only because the substitution layers are: the +/// S-box circuits take one class word from each block per pass, and everything else is per block. +pub(crate) fn rounds( + blocks: &mut [Block; LANES], + num_rounds: usize, + key: impl Fn(usize) -> RoundKey, +) { + // P as four big-endian row words per block. + let mut s = [[0u32; 4]; LANES]; + for (st, block) in s.iter_mut().zip(blocks.iter()) { + // Sixteen bytes are exactly four four-byte words; the remainder is provably empty. + let (words, _) = block.as_chunks::<4>(); + for (w, bytes) in st.iter_mut().zip(words) { + *w = u32::from_be_bytes(*bytes); + } + } + + // Rounds 1 .. n-1: FO on odd rounds, FE on even rounds. + for i in 1..num_rounds { + let rk = key(i); + if i % 2 == 1 { + fo(&mut s, &rk); + } else { + fe(&mut s, &rk); + } + } + + // Round n: C = SL2(P_{n-1} ^ ek_n) ^ ek_{n+1}. + let rk_n = key(num_rounds); + let rk_last = key(num_rounds + 1); + for st in s.iter_mut() { + add_key(st, &rk_n); + } + sl2(&mut s); + for (st, block) in s.iter_mut().zip(blocks.iter_mut()) { + add_key(st, &rk_last); + let (words, _) = block.as_chunks_mut::<4>(); + for (bytes, w) in words.iter_mut().zip(st.iter()) { + *bytes = w.to_be_bytes(); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::sbox::tests::{SB1, SB2, SB3, SB4}; + + /// Sec 2.4.3, the sixteen equations, byte by byte. Each row is the seven `x` indices. + const A_ROWS: [[usize; 7]; 16] = [ + [3, 4, 6, 8, 9, 13, 14], + [2, 5, 7, 8, 9, 12, 15], + [1, 4, 6, 10, 11, 12, 15], + [0, 5, 7, 10, 11, 13, 14], + [0, 2, 5, 8, 11, 14, 15], + [1, 3, 4, 9, 10, 14, 15], + [0, 2, 7, 9, 10, 12, 13], + [1, 3, 6, 8, 11, 12, 13], + [0, 1, 4, 7, 10, 13, 15], + [0, 1, 5, 6, 11, 12, 14], + [2, 3, 5, 6, 8, 13, 15], + [2, 3, 4, 7, 9, 12, 14], + [1, 2, 6, 7, 9, 11, 12], + [0, 3, 6, 7, 8, 10, 13], + [0, 3, 4, 5, 9, 11, 14], + [1, 2, 4, 5, 8, 10, 15], + ]; + + fn a_literal(x: &[u8; 16]) -> [u8; 16] { + core::array::from_fn(|p| A_ROWS[p].iter().fold(0u8, |acc, &q| acc ^ x[q])) + } + + fn to_state(x: &[u8; 16]) -> State { + let (w, _) = x.as_chunks::<4>(); + core::array::from_fn(|i| u32::from_be_bytes(w[i])) + } + fn to_bytes(s: &State) -> [u8; 16] { + let mut out = [0u8; 16]; + for (chunk, w) in out.as_chunks_mut::<4>().0.iter_mut().zip(s.iter()) { + *chunk = w.to_be_bytes(); + } + out + } + + /// Deterministic pseudo-random bytes (xorshift32), so the tests need no RNG crate. + fn pseudo_random(seed: &mut u32) -> [u8; N] { + core::array::from_fn(|_| { + *seed ^= *seed << 13; + *seed ^= *seed >> 17; + *seed ^= *seed << 5; + (*seed >> 24) as u8 + }) + } + + #[test] + fn test_diffusion_matches_the_rfc_equations() { + // Linear, so the 128 unit vectors determine it; random blocks are an independent check. + for byte in 0..16 { + for bit in 0..8 { + let mut x = [0u8; 16]; + x[byte] = 1 << bit; + let mut s = to_state(&x); + diffuse(&mut s); + assert_eq!(to_bytes(&s), a_literal(&x), "unit vector byte {byte} bit {bit}"); + } + } + let mut seed = 0x1234_5678; + for _ in 0..1000 { + let x: [u8; 16] = pseudo_random(&mut seed); + let mut s = to_state(&x); + diffuse(&mut s); + assert_eq!(to_bytes(&s), a_literal(&x)); + } + } + + #[test] + fn test_diffusion_is_an_involution() { + let mut seed = 0x0BAD_F00D; + for _ in 0..1000 { + let x: [u8; 16] = pseudo_random(&mut seed); + let mut s = to_state(&x); + diffuse(&mut s); + assert_ne!(to_bytes(&s), x); + diffuse(&mut s); + assert_eq!(to_bytes(&s), x); + } + } + + #[test] + fn test_transpose_is_the_byte_transpose() { + let x: [u8; 16] = core::array::from_fn(|i| i as u8); + let mut s = to_state(&x); + transpose(&mut s); + // c[j] = x_j || x_{j+4} || x_{j+8} || x_{j+12} + assert_eq!(s, [0x0004_080C, 0x0105_090D, 0x0206_0A0E, 0x0307_0B0F]); + transpose(&mut s); + assert_eq!(s, to_state(&x), "transpose must be an involution"); + let mut seed = 0xC0FF_EE00; + for _ in 0..200 { + let x: [u8; 16] = pseudo_random(&mut seed); + let mut s = to_state(&x); + transpose(&mut s); + let expected: [u8; 16] = core::array::from_fn(|k| x[4 * (k % 4) + k / 4]); + assert_eq!(to_bytes(&s), expected); + } + } + + /// `SL1` / `SL2` written literally from the tables of Sec 2.4.2, on one block. + fn sl_literal(x: &[u8; 16], type1: bool) -> [u8; 16] { + let order: [&[u8; 256]; 4] = + if type1 { [&SB1, &SB2, &SB3, &SB4] } else { [&SB3, &SB4, &SB1, &SB2] }; + core::array::from_fn(|p| order[p % 4][x[p] as usize]) + } + + #[test] + fn test_substitution_layers_match_the_tables_in_every_lane() { + let mut seed = 0x5EED_5EED; + for _ in 0..200 { + let xs: [[u8; 16]; LANES] = core::array::from_fn(|_| pseudo_random(&mut seed)); + let mut s: [State; LANES] = core::array::from_fn(|b| to_state(&xs[b])); + sl1(&mut s); + for b in 0..LANES { + assert_eq!(to_bytes(&s[b]), sl_literal(&xs[b], true), "SL1 lane {b}"); + } + sl2(&mut s); + for b in 0..LANES { + assert_eq!(to_bytes(&s[b]), xs[b], "SL2 must invert SL1, lane {b}"); + } + let mut s: [State; LANES] = core::array::from_fn(|b| to_state(&xs[b])); + sl2(&mut s); + for b in 0..LANES { + assert_eq!(to_bytes(&s[b]), sl_literal(&xs[b], false), "SL2 lane {b}"); + } + } + } + + #[test] + fn test_fo1_and_fe1_lanes_agree() { + // The single-value forms fill all four lanes; every lane must come back identical. + let d: State = [0x0001_0203, 0x0405_0607, 0x0809_0A0B, 0x0C0D_0E0F]; + let rk: RoundKey = [0x517c_c1b7, 0x2722_0a94, 0xfe13_abe8, 0xfa9a_6ee0]; + let mut s = [d; LANES]; + fo(&mut s, &rk); + assert!(s.iter().all(|x| *x == s[0])); + assert_eq!(fo1(d, &rk), s[0]); + let mut s = [d; LANES]; + fe(&mut s, &rk); + assert!(s.iter().all(|x| *x == s[0])); + assert_eq!(fe1(d, &rk), s[0]); + // And they match the literal definitions FO = A(SL1(D ^ RK)), FE = A(SL2(D ^ RK)). + let xored: State = core::array::from_fn(|i| d[i] ^ rk[i]); + let mut lit = to_state(&sl_literal(&to_bytes(&xored), true)); + diffuse(&mut lit); + assert_eq!(fo1(d, &rk), lit); + let mut lit = to_state(&sl_literal(&to_bytes(&xored), false)); + diffuse(&mut lit); + assert_eq!(fe1(d, &rk), lit); + } +} diff --git a/crypto/aria/src/sbox.rs b/crypto/aria/src/sbox.rs new file mode 100644 index 000000000..da0ce2634 --- /dev/null +++ b/crypto/aria/src/sbox.rs @@ -0,0 +1,941 @@ +//! `SB1`, `SB2`, `SB3` and `SB4` (RFC 5794 Sec 2.4.2) as Boolean circuits over bit-planes. +//! +//! # Why circuits and not tables +//! +//! Sec 2.4.2 defines the four S-boxes by 256-entry tables, and a straightforward implementation +//! stores them as such (OpenSSL's `aria.c` as four 1 KiB `u32` tables with part of the diffusion +//! folded in). A table indexed by a byte of the state is indexed by *secret data*, and on any CPU +//! with a data cache the access pattern -- hence the timing -- depends on that secret. That is the +//! standard cache-timing side channel of every table-driven block cipher, and it cannot be closed +//! while keeping the lookup. +//! +//! So this module has no tables (outside its tests). It computes the same four functions with AND, +//! XOR, XNOR and NOT gates applied to the bit-planes of [`crate::bitslice`]. Every operation is a +//! straight-line word operation on public *positions*, so there is no secret-dependent memory +//! access and no secret-dependent branch. This is the only place in the crate where secret data +//! meets non-linear logic; everything else is XOR, rotation and byte permutation. +//! +//! Because the planes hold one class word (four bytes) from each of four blocks, one pass of a +//! circuit is that S-box's share of a substitution layer for four blocks at once. +//! +//! # What the circuits compute, and where they came from +//! +//! `SB1` is the AES S-box (Sec 2.4.2's table is FIPS 197 Table 4, which `test_sb1_is_the_aes_sbox` +//! checks), so [`sb1`] is the 113-gate Boyar-Peralta straight-line program for it, copied verbatim +//! from `bouncycastle-aes` (`src/sbox.rs`, a transcription of Peralta's +//! `SLP_AES_113.txt`), with the SLP's `U0`-is-MSB input convention mapped onto the planes. +//! +//! The other three have the same algebraic shape -- an affine map, inversion in GF(2^8), and +//! another affine map. Writing `inv` for inversion in the AES field (`x^8 + x^4 + x^3 + x + 1`, +//! with `inv(0) = 0`), and `M`, `N` for 8x8 matrices over GF(2) given by their columns (column `i` +//! is the image of input bit `i`, bit 0 least significant): +//! +//! ```text +//! SB2(x) = N * inv(M * x xor 0x00) xor 0xe2 +//! M columns: 0x20, 0x80, 0x36, 0xd8, 0x4d, 0x2f, 0xbc, 0xc6 +//! N columns: 0xce, 0x83, 0xd5, 0x26, 0xae, 0xa7, 0x87, 0xfb +//! SB3(x) = N * inv(M * x xor 0xff) xor 0x00 +//! M columns: 0xf1, 0x89, 0xe2, 0xa2, 0xce, 0xfe, 0x79, 0x7b +//! N columns: 0x54, 0x55, 0x18, 0xe2, 0xa8, 0xaa, 0x30, 0xdf +//! SB4(x) = N * inv(M * x xor 0xa0) xor 0x00 +//! M columns: 0x2b, 0x61, 0x64, 0x80, 0xe7, 0x4d, 0xd4, 0x58 +//! N columns: 0x59, 0xb6, 0xb2, 0x77, 0x7f, 0xee, 0xfe, 0xc7 +//! ``` +//! +//! These decompositions were **found by search, not recalled** (`SB3 = SB1^-1` and `SB4 = SB2^-1` +//! make theirs a consequence of `SB1`'s and `SB2`'s, but each was searched for independently and +//! the results agree): for each of the 256 possible input constants the remaining linear +//! equivalence to `inv` was searched exhaustively (the search fixes the multiplicative and +//! Frobenius self-equivalences of `inv`, which is what makes it finite), against the tables +//! extracted from the text of RFC 5794. For each S-box exactly one constant admits a +//! decomposition -- `0x00` for `SB2` (whose input affine map is therefore linear, as `SB1`'s is), +//! `0x63` for `SB3` and `0xe2` for `SB4`, the output constants of `SB1` and `SB2` -- and it is +//! unique up to those self-equivalences. The 2040 equivalent forms of each give circuits of +//! different sizes; the ones above were chosen for the fewest gates. +//! +//! Each generated circuit is therefore a **top affine layer** (generated: `M * x xor c` composed +//! with the Boyar-Peralta input basis change, as one affine map from the eight input planes to the +//! 22 signals the non-linear section consumes), the **non-linear section** of the Boyar-Peralta +//! circuit (**copied verbatim**: 32 AND and 30 XOR gates computing the inverse in a tower-field +//! representation), and a **bottom affine layer** (generated: the tower field back to the AES +//! polynomial basis, then `N * . xor d`, as one affine map from the 18 non-linear outputs to the +//! eight output planes). The affine layers were fitted by linear algebra over GF(2) and reduced +//! with a greedy common-subexpression pass. Gate counts: +//! +//! | S-box | AND | XOR | XNOR | NOT | total | +//! |---|---|---|---|---|---| +//! | `SB1` (Boyar-Peralta) | 32 | 77 | 4 | 0 | 113 | +//! | `SB2` | 32 | 83 | 4 | 0 | 119 | +//! | `SB3` | 32 | 87 | 5 | 0 | 124 | +//! | `SB4` | 32 | 78 | 11 | 1 | 122 | +//! +//! The generator verified every assembled circuit -- the emitted text, re-evaluated -- against its +//! table before any of it was written here, and `test_sboxes_match_rfc_5794_tables` re-verifies all +//! 256 inputs of all four in Rust. The gate lists are not meaningful line by line and should not +//! be "tidied". +//! +//! # Bit numbering +//! +//! Plane `k` holds bit `k` of every byte (bit 0 least significant), for inputs and outputs alike. +//! The Boyar-Peralta sections keep their signal names (`y*`, `t*`, `z*`) so they can be diffed +//! against the AES, SM4 and Camellia crates. In [`sb1`] the SLP's own input and output names +//! `U0 .. U7` / `S0 .. S7` appear with `U0` the most significant bit, i.e. plane `q[7]`; in the +//! generated circuits the inputs are bound from the top layer, so that convention never appears. + +use crate::bitslice::Planes; + +/// `SB1` (Sec 2.4.2) -- the AES S-box -- applied to every byte position of the planes: `q[k]` +/// holds bit `k` of each byte on entry and on exit. The 113-gate Boyar-Peralta program; see the +/// module docs. +pub(crate) fn sb1(q: &mut Planes) { + // SLP inputs U0..U7, most-significant bit first, so U0 is the highest plane. + let u0 = q[7]; + let u1 = q[6]; + let u2 = q[5]; + let u3 = q[4]; + let u4 = q[3]; + let u5 = q[2]; + let u6 = q[1]; + let u7 = q[0]; + + // Top linear transformation (23 gates): the input basis change. + let y14 = u3 ^ u5; + let y13 = u0 ^ u6; + let y9 = u0 ^ u3; + let y8 = u0 ^ u5; + let t0 = u1 ^ u2; + let y1 = t0 ^ u7; + let y4 = y1 ^ u3; + let y12 = y13 ^ y14; + let y2 = y1 ^ u0; + let y5 = y1 ^ u6; + let y3 = y5 ^ y8; + let t1 = u4 ^ y12; + let y15 = t1 ^ u5; + let y20 = t1 ^ u1; + let y6 = y15 ^ u7; + let y10 = y15 ^ t0; + let y11 = y20 ^ y9; + let y7 = u7 ^ y11; + let y17 = y10 ^ y11; + let y19 = y10 ^ y8; + let y16 = t0 ^ y11; + let y21 = y13 ^ y16; + let y18 = u0 ^ y16; + + // Non-linear section (62 gates): the GF(2^8) inversion, and the only ANDs in the circuit. + let t2 = y12 & y15; + let t3 = y3 & y6; + let t4 = t3 ^ t2; + let t5 = y4 & u7; + let t6 = t5 ^ t2; + let t7 = y13 & y16; + let t8 = y5 & y1; + let t9 = t8 ^ t7; + let t10 = y2 & y7; + let t11 = t10 ^ t7; + let t12 = y9 & y11; + let t13 = y14 & y17; + let t14 = t13 ^ t12; + let t15 = y8 & y10; + let t16 = t15 ^ t12; + let t17 = t4 ^ y20; + let t18 = t6 ^ t16; + let t19 = t9 ^ t14; + let t20 = t11 ^ t16; + let t21 = t17 ^ t14; + let t22 = t18 ^ y19; + let t23 = t19 ^ y21; + let t24 = t20 ^ y18; + let t25 = t21 ^ t22; + let t26 = t21 & t23; + let t27 = t24 ^ t26; + let t28 = t25 & t27; + let t29 = t28 ^ t22; + let t30 = t23 ^ t24; + let t31 = t22 ^ t26; + let t32 = t31 & t30; + let t33 = t32 ^ t24; + let t34 = t23 ^ t33; + let t35 = t27 ^ t33; + let t36 = t24 & t35; + // `cargo mutants` reports the `^ -> |` mutant on the next line as surviving. That is a true + // equivalence, not a gap: `t36` and `t34` are never both 1 for any of the 256 possible input + // bytes, so XOR and OR agree here. It is the same gate the AES, SM4 and Camellia crates + // document, since this section is copied from there. + let t37 = t36 ^ t34; + let t38 = t27 ^ t36; + let t39 = t29 & t38; + let t40 = t25 ^ t39; + let t41 = t40 ^ t37; + let t42 = t29 ^ t33; + let t43 = t29 ^ t40; + let t44 = t33 ^ t37; + let t45 = t42 ^ t41; + let z0 = t44 & y15; + let z1 = t37 & y6; + let z2 = t33 & u7; + let z3 = t43 & y16; + let z4 = t40 & y1; + let z5 = t29 & y7; + let z6 = t42 & y11; + let z7 = t45 & y17; + let z8 = t41 & y10; + let z9 = t44 & y12; + let z10 = t37 & y3; + let z11 = t33 & y4; + let z12 = t43 & y13; + let z13 = t40 & y5; + let z14 = t29 & y2; + let z15 = t42 & y9; + let z16 = t45 & y14; + let z17 = t41 & y8; + + // Bottom linear transformation (28 gates): the output basis change and the AES affine map, + // whose `{63}` constant is the four XNORs. + let tc1 = z15 ^ z16; + let tc2 = z10 ^ tc1; + let tc3 = z9 ^ tc2; + let tc4 = z0 ^ z2; + let tc5 = z1 ^ z0; + let tc6 = z3 ^ z4; + let tc7 = z12 ^ tc4; + let tc8 = z7 ^ tc6; + let tc9 = z8 ^ tc7; + let tc10 = tc8 ^ tc9; + let tc11 = tc6 ^ tc5; + let tc12 = z3 ^ z5; + let tc13 = z13 ^ tc1; + let tc14 = tc4 ^ tc12; + let s3 = tc3 ^ tc11; + let tc16 = z6 ^ tc8; + let tc17 = z14 ^ tc10; + let tc18 = tc13 ^ tc14; + let s7 = !(z12 ^ tc18); + let tc20 = z15 ^ tc16; + let tc21 = tc2 ^ z11; + let s0 = tc3 ^ tc16; + let s6 = !(tc10 ^ tc18); + let s4 = tc14 ^ s3; + let s1 = !(s3 ^ tc16); + let tc26 = tc17 ^ tc20; + let s2 = !(tc26 ^ z17); + let s5 = tc21 ^ tc17; + + // SLP outputs S0..S7, most-significant bit first, mirroring the input mapping. + q[7] = s0; + q[6] = s1; + q[5] = s2; + q[4] = s3; + q[3] = s4; + q[2] = s5; + q[1] = s6; + q[0] = s7; +} + +/// `SB2` (Sec 2.4.2) applied to every byte position of the planes. Generated; see the module docs. +pub(crate) fn sb2(q: &mut Planes) { + // Inputs: plane k holds bit k of every byte. + let x0 = q[0]; + let x1 = q[1]; + let x2 = q[2]; + let x3 = q[3]; + let x4 = q[4]; + let x5 = q[5]; + let x6 = q[6]; + let x7 = q[7]; + + // Top affine layer (29 gates, generated): the input affine map `M * x xor 0x00` and the + // Boyar-Peralta input basis change, folded into one affine map over x0..x7. + let a1 = x1 ^ x2; + let a2 = x3 ^ x6; + let a3 = x4 ^ x5; + let a4 = x0 ^ a1; + let a5 = x5 ^ a2; + let a6 = x0 ^ x7; + let a7 = x2 ^ a2; + let a8 = a1 ^ a3; + let a9 = x7 ^ a3; + let a10 = x7 ^ a1; + let a11 = x7 ^ a4; + let a12 = x4 ^ x6; + let a13 = a6 ^ a7; + let a14 = a4 ^ a12; + let a15 = x0 ^ a5; + let a16 = x1 ^ a5; + let a17 = x3 ^ a8; + let a18 = x5 ^ a11; + let a19 = a10 ^ a12; + let a20 = a1 ^ a5; + let a21 = x3 ^ a9; + let a22 = x1 ^ x4; + let a23 = a22 ^ a2; + let a24 = x0 ^ a7; + let a25 = x4 ^ a4; + let a26 = x3 ^ x4; + let a27 = a26 ^ a6; + let a28 = x0 ^ x1; + let a29 = a28 ^ x5; + let u7 = a3; + let y1 = a13; + let y2 = a4; + let y3 = a14; + let y4 = a6; + let y5 = a15; + let y6 = a16; + let y7 = x7; + let y8 = a17; + let y9 = a10; + let y10 = a18; + let y11 = a9; + let y12 = a19; + let y13 = a20; + let y14 = a21; + let y15 = a23; + let y16 = a24; + let y17 = a25; + let y18 = a11; + let y19 = a27; + let y20 = a8; + let y21 = a29; + + // Non-linear section (62 gates): GF(2^8) inversion, copied verbatim from the Boyar-Peralta + // AES circuit in `bouncycastle-aes` (`src/sbox.rs`). + let t2 = y12 & y15; + let t3 = y3 & y6; + let t4 = t3 ^ t2; + let t5 = y4 & u7; + let t6 = t5 ^ t2; + let t7 = y13 & y16; + let t8 = y5 & y1; + let t9 = t8 ^ t7; + let t10 = y2 & y7; + let t11 = t10 ^ t7; + let t12 = y9 & y11; + let t13 = y14 & y17; + let t14 = t13 ^ t12; + let t15 = y8 & y10; + let t16 = t15 ^ t12; + let t17 = t4 ^ y20; + let t18 = t6 ^ t16; + let t19 = t9 ^ t14; + let t20 = t11 ^ t16; + let t21 = t17 ^ t14; + let t22 = t18 ^ y19; + let t23 = t19 ^ y21; + let t24 = t20 ^ y18; + let t25 = t21 ^ t22; + let t26 = t21 & t23; + let t27 = t24 ^ t26; + let t28 = t25 & t27; + let t29 = t28 ^ t22; + let t30 = t23 ^ t24; + let t31 = t22 ^ t26; + let t32 = t31 & t30; + let t33 = t32 ^ t24; + let t34 = t23 ^ t33; + let t35 = t27 ^ t33; + let t36 = t24 & t35; + // `cargo mutants` reports the `^ -> |` mutant on the next line as surviving. That is a true + // equivalence, not a gap: `t36` and `t34` are never both 1 for any of the 256 possible input + // bytes, so XOR and OR agree here. It is the same gate the AES, SM4 and Camellia crates + // document, since this section is copied from there. + let t37 = t36 ^ t34; + let t38 = t27 ^ t36; + let t39 = t29 & t38; + let t40 = t25 ^ t39; + let t41 = t40 ^ t37; + let t42 = t29 ^ t33; + let t43 = t29 ^ t40; + let t44 = t33 ^ t37; + let t45 = t42 ^ t41; + let z0 = t44 & y15; + let z1 = t37 & y6; + let z2 = t33 & u7; + let z3 = t43 & y16; + let z4 = t40 & y1; + let z5 = t29 & y7; + let z6 = t42 & y11; + let z7 = t45 & y17; + let z8 = t41 & y10; + let z9 = t44 & y12; + let z10 = t37 & y3; + let z11 = t33 & y4; + let z12 = t43 & y13; + let z13 = t40 & y5; + let z14 = t29 & y2; + let z15 = t42 & y9; + let z16 = t45 & y14; + let z17 = t41 & y8; + + // Bottom affine layer (28 gates, generated): tower field back to the AES polynomial basis + // and the output affine map `N * . xor 0xe2`. Plane j receives bit j of the output. + let b1 = z11 ^ z17; + let b2 = z0 ^ b1; + let b3 = z1 ^ z7; + let b4 = z16 ^ b3; + let b5 = z15 ^ b2; + let b6 = z9 ^ b5; + let b7 = z12 ^ z13; + let b8 = z1 ^ z3; + let b9 = z8 ^ b4; + let b10 = z4 ^ b8; + let b11 = z2 ^ b9; + let b12 = z15 ^ b7; + let b13 = z6 ^ b3; + let b14 = b13 ^ b6; + let b15 = z10 ^ b1; + let b16 = !(b15 ^ b12); + let b17 = z6 ^ z10; + let b18 = b17 ^ b2; + let b19 = b18 ^ b4; + let b20 = z2 ^ z3; + let b21 = b20 ^ z5; + let b22 = b21 ^ b6; + let b23 = z0 ^ b10; + let b24 = z13 ^ z14; + let b25 = b24 ^ z17; + let b26 = !(b25 ^ b11); + let b27 = !(b6 ^ b10); + let b28 = !(b11 ^ b12); + q[0] = b14; + q[1] = b16; + q[2] = b19; + q[3] = b22; + q[4] = b23; + q[5] = b26; + q[6] = b27; + q[7] = b28; +} + +/// `SB3 = SB1^-1` (Sec 2.4.2) applied to every byte position of the planes. Generated; see the +/// module docs. +pub(crate) fn sb3(q: &mut Planes) { + // Inputs: plane k holds bit k of every byte. + let x0 = q[0]; + let x1 = q[1]; + let x2 = q[2]; + let x3 = q[3]; + let x4 = q[4]; + let x5 = q[5]; + let x6 = q[6]; + let x7 = q[7]; + + // Top affine layer (29 gates, generated): the input affine map `M * x xor 0xff` and the + // Boyar-Peralta input basis change, folded into one affine map over x0..x7. + let a1 = x0 ^ x1; + let a2 = x4 ^ x6; + let a3 = x7 ^ a1; + let a4 = x2 ^ x3; + let a5 = x5 ^ x6; + let a6 = x0 ^ a2; + let a7 = x1 ^ a2; + let a8 = x2 ^ a5; + let a9 = x3 ^ a3; + let a10 = x7 ^ a4; + let a11 = !(x6 ^ a3); + let a12 = !(a2 ^ a9); + let a13 = x7 ^ a8; + let a14 = x3 ^ a5; + let a15 = x1 ^ x3; + let a16 = a15 ^ x4; + let a17 = a16 ^ x5; + let a18 = a1 ^ a8; + let a19 = !(a1 ^ a2); + let a20 = !(x3 ^ x6); + let a21 = a1 ^ a4; + let a22 = a7 ^ a10; + let a23 = x3 ^ x7; + let a24 = x7 ^ a6; + let a25 = x4 ^ x7; + let a26 = x4 ^ a3; + let a27 = !(x5 ^ a10); + let a28 = x2 ^ a3; + let a29 = x2 ^ a6; + let u7 = a11; + let y1 = a12; + let y2 = a13; + let y3 = a14; + let y4 = a17; + let y5 = a18; + let y6 = a19; + let y7 = a20; + let y8 = a21; + let y9 = a22; + let y10 = a23; + let y11 = a9; + let y12 = a7; + let y13 = a3; + let y14 = a24; + let y15 = a25; + let y16 = a26; + let y17 = a1; + let y18 = a27; + let y19 = a28; + let y20 = a29; + let y21 = x4; + + // Non-linear section (62 gates): GF(2^8) inversion, copied verbatim from the Boyar-Peralta + // AES circuit in `bouncycastle-aes` (`src/sbox.rs`). + let t2 = y12 & y15; + let t3 = y3 & y6; + let t4 = t3 ^ t2; + let t5 = y4 & u7; + let t6 = t5 ^ t2; + let t7 = y13 & y16; + let t8 = y5 & y1; + let t9 = t8 ^ t7; + let t10 = y2 & y7; + let t11 = t10 ^ t7; + let t12 = y9 & y11; + let t13 = y14 & y17; + let t14 = t13 ^ t12; + let t15 = y8 & y10; + let t16 = t15 ^ t12; + let t17 = t4 ^ y20; + let t18 = t6 ^ t16; + let t19 = t9 ^ t14; + let t20 = t11 ^ t16; + let t21 = t17 ^ t14; + let t22 = t18 ^ y19; + let t23 = t19 ^ y21; + let t24 = t20 ^ y18; + let t25 = t21 ^ t22; + let t26 = t21 & t23; + let t27 = t24 ^ t26; + let t28 = t25 & t27; + let t29 = t28 ^ t22; + let t30 = t23 ^ t24; + let t31 = t22 ^ t26; + let t32 = t31 & t30; + let t33 = t32 ^ t24; + let t34 = t23 ^ t33; + let t35 = t27 ^ t33; + let t36 = t24 & t35; + // `cargo mutants` reports the `^ -> |` mutant on the next line as surviving. That is a true + // equivalence, not a gap: `t36` and `t34` are never both 1 for any of the 256 possible input + // bytes, so XOR and OR agree here. It is the same gate the AES, SM4 and Camellia crates + // document, since this section is copied from there. + let t37 = t36 ^ t34; + let t38 = t27 ^ t36; + let t39 = t29 & t38; + let t40 = t25 ^ t39; + let t41 = t40 ^ t37; + let t42 = t29 ^ t33; + let t43 = t29 ^ t40; + let t44 = t33 ^ t37; + let t45 = t42 ^ t41; + let z0 = t44 & y15; + let z1 = t37 & y6; + let z2 = t33 & u7; + let z3 = t43 & y16; + let z4 = t40 & y1; + let z5 = t29 & y7; + let z6 = t42 & y11; + let z7 = t45 & y17; + let z8 = t41 & y10; + let z9 = t44 & y12; + let z10 = t37 & y3; + let z11 = t33 & y4; + let z12 = t43 & y13; + let z13 = t40 & y5; + let z14 = t29 & y2; + let z15 = t42 & y9; + let z16 = t45 & y14; + let z17 = t41 & y8; + + // Bottom affine layer (33 gates, generated): tower field back to the AES polynomial basis + // and the output affine map `N * . xor 0x00`. Plane j receives bit j of the output. + let b1 = z7 ^ z8; + let b2 = z4 ^ b1; + let b3 = z0 ^ z12; + let b4 = z5 ^ b2; + let b5 = z10 ^ z13; + let b6 = z1 ^ z14; + let b7 = z2 ^ z16; + let b8 = z6 ^ b3; + let b9 = z9 ^ b4; + let b10 = z11 ^ z14; + let b11 = z15 ^ z17; + let b12 = z7 ^ b6; + let b13 = b12 ^ b8; + let b14 = b13 ^ b11; + let b15 = z11 ^ b9; + let b16 = b15 ^ b11; + let b17 = b5 ^ b10; + let b18 = z3 ^ z10; + let b19 = b18 ^ z15; + let b20 = b19 ^ b2; + let b21 = b20 ^ b3; + let b22 = b21 ^ b7; + let b23 = b22 ^ b10; + let b24 = z12 ^ b5; + let b25 = b24 ^ b9; + let b26 = z2 ^ z8; + let b27 = b26 ^ z9; + let b28 = b27 ^ b5; + let b29 = b28 ^ b8; + let b30 = z13 ^ z17; + let b31 = b30 ^ b1; + let b32 = b31 ^ b6; + let b33 = b32 ^ b7; + q[0] = b4; + q[1] = b14; + q[2] = b16; + q[3] = b17; + q[4] = b23; + q[5] = b25; + q[6] = b29; + q[7] = b33; +} + +/// `SB4 = SB2^-1` (Sec 2.4.2) applied to every byte position of the planes. Generated; see the +/// module docs. +pub(crate) fn sb4(q: &mut Planes) { + // Inputs: plane k holds bit k of every byte. + let x0 = q[0]; + let x1 = q[1]; + let x2 = q[2]; + let x3 = q[3]; + let x4 = q[4]; + let x5 = q[5]; + let x6 = q[6]; + let x7 = q[7]; + + // Top affine layer (30 gates, generated): the input affine map `M * x xor 0xa0` and the + // Boyar-Peralta input basis change, folded into one affine map over x0..x7. + let a1 = x3 ^ x7; + let a2 = x1 ^ x4; + let a3 = x0 ^ x6; + let a4 = x2 ^ x5; + let a5 = a1 ^ a3; + let a6 = x5 ^ a2; + let a7 = x7 ^ a3; + let a8 = x1 ^ a1; + let a9 = a4 ^ a5; + let a10 = x0 ^ a6; + let a11 = x6 ^ x7; + let a12 = !(a11 ^ a2); + let a13 = x1 ^ a9; + let a14 = !(x1 ^ a7); + let a15 = x0 ^ x3; + let a16 = !(a15 ^ a2); + let a17 = !(x3 ^ a4); + let a18 = !(x4 ^ a1); + let a19 = !(x4 ^ a9); + let a20 = !(x3 ^ a3); + let a21 = x4 ^ x7; + let a22 = a21 ^ a4; + let a23 = !(x3 ^ x5); + let a24 = !(a2 ^ a7); + let a25 = a5 ^ a6; + let a26 = x0 ^ a8; + let a27 = !(a4 ^ a7); + let a28 = !(x5 ^ a8); + let a29 = a1 ^ a2; + let u7 = a10; + let y1 = a12; + let y2 = a8; + let y3 = a13; + let y4 = !a2; + let y5 = a14; + let y6 = a16; + let y7 = x0; + let y8 = a17; + let y9 = a18; + let y10 = a5; + let y11 = a6; + let y12 = a19; + let y13 = a20; + let y14 = a22; + let y15 = a23; + let y16 = a24; + let y17 = a25; + let y18 = a26; + let y19 = a27; + let y20 = a28; + let y21 = a29; + + // Non-linear section (62 gates): GF(2^8) inversion, copied verbatim from the Boyar-Peralta + // AES circuit in `bouncycastle-aes` (`src/sbox.rs`). + let t2 = y12 & y15; + let t3 = y3 & y6; + let t4 = t3 ^ t2; + let t5 = y4 & u7; + let t6 = t5 ^ t2; + let t7 = y13 & y16; + let t8 = y5 & y1; + let t9 = t8 ^ t7; + let t10 = y2 & y7; + let t11 = t10 ^ t7; + let t12 = y9 & y11; + let t13 = y14 & y17; + let t14 = t13 ^ t12; + let t15 = y8 & y10; + let t16 = t15 ^ t12; + let t17 = t4 ^ y20; + let t18 = t6 ^ t16; + let t19 = t9 ^ t14; + let t20 = t11 ^ t16; + let t21 = t17 ^ t14; + let t22 = t18 ^ y19; + let t23 = t19 ^ y21; + let t24 = t20 ^ y18; + let t25 = t21 ^ t22; + let t26 = t21 & t23; + let t27 = t24 ^ t26; + let t28 = t25 & t27; + let t29 = t28 ^ t22; + let t30 = t23 ^ t24; + let t31 = t22 ^ t26; + let t32 = t31 & t30; + let t33 = t32 ^ t24; + let t34 = t23 ^ t33; + let t35 = t27 ^ t33; + let t36 = t24 & t35; + // `cargo mutants` reports the `^ -> |` mutant on the next line as surviving. That is a true + // equivalence, not a gap: `t36` and `t34` are never both 1 for any of the 256 possible input + // bytes, so XOR and OR agree here. It is the same gate the AES, SM4 and Camellia crates + // document, since this section is copied from there. + let t37 = t36 ^ t34; + let t38 = t27 ^ t36; + let t39 = t29 & t38; + let t40 = t25 ^ t39; + let t41 = t40 ^ t37; + let t42 = t29 ^ t33; + let t43 = t29 ^ t40; + let t44 = t33 ^ t37; + let t45 = t42 ^ t41; + let z0 = t44 & y15; + let z1 = t37 & y6; + let z2 = t33 & u7; + let z3 = t43 & y16; + let z4 = t40 & y1; + let z5 = t29 & y7; + let z6 = t42 & y11; + let z7 = t45 & y17; + let z8 = t41 & y10; + let z9 = t44 & y12; + let z10 = t37 & y3; + let z11 = t33 & y4; + let z12 = t43 & y13; + let z13 = t40 & y5; + let z14 = t29 & y2; + let z15 = t42 & y9; + let z16 = t45 & y14; + let z17 = t41 & y8; + + // Bottom affine layer (30 gates, generated): tower field back to the AES polynomial basis + // and the output affine map `N * . xor 0x00`. Plane j receives bit j of the output. + let b1 = z0 ^ z6; + let b2 = z4 ^ z10; + let b3 = z1 ^ z9; + let b4 = z3 ^ b2; + let b5 = z8 ^ b1; + let b6 = z12 ^ z13; + let b7 = z2 ^ b5; + let b8 = z6 ^ z7; + let b9 = z9 ^ b6; + let b10 = z11 ^ z17; + let b11 = z15 ^ b3; + let b12 = b4 ^ b8; + let b13 = b9 ^ b12; + let b14 = z5 ^ z16; + let b15 = b14 ^ b2; + let b16 = b15 ^ b5; + let b17 = b16 ^ b11; + let b18 = z10 ^ b9; + let b19 = z13 ^ z14; + let b20 = b19 ^ z16; + let b21 = b20 ^ z17; + let b22 = b21 ^ b7; + let b23 = z0 ^ b3; + let b24 = b23 ^ b4; + let b25 = b24 ^ b6; + let b26 = z16 ^ b10; + let b27 = b26 ^ b12; + let b28 = z7 ^ b1; + let b29 = b28 ^ b10; + let b30 = b29 ^ b11; + q[0] = b13; + q[1] = b17; + q[2] = b18; + q[3] = b7; + q[4] = b22; + q[5] = b25; + q[6] = b27; + q[7] = b30; +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use crate::bitslice::ortho; + + /// Sec 2.4.2 `SB1`, row-major: `SB1[16 * row + column]`. Extracted mechanically from the text + /// of RFC 5794; byte-for-byte BC Java's `SB1_sbox`. Test data only -- the engine never reads it. + #[rustfmt::skip] + pub(crate) const SB1: [u8; 256] = [ + 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, + 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, + 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, + 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, + 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, + 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, + 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, + 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, + 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, + 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, + 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, + 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, + 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, + 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, + 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, + 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16, + ]; + + /// Sec 2.4.2 `SB2`; byte-for-byte BC Java's `SB2_sbox`. Test data only. + #[rustfmt::skip] + pub(crate) const SB2: [u8; 256] = [ + 0xe2, 0x4e, 0x54, 0xfc, 0x94, 0xc2, 0x4a, 0xcc, 0x62, 0x0d, 0x6a, 0x46, 0x3c, 0x4d, 0x8b, 0xd1, + 0x5e, 0xfa, 0x64, 0xcb, 0xb4, 0x97, 0xbe, 0x2b, 0xbc, 0x77, 0x2e, 0x03, 0xd3, 0x19, 0x59, 0xc1, + 0x1d, 0x06, 0x41, 0x6b, 0x55, 0xf0, 0x99, 0x69, 0xea, 0x9c, 0x18, 0xae, 0x63, 0xdf, 0xe7, 0xbb, + 0x00, 0x73, 0x66, 0xfb, 0x96, 0x4c, 0x85, 0xe4, 0x3a, 0x09, 0x45, 0xaa, 0x0f, 0xee, 0x10, 0xeb, + 0x2d, 0x7f, 0xf4, 0x29, 0xac, 0xcf, 0xad, 0x91, 0x8d, 0x78, 0xc8, 0x95, 0xf9, 0x2f, 0xce, 0xcd, + 0x08, 0x7a, 0x88, 0x38, 0x5c, 0x83, 0x2a, 0x28, 0x47, 0xdb, 0xb8, 0xc7, 0x93, 0xa4, 0x12, 0x53, + 0xff, 0x87, 0x0e, 0x31, 0x36, 0x21, 0x58, 0x48, 0x01, 0x8e, 0x37, 0x74, 0x32, 0xca, 0xe9, 0xb1, + 0xb7, 0xab, 0x0c, 0xd7, 0xc4, 0x56, 0x42, 0x26, 0x07, 0x98, 0x60, 0xd9, 0xb6, 0xb9, 0x11, 0x40, + 0xec, 0x20, 0x8c, 0xbd, 0xa0, 0xc9, 0x84, 0x04, 0x49, 0x23, 0xf1, 0x4f, 0x50, 0x1f, 0x13, 0xdc, + 0xd8, 0xc0, 0x9e, 0x57, 0xe3, 0xc3, 0x7b, 0x65, 0x3b, 0x02, 0x8f, 0x3e, 0xe8, 0x25, 0x92, 0xe5, + 0x15, 0xdd, 0xfd, 0x17, 0xa9, 0xbf, 0xd4, 0x9a, 0x7e, 0xc5, 0x39, 0x67, 0xfe, 0x76, 0x9d, 0x43, + 0xa7, 0xe1, 0xd0, 0xf5, 0x68, 0xf2, 0x1b, 0x34, 0x70, 0x05, 0xa3, 0x8a, 0xd5, 0x79, 0x86, 0xa8, + 0x30, 0xc6, 0x51, 0x4b, 0x1e, 0xa6, 0x27, 0xf6, 0x35, 0xd2, 0x6e, 0x24, 0x16, 0x82, 0x5f, 0xda, + 0xe6, 0x75, 0xa2, 0xef, 0x2c, 0xb2, 0x1c, 0x9f, 0x5d, 0x6f, 0x80, 0x0a, 0x72, 0x44, 0x9b, 0x6c, + 0x90, 0x0b, 0x5b, 0x33, 0x7d, 0x5a, 0x52, 0xf3, 0x61, 0xa1, 0xf7, 0xb0, 0xd6, 0x3f, 0x7c, 0x6d, + 0xed, 0x14, 0xe0, 0xa5, 0x3d, 0x22, 0xb3, 0xf8, 0x89, 0xde, 0x71, 0x1a, 0xaf, 0xba, 0xb5, 0x81, + ]; + + /// Sec 2.4.2 `SB3`; byte-for-byte BC Java's `SB3_sbox`. Test data only. + #[rustfmt::skip] + pub(crate) const SB3: [u8; 256] = [ + 0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, + 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, + 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, + 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25, + 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, + 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, + 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06, + 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, + 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, + 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e, + 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, + 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, + 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f, + 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, + 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, + 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d, + ]; + + /// Sec 2.4.2 `SB4`; byte-for-byte BC Java's `SB4_sbox`. Test data only. (The RFC prints the + /// entry for `0x0d` as a bare `9`; it is `0x09`.) + #[rustfmt::skip] + pub(crate) const SB4: [u8; 256] = [ + 0x30, 0x68, 0x99, 0x1b, 0x87, 0xb9, 0x21, 0x78, 0x50, 0x39, 0xdb, 0xe1, 0x72, 0x09, 0x62, 0x3c, + 0x3e, 0x7e, 0x5e, 0x8e, 0xf1, 0xa0, 0xcc, 0xa3, 0x2a, 0x1d, 0xfb, 0xb6, 0xd6, 0x20, 0xc4, 0x8d, + 0x81, 0x65, 0xf5, 0x89, 0xcb, 0x9d, 0x77, 0xc6, 0x57, 0x43, 0x56, 0x17, 0xd4, 0x40, 0x1a, 0x4d, + 0xc0, 0x63, 0x6c, 0xe3, 0xb7, 0xc8, 0x64, 0x6a, 0x53, 0xaa, 0x38, 0x98, 0x0c, 0xf4, 0x9b, 0xed, + 0x7f, 0x22, 0x76, 0xaf, 0xdd, 0x3a, 0x0b, 0x58, 0x67, 0x88, 0x06, 0xc3, 0x35, 0x0d, 0x01, 0x8b, + 0x8c, 0xc2, 0xe6, 0x5f, 0x02, 0x24, 0x75, 0x93, 0x66, 0x1e, 0xe5, 0xe2, 0x54, 0xd8, 0x10, 0xce, + 0x7a, 0xe8, 0x08, 0x2c, 0x12, 0x97, 0x32, 0xab, 0xb4, 0x27, 0x0a, 0x23, 0xdf, 0xef, 0xca, 0xd9, + 0xb8, 0xfa, 0xdc, 0x31, 0x6b, 0xd1, 0xad, 0x19, 0x49, 0xbd, 0x51, 0x96, 0xee, 0xe4, 0xa8, 0x41, + 0xda, 0xff, 0xcd, 0x55, 0x86, 0x36, 0xbe, 0x61, 0x52, 0xf8, 0xbb, 0x0e, 0x82, 0x48, 0x69, 0x9a, + 0xe0, 0x47, 0x9e, 0x5c, 0x04, 0x4b, 0x34, 0x15, 0x79, 0x26, 0xa7, 0xde, 0x29, 0xae, 0x92, 0xd7, + 0x84, 0xe9, 0xd2, 0xba, 0x5d, 0xf3, 0xc5, 0xb0, 0xbf, 0xa4, 0x3b, 0x71, 0x44, 0x46, 0x2b, 0xfc, + 0xeb, 0x6f, 0xd5, 0xf6, 0x14, 0xfe, 0x7c, 0x70, 0x5a, 0x7d, 0xfd, 0x2f, 0x18, 0x83, 0x16, 0xa5, + 0x91, 0x1f, 0x05, 0x95, 0x74, 0xa9, 0xc1, 0x5b, 0x4a, 0x85, 0x6d, 0x13, 0x07, 0x4f, 0x4e, 0x45, + 0xb2, 0x0f, 0xc9, 0x1c, 0xa6, 0xbc, 0xec, 0x73, 0x90, 0x7b, 0xcf, 0x59, 0x8f, 0xa1, 0xf9, 0x2d, + 0xf2, 0xb1, 0x00, 0x94, 0x37, 0x9f, 0xd0, 0x2e, 0x9c, 0x6e, 0x28, 0x3f, 0x80, 0xf0, 0x3d, 0xd3, + 0x25, 0x8a, 0xb5, 0xe7, 0x42, 0xb3, 0xc7, 0xea, 0xf7, 0x4c, 0x11, 0x33, 0x03, 0xa2, 0xac, 0x60, + ]; + + #[test] + fn test_tables_match_the_worked_examples() { + // Sec 2.4.2: "SB1(0x23) = 0x26 and SB4(0xef) = 0xd3". + assert_eq!(SB1[0x23], 0x26); + assert_eq!(SB4[0xef], 0xd3); + // "SB3 and SB4 are the inverse functions of SB1 and SB2, respectively." + for x in 0..=255u8 { + assert_eq!(SB3[SB1[x as usize] as usize], x); + assert_eq!(SB1[SB3[x as usize] as usize], x); + assert_eq!(SB4[SB2[x as usize] as usize], x); + assert_eq!(SB2[SB4[x as usize] as usize], x); + } + } + + #[test] + fn test_sb1_is_the_aes_sbox() { + // FIPS 197 Sec 5.1.1's worked example, S(0x53) = 0xed, and Table 4's first and last rows. + assert_eq!(SB1[0x53], 0xed); + assert_eq!(&SB1[..4], &[0x63, 0x7c, 0x77, 0x7b]); + assert_eq!(&SB1[252..], &[0xb0, 0x54, 0xbb, 0x16]); + } + + /// Runs a circuit on every one of the 256 byte values -- 16 positions per pass, sixteen + /// passes -- and checks each against the table. Exhaustive; must stay so. + fn check_exhaustive(name: &str, circuit: fn(&mut Planes), table: &[u8; 256]) { + for pass in 0..16u16 { + let mut q: Planes = core::array::from_fn(|w| { + let base = 16 * pass + 2 * w as u16; + u16::from_le_bytes([base as u8, base as u8 + 1]) + }); + let inputs = q; + ortho(&mut q); + circuit(&mut q); + ortho(&mut q); + for (w, (got, input)) in q.iter().zip(inputs.iter()).enumerate() { + for (byte, expected) in got + .to_le_bytes() + .iter() + .zip(input.to_le_bytes().iter().map(|&b| table[b as usize])) + { + assert_eq!( + *byte, expected, + "{name}: pass {pass}, word {w}, input {input:#06x}" + ); + } + } + } + } + + #[test] + fn test_sboxes_match_rfc_5794_tables() { + check_exhaustive("SB1", sb1, &SB1); + check_exhaustive("SB2", sb2, &SB2); + check_exhaustive("SB3", sb3, &SB3); + check_exhaustive("SB4", sb4, &SB4); + } + + #[test] + fn test_sb3_and_sb4_invert_sb1_and_sb2_on_planes() { + // Sec 2.4.2's inverse relation, on the circuits themselves rather than the tables. + let mut seed = 0xACE1u16; + for _ in 0..64 { + let q0: Planes = core::array::from_fn(|_| { + seed ^= seed << 7; + seed ^= seed >> 9; + seed ^= seed << 8; + seed + }); + let mut q = q0; + sb1(&mut q); + sb3(&mut q); + assert_eq!(q, q0, "SB3(SB1(x)) = x"); + sb2(&mut q); + sb4(&mut q); + assert_eq!(q, q0, "SB4(SB2(x)) = x"); + sb4(&mut q); + sb2(&mut q); + assert_eq!(q, q0, "SB2(SB4(x)) = x"); + } + } +} diff --git a/crypto/aria/src/schedule.rs b/crypto/aria/src/schedule.rs new file mode 100644 index 000000000..d8ead722f --- /dev/null +++ b/crypto/aria/src/schedule.rs @@ -0,0 +1,256 @@ +//! The key scheduling part (RFC 5794 Sec 2.2), and the parameter sets that distinguish the three +//! key lengths. +//! +//! The schedule is the encryption round keys `ek1 .. ek{n+1}` -- 13, 15 or 17 of them, 16 bytes +//! each (208, 240 or 272 bytes) -- computed once by [`expand`] and stored in a [`Secret`]. +//! Decryption round keys are derived from them (Sec 2.2: `dk1 = ek{n+1}`, `dk{i} = A(ek{n+2-i})` +//! for `i = 2 .. n`, `dk{n+1} = ek1`), and this port derives them *at use*: one application of the +//! diffusion layer `A` per round, cheap next to the substitution layer, rather than a second +//! stored schedule, unlike a direction-aware engine that lays the keys out differently per +//! direction at initialisation. +//! +//! # Layout +//! +//! One flat `[u32; 4 * (ROUNDS + 1)]`: `ek_i` is the four big-endian words at +//! `[4 * (i - 1) .. 4 * i]`. [`ek`] takes the RFC's 1-based index. +//! +//! # Constant-time +//! +//! The schedule routes secret key material through `FO` and `FE` three times, so the expansion +//! needs the same treatment as the cipher: those calls go through the bit-sliced S-box circuits of +//! [`crate::sbox`], not tables. The rest of the schedule is XORs and rotations of 128-bit values +//! by public amounts. + +use crate::round::{RoundKey, State, fe1, fo1}; +use bouncycastle_utils::secret::{Secret, ZeroizablePrimitive}; + +/// Sealed: the three parameter sets are the only implementors. +mod sealed { + pub trait ARIAParamsInternalTrait {} +} +pub(crate) use sealed::ARIAParamsInternalTrait; + +/// The parameters that distinguish ARIA-128 from ARIA-192 and ARIA-256 (RFC 5794 Sec 2.2). +/// +/// Sealed: implemented by `ARIA128Params`, `ARIA192Params` and `ARIA256Params` only, so +/// there is no fourth instantiation. +pub trait ARIAParams: ARIAParamsInternalTrait { + /// Key length in bytes: 16, 24 or 32. + const KEY_LEN: usize; + /// Number of rounds: 12, 14 or 16 (Sec 2.2, "Key size / Number of Rounds"). + const ROUNDS: usize; + /// `CK1, CK2, CK3` as indices into `C` (Sec 2.2's table: `C1 C2 C3` for 128-bit keys, + /// `C2 C3 C1` for 192, `C3 C1 C2` for 256). + const CK: [usize; 3]; + /// The algorithm name, as reported by `Algorithm::ALG_NAME`. + const ALG_NAME: &'static str; + /// `[u32; 4 * (ROUNDS + 1)]` -- the encryption round keys. See the module docs for the layout. + type Schedule: ZeroizablePrimitive + AsRef<[u32]> + AsMut<[u32]>; +} + +/// ARIA-128 parameters: 16-byte key, 12 rounds, `CK = (C1, C2, C3)`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ARIA128Params; +/// ARIA-192 parameters: 24-byte key, 14 rounds, `CK = (C2, C3, C1)`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ARIA192Params; +/// ARIA-256 parameters: 32-byte key, 16 rounds, `CK = (C3, C1, C2)`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ARIA256Params; + +impl ARIAParamsInternalTrait for ARIA128Params {} +impl ARIAParamsInternalTrait for ARIA192Params {} +impl ARIAParamsInternalTrait for ARIA256Params {} + +impl ARIAParams for ARIA128Params { + const KEY_LEN: usize = 16; + const ROUNDS: usize = 12; + const CK: [usize; 3] = [0, 1, 2]; + const ALG_NAME: &'static str = "ARIA-128"; + type Schedule = [u32; 52]; // 4 * (12 + 1) +} + +impl ARIAParams for ARIA192Params { + const KEY_LEN: usize = 24; + const ROUNDS: usize = 14; + const CK: [usize; 3] = [1, 2, 0]; + const ALG_NAME: &'static str = "ARIA-192"; + type Schedule = [u32; 60]; // 4 * (14 + 1) +} + +impl ARIAParams for ARIA256Params { + const KEY_LEN: usize = 32; + const ROUNDS: usize = 16; + const CK: [usize; 3] = [2, 0, 1]; + const ALG_NAME: &'static str = "ARIA-256"; + type Schedule = [u32; 68]; // 4 * (16 + 1) +} + +/// `C1, C2, C3` (Sec 2.2): "the first 128*3 bits of the fractional part of 1/PI". Transcribed from +/// the text of RFC 5794. +pub(crate) const C: [u128; 3] = [ + 0x517c_c1b7_2722_0a94_fe13_abe8_fa9a_6ee0, + 0x6db1_4acc_9e21_c820_ff28_b1d5_ef5d_e2b0, + 0xdb92_371d_2126_e970_0324_9775_04e8_c90e, +]; + +/// A 128-bit value as four big-endian row words. +#[inline(always)] +fn to_state(v: u128) -> State { + let b = v.to_be_bytes(); + let (words, _) = b.as_chunks::<4>(); + core::array::from_fn(|i| u32::from_be_bytes(words[i])) +} + +/// Four big-endian row words as a 128-bit value. +#[inline(always)] +fn to_u128(s: &State) -> u128 { + let mut b = [0u8; 16]; + for (chunk, w) in b.as_chunks_mut::<4>().0.iter_mut().zip(s.iter()) { + *chunk = w.to_be_bytes(); + } + u128::from_be_bytes(b) +} + +/// `ek_i`, `i` in `1 ..= ROUNDS + 1`, as a round key. +#[inline(always)] +pub(crate) fn ek(schedule: &[u32], i: usize) -> RoundKey { + let base = 4 * (i - 1); + [schedule[base], schedule[base + 1], schedule[base + 2], schedule[base + 3]] +} + +/// Expands a key into the encryption round keys (Sec 2.2). `key` is exactly `P::KEY_LEN` bytes -- +/// the engine checks that before calling. +/// +/// Line by line against Sec 2.2: +/// +/// * `KL || KR = K || 0 ... 0` -- `KL` is the leftmost 128 bits of `K`, `KR` the rest, +/// right-padded with zeros to 128 bits; +/// * `W0 = KL; W1 = FO(W0, CK1) ^ KR; W2 = FE(W1, CK2) ^ W0; W3 = FO(W2, CK3) ^ W1` -- the +/// "3-round, 256-bit Feistel cipher" with the constants `CK1 .. CK3` chosen by key length; +/// * `ek1 = W0 ^ (W1 >>> 19)`, `ek2 = W1 ^ (W2 >>> 19)`, `ek3 = W2 ^ (W3 >>> 19)`, +/// `ek4 = (W0 >>> 19) ^ W3`, then the same four pairings with `>>> 31`, `<<< 61`, `<<< 31`, +/// and `ek17 = W0 ^ (W1 <<< 19)` -- of which only the first `ROUNDS + 1` are kept. +/// +/// `KL`, `KR` and `W0 .. W3` are held in a `Secret` so they are scrubbed on return. +pub(crate) fn expand(key: &[u8]) -> Secret { + // KL || KR = K || 0...0: copy the key into a zeroed 32-byte buffer. + let mut kbuf = Secret::<[u8; 32]>::new(); + kbuf[..key.len()].copy_from_slice(key); + // [KL, KR, W0, W1, W2, W3] + let mut w = Secret::<[u128; 6]>::new(); + w[0] = u128::from_be_bytes(kbuf[..16].try_into().expect("16 bytes")); // KL + w[1] = u128::from_be_bytes(kbuf[16..].try_into().expect("16 bytes")); // KR (zero-padded) + let ck: [RoundKey; 3] = core::array::from_fn(|i| to_state(C[P::CK[i]])); + w[2] = w[0]; // W0 = KL + w[3] = to_u128(&fo1(to_state(w[2]), &ck[0])) ^ w[1]; // W1 = FO(W0, CK1) ^ KR + w[4] = to_u128(&fe1(to_state(w[3]), &ck[1])) ^ w[2]; // W2 = FE(W1, CK2) ^ W0 + w[5] = to_u128(&fo1(to_state(w[4]), &ck[2])) ^ w[3]; // W3 = FO(W2, CK3) ^ W1 + let (w0, w1, w2, w3) = (w[2], w[3], w[4], w[5]); + + // The 17 candidate round keys, in order; the schedule keeps the first ROUNDS + 1. + let all: [u128; 17] = [ + w0 ^ w1.rotate_right(19), // ek1 = W0 ^ (W1 >>> 19) + w1 ^ w2.rotate_right(19), // ek2 = W1 ^ (W2 >>> 19) + w2 ^ w3.rotate_right(19), // ek3 = W2 ^ (W3 >>> 19) + w0.rotate_right(19) ^ w3, // ek4 = (W0 >>> 19) ^ W3 + w0 ^ w1.rotate_right(31), // ek5 = W0 ^ (W1 >>> 31) + w1 ^ w2.rotate_right(31), // ek6 = W1 ^ (W2 >>> 31) + w2 ^ w3.rotate_right(31), // ek7 = W2 ^ (W3 >>> 31) + w0.rotate_right(31) ^ w3, // ek8 = (W0 >>> 31) ^ W3 + w0 ^ w1.rotate_left(61), // ek9 = W0 ^ (W1 <<< 61) + w1 ^ w2.rotate_left(61), // ek10 = W1 ^ (W2 <<< 61) + w2 ^ w3.rotate_left(61), // ek11 = W2 ^ (W3 <<< 61) + w0.rotate_left(61) ^ w3, // ek12 = (W0 <<< 61) ^ W3 + w0 ^ w1.rotate_left(31), // ek13 = W0 ^ (W1 <<< 31) + w1 ^ w2.rotate_left(31), // ek14 = W1 ^ (W2 <<< 31) + w2 ^ w3.rotate_left(31), // ek15 = W2 ^ (W3 <<< 31) + w0.rotate_left(31) ^ w3, // ek16 = (W0 <<< 31) ^ W3 + w0 ^ w1.rotate_left(19), // ek17 = W0 ^ (W1 <<< 19) + ]; + let mut schedule = Secret::::new(); + for (i, chunk) in schedule.as_mut().as_chunks_mut::<4>().0.iter_mut().enumerate() { + *chunk = to_state(all[i]); + } + schedule +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_schedule_lengths_and_ck_orders() { + assert_eq!(size_of::<::Schedule>(), 4 * 4 * 13); + assert_eq!(size_of::<::Schedule>(), 4 * 4 * 15); + assert_eq!(size_of::<::Schedule>(), 4 * 4 * 17); + // Sec 2.2's table: 128 -> C1 C2 C3, 192 -> C2 C3 C1, 256 -> C3 C1 C2. + assert_eq!(ARIA128Params::CK, [0, 1, 2]); + assert_eq!(ARIA192Params::CK, [1, 2, 0]); + assert_eq!(ARIA256Params::CK, [2, 0, 1]); + } + + #[test] + fn test_constants_match_section_2_2() { + assert_eq!(C[0], 0x517cc1b727220a94fe13abe8fa9a6ee0); + assert_eq!(C[1], 0x6db14acc9e21c820ff28b1d5ef5de2b0); + assert_eq!(C[2], 0xdb92371d2126e9700324977504e8c90e); + } + + #[test] + fn test_state_conversions_round_trip() { + let v = 0x0001_0203_0405_0607_0809_0a0b_0c0d_0e0fu128; + assert_eq!(to_state(v), [0x0001_0203, 0x0405_0607, 0x0809_0a0b, 0x0c0d_0e0f]); + assert_eq!(to_u128(&to_state(v)), v); + } + + /// Appendix A.1 (128-bit key): the round key generators `W0 .. W3` and all thirteen + /// encryption round keys, transcribed from the RFC. + #[test] + fn test_appendix_a_1_round_keys() { + let key: [u8; 16] = core::array::from_fn(|i| i as u8); + let s = expand::(&key); + let expected: [u128; 13] = [ + 0xd415a75c794b85c5e0d2a0b3cb793bf6, + 0x369c65e4b11777ab713a3e1e6601b8f4, + 0x0368d4f13d14497b6529ad7ac809e7d0, + 0xc644552b549a263fb8d0b50906229eec, + 0x5f9c434951f2d2ef342787b1a781794c, + 0xafea2c0ce71db6de42a47461f4323c54, + 0x324286db44ba4db6c44ac306f2a84b2c, + 0x7f9fa93574d842b9101a58063771eb7b, + 0xaab9c57731fcd213ad5677458fcfe6d4, + 0x2f4423bb06465abada5694a19eb88459, + 0x9f8772808f5d580d810ef8ddac13abeb, + 0x8684946a155be77ef810744847e35fad, + 0x0f0aa16daee61bd7dfee5a599970fb35, + ]; + for (i, want) in expected.iter().enumerate() { + assert_eq!(to_u128(&ek(s.as_ref(), i + 1)), *want, "ek{}", i + 1); + } + // W0 .. W3 are not stored, but ek1 = W0 ^ (W1 >>> 19) with W0 = KL pins W1, and so on. + let w0 = 0x000102030405060708090a0b0c0d0e0fu128; + let w1 = 0x2afbea741e1746dd55c63ba1afcea0a5u128; + let w2 = 0x7c8578018bb127e02dfe4e78c288e33cu128; + let w3 = 0x6785b52b74da46bf181054082763ff6du128; + assert_eq!(expected[0], w0 ^ w1.rotate_right(19)); + assert_eq!(expected[1], w1 ^ w2.rotate_right(19)); + assert_eq!(expected[2], w2 ^ w3.rotate_right(19)); + assert_eq!(expected[3], w0.rotate_right(19) ^ w3); + assert_eq!(expected[12], w0 ^ w1.rotate_left(31)); + } + + #[test] + fn test_longer_keys_use_more_round_keys() { + let key: [u8; 32] = core::array::from_fn(|i| i as u8); + let s128 = expand::(&key[..16]); + let s192 = expand::(&key[..24]); + let s256 = expand::(&key); + assert_eq!(s128.as_ref().len(), 52); + assert_eq!(s192.as_ref().len(), 60); + assert_eq!(s256.as_ref().len(), 68); + // Different CK orders: the schedules differ even where the key bytes agree. + assert_ne!(ek(s128.as_ref(), 1), ek(s192.as_ref(), 1)); + assert_ne!(ek(s192.as_ref(), 1), ek(s256.as_ref(), 1)); + } +} diff --git a/crypto/aria/tests/alg_name_tests.rs b/crypto/aria/tests/alg_name_tests.rs new file mode 100644 index 000000000..d4b985165 --- /dev/null +++ b/crypto/aria/tests/alg_name_tests.rs @@ -0,0 +1,185 @@ +//! What each public ARIA alias is: the algorithm it reports and the type it resolves to. +//! +//! Each alias reports its own algorithm through `Algorithm`, not the engine it is built on: +//! `ARIA_CBC_128` is `"ARIA_CBC_128"`, while `ARIA128Internal` stays `"ARIA-128"`. The strength +//! is the key's. The direction and the padding are type parameters of one algorithm, not +//! different algorithms, so every combination of them must report the same name. + +use bouncycastle_aria::hazmat::{ARIA128Internal, ARIA192Internal, ARIA256Internal}; +use bouncycastle_aria::{ + ARIA_CBC_128, ARIA_CBC_192, ARIA_CBC_256, ARIA_CFB_128, ARIA_CFB_192, ARIA_CFB_256, + ARIA_CFB8_128, ARIA_CFB8_192, ARIA_CFB8_256, ARIA_CTR_128, ARIA_CTR_192, ARIA_CTR_256, + ARIA128Key, BLOCK_LEN, CTR_NONCE_LEN, +}; +use bouncycastle_cipher::modes::{CBC, CFB, CFB8, CTR, Cbc, Cfb, Cfb8, Ctr}; +use bouncycastle_cipher::padding::{ + NoPadding, PKCS7, PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor, +}; +use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::symmetric_key::{ + FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey, +}; +use bouncycastle_core::traits::Algorithm; +use bouncycastle_rng::HashDRBG_SHA512; +use core::mem::size_of; + +/// Both directions of one algorithm report `name` and `strength`. +fn check(name: &str, strength: SecurityStrength) { + assert_eq!(Enc::ALG_NAME, name); + assert_eq!(Dec::ALG_NAME, name); + assert_eq!(Enc::MAX_SECURITY_STRENGTH, strength, "{name}"); + assert_eq!(Dec::MAX_SECURITY_STRENGTH, strength, "{name}"); +} + +#[test] +fn cbc() { + use SecurityStrength::*; + check::, ARIA_CBC_128<'_, Decrypting, PKCS7>>( + "ARIA_CBC_128", _128bit, + ); + check::, ARIA_CBC_128<'_, Decrypting, NoPadding>>( + "ARIA_CBC_128", _128bit, + ); + check::, ARIA_CBC_192<'_, Decrypting, PKCS7>>( + "ARIA_CBC_192", _192bit, + ); + check::, ARIA_CBC_256<'_, Decrypting, PKCS7>>( + "ARIA_CBC_256", _256bit, + ); +} + +#[test] +fn cfb_cfb8_ctr() { + use SecurityStrength::*; + check::, ARIA_CFB_128<'_, Decrypting>>("ARIA_CFB_128", _128bit); + check::, ARIA_CFB_192<'_, Decrypting>>("ARIA_CFB_192", _192bit); + check::, ARIA_CFB_256<'_, Decrypting>>("ARIA_CFB_256", _256bit); + check::, ARIA_CFB8_128<'_, Decrypting>>("ARIA_CFB8_128", _128bit); + check::, ARIA_CFB8_192<'_, Decrypting>>("ARIA_CFB8_192", _192bit); + check::, ARIA_CFB8_256<'_, Decrypting>>("ARIA_CFB8_256", _256bit); + check::, ARIA_CTR_128<'_, Decrypting>>("ARIA_CTR_128", _128bit); + check::, ARIA_CTR_192<'_, Decrypting>>("ARIA_CTR_192", _192bit); + check::, ARIA_CTR_256<'_, Decrypting>>("ARIA_CTR_256", _256bit); +} + +#[test] +fn the_engines_keep_their_own_names() { + assert_eq!(ARIA128Internal::ALG_NAME, "ARIA-128"); + assert_eq!(ARIA192Internal::ALG_NAME, "ARIA-192"); + assert_eq!(ARIA256Internal::ALG_NAME, "ARIA-256"); + assert_eq!(ARIA128Internal::MAX_SECURITY_STRENGTH, SecurityStrength::_128bit); + assert_eq!(ARIA192Internal::MAX_SECURITY_STRENGTH, SecurityStrength::_192bit); + assert_eq!(ARIA256Internal::MAX_SECURITY_STRENGTH, SecurityStrength::_256bit); +} + +/// The aliases must resolve to exactly the types they claim to, at both directions, and be what +/// the keys hand out. A type alias that quietly resolved to something else would still compile +/// everywhere it is used, so this pins the projection by asserting the layouts coincide with the +/// fully spelled-out types. +#[test] +fn the_aliases_name_the_expected_types() { + assert_eq!( + size_of::>(), + size_of::< + PaddedBlockCipherEncryptor< + Cbc<'_, ARIA128Internal, Encrypting, BLOCK_LEN>, + PKCS7, + BLOCK_LEN, + BLOCK_LEN, + >, + >() + ); + assert_eq!( + size_of::>(), + size_of::< + PaddedBlockCipherDecryptor< + Cbc<'_, ARIA192Internal, Decrypting, BLOCK_LEN>, + PKCS7, + BLOCK_LEN, + BLOCK_LEN, + >, + >() + ); + assert_eq!( + size_of::>(), + size_of::< + PaddedBlockCipherEncryptor< + Cbc<'_, ARIA256Internal, Encrypting, BLOCK_LEN>, + NoPadding, + BLOCK_LEN, + BLOCK_LEN, + >, + >() + ); + // The two directions are genuinely different types, so the encryptor and the decryptor do not + // have to agree in size -- and here they do not, which is itself evidence the projection + // selected two different adapters rather than one. + assert_ne!( + size_of::>(), + size_of::>() + ); + + assert_eq!( + size_of::>(), + size_of::>() + ); + assert_eq!( + size_of::>(), + size_of::>() + ); + assert_eq!( + size_of::>(), + size_of::>() + ); + assert_eq!( + size_of::>(), + size_of::>() + ); + assert_eq!( + size_of::>(), + size_of::>() + ); + assert_eq!( + size_of::>(), + size_of::>() + ); + assert_eq!( + size_of::>(), + size_of::>() + ); + assert_eq!( + size_of::>(), + size_of::>() + ); + assert_eq!( + size_of::>(), + size_of::>() + ); + + // ...and the key hands out exactly those types. + let bytes: [u8; 16] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); + let material = + KeyMaterial::<16>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).unwrap(); + let mut rng = HashDRBG_SHA512::new_from_os(); + let key = ARIA128Key::>::from_key_material(&material).unwrap(); + let (enc, iv) = key.create_encryptor(&mut rng).unwrap(); + let _: ARIA_CBC_128<'_, Encrypting, PKCS7> = enc; + let _: ARIA_CBC_128<'_, Decrypting, PKCS7> = key.create_decryptor(&iv); + let key = ARIA128Key::::from_key_material(&material).unwrap(); + let _: ARIA_CFB_128<'_, Encrypting> = key.create_encryptor(&mut rng).unwrap().0; + let key = ARIA128Key::::from_key_material(&material).unwrap(); + let _: ARIA_CFB8_128<'_, Encrypting> = key.create_encryptor(&mut rng).unwrap().0; + let key = ARIA128Key::::from_key_material(&material).unwrap(); + let (enc, nonce) = key.create_encryptor(&mut rng).unwrap(); + let _: ARIA_CTR_128<'_, Encrypting> = enc; + let _: ARIA_CTR_128<'_, Decrypting> = key.create_decryptor(&nonce); +} + +/// The const parameters are ARIA's, and CTR's nonce leaves the four-byte counter the mode caps at. +#[test] +fn the_const_parameters_are_arias() { + assert_eq!(BLOCK_LEN, 16); + assert_eq!(CTR_NONCE_LEN, 12, "a 12-byte nonce leaves the 4-byte counter CTR caps at"); +} diff --git a/crypto/aria/tests/bc_java_tests.rs b/crypto/aria/tests/bc_java_tests.rs new file mode 100644 index 000000000..6a72b626a --- /dev/null +++ b/crypto/aria/tests/bc_java_tests.rs @@ -0,0 +1,115 @@ +//! The checks of Bouncy Castle Java's `ARIATest` +//! (`core/src/test/java/org/bouncycastle/crypto/test/ARIATest.java`), run against this port. +//! +//! `ARIATest` does three things: `checkTestVectors_RFC5794()` runs the three Appendix A vectors in +//! both directions through `processBlock`; `checkRandomRoundtrips()` encrypts and decrypts a +//! chain of 100 random blocks for each key length; and `MyARIAEngine.checkImplementation()` checks +//! that `A` is an involution on 100 random inputs and that `SB3`/`SB4` invert `SB1`/`SB2` on all +//! 256 bytes. The first and second are reproduced here in the same terms (with a fixed +//! pseudo-random source in place of `SecureRandom`); the third is pinned inside the crate, where +//! `A` and the S-box circuits are visible (`round::tests` and `sbox::tests`), and its S-box half is +//! repeated here against the reference tables. + +mod common; + +use bouncycastle_aria::BLOCK_LEN; +use bouncycastle_aria::hazmat::{ARIA128Internal, ARIA192Internal, ARIA256Internal}; +use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use common::{SB1, SB2, bytes, sb3, sb4}; + +/// `TEST_VECTORS_RFC5794`: `(name, key, plaintext, ciphertext)`. +const TEST_VECTORS_RFC5794: [(&str, &str, &str, &str); 3] = [ + ( + "128-Bit Key", + "000102030405060708090a0b0c0d0e0f", + "00112233445566778899aabbccddeeff", + "d718fbd6ab644c739da95f3be6451778", + ), + ( + "192-Bit Key", + "000102030405060708090a0b0c0d0e0f1011121314151617", + "00112233445566778899aabbccddeeff", + "26449c1805dbe7aa25a468ce263a9e79", + ), + ( + "256-Bit Key", + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + "00112233445566778899aabbccddeeff", + "f92bd7c79fb72e2f2b8f80c1972d24fc", + ), +]; + +fn engine>(key: &[u8; KEY_LEN]) -> P { +

>::new( + &KeyMaterial::::from_bytes_as_type(key, KeyType::SymmetricCipherKey) + .expect("a valid symmetric cipher key"), + ) + .expect("a valid key") +} + +/// `checkTestVector_RFC5794(tv)`: `init(true, key)`, `processBlock`, compare ("Incorrect +/// ciphertext computed"); `init(false, key)`, `processBlock`, compare ("Incorrect plaintext"). +fn check_test_vector>( + name: &str, + key: &str, + plaintext: &str, + ciphertext: &str, +) { + let c: P = engine(&bytes::(key)); + let plaintext: [u8; BLOCK_LEN] = bytes(plaintext); + let ciphertext: [u8; BLOCK_LEN] = bytes(ciphertext); + let mut actual = plaintext; + c.encrypt_block(&mut actual); + assert_eq!(actual, ciphertext, "Incorrect ciphertext computed for '{name}'"); + c.decrypt_block(&mut actual); + assert_eq!(actual, plaintext, "Incorrect plaintext computed for '{name}'"); +} + +#[test] +fn check_test_vectors_rfc5794() { + let [v128, v192, v256] = TEST_VECTORS_RFC5794; + check_test_vector::<16, ARIA128Internal>(v128.0, v128.1, v128.2, v128.3); + check_test_vector::<24, ARIA192Internal>(v192.0, v192.1, v192.2, v192.3); + check_test_vector::<32, ARIA256Internal>(v256.0, v256.1, v256.2, v256.3); +} + +/// `checkRandomRoundtrips()`: for each key length, a random key and block; 100 times, encrypt, +/// decrypt, compare, and feed the ciphertext back in as the next plaintext. Run for 100 keys as +/// `performTest()` does. +fn random_roundtrips>(seed: &mut u32) { + let key: [u8; KEY_LEN] = common::pseudo_random(seed); + let ce: P = engine(&key); + let mut txt: [u8; BLOCK_LEN] = common::pseudo_random(seed); + for _ in 0..100 { + let mut enc = txt; + ce.encrypt_block(&mut enc); + let mut dec = enc; + ce.decrypt_block(&mut dec); + assert_eq!(txt, dec); + txt = enc; + } +} + +#[test] +fn check_random_roundtrips() { + let mut seed = 0xA51A_0001; + for _ in 0..100 { + random_roundtrips::<16, ARIA128Internal>(&mut seed); + random_roundtrips::<24, ARIA192Internal>(&mut seed); + random_roundtrips::<32, ARIA256Internal>(&mut seed); + } +} + +/// `checkSBoxes()`: `x == SB1(SB3(x))`, `x == SB3(SB1(x))`, `x == SB2(SB4(x))`, `x == SB4(SB2(x))` +/// for every byte, on the reference tables. +#[test] +fn check_sboxes() { + let (s3, s4) = (sb3(), sb4()); + for x in 0..=255u8 { + assert_eq!(SB1[s3[x as usize] as usize], x); + assert_eq!(s3[SB1[x as usize] as usize], x); + assert_eq!(SB2[s4[x as usize] as usize], x); + assert_eq!(s4[SB2[x as usize] as usize], x); + } +} diff --git a/crypto/aria/tests/common/mod.rs b/crypto/aria/tests/common/mod.rs new file mode 100644 index 000000000..248455f2d --- /dev/null +++ b/crypto/aria/tests/common/mod.rs @@ -0,0 +1,377 @@ +//! A table-driven reference ARIA, for cross-checking the constant-time engine. +//! +//! This is BC Java's `ARIAEngine` transcribed as directly as Rust allows: the four 256-byte S-box +//! tables of RFC 5794 Sec 2.4.2, the byte-array `A`, `SL1`, `SL2`, `FO`, `FE`, the byte-oriented +//! `keySchedule` with its `keyScheduleRound` rotations and the direction-dependent `reverseKeys` / +//! `A(rks[i])` for decryption, and the `processBlock` of the current engine, which fuses each +//! substitution with the diffusion layer through the "SWAR multiply-broadcast" masks `M_HI` / +//! `M_LO` (each substituted byte, multiplied by a mask of `0x01` bytes, lands on the seven output +//! positions of its column of `A`). It exists only in the tests, where its cache-timing behaviour +//! does not matter, and it is deliberately independent of the crate's own code: byte arrays and +//! 64-bit halves where the engine uses 32-bit row words, class words and bit-planes, so agreement +//! between the two is meaningful. +//! +//! It is itself checked against RFC 5794 Appendix A in `reference_sanity`, so a bug here cannot +//! silently validate a bug in the engine. + +#![allow(dead_code)] + +/// Sec 2.4.2 `SB1`; identical to BC Java's `SB1_sbox`. +#[rustfmt::skip] +pub const SB1: [u8; 256] = [ + 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, + 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, + 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, + 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, + 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, + 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, + 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, + 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, + 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, + 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, + 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, + 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, + 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, + 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, + 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, + 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16, +]; + +/// Sec 2.4.2 `SB2`; identical to BC Java's `SB2_sbox`. +#[rustfmt::skip] +pub const SB2: [u8; 256] = [ + 0xe2, 0x4e, 0x54, 0xfc, 0x94, 0xc2, 0x4a, 0xcc, 0x62, 0x0d, 0x6a, 0x46, 0x3c, 0x4d, 0x8b, 0xd1, + 0x5e, 0xfa, 0x64, 0xcb, 0xb4, 0x97, 0xbe, 0x2b, 0xbc, 0x77, 0x2e, 0x03, 0xd3, 0x19, 0x59, 0xc1, + 0x1d, 0x06, 0x41, 0x6b, 0x55, 0xf0, 0x99, 0x69, 0xea, 0x9c, 0x18, 0xae, 0x63, 0xdf, 0xe7, 0xbb, + 0x00, 0x73, 0x66, 0xfb, 0x96, 0x4c, 0x85, 0xe4, 0x3a, 0x09, 0x45, 0xaa, 0x0f, 0xee, 0x10, 0xeb, + 0x2d, 0x7f, 0xf4, 0x29, 0xac, 0xcf, 0xad, 0x91, 0x8d, 0x78, 0xc8, 0x95, 0xf9, 0x2f, 0xce, 0xcd, + 0x08, 0x7a, 0x88, 0x38, 0x5c, 0x83, 0x2a, 0x28, 0x47, 0xdb, 0xb8, 0xc7, 0x93, 0xa4, 0x12, 0x53, + 0xff, 0x87, 0x0e, 0x31, 0x36, 0x21, 0x58, 0x48, 0x01, 0x8e, 0x37, 0x74, 0x32, 0xca, 0xe9, 0xb1, + 0xb7, 0xab, 0x0c, 0xd7, 0xc4, 0x56, 0x42, 0x26, 0x07, 0x98, 0x60, 0xd9, 0xb6, 0xb9, 0x11, 0x40, + 0xec, 0x20, 0x8c, 0xbd, 0xa0, 0xc9, 0x84, 0x04, 0x49, 0x23, 0xf1, 0x4f, 0x50, 0x1f, 0x13, 0xdc, + 0xd8, 0xc0, 0x9e, 0x57, 0xe3, 0xc3, 0x7b, 0x65, 0x3b, 0x02, 0x8f, 0x3e, 0xe8, 0x25, 0x92, 0xe5, + 0x15, 0xdd, 0xfd, 0x17, 0xa9, 0xbf, 0xd4, 0x9a, 0x7e, 0xc5, 0x39, 0x67, 0xfe, 0x76, 0x9d, 0x43, + 0xa7, 0xe1, 0xd0, 0xf5, 0x68, 0xf2, 0x1b, 0x34, 0x70, 0x05, 0xa3, 0x8a, 0xd5, 0x79, 0x86, 0xa8, + 0x30, 0xc6, 0x51, 0x4b, 0x1e, 0xa6, 0x27, 0xf6, 0x35, 0xd2, 0x6e, 0x24, 0x16, 0x82, 0x5f, 0xda, + 0xe6, 0x75, 0xa2, 0xef, 0x2c, 0xb2, 0x1c, 0x9f, 0x5d, 0x6f, 0x80, 0x0a, 0x72, 0x44, 0x9b, 0x6c, + 0x90, 0x0b, 0x5b, 0x33, 0x7d, 0x5a, 0x52, 0xf3, 0x61, 0xa1, 0xf7, 0xb0, 0xd6, 0x3f, 0x7c, 0x6d, + 0xed, 0x14, 0xe0, 0xa5, 0x3d, 0x22, 0xb3, 0xf8, 0x89, 0xde, 0x71, 0x1a, 0xaf, 0xba, 0xb5, 0x81, +]; + +/// `SB3 = SB1^-1`, built as the Java's `SB3_sbox` table would be read: the inverse permutation. +pub fn sb3() -> [u8; 256] { + let mut t = [0u8; 256]; + for (x, &y) in SB1.iter().enumerate() { + t[y as usize] = x as u8; + } + t +} + +/// `SB4 = SB2^-1`. +pub fn sb4() -> [u8; 256] { + let mut t = [0u8; 256]; + for (x, &y) in SB2.iter().enumerate() { + t[y as usize] = x as u8; + } + t +} + +/// `C`: the three constants, as byte arrays as in the Java. +const C: [[u8; 16]; 3] = [ + 0x517cc1b727220a94fe13abe8fa9a6ee0u128.to_be_bytes(), + 0x6db14acc9e21c820ff28b1d5ef5de2b0u128.to_be_bytes(), + 0xdb92371d2126e9700324977504e8c90eu128.to_be_bytes(), +]; + +/// `ARIAEngine.A(byte[] z)`. +fn a(z: &mut [u8; 16]) { + let x = *z; + z[0] = x[3] ^ x[4] ^ x[6] ^ x[8] ^ x[9] ^ x[13] ^ x[14]; + z[1] = x[2] ^ x[5] ^ x[7] ^ x[8] ^ x[9] ^ x[12] ^ x[15]; + z[2] = x[1] ^ x[4] ^ x[6] ^ x[10] ^ x[11] ^ x[12] ^ x[15]; + z[3] = x[0] ^ x[5] ^ x[7] ^ x[10] ^ x[11] ^ x[13] ^ x[14]; + z[4] = x[0] ^ x[2] ^ x[5] ^ x[8] ^ x[11] ^ x[14] ^ x[15]; + z[5] = x[1] ^ x[3] ^ x[4] ^ x[9] ^ x[10] ^ x[14] ^ x[15]; + z[6] = x[0] ^ x[2] ^ x[7] ^ x[9] ^ x[10] ^ x[12] ^ x[13]; + z[7] = x[1] ^ x[3] ^ x[6] ^ x[8] ^ x[11] ^ x[12] ^ x[13]; + z[8] = x[0] ^ x[1] ^ x[4] ^ x[7] ^ x[10] ^ x[13] ^ x[15]; + z[9] = x[0] ^ x[1] ^ x[5] ^ x[6] ^ x[11] ^ x[12] ^ x[14]; + z[10] = x[2] ^ x[3] ^ x[5] ^ x[6] ^ x[8] ^ x[13] ^ x[15]; + z[11] = x[2] ^ x[3] ^ x[4] ^ x[7] ^ x[9] ^ x[12] ^ x[14]; + z[12] = x[1] ^ x[2] ^ x[6] ^ x[7] ^ x[9] ^ x[11] ^ x[12]; + z[13] = x[0] ^ x[3] ^ x[6] ^ x[7] ^ x[8] ^ x[10] ^ x[13]; + z[14] = x[0] ^ x[3] ^ x[4] ^ x[5] ^ x[9] ^ x[11] ^ x[14]; + z[15] = x[1] ^ x[2] ^ x[4] ^ x[5] ^ x[8] ^ x[10] ^ x[15]; +} + +/// `SL1(byte[] z)`. +fn sl1(z: &mut [u8; 16]) { + let (s3, s4) = (sb3(), sb4()); + for i in 0..16 { + z[i] = match i % 4 { + 0 => SB1[z[i] as usize], + 1 => SB2[z[i] as usize], + 2 => s3[z[i] as usize], + _ => s4[z[i] as usize], + }; + } +} + +/// `SL2(byte[] z)`. +fn sl2(z: &mut [u8; 16]) { + let (s3, s4) = (sb3(), sb4()); + for i in 0..16 { + z[i] = match i % 4 { + 0 => s3[z[i] as usize], + 1 => s4[z[i] as usize], + 2 => SB1[z[i] as usize], + _ => SB2[z[i] as usize], + }; + } +} + +fn xor(z: &mut [u8; 16], x: &[u8; 16]) { + for i in 0..16 { + z[i] ^= x[i]; + } +} + +/// `FO(byte[] D, byte[] RK)`. +fn fo(d: &mut [u8; 16], rk: &[u8; 16]) { + xor(d, rk); + sl1(d); + a(d); +} + +/// `FE(byte[] D, byte[] RK)`. +fn fe(d: &mut [u8; 16], rk: &[u8; 16]) { + xor(d, rk); + sl2(d); + a(d); +} + +/// `keyScheduleRound(rk, w, wr, n)`: `rk = w ^ (wr >>> n)`, byte by byte as the Java does it. +fn key_schedule_round(rk: &mut [u8; 16], w: &[u8; 16], wr: &[u8; 16], n: u32) { + let off = (n >> 3) as usize; + let right = n & 7; + let left = 8 - right; + let mut hi = wr[15 - off] as u32; + for to in 0..16 { + let lo = wr[(to + 16 - off) & 0xF] as u32; + let mut b = (hi << left) | (lo >> right); + b ^= w[to] as u32; + rk[to] = b as u8; + hi = lo; + } +} + +/// `keySchedule(forEncryption, K)`. +fn key_schedule(for_encryption: bool, k: &[u8]) -> Vec<[u8; 16]> { + let key_len = k.len(); + assert!((16..=32).contains(&key_len) && key_len % 8 == 0, "Key length not 128/192/256 bits."); + let key_len_idx = (key_len >> 3) - 2; + let ck1 = &C[key_len_idx]; + let ck2 = &C[(key_len_idx + 1) % 3]; + let ck3 = &C[(key_len_idx + 2) % 3]; + + let mut kl = [0u8; 16]; + let mut kr = [0u8; 16]; + kl.copy_from_slice(&k[..16]); + kr[..key_len - 16].copy_from_slice(&k[16..]); + + let w0 = kl; + let mut w1 = w0; + fo(&mut w1, ck1); + xor(&mut w1, &kr); + let mut w2 = w1; + fe(&mut w2, ck2); + xor(&mut w2, &w0); + let mut w3 = w2; + fo(&mut w3, ck3); + xor(&mut w3, &w1); + + let num_rounds = 12 + key_len_idx * 2; + let mut rks = vec![[0u8; 16]; num_rounds + 1]; + key_schedule_round(&mut rks[0], &w0, &w1, 19); + key_schedule_round(&mut rks[1], &w1, &w2, 19); + key_schedule_round(&mut rks[2], &w2, &w3, 19); + key_schedule_round(&mut rks[3], &w3, &w0, 19); + key_schedule_round(&mut rks[4], &w0, &w1, 31); + key_schedule_round(&mut rks[5], &w1, &w2, 31); + key_schedule_round(&mut rks[6], &w2, &w3, 31); + key_schedule_round(&mut rks[7], &w3, &w0, 31); + key_schedule_round(&mut rks[8], &w0, &w1, 67); + key_schedule_round(&mut rks[9], &w1, &w2, 67); + key_schedule_round(&mut rks[10], &w2, &w3, 67); + key_schedule_round(&mut rks[11], &w3, &w0, 67); + key_schedule_round(&mut rks[12], &w0, &w1, 97); + if num_rounds > 12 { + key_schedule_round(&mut rks[13], &w1, &w2, 97); + key_schedule_round(&mut rks[14], &w2, &w3, 97); + if num_rounds > 14 { + key_schedule_round(&mut rks[15], &w3, &w0, 97); + key_schedule_round(&mut rks[16], &w0, &w1, 109); + } + } + + if !for_encryption { + rks.reverse(); // reverseKeys + for rk in rks.iter_mut().take(num_rounds).skip(1) { + a(rk); + } + } + rks +} + +/// The Java engine after `init`: round keys as big-endian `long` halves, plus the broadcast masks. +pub struct Reference { + rk_hi: Vec, + rk_lo: Vec, + m_hi: [u64; 16], + m_lo: [u64; 16], +} + +impl Reference { + pub fn new(for_encryption: bool, key: &[u8]) -> Self { + let rks = key_schedule(for_encryption, key); + let rk_hi = rks.iter().map(|k| u64::from_be_bytes(k[..8].try_into().unwrap())).collect(); + let rk_lo = rks.iter().map(|k| u64::from_be_bytes(k[8..].try_into().unwrap())).collect(); + // M_HI / M_LO: A applied to each unit byte, as the Java's static initialiser does. + let mut m_hi = [0u64; 16]; + let mut m_lo = [0u64; 16]; + for i in 0..16 { + let mut u = [0u8; 16]; + u[i] = 1; + a(&mut u); + m_hi[i] = u64::from_be_bytes(u[..8].try_into().unwrap()); + m_lo[i] = u64::from_be_bytes(u[8..].try_into().unwrap()); + } + Self { rk_hi, rk_lo, m_hi, m_lo } + } + + /// `applyFO(sh, sl, M)`: SL1 substitution fused with A through the broadcast masks. + fn apply_fo(sh: u64, sl: u64, m: &[u64; 16]) -> u64 { + let (s3, s4) = (sb3(), sb4()); + let byte = |w: u64, i: u32| ((w >> (56 - 8 * i)) & 0xFF) as usize; + let mut out = 0u64; + for i in 0..8u32 { + let s = match i % 4 { + 0 => SB1[byte(sh, i)], + 1 => SB2[byte(sh, i)], + 2 => s3[byte(sh, i)], + _ => s4[byte(sh, i)], + }; + out ^= (s as u64).wrapping_mul(m[i as usize]); + let s = match i % 4 { + 0 => SB1[byte(sl, i)], + 1 => SB2[byte(sl, i)], + 2 => s3[byte(sl, i)], + _ => s4[byte(sl, i)], + }; + out ^= (s as u64).wrapping_mul(m[8 + i as usize]); + } + out + } + + /// `applyFE(sh, sl, M)`: SL2 substitution fused with A. + fn apply_fe(sh: u64, sl: u64, m: &[u64; 16]) -> u64 { + let (s3, s4) = (sb3(), sb4()); + let byte = |w: u64, i: u32| ((w >> (56 - 8 * i)) & 0xFF) as usize; + let mut out = 0u64; + for i in 0..8u32 { + let s = match i % 4 { + 0 => s3[byte(sh, i)], + 1 => s4[byte(sh, i)], + 2 => SB1[byte(sh, i)], + _ => SB2[byte(sh, i)], + }; + out ^= (s as u64).wrapping_mul(m[i as usize]); + let s = match i % 4 { + 0 => s3[byte(sl, i)], + 1 => s4[byte(sl, i)], + 2 => SB1[byte(sl, i)], + _ => SB2[byte(sl, i)], + }; + out ^= (s as u64).wrapping_mul(m[8 + i as usize]); + } + out + } + + /// `sl2Word(w)`: SL2 on one 8-byte half, no diffusion. + fn sl2_word(w: u64) -> u64 { + let (s3, s4) = (sb3(), sb4()); + let mut out = 0u64; + for i in 0..8u32 { + let x = ((w >> (56 - 8 * i)) & 0xFF) as usize; + let s = match i % 4 { + 0 => s3[x], + 1 => s4[x], + 2 => SB1[x], + _ => SB2[x], + }; + out |= (s as u64) << (56 - 8 * i); + } + out + } + + /// `processBlock`. + pub fn process_block(&self, input: &[u8; 16], out: &mut [u8; 16]) { + let (kh, kl) = (&self.rk_hi, &self.rk_lo); + let mut hi = u64::from_be_bytes(input[..8].try_into().unwrap()); + let mut lo = u64::from_be_bytes(input[8..].try_into().unwrap()); + let mut i = 0; + let rounds = kh.len() - 3; + while i < rounds { + let (sh, sl) = (hi ^ kh[i], lo ^ kl[i]); + hi = Self::apply_fo(sh, sl, &self.m_hi); + lo = Self::apply_fo(sh, sl, &self.m_lo); + i += 1; + let (sh, sl) = (hi ^ kh[i], lo ^ kl[i]); + hi = Self::apply_fe(sh, sl, &self.m_hi); + lo = Self::apply_fe(sh, sl, &self.m_lo); + i += 1; + } + // Final FO + let (sh, sl) = (hi ^ kh[i], lo ^ kl[i]); + hi = Self::apply_fo(sh, sl, &self.m_hi); + lo = Self::apply_fo(sh, sl, &self.m_lo); + i += 1; + // Add round key, SL2 without A, add round key + let (sh, sl) = (hi ^ kh[i], lo ^ kl[i]); + hi = Self::sl2_word(sh); + lo = Self::sl2_word(sl); + i += 1; + hi ^= kh[i]; + lo ^= kl[i]; + out[..8].copy_from_slice(&hi.to_be_bytes()); + out[8..].copy_from_slice(&lo.to_be_bytes()); + } +} + +/// Reference encryption of one block: `init(true, key)` then `processBlock`. +pub fn encrypt_block(key: &[u8], block: &mut [u8; 16]) { + let input = *block; + Reference::new(true, key).process_block(&input, block); +} + +/// Reference decryption of one block: `init(false, key)` then `processBlock`. +pub fn decrypt_block(key: &[u8], block: &mut [u8; 16]) { + let input = *block; + Reference::new(false, key).process_block(&input, block); +} + +/// Deterministic pseudo-random bytes (xorshift), so the cross-check needs no RNG. +pub fn pseudo_random(seed: &mut u32) -> [u8; N] { + core::array::from_fn(|_| { + *seed ^= *seed << 13; + *seed ^= *seed >> 17; + *seed ^= *seed << 5; + (*seed >> 24) as u8 + }) +} + +/// Parses hex into a fixed-size array. +pub fn bytes(hex_str: &str) -> [u8; N] { + assert!(hex_str.len() == 2 * N, "expected {N} bytes of hex, got {}", hex_str.len() / 2); + core::array::from_fn(|i| u8::from_str_radix(&hex_str[2 * i..2 * i + 2], 16).expect("valid hex")) +} diff --git a/crypto/aria/tests/electronic_code_book_tests.rs b/crypto/aria/tests/electronic_code_book_tests.rs new file mode 100644 index 000000000..7f023d642 --- /dev/null +++ b/crypto/aria/tests/electronic_code_book_tests.rs @@ -0,0 +1,26 @@ +//! `ElectronicCodeBook` trait conformance, via the shared test framework, for all three key +//! lengths. +//! +//! The framework checks the properties every implementor must have -- both directions are +//! inverses, the permutation is injective, the pair and four-block methods are indistinguishable +//! from single-block calls in every slot, and the key checks behave. All three types override the +//! pair and four-block methods, so this is what pins those overrides to the trait's contract. + +use bouncycastle_aria::BLOCK_LEN; +use bouncycastle_aria::hazmat::{ARIA128Internal, ARIA192Internal, ARIA256Internal}; +use bouncycastle_core_test_framework::electronic_code_book::TestFrameworkElectronicCodeBook; + +#[test] +fn aria128_conforms_to_electronic_code_book() { + TestFrameworkElectronicCodeBook::new().test::<16, BLOCK_LEN, ARIA128Internal>(); +} + +#[test] +fn aria192_conforms_to_electronic_code_book() { + TestFrameworkElectronicCodeBook::new().test::<24, BLOCK_LEN, ARIA192Internal>(); +} + +#[test] +fn aria256_conforms_to_electronic_code_book() { + TestFrameworkElectronicCodeBook::new().test::<32, BLOCK_LEN, ARIA256Internal>(); +} diff --git a/crypto/aria/tests/kisa_vectors_tests.rs b/crypto/aria/tests/kisa_vectors_tests.rs new file mode 100644 index 000000000..2cb5ad64b --- /dev/null +++ b/crypto/aria/tests/kisa_vectors_tests.rs @@ -0,0 +1,162 @@ +//! KISA's published ARIA test vectors -- 10 blocks under each key length, in ECB and in CBC -- as +//! carried in OpenSSL's `test/recipes/30-test_evp_data/evpciph_aria.txt` (OpenSSL 3.6.2), which +//! attributes them to `ARIA-testvector-e.pdf` from the ARIA site. Transcribed from the OpenSSL +//! file. +//! +//! The ECB vectors are 10 independent blocks, so they exercise the four-block, pair +//! and single-block paths against published answers (two fours and a pair). The CBC vectors go +//! through `ARIA128Key>` and its siblings, the block-aligned keys the padded +//! `ARIA_CBC_*` aliases wrap: encryption is driven through the key's `create_encryptor` with a +//! [`FixedSeedRNG`] whose stream is the vector's IV (there is no API for supplying one), and the +//! test asserts the returned init data really is that IV before comparing any ciphertext; +//! decryption takes the IV directly. + +mod common; + +use bouncycastle_aria::hazmat::{ARIA128Internal, ARIA192Internal, ARIA256Internal, ARIAInternal}; +use bouncycastle_aria::{ARIA128Key, ARIA192Key, ARIA256Key, ARIAParams, BLOCK_LEN}; +use bouncycastle_cipher::modes::{Blocks, CBC}; +use bouncycastle_core::hazmat::{BlockPermutation, ElectronicCodeBook}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::symmetric_key::{BlockDecryptionKey, BlockEncryptionKey, FromKeyMaterial}; +use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use common::bytes; + +/// The 160-byte plaintext shared by all six vectors. +const PLAINTEXT: &str = "11111111aaaaaaaa11111111bbbbbbbb11111111cccccccc11111111dddddddd22222222aaaaaaaa22222222bbbbbbbb22222222cccccccc22222222dddddddd33333333aaaaaaaa33333333bbbbbbbb33333333cccccccc33333333dddddddd44444444aaaaaaaa44444444bbbbbbbb44444444cccccccc44444444dddddddd55555555aaaaaaaa55555555bbbbbbbb55555555cccccccc55555555dddddddd"; + +/// The IV shared by the three CBC vectors. +const IV: &str = "0f1e2d3c4b5a69788796a5b4c3d2e1f0"; + +const KEY_128: &str = "00112233445566778899aabbccddeeff"; +const ECB_128: &str = "c6ecd08e22c30abdb215cf74e2075e6e29ccaac63448708d331b2f816c51b17d9e133d1528dbf0af5787c7f3a3f5c2bf6b6f345907a3055612ce072ff54de7d788424da6e8ccfe8172b391be499354165665ba7864917000a6eeb2ecb4a698edfc7887e7f556377614ab0a282293e6d884dbb84206cdb16ed1754e77a1f243fd086953f752cc1e46c7c794ae85537dcaec8dd721f55c93b6edfe2adea43873e8"; +const CBC_128: &str = "49d61860b14909109cef0d22a9268134fadf9fb23151e9645fba75018bdb1538b53334634bbf7d4cd4b5377033060c155fe3948ca75de1031e1d85619e0ad61eb419a866b3c2dbfd10a4ed18b22149f75897f0b8668b0c1c542c687778835fb7cd46e45f85eaa7072437dd9fa6793d6f8d4ccefc4eb1ac641ac1bd30b18c6d64c49bca137eb21c2e04da62712ca2b4f540c57112c38791852cfac7a5d19ed83a"; + +const KEY_192: &str = "00112233445566778899aabbccddeeff0011223344556677"; +const ECB_192: &str = "8d1470625f59ebacb0e55b534b3e462b5f23d33bff78f46c3c15911f4a21809aaccad80b4bda915aa9dae6bcebe06a6c83f77fd5391acfe61de2f646b5d447edbfd5bb49b12fbb9145b227895a757b2af1f7188734863d7b8b6ede5a5b2f06a0a233c8523d2db778fb31b0e311f32700152f33861e9d040c83b5eb40cd88ea49975709dc629365a189f78a3ec40345fc6a5a307a8f9a4413091e007eca5645a0"; +const CBC_192: &str = "afe6cf23974b533c672a826264ea785f4e4f7f780dc7f3f1e0962b80902386d514e9c3e77259de92dd1102ffab086c1ea52a71260db5920a83295c25320e421147ca45d532f327b856ea947cd2196ae2e040826548b4c891b0ed0ca6e714dbc4631998d548110d666b3d54c2a091955c6f05beb4f62309368696c9791fc4c551564a2637f194346ec45fbca6c72a5b4612e208d531d6c34cc5c64eac6bd0cf8c"; + +const KEY_256: &str = "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"; +const ECB_256: &str = "58a875e6044ad7fffa4f58420f7f442d8e191016f28e79aefc01e204773280d7018e5f7a938ec30711719953bae86542cd7ebc752474c1a5f6eaaace2a7e29462ee7dfa5afdb84177ead95ccd4b4bb6e1ed17b9534cff0a5fc2941429cfee2ee49c7adbeb7e9d1b0d2a8531d942079596a27ed79f5b1dd13ecd604b07a48885a3afa0627a0e4e60a3c703af292f1baa77b702f16c54aa74bc727ea95c7468b00"; +const CBC_256: &str = "523a8a806ae621f155fdd28dbc34e1ab7b9b42432ad8b2efb96e23b13f0a6e52f36185d50ad002c5f601bee5493f118b243ee2e313642bffc3902e7b2efd9a12fa682edd2d23c8b9c5f043c18b17c1ec4b5867918270fbec1027c19ed6af833da5d620994668ca22f599791d292dd6273b2959082aafb7a996167cce1eec5f0cfd15f610d87e2dda9ba68ce1260ca54b222491418374294e7909b1e8551cd8de"; + +fn key(hex_str: &str) -> KeyMaterial { + KeyMaterial::::from_bytes_as_type(&bytes::(hex_str), KeyType::SymmetricCipherKey) + .expect("a valid symmetric cipher key") +} + +fn blocks10(hex_str: &str) -> [[u8; BLOCK_LEN]; 10] { + let flat: [u8; 160] = bytes(hex_str); + let (chunks, _) = flat.as_chunks::(); + chunks.try_into().unwrap() +} + +/// ECB: 10 independent blocks through every batching, both directions. +fn check_ecb(name: &str, perm: &ARIAInternal

, ct_hex: &str) +where + ARIAInternal

: BlockPermutation, +{ + let pt = blocks10(PLAINTEXT); + let ct = blocks10(ct_hex); + + // Two fours and a pair. + let mut fours = pt; + { + let (head, tail) = fours.split_at_mut(8); + for four in head.as_chunks_mut::<4>().0 { + perm.encrypt_4blocks(four); + } + perm.encrypt_2blocks(tail.try_into().unwrap()); + } + assert_eq!(fours, ct, "{name}: encrypt_4blocks"); + { + let (head, tail) = fours.split_at_mut(8); + for four in head.as_chunks_mut::<4>().0 { + perm.decrypt_4blocks(four); + } + perm.decrypt_2blocks(tail.try_into().unwrap()); + } + assert_eq!(fours, pt, "{name}: decrypt_4blocks"); + + // Five pairs, and ten singles. + let mut pairs = ct; + for pair in pairs.as_mut_slice().as_chunks_mut::<2>().0 { + perm.decrypt_2blocks(pair); + } + assert_eq!(pairs, pt, "{name}: decrypt_2blocks"); + for (i, (p, c)) in pt.iter().zip(ct.iter()).enumerate() { + let mut b = *p; + perm.encrypt_block(&mut b); + assert_eq!(&b, c, "{name}: encrypt_block #{i}"); + } +} + +/// CBC: the 10-block message in one call, block by block, both directions, through a +/// `CBC` key. +fn check_cbc<'k, const KEY_LEN: usize, K>(name: &str, key: &'k K, ct_hex: &str) +where + K: BlockEncryptionKey + + BlockDecryptionKey, +{ + let iv: [u8; 16] = bytes(IV); + let pt: [u8; 160] = bytes(PLAINTEXT); + let ct: [u8; 160] = bytes(ct_hex); + + let (mut enc, got_iv) = key.create_encryptor(&mut FixedSeedRNG::<16>::new(iv)).unwrap(); + assert_eq!(got_iv, iv, "{name}: the pinned RNG should produce the vector's IV"); + let mut data = pt; + enc.do_encrypt_blocks_inplace(data.as_chunks_mut::().0).unwrap(); + assert_eq!(data, ct, "{name}: encrypt, all blocks in one call"); + + let (mut enc, _) = key.create_encryptor(&mut FixedSeedRNG::<16>::new(iv)).unwrap(); + let mut data = pt; + for block in data.as_chunks_mut::().0.iter_mut() { + enc.do_encrypt_blocks_inplace(core::slice::from_mut(block)).unwrap(); + } + assert_eq!(data, ct, "{name}: encrypt, one block at a time"); + + let mut data = ct; + key.decrypt_inplace(&iv, &mut data).unwrap(); + assert_eq!(data, pt, "{name}: decrypt, one shot"); + + let mut dec = key.create_decryptor(&iv); + let mut data = ct; + for block in data.as_chunks_mut::().0.iter_mut() { + dec.do_decrypt_blocks_inplace(core::slice::from_mut(block)).unwrap(); + } + assert_eq!(data, pt, "{name}: decrypt, one block at a time"); +} + +#[test] +fn aria_128_ecb() { + check_ecb("ARIA-128-ECB", &ARIA128Internal::new(&key::<16>(KEY_128)).unwrap(), ECB_128); +} + +#[test] +fn aria_192_ecb() { + check_ecb("ARIA-192-ECB", &ARIA192Internal::new(&key::<24>(KEY_192)).unwrap(), ECB_192); +} + +#[test] +fn aria_256_ecb() { + check_ecb("ARIA-256-ECB", &ARIA256Internal::new(&key::<32>(KEY_256)).unwrap(), ECB_256); +} + +#[test] +fn aria_128_cbc() { + let key = ARIA128Key::>::from_key_material(&key::<16>(KEY_128)).unwrap(); + check_cbc::<16, _>("ARIA-128-CBC", &key, CBC_128); +} + +#[test] +fn aria_192_cbc() { + let key = ARIA192Key::>::from_key_material(&key::<24>(KEY_192)).unwrap(); + check_cbc::<24, _>("ARIA-192-CBC", &key, CBC_192); +} + +#[test] +fn aria_256_cbc() { + let key = ARIA256Key::>::from_key_material(&key::<32>(KEY_256)).unwrap(); + check_cbc::<32, _>("ARIA-256-CBC", &key, CBC_256); +} diff --git a/crypto/aria/tests/mode_key_tests.rs b/crypto/aria/tests/mode_key_tests.rs new file mode 100644 index 000000000..611c8bf52 --- /dev/null +++ b/crypto/aria/tests/mode_key_tests.rs @@ -0,0 +1,120 @@ +//! The ARIA keys through the shared conformance suites: construction policy for every mode a key +//! can carry, then the block, symmetric and stream suites at the 128- and 256-bit key +//! lengths (ARIA-192 sits between them in rounds and differs in nothing else the suites see). + +use bouncycastle_aria::hazmat::{ARIA128Internal, ARIA256Internal}; +use bouncycastle_aria::{ARIA128Key, ARIA256Key}; +use bouncycastle_cipher::modes::hazmat::ecb::ECB; +use bouncycastle_cipher::modes::{Blocks, CBC, CFB, CFB8, CTR, ModeNames}; +use bouncycastle_cipher::padding::{NoPadding, PKCS7}; +use bouncycastle_core::hazmat::Mode; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::symmetric_key::FromKeyMaterial; +use bouncycastle_core_test_framework::block_cipher::TestFrameworkBlockCipher; +use bouncycastle_core_test_framework::from_key_material::TestFrameworkFromKeyMaterial; +use bouncycastle_core_test_framework::symmetric_ciphers::{ + TestFrameworkStreamCipher, TestFrameworkSymmetricCipher, +}; +use bouncycastle_rng::HashDRBG_SHA512; + +fn material() -> KeyMaterial { + let bytes: [u8; N] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); + KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).expect("a valid key") +} + +fn key128>() -> ARIA128Key { + ARIA128Key::from_key_material(&material::<16>()).expect("a valid key") +} + +fn key256>() -> ARIA256Key { + ARIA256Key::from_key_material(&material::<32>()).expect("a valid key") +} + +fn rng() -> HashDRBG_SHA512 { + HashDRBG_SHA512::new_from_os() +} + +/// The construction suite -- key type, strength and `generate` -- for the mode `M` at both key +/// lengths, against the name the permutation gives that mode. +fn construction_suite(name_128: &'static str, name_256: &'static str) +where + M: Mode + Mode, +{ + let framework = TestFrameworkFromKeyMaterial::new(); + framework.test::<16, ARIA128Key>(name_128, SecurityStrength::_128bit); + framework.test::<32, ARIA256Key>(name_256, SecurityStrength::_256bit); +} + +#[test] +fn every_mode_key_enforces_the_material_policy() { + construction_suite::>(ARIA128Internal::CBC_ALG_NAME, ARIA256Internal::CBC_ALG_NAME); + construction_suite::>(ARIA128Internal::CBC_ALG_NAME, ARIA256Internal::CBC_ALG_NAME); + construction_suite::>( + ARIA128Internal::CBC_ALG_NAME, + ARIA256Internal::CBC_ALG_NAME, + ); + construction_suite::>(ARIA128Internal::ECB_ALG_NAME, ARIA256Internal::ECB_ALG_NAME); + construction_suite::(ARIA128Internal::CFB_ALG_NAME, ARIA256Internal::CFB_ALG_NAME); + construction_suite::(ARIA128Internal::CFB8_ALG_NAME, ARIA256Internal::CFB8_ALG_NAME); + construction_suite::(ARIA128Internal::CTR_ALG_NAME, ARIA256Internal::CTR_ALG_NAME); +} + +#[test] +fn cbc_blocks_keys_pass_the_block_cipher_framework() { + let framework = TestFrameworkBlockCipher::new(); + framework.test_key::<16, 16, 16, _>(&key128::>(), &mut rng()); + framework.test_key::<32, 16, 16, _>(&key256::>(), &mut rng()); +} + +#[test] +fn padded_keys_pass_the_symmetric_cipher_framework() { + let framework = TestFrameworkSymmetricCipher::new(); + framework.test_key::<16, 16, 16, _>(&key128::>(), &mut rng()); + framework.test_key::<32, 16, 16, _>(&key256::>(), &mut rng()); + framework.test_key::<16, 0, 16, _>(&key128::>(), &mut rng()); + framework.test_key::<32, 0, 16, _>(&key256::>(), &mut rng()); +} + +/// `NoPadding` turns the adapters into an alignment check: the framework is told so, and asserts +/// that every other length is refused. +#[test] +fn no_padding_keys_pass_the_symmetric_cipher_framework() { + let mut framework = TestFrameworkSymmetricCipher::new(); + framework.required_alignment = 16; + framework.test_key::<16, 16, 16, _>(&key128::>(), &mut rng()); + framework.test_key::<32, 16, 16, _>(&key256::>(), &mut rng()); +} + +#[test] +fn stream_mode_keys_pass_the_stream_cipher_framework() { + let framework = TestFrameworkStreamCipher::new(); + framework.test_key::<16, 16, _>(&key128::(), &mut rng()); + framework.test_key::<32, 16, _>(&key256::(), &mut rng()); + framework.test_key::<16, 16, _>(&key128::(), &mut rng()); + framework.test_key::<32, 16, _>(&key256::(), &mut rng()); + framework.test_key::<16, 12, _>(&key128::(), &mut rng()); + framework.test_key::<32, 12, _>(&key256::(), &mut rng()); +} + +/// `material::()` with one bit of its last byte flipped. +fn material_flipped() -> KeyMaterial { + let mut bytes: [u8; N] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); + bytes[N - 1] ^= 1; + KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).expect("a valid key") +} + +/// Equality is over the expanded schedule, so it follows the material: the same bytes compare +/// equal, as does a clone, and a single flipped bit does not. Keys have no `Debug`, hence +/// `assert!` over `==`. +#[test] +fn keys_compare_equal_exactly_when_their_material_does() { + let key = key128::(); + assert!(key == key.clone()); + assert!(key == key128::()); + assert!(key != ARIA128Key::::from_key_material(&material_flipped::<16>()).unwrap()); + + let key = key256::(); + assert!(key == key256::()); + assert!(key != ARIA256Key::::from_key_material(&material_flipped::<32>()).unwrap()); +} diff --git a/crypto/aria/tests/padded_alias_tests.rs b/crypto/aria/tests/padded_alias_tests.rs new file mode 100644 index 000000000..4083edd40 --- /dev/null +++ b/crypto/aria/tests/padded_alias_tests.rs @@ -0,0 +1,110 @@ +//! The padded CBC mode through the three ARIA keys. +//! +//! The aliases are only type aliases, so what is worth testing is that both parameters actually +//! select: the direction picks the encryptor or the decryptor, and the padding scheme changes the +//! behaviour rather than being decorative. The mode and the padding layer are tested in +//! `bouncycastle-cipher`; this checks the wiring between them and the key that hands the aliases +//! out. Which type each alias resolves to is pinned in `alg_name_tests.rs`. + +use bouncycastle_aria::{ARIA128Key, ARIA192Key, ARIA256Key, BLOCK_LEN}; +use bouncycastle_cipher::modes::CBC; +use bouncycastle_cipher::padding::{NoPadding, PKCS7}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::symmetric_key::{ + FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey, +}; +use bouncycastle_rng::HashDRBG_SHA512; + +fn material() -> KeyMaterial { + let bytes: [u8; N] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); + KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).expect("a valid key") +} + +fn rng() -> HashDRBG_SHA512 { + HashDRBG_SHA512::new_from_os() +} + +/// Round trips at lengths that need padding and lengths that do not, at every key length. +fn round_trips(name: &str, key: &K) +where + K: SymmetricEncryptionKey + + SymmetricDecryptionKey, +{ + for len in [0usize, 1, 15, 16, 17, 31, 32, 33, 127, 128] { + let plaintext: Vec = (0..len).map(|i| (i * 11 + 3) as u8).collect(); + let (iv, ciphertext) = key.encrypt(&mut rng(), &plaintext).expect("encryption"); + assert_eq!(iv.len(), BLOCK_LEN); + assert_eq!( + ciphertext.len(), + (len / BLOCK_LEN + 1) * BLOCK_LEN, + "{name}, len {len}: PKCS7 pads up to the next whole block" + ); + let recovered = key.decrypt(&iv, &ciphertext).expect("decryption"); + assert_eq!(recovered, plaintext, "{name}, len {len}: round trip"); + } +} + +#[test] +fn every_key_length_round_trips() { + round_trips::<16, _>( + "ARIA-128", + &ARIA128Key::>::from_key_material(&material()).unwrap(), + ); + round_trips::<24, _>( + "ARIA-192", + &ARIA192Key::>::from_key_material(&material()).unwrap(), + ); + round_trips::<32, _>( + "ARIA-256", + &ARIA256Key::>::from_key_material(&material()).unwrap(), + ); +} + +/// The padding parameter must actually select the scheme, not merely be carried around. +#[test] +fn the_padding_parameter_selects_the_scheme() { + let pkcs7_key = ARIA128Key::>::from_key_material(&material()).unwrap(); + let nopad_key = ARIA128Key::>::from_key_material(&material()).unwrap(); + + // A whole block: both schemes accept it, and they disagree about the length. + let aligned = [0x5Au8; BLOCK_LEN]; + let (_, pkcs7) = pkcs7_key.encrypt(&mut rng(), &aligned).expect("PKCS7 accepts it"); + let (_, nopad) = nopad_key.encrypt(&mut rng(), &aligned).expect("NoPadding accepts it"); + assert_eq!(pkcs7.len(), 32, "PKCS7 adds a whole block of padding to aligned data"); + assert_eq!(nopad.len(), 16, "NoPadding adds nothing"); + + // Five bytes: PKCS7 pads it, NoPadding refuses rather than silently padding. + assert!(pkcs7_key.encrypt(&mut rng(), b"hello").is_ok(), "PKCS7 pads a partial block"); + assert!(nopad_key.encrypt(&mut rng(), b"hello").is_err(), "NoPadding must refuse it"); +} + +/// A ciphertext made under one scheme must not decrypt cleanly under the other. +#[test] +fn the_two_schemes_are_not_interchangeable() { + let pkcs7_key = ARIA128Key::>::from_key_material(&material()).unwrap(); + let nopad_key = ARIA128Key::>::from_key_material(&material()).unwrap(); + let aligned = [0x5Au8; BLOCK_LEN]; + let (iv, pkcs7) = pkcs7_key.encrypt(&mut rng(), &aligned).expect("encryption"); + + // NoPadding hands back the padding block as if it were data: the silent mismatch the type + // parameter is there to prevent. + let as_nopad = nopad_key.decrypt(&iv, &pkcs7).expect("NoPadding cannot tell"); + assert_ne!(as_nopad, aligned); + assert_eq!(as_nopad.len(), 32, "it keeps the padding block as data"); + + assert_eq!(pkcs7_key.decrypt(&iv, &pkcs7).expect("decryption"), aligned); +} + +/// The IV is drawn per encryption, so the same plaintext gives different ciphertext. +#[test] +fn each_encryption_gets_a_fresh_iv() { + let key = ARIA128Key::>::from_key_material(&material()).unwrap(); + let mut rng = rng(); + let plaintext = [0x77u8; 32]; + let mut seen = std::collections::BTreeSet::new(); + for _ in 0..16 { + let (iv, ct) = key.encrypt(&mut rng, &plaintext).unwrap(); + assert!(seen.insert(iv), "IV repeated across encryptions"); + assert_eq!(key.decrypt(&iv, &ct).unwrap(), plaintext); + } +} diff --git a/crypto/aria/tests/reference_tests.rs b/crypto/aria/tests/reference_tests.rs new file mode 100644 index 000000000..a1e5f1ce5 --- /dev/null +++ b/crypto/aria/tests/reference_tests.rs @@ -0,0 +1,152 @@ +//! Cross-check of the constant-time engine against a transcription of BC Java's `ARIAEngine` +//! (`tests/common/mod.rs`), on many keys and blocks of every length, in every lane, in both +//! directions. +//! +//! The known-answer files pin published values. This file pins *agreement with the source +//! implementation* over thousands of inputs, which is what catches a circuit, transpose or +//! schedule error that happens not to be exercised by the published examples -- and the reference +//! is organised quite differently (byte arrays and 64-bit halves, table S-boxes fused with the +//! diffusion layer by multiply-broadcast, direction-dependent key layout), so agreement is not +//! tautological. + +mod common; + +use bouncycastle_aria::hazmat::{ARIA128Internal, ARIA192Internal, ARIA256Internal}; +use bouncycastle_aria::{BLOCK_LEN, LANES}; +use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; + +fn engine>(key: &[u8; KEY_LEN]) -> P { +

>::new( + &KeyMaterial::::from_bytes_as_type(key, KeyType::SymmetricCipherKey) + .expect("a valid symmetric cipher key"), + ) + .expect("a valid key") +} + +/// The reference itself reproduces RFC 5794 Appendix A, so it can be trusted to judge. +#[test] +fn reference_sanity() { + let pt: [u8; BLOCK_LEN] = common::bytes("00112233445566778899aabbccddeeff"); + let key32 = + common::bytes::<32>("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"); + for (key, ct) in [ + (&key32[..16], "d718fbd6ab644c739da95f3be6451778"), + (&key32[..24], "26449c1805dbe7aa25a468ce263a9e79"), + (&key32[..], "f92bd7c79fb72e2f2b8f80c1972d24fc"), + ] { + let mut block = pt; + common::encrypt_block(key, &mut block); + assert_eq!(block, common::bytes::<16>(ct), "{}-byte key", key.len()); + common::decrypt_block(key, &mut block); + assert_eq!(block, pt); + } +} + +fn single_block_agrees>(seed: u32) { + let mut seed = seed; + for _ in 0..256 { + let key: [u8; KEY_LEN] = common::pseudo_random(&mut seed); + let perm: P = engine(&key); + for _ in 0..8 { + let block: [u8; BLOCK_LEN] = common::pseudo_random(&mut seed); + + let mut ours = block; + let mut theirs = block; + perm.encrypt_block(&mut ours); + common::encrypt_block(&key, &mut theirs); + assert_eq!(ours, theirs, "encrypt, key {key:02x?}, block {block:02x?}"); + + let mut ours = block; + let mut theirs = block; + perm.decrypt_block(&mut ours); + common::decrypt_block(&key, &mut theirs); + assert_eq!(ours, theirs, "decrypt, key {key:02x?}, block {block:02x?}"); + } + } +} + +fn two_block_override_matches>(seed: u32) { + let mut seed = seed; + for _ in 0..64 { + let key: [u8; KEY_LEN] = common::pseudo_random(&mut seed); + let perm: P = engine(&key); + let a: [u8; BLOCK_LEN] = common::pseudo_random(&mut seed); + let b: [u8; BLOCK_LEN] = common::pseudo_random(&mut seed); + + let mut singly = [a, b]; + perm.encrypt_block(&mut singly[0]); + perm.encrypt_block(&mut singly[1]); + let mut paired = [a, b]; + perm.encrypt_2blocks(&mut paired); + assert_eq!(paired, singly); + let mut theirs = [a, b]; + common::encrypt_block(&key, &mut theirs[0]); + common::encrypt_block(&key, &mut theirs[1]); + assert_eq!(paired, theirs); + + perm.decrypt_2blocks(&mut paired); + assert_eq!(paired, [a, b]); + } +} + +/// Single-block API against the reference: 256 keys of each length, 8 blocks each, both directions. +#[test] +fn single_block_agrees_with_the_reference() { + single_block_agrees::<16, ARIA128Internal>(0x5EED_0001); + single_block_agrees::<24, ARIA192Internal>(0x5EED_0002); + single_block_agrees::<32, ARIA256Internal>(0x5EED_0003); +} + +/// The four-lane API -- the natural unit -- against the reference, every lane, both directions. +#[test] +fn four_lanes_agree_with_the_reference() { + fn run( + mut seed: u32, + new: impl Fn(&[u8; KEY_LEN]) -> bouncycastle_aria::hazmat::ARIAInternal

, + ) where + bouncycastle_aria::hazmat::ARIAInternal

: ElectronicCodeBook, + { + for _ in 0..128 { + let key: [u8; KEY_LEN] = common::pseudo_random(&mut seed); + let perm = new(&key); + let blocks: [[u8; BLOCK_LEN]; LANES] = + core::array::from_fn(|_| common::pseudo_random(&mut seed)); + + let mut ours = blocks; + perm.encrypt_4blocks(&mut ours); + for (lane, (o, b)) in ours.iter().zip(blocks.iter()).enumerate() { + let mut theirs = *b; + common::encrypt_block(&key, &mut theirs); + assert_eq!(*o, theirs, "encrypt lane {lane}"); + } + perm.decrypt_4blocks(&mut ours); + assert_eq!(ours, blocks, "decrypt_4blocks must invert encrypt_4blocks"); + + let mut ours = blocks; + perm.decrypt_4blocks(&mut ours); + for (lane, (o, b)) in ours.iter().zip(blocks.iter()).enumerate() { + let mut theirs = *b; + common::decrypt_block(&key, &mut theirs); + assert_eq!(*o, theirs, "decrypt lane {lane}"); + } + } + } + let km = + |k: &[u8]| KeyMaterial::<16>::from_bytes_as_type(k, KeyType::SymmetricCipherKey).unwrap(); + run::<16, _>(0x5EED_0031, |k| ARIA128Internal::new(&km(k)).unwrap()); + let km = + |k: &[u8]| KeyMaterial::<24>::from_bytes_as_type(k, KeyType::SymmetricCipherKey).unwrap(); + run::<24, _>(0x5EED_0032, |k| ARIA192Internal::new(&km(k)).unwrap()); + let km = + |k: &[u8]| KeyMaterial::<32>::from_bytes_as_type(k, KeyType::SymmetricCipherKey).unwrap(); + run::<32, _>(0x5EED_0033, |k| ARIA256Internal::new(&km(k)).unwrap()); +} + +/// The two-block override against two single-block calls and the reference, in both slots. +#[test] +fn two_block_override_matches_two_single_calls() { + two_block_override_matches::<16, ARIA128Internal>(0x5EED_0021); + two_block_override_matches::<24, ARIA192Internal>(0x5EED_0022); + two_block_override_matches::<32, ARIA256Internal>(0x5EED_0023); +} diff --git a/crypto/aria/tests/rfc5794_tests.rs b/crypto/aria/tests/rfc5794_tests.rs new file mode 100644 index 000000000..e0fd6bfe5 --- /dev/null +++ b/crypto/aria/tests/rfc5794_tests.rs @@ -0,0 +1,125 @@ +//! Known-answer tests from RFC 5794 Appendix A, "Example Data of ARIA": one vector per key length, +//! checked in both directions, through every lane of the four-block path (which is also the +//! trait's batch) and both slots of the pair path. (The round keys and intermediate round values +//! of A.1 are pinned inside the crate, where they are visible: `schedule::tests` and +//! `aria::tests`.) +//! +//! All values are transcribed from the downloaded text of the RFC. + +mod common; + +use bouncycastle_aria::hazmat::{ARIA128Internal, ARIA192Internal, ARIA256Internal, ARIAInternal}; +use bouncycastle_aria::{ARIAParams, BLOCK_LEN, LANES}; +use bouncycastle_core::hazmat::{BlockPermutation, ElectronicCodeBook}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use common::bytes; + +/// The plaintext shared by all three examples. +const PLAINTEXT: &str = "00112233445566778899aabbccddeeff"; + +/// A.1, "128-Bit Key". +const KEY_128: &str = "000102030405060708090a0b0c0d0e0f"; +const CT_128: &str = "d718fbd6ab644c739da95f3be6451778"; + +/// A.2, "192-Bit Key". +const KEY_192: &str = "000102030405060708090a0b0c0d0e0f1011121314151617"; +const CT_192: &str = "26449c1805dbe7aa25a468ce263a9e79"; + +/// A.3, "256-Bit Key". +const KEY_256: &str = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"; +const CT_256: &str = "f92bd7c79fb72e2f2b8f80c1972d24fc"; + +fn key(hex_str: &str) -> KeyMaterial { + KeyMaterial::::from_bytes_as_type(&bytes::(hex_str), KeyType::SymmetricCipherKey) + .expect("a valid symmetric cipher key") +} + +/// Every entry point, both directions, for one vector. +fn check( + name: &str, + perm: &ARIAInternal

, + plaintext: &[u8; BLOCK_LEN], + ciphertext: &[u8; BLOCK_LEN], +) where + ARIAInternal

: ElectronicCodeBook, +{ + // Single block. + let mut b = *plaintext; + perm.encrypt_block(&mut b); + assert_eq!(&b, ciphertext, "{name}: encrypt_block"); + perm.decrypt_block(&mut b); + assert_eq!(&b, plaintext, "{name}: decrypt_block"); + + // Four lanes: the vector in each lane in turn, with unrelated blocks in the others, which + // must come out as they do on their own. + let others: [[u8; BLOCK_LEN]; LANES] = core::array::from_fn(|i| [i as u8 * 17 + 1; 16]); + let mut alone = others; + for b in alone.iter_mut() { + perm.encrypt_block(b); + } + for lane in 0..LANES { + let mut mixed = others; + mixed[lane] = *plaintext; + perm.encrypt_4blocks(&mut mixed); + assert_eq!(&mixed[lane], ciphertext, "{name}: encrypt_4blocks lane {lane}"); + for other in (0..LANES).filter(|&o| o != lane) { + assert_eq!(mixed[other], alone[other], "{name}: lane {lane} disturbed lane {other}"); + } + perm.decrypt_4blocks(&mut mixed); + assert_eq!(&mixed[lane], plaintext, "{name}: decrypt_4blocks lane {lane}"); + assert_eq!(&mixed[..lane], &others[..lane]); + assert_eq!(&mixed[lane + 1..], &others[lane + 1..]); + } + + // The pair path, in both slots. + let mut pair = [*plaintext, others[3]]; + perm.encrypt_2blocks(&mut pair); + assert_eq!(&pair[0], ciphertext, "{name}: encrypt_2blocks slot 0"); + assert_eq!(pair[1], alone[3]); + perm.decrypt_2blocks(&mut pair); + assert_eq!(pair, [*plaintext, others[3]], "{name}: decrypt_2blocks"); + let mut pair = [others[2], *plaintext]; + perm.encrypt_2blocks(&mut pair); + assert_eq!(&pair[1], ciphertext, "{name}: encrypt_2blocks slot 1"); +} + +#[test] +fn appendix_a_1_128_bit_key() { + let perm = ARIA128Internal::new(&key::<16>(KEY_128)).unwrap(); + check::<16, _>("Appendix A.1", &perm, &bytes(PLAINTEXT), &bytes(CT_128)); +} + +#[test] +fn appendix_a_2_192_bit_key() { + let perm = ARIA192Internal::new(&key::<24>(KEY_192)).unwrap(); + check::<24, _>("Appendix A.2", &perm, &bytes(PLAINTEXT), &bytes(CT_192)); +} + +#[test] +fn appendix_a_3_256_bit_key() { + let perm = ARIA256Internal::new(&key::<32>(KEY_256)).unwrap(); + check::<32, _>("Appendix A.3", &perm, &bytes(PLAINTEXT), &bytes(CT_256)); +} + +/// The three key lengths are different ciphers: the same 16 key bytes extended to 24 or 32 must +/// not produce the 128-bit answer (a schedule that ignored `KR` or the `CK` order would). +#[test] +fn key_lengths_are_distinct_ciphers() { + let mut a = bytes::<16>(PLAINTEXT); + let mut b = a; + let mut c = a; + ARIA128Internal::new(&key::<16>(KEY_128)).unwrap().encrypt_block(&mut a); + ARIA192Internal::new(&key::<24>(KEY_192)).unwrap().encrypt_block(&mut b); + ARIA256Internal::new(&key::<32>(KEY_256)).unwrap().encrypt_block(&mut c); + assert_ne!(a, b); + assert_ne!(b, c); + assert_ne!(a, c); +} + +/// `Debug` prints the algorithm name only, never the round keys. +#[test] +fn debug_prints_only_the_name() { + assert_eq!(format!("{:?}", ARIA128Internal::new(&key::<16>(KEY_128)).unwrap()), "ARIA-128"); + assert_eq!(format!("{:?}", ARIA192Internal::new(&key::<24>(KEY_192)).unwrap()), "ARIA-192"); + assert_eq!(format!("{:?}", ARIA256Internal::new(&key::<32>(KEY_256)).unwrap()), "ARIA-256"); +} diff --git a/crypto/aria/tests/stream_mode_alias_tests.rs b/crypto/aria/tests/stream_mode_alias_tests.rs new file mode 100644 index 000000000..b66849baf --- /dev/null +++ b/crypto/aria/tests/stream_mode_alias_tests.rs @@ -0,0 +1,192 @@ +//! Tests for the stream-mode keys `ARIA_CFB_*`, `ARIA_CFB8_*` and `ARIA_CTR_*`. +//! +//! The aliases are only type aliases, so what is worth testing is that they name the right types +//! and behave as the mode they claim to be: the direction selects the encryptor or the decryptor, +//! the init data is the length the mode defines, and the three modes are distinct from each other. +//! The known-answer coverage is in `stream_mode_tests.rs`, and the modes themselves are tested in +//! their own right in `bouncycastle_cipher::modes`; this checks the wiring between them. + +use bouncycastle_aria::hazmat::{ARIA128Internal, ARIA192Internal, ARIA256Internal}; +use bouncycastle_aria::{ARIA128Key, ARIA192Key, ARIA256Key, BLOCK_LEN, CTR_NONCE_LEN}; +use bouncycastle_cipher::modes::{CFB, CFB8, CTR}; +use bouncycastle_core::hazmat::Mode; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::symmetric_key::{ + FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey, +}; +use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_rng::HashDRBG_SHA512; + +fn material() -> KeyMaterial { + let bytes: [u8; N] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); + KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).expect("a valid key") +} + +fn key128>() -> ARIA128Key { + ARIA128Key::from_key_material(&material::<16>()).expect("a valid key") +} +fn key192>() -> ARIA192Key { + ARIA192Key::from_key_material(&material::<24>()).expect("a valid key") +} +fn key256>() -> ARIA256Key { + ARIA256Key::from_key_material(&material::<32>()).expect("a valid key") +} + +fn rng() -> HashDRBG_SHA512 { + HashDRBG_SHA512::new_from_os() +} + +/// Deterministic pseudo-random bytes, so the tests do not depend on an RNG. +fn filler(len: usize, seed: u32) -> Vec { + let mut state = seed.wrapping_mul(2_654_435_761).wrapping_add(1); + (0..len) + .map(|_| { + state ^= state << 13; + state ^= state >> 17; + state ^= state << 5; + (state >> 24) as u8 + }) + .collect() +} + +/// Every stream-mode key round-trips at any length, with the ciphertext exactly as long as the +/// plaintext -- the property that separates these nine from the CBC keys, which pad. +#[test] +fn every_stream_key_round_trips_at_any_length() { + fn check<'k, const KEY_LEN: usize, const INIT_DATA_LEN: usize, K>(name: &str, key: &'k K) + where + K: SymmetricEncryptionKey + + SymmetricDecryptionKey, + K::Encryptor<'k>: StreamCipherEncryptor, + K::Decryptor<'k>: StreamCipherDecryptor, + { + for len in [0usize, 1, 15, 16, 17, 64, 257] { + let plaintext = filler(len, len as u32); + let mut data = plaintext.clone(); + + let (_, init) = key.encrypt_inplace(&mut rng(), &mut data).expect("encryption"); + assert_eq!(data.len(), len, "{name}, len {len}: no padding is added"); + assert_eq!(init.len(), INIT_DATA_LEN, "{name}: init data length"); + + key.decrypt_inplace(&init, &mut data).expect("decryption"); + assert_eq!(data, plaintext, "{name}, len {len}: round trip"); + } + } + + check::<16, 16, _>("ARIA_CFB_128", &key128::()); + check::<24, 16, _>("ARIA_CFB_192", &key192::()); + check::<32, 16, _>("ARIA_CFB_256", &key256::()); + check::<16, 16, _>("ARIA_CFB8_128", &key128::()); + check::<24, 16, _>("ARIA_CFB8_192", &key192::()); + check::<32, 16, _>("ARIA_CFB8_256", &key256::()); + check::<16, 12, _>("ARIA_CTR_128", &key128::()); + check::<24, 12, _>("ARIA_CTR_192", &key192::()); + check::<32, 12, _>("ARIA_CTR_256", &key256::()); +} + +/// Streaming in arbitrary pieces gives the same answer as one call: a sequence of calls must be +/// equivalent to one over the concatenation, whatever the chunking. +#[test] +fn chunking_does_not_change_the_ciphertext() { + fn check<'k, const KEY_LEN: usize, const INIT_DATA_LEN: usize, K>(name: &str, key: &'k K) + where + K: SymmetricEncryptionKey + + SymmetricDecryptionKey, + K::Encryptor<'k>: StreamCipherEncryptor, + K::Decryptor<'k>: StreamCipherDecryptor, + { + let plaintext = filler(100, 99); + let mut whole = plaintext.clone(); + let (_, init) = key.encrypt_inplace(&mut rng(), &mut whole).expect("encryption"); + + for chunk in [1usize, 3, 7, 16, 33] { + let mut dec = key.create_decryptor(&init); + let mut data = whole.clone(); + for piece in data.chunks_mut(chunk) { + dec.do_decrypt_inplace(piece).expect("decryption"); + } + assert_eq!(data, plaintext, "{name}: {chunk}-byte pieces should decrypt the same"); + } + } + + check::<16, 16, _>("ARIA_CFB_128", &key128::()); + check::<24, 16, _>("ARIA_CFB8_192", &key192::()); + check::<32, 12, _>("ARIA_CTR_256", &key256::()); +} + +/// A fresh IV or nonce per encryption, so the same plaintext gives different ciphertext. For CTR +/// this is the whole security argument: a repeated nonce leaks the XOR of the two messages. +#[test] +fn each_encryption_gets_fresh_init_data() { + fn check<'k, const INIT_DATA_LEN: usize, K>(name: &str, key: &'k K) + where + K: SymmetricEncryptionKey<16, INIT_DATA_LEN, 0> + + SymmetricDecryptionKey<16, INIT_DATA_LEN, 0>, + K::Encryptor<'k>: StreamCipherEncryptor, + K::Decryptor<'k>: StreamCipherDecryptor, + { + let plaintext = [0x77u8; 32]; + let mut rng = rng(); + let mut seen = std::collections::BTreeSet::new(); + for _ in 0..16 { + let mut data = plaintext; + let (_, init) = key.encrypt_inplace(&mut rng, &mut data).expect("encryption"); + assert!(seen.insert(init.to_vec()), "{name}: init data repeated across encryptions"); + key.decrypt_inplace(&init, &mut data).expect("decryption"); + assert_eq!(data, plaintext, "{name}: round trip"); + } + } + + check::<16, _>("ARIA_CFB_128", &key128::()); + check::<16, _>("ARIA_CFB8_128", &key128::()); + check::<12, _>("ARIA_CTR_128", &key128::()); +} + +/// CFB128 and CFB8 are different, non-interoperable modes. Under the same key and IV their first +/// bytes agree -- both are `P_1 XOR MSB_8(O_1)` of the same first output block -- and everything +/// after that diverges, because CFB8 re-enciphers after every byte while CFB128 waits for a block. +#[test] +fn cfb128_and_cfb8_are_not_interchangeable() { + let iv = [0x0Au8; BLOCK_LEN]; + let plaintext = filler(32, 12_345); + + let cfb_key = key128::(); + let mut as_cfb = plaintext.clone(); + let (mut enc, got) = + cfb_key.create_encryptor(&mut FixedSeedRNG::::new(iv)).expect("CFB128 init"); + assert_eq!(got, iv, "the pinned RNG should produce the chosen IV"); + enc.do_encrypt_inplace(&mut as_cfb).expect("CFB128"); + + let cfb8_key = key128::(); + let mut as_cfb8 = plaintext.clone(); + let (mut enc8, got8) = + cfb8_key.create_encryptor(&mut FixedSeedRNG::::new(iv)).expect("CFB8 init"); + assert_eq!(got8, iv); + enc8.do_encrypt_inplace(&mut as_cfb8).expect("CFB8"); + + assert_eq!(as_cfb[0], as_cfb8[0], "both take MSB_8 of the same first output block"); + assert_ne!(as_cfb[1..], as_cfb8[1..], "and diverge from the second byte on"); +} + +/// CTR is not CFB either, and its init data is shorter: 12 bytes against 16. +#[test] +fn ctr_is_not_cfb() { + let plaintext = filler(32, 999); + + let mut as_ctr = plaintext.clone(); + let (_, nonce) = key128::().encrypt_inplace(&mut rng(), &mut as_ctr).unwrap(); + assert_eq!(nonce.len(), CTR_NONCE_LEN); + + let cfb_key = key128::(); + let mut as_cfb = plaintext.clone(); + let (_, iv) = cfb_key.encrypt_inplace(&mut rng(), &mut as_cfb).unwrap(); + assert_eq!(iv.len(), BLOCK_LEN); + + // A CFB decryptor handed a CTR ciphertext cannot recover the plaintext. + let mut misread = as_ctr.clone(); + let mut iv_from_nonce = [0u8; BLOCK_LEN]; + iv_from_nonce[..CTR_NONCE_LEN].copy_from_slice(&nonce); + cfb_key.decrypt_inplace(&iv_from_nonce, &mut misread).expect("decryption runs"); + assert_ne!(misread, plaintext, "CFB must not decrypt a CTR ciphertext"); +} diff --git a/crypto/aria/tests/stream_mode_tests.rs b/crypto/aria/tests/stream_mode_tests.rs new file mode 100644 index 000000000..4637ec143 --- /dev/null +++ b/crypto/aria/tests/stream_mode_tests.rs @@ -0,0 +1,339 @@ +//! Known-answer tests for the stream modes of operation over ARIA: the `ARIA-*-CFB`, +//! `ARIA-*-CFB8` and `ARIA-*-CTR` entries of OpenSSL's +//! `test/recipes/30-test_evp_data/evpciph_aria.txt` (OpenSSL 3.6.2), which attributes them to +//! `ARIA-testvector-e.pdf` from the ARIA site. Transcribed from the OpenSSL file. +//! +//! RFC 5794 itself publishes only single-block values (Appendix A), so these KISA vectors are the +//! only published mode answers for ARIA -- and unlike Camellia's and SM4's they line up with these +//! aliases exactly, in all three modes: +//! +//! * The CFB128 and CFB8 vectors share the IV `0f1e2d3c..f0` that the CBC vectors use, and are +//! driven through the key's `create_encryptor` with a [`FixedSeedRNG`] whose stream is that IV +//! (there is no API for supplying one); the test asserts the returned init data really is the +//! vector's IV before comparing any ciphertext. Decryption takes the IV directly. +//! * The CTR vectors start from an **all-zero counter block** and their `NextIV` is `..0a` after +//! the 160-byte, ten-block message, so the counter runs 0, 1, ... 9. That is exactly what the +//! nonce/counter split of `Ctr` produces from a twelve-byte zero nonce (see the +//! [`ARIA_CTR_128`](bouncycastle_aria::ARIA_CTR_128) docs), so they are straight known-answer +//! tests through the key too. +//! +//! The same 160-byte plaintext runs through all three modes, and it is the one the ECB and CBC +//! vectors in `kisa_vectors_tests.rs` use. + +mod common; + +use bouncycastle_aria::{ARIA128Key, ARIA192Key, ARIA256Key, BLOCK_LEN, CTR_NONCE_LEN}; +use bouncycastle_cipher::modes::{CFB, CFB8, CTR}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::symmetric_key::{ + FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey, +}; +use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use common::bytes; + +/// The 160-byte plaintext shared by all nine vectors. +const PLAINTEXT: &str = "11111111aaaaaaaa11111111bbbbbbbb11111111cccccccc11111111dddddddd22222222aaaaaaaa22222222bbbbbbbb22222222cccccccc22222222dddddddd33333333aaaaaaaa33333333bbbbbbbb33333333cccccccc33333333dddddddd44444444aaaaaaaa44444444bbbbbbbb44444444cccccccc44444444dddddddd55555555aaaaaaaa55555555bbbbbbbb55555555cccccccc55555555dddddddd"; + +/// The number of bytes in that plaintext: ten whole blocks. +const LEN: usize = 160; + +/// The IV shared by the CFB128 and CFB8 vectors -- the same one the CBC vectors use. +const IV: &str = "0f1e2d3c4b5a69788796a5b4c3d2e1f0"; + +const KEY_128: &str = "00112233445566778899aabbccddeeff"; +const KEY_192: &str = "00112233445566778899aabbccddeeff0011223344556677"; +const KEY_256: &str = "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"; + +/// `ARIA-128-CFB` ciphertext (CFB128). +const CFB_CT_128: &str = concat!( + "3720e53ba7d615383406b09f0a05a200", + "c07c21e6370f413a5d132500a6828501", + "7c61b434c7b7ca9685a51071861e4d4b", + "b873b599b479e2d573dddeafba89f812", + "ac6a9e44d554078eb3be94839db4b33d", + "a3f59c063123a7ef6f20e10579fa4fd2", + "39100ca73b52d4fcafeadee73f139f78", + "f9b7614c2b3b9dbe010f87db06a89a94", + "35f79ce8121431371f4e87b984e0230c", + "22a6dacb32fc42dcc6accef33285bf11", +); +/// `ARIA-192-CFB` ciphertext (CFB128). +const CFB_CT_192: &str = concat!( + "4171f7192bf4495494d2736129640f5c", + "4d87a9a213664c9448477c6ecc201359", + "8d9766952dd8c3868f17e36ef66fd84b", + "fa45d1593d2d6ee3ea2115047d710d4f", + "b66187caa3a315b3c8ea2d313962edcf", + "e5a3e2028d5ba9a09fd5c65c19d3440e", + "477f0cab0628ec6902c73ee02f1afee9", + "f80115be7b9df82d1e28228e28581a20", + "560e195cbb9e2b327bf56fd2d0ae5502", + "e42c13e9b4015d4da42dc859252e7da4", +); +/// `ARIA-256-CFB` ciphertext (CFB128). +const CFB_CT_256: &str = concat!( + "26834705b0f2c0e2588d4a7f09009635", + "f28bb93d8c31f870ec1e0bdb082b66fa", + "402dd9c202be300c4517d196b14d4ce1", + "1dce97f7aaba54341b0d872cc9b63753", + "a3e8556a14be6f7b3e27e3cfc39caf80", + "f2a355aa50dc83c09c7b11828694f8e4", + "aa726c528976b53f2c877f4991a3a8d2", + "8adb63bd751846ffb2350265e179d499", + "0753ae8485ff9b4133ddad5875b84a90", + "cbcfa62a045d726df71b6bda0eeca0be", +); +/// `ARIA-128-CFB8` ciphertext. +const CFB8_CT_128: &str = concat!( + "373c8f6a965599ec785cc8f8149f6c81", + "b632ccb8e0c6eb6a9707ae52c59257a4", + "1f94701c1096933127a90195ed0c8e98", + "690547572423bb45c3d70e4a18ee56b9", + "67c10e000ba4df5fba7c404134a343d8", + "375d04b151d161ef83417fe1748447d3", + "0a6723c406733df7d18aa39a20752d23", + "81942e244811bb97f72eae446b1815aa", + "690cd1b1adcbd007c0088ecdc91cb2e2", + "caf0e11e72459878137eea64ac62a9a1", +); +/// `ARIA-192-CFB8` ciphertext. +const CFB8_CT_192: &str = concat!( + "411d3b4f57f705aa4d13c46e2cf426af", + "7c8c916ed7923d889f0047bbf11471b6", + "d54f8757ef519339105be3cb69babb97", + "6a57d5631fc23cc3051fe9d36e8b8e27", + "a2b2c0c4d31928ccbf30ea8239b46ba1", + "b77f6198e7ecd2ce27b35958148e826f", + "06aaf385bd30362ff141583e7c1d8924", + "d44d36a1133094074631e18adafa9d2e", + "55de98f6895c89d4266ebd33f3d4be51", + "53a96fa12132ece2e81e66e55baa7ade", +); +/// `ARIA-256-CFB8` ciphertext. +const CFB8_CT_256: &str = concat!( + "26baa33651e1f66434fec88ef27fd2b9", + "a79e246dd89a3ffa00e8bdb37155433e", + "6c24bd0b87d9a85baa9f485ccb984f5e", + "c24d6a3ef5e3c81396177f039cf580df", + "db55d6e1c47a28921dfe369e12fd357b", + "289ad3a5544e1c1bd616d454db9c5f91", + "f603373f29d5b2ed1b4b51de80f28537", + "bbd43d5e3b5dd071dc91153cbbe732df", + "c325821b06ed8acaae656dcf2da9f13e", + "4f29db671476f1e644ff06d9b67d6bd4", +); +/// `ARIA-128-CTR` ciphertext. +const CTR_CT_128: &str = concat!( + "ac5d7de805a0bf1c57c854501af60fa1", + "1497e2a34519dea1569e91e5b5ccae2f", + "f3bfa1bf975f4571f48be191613546c3", + "911163c085f871f0e7ae5f2a085b8185", + "1c2a3ddf20ecb8fa51901aec8ee4ba32", + "a35dab67bb72cd9140ad188a967ac0fb", + "bdfa94ea6cce47dcf8525ab5a814cfeb", + "2bb60ee2b126e2d9d847c1a9e96f9019", + "e3e6a7fe40d3829afb73db1cc245646a", + "ddb62d9b907baaafbe46a73dbc131d3d", +); +/// `ARIA-192-CTR` ciphertext. +const CTR_CT_192: &str = concat!( + "08625ca8fe569c19ba7af3760a6ed1ce", + "f4d199263e999dde14082dbba7560b79", + "a4c6b456b8707dce751f9854f18893df", + "db3f4e5afa539733e6f1e70b98ba3789", + "1f8f81e95df8efc26c7ce043504cb189", + "58b865e4e316cd2aa1c97f31bf23dc04", + "6ef326b95a692a191ba0f2a41c5fe9ae", + "070f236ff7078e703b42666caafbdd20", + "bad74ac4c20c0f46c7ca24c151716575", + "c947da16c90cfe1bf217a41cfebe7531", +); +/// `ARIA-256-CTR` ciphertext. +const CTR_CT_256: &str = concat!( + "30026c329666141721178b99c0a1f1b2", + "f06940253f7b3089e2a30ea86aa3c88f", + "5940f05ad7ee41d71347bb7261e348f1", + "8360473fdf7d4e7723bffb4411cc13f6", + "cdd89f3bc7b9c768145022c7a74f14d7", + "c305cd012a10f16050c23f1ae5c23f45", + "998d13fbaa041e51619577e077276489", + "6a5d4516d8ffceb3bf7e05f613edd9a6", + "0cdcedaff9cfcaf4e00d445a54334f73", + "ab2cad944e51d266548e61c6eb0aa1cd", +); + +fn material(hex_str: &str) -> KeyMaterial { + KeyMaterial::::from_bytes_as_type(&bytes::(hex_str), KeyType::SymmetricCipherKey) + .expect("a valid symmetric cipher key") +} + +/// One vector, through the key: encrypted under the vector's own init data, in one call and then +/// in pieces, and decrypted back both ways. +/// +/// `init` is the IV for the CFB modes and the nonce for CTR; the only difference between the three +/// modes here is its length, which is why one function covers all nine vectors. +fn check<'k, const KEY_LEN: usize, const INIT_DATA_LEN: usize, K>( + name: &str, + key: &'k K, + init: [u8; INIT_DATA_LEN], + expected: &str, +) where + K: SymmetricEncryptionKey + + SymmetricDecryptionKey, + K::Encryptor<'k>: StreamCipherEncryptor, + K::Decryptor<'k>: StreamCipherDecryptor, +{ + let pt = bytes::(PLAINTEXT); + let ct = bytes::(expected); + + // There is no API for supplying init data, so pin the RNG to the vector's and check it came + // back before trusting any ciphertext. + let (mut enc, got) = key + .create_encryptor(&mut FixedSeedRNG::::new(init)) + .expect("encryption init"); + assert_eq!(got, init, "{name}: the pinned RNG should produce the vector's init data"); + let mut data = pt; + enc.do_encrypt_inplace(&mut data).expect("encryption"); + assert_eq!(data, ct, "{name} encrypt, one call"); + + // Block by block: the answer must not depend on where the call boundaries fall. + let (mut enc, _) = key + .create_encryptor(&mut FixedSeedRNG::::new(init)) + .expect("encryption init"); + let mut data = pt; + for block in data.chunks_mut(BLOCK_LEN) { + enc.do_encrypt_inplace(block).expect("encryption"); + } + assert_eq!(data, ct, "{name} encrypt, one block at a time"); + + let mut data = ct; + key.decrypt_inplace(&init, &mut data).expect("decryption"); + assert_eq!(data, pt, "{name} decrypt, one shot"); + + // Seven bytes at a time, which crosses no boundary the mode cares about and every boundary it + // must not. + let mut dec = key.create_decryptor(&init); + let mut data = ct; + for piece in data.chunks_mut(7) { + dec.do_decrypt_inplace(piece).expect("decryption"); + } + assert_eq!(data, pt, "{name} decrypt, seven bytes at a time"); +} + +// ---- CFB128 -------------------------------------------------------------------------------- + +#[test] +fn aria_128_cfb_vectors() { + check::<16, _, _>( + "ARIA-128-CFB", + &ARIA128Key::::from_key_material(&material::<16>(KEY_128)).unwrap(), + bytes::(IV), + CFB_CT_128, + ); +} + +#[test] +fn aria_192_cfb_vectors() { + check::<24, _, _>( + "ARIA-192-CFB", + &ARIA192Key::::from_key_material(&material::<24>(KEY_192)).unwrap(), + bytes::(IV), + CFB_CT_192, + ); +} + +#[test] +fn aria_256_cfb_vectors() { + check::<32, _, _>( + "ARIA-256-CFB", + &ARIA256Key::::from_key_material(&material::<32>(KEY_256)).unwrap(), + bytes::(IV), + CFB_CT_256, + ); +} + +// ---- CFB8 ---------------------------------------------------------------------------------- + +#[test] +fn aria_128_cfb8_vectors() { + check::<16, _, _>( + "ARIA-128-CFB8", + &ARIA128Key::::from_key_material(&material::<16>(KEY_128)).unwrap(), + bytes::(IV), + CFB8_CT_128, + ); +} + +#[test] +fn aria_192_cfb8_vectors() { + check::<24, _, _>( + "ARIA-192-CFB8", + &ARIA192Key::::from_key_material(&material::<24>(KEY_192)).unwrap(), + bytes::(IV), + CFB8_CT_192, + ); +} + +#[test] +fn aria_256_cfb8_vectors() { + check::<32, _, _>( + "ARIA-256-CFB8", + &ARIA256Key::::from_key_material(&material::<32>(KEY_256)).unwrap(), + bytes::(IV), + CFB8_CT_256, + ); +} + +// ---- CTR ----------------------------------------------------------------------------------- + +/// The vectors' counter block is all zeros, so the nonce these aliases split off it is twelve zero +/// bytes and the counter starts at zero -- which is what `Ctr` does anyway. +const CTR_NONCE: [u8; CTR_NONCE_LEN] = [0u8; CTR_NONCE_LEN]; + +#[test] +fn aria_128_ctr_vectors() { + check::<16, _, _>( + "ARIA-128-CTR", + &ARIA128Key::::from_key_material(&material::<16>(KEY_128)).unwrap(), + CTR_NONCE, + CTR_CT_128, + ); +} + +#[test] +fn aria_192_ctr_vectors() { + check::<24, _, _>( + "ARIA-192-CTR", + &ARIA192Key::::from_key_material(&material::<24>(KEY_192)).unwrap(), + CTR_NONCE, + CTR_CT_192, + ); +} + +#[test] +fn aria_256_ctr_vectors() { + check::<32, _, _>( + "ARIA-256-CTR", + &ARIA256Key::::from_key_material(&material::<32>(KEY_256)).unwrap(), + CTR_NONCE, + CTR_CT_256, + ); +} + +/// CFB128 and CFB8 are different modes, and the vectors say so: under the same key and IV the two +/// published ciphertexts agree on their first byte -- both are `P_1 XOR MSB_8(O_1)` of the same +/// first output block -- and differ everywhere after it. +#[test] +fn the_published_cfb128_and_cfb8_vectors_differ_after_the_first_byte() { + for (name, cfb, cfb8) in [ + ("ARIA-128", CFB_CT_128, CFB8_CT_128), + ("ARIA-192", CFB_CT_192, CFB8_CT_192), + ("ARIA-256", CFB_CT_256, CFB8_CT_256), + ] { + let cfb = bytes::(cfb); + let cfb8 = bytes::(cfb8); + assert_eq!(cfb[0], cfb8[0], "{name}: the first byte comes from the same output block"); + assert_ne!(cfb[1..], cfb8[1..], "{name}: and the rest must not agree"); + } +} diff --git a/crypto/aria/tests/wycheproof_cbc_tests.rs b/crypto/aria/tests/wycheproof_cbc_tests.rs new file mode 100644 index 000000000..e5bbb92f6 --- /dev/null +++ b/crypto/aria/tests/wycheproof_cbc_tests.rs @@ -0,0 +1,150 @@ +//! Known-answer tests against Google/C2SP **Wycheproof** `ARIA-CBC-PKCS5` vectors. +//! +//! Requires `bc-test-data` to be cloned alongside this repository, with the Wycheproof file under +//! `crypto/wycheproof/`. If it is absent the test prints a warning and passes, matching the +//! convention the ACVP suites use -- `cargo test` must stay green for someone who has only cloned +//! this repository. +//! +//! # Why these are worth having on top of the ACVP and published vectors +//! +//! Every other vector set here is block-aligned and valid: it checks that correct input produces +//! the right ciphertext. Wycheproof's value is the other half. Of its 216 cases, **72 are valid and +//! 144 are invalid**, and 141 of those carry the `BadPadding` flag: ciphertexts whose plaintext +//! does not end in well-formed PKCS#7 padding, which decryption must **reject**. That is the path +//! `bouncycastle_cipher::padding`'s constant-time `unpad` exists for, and nothing else in the tree drives +//! it with adversarially chosen input. +//! +//! PKCS#5 and PKCS#7 are the same padding for a 16-byte block; the name in the file is Wycheproof's. +//! +//! # How the vectors map onto this API +//! +//! Decryption takes the IV as init data, so an invalid case is simply required to fail and a valid +//! one to reproduce the message. Encryption never accepts an IV -- see the crate docs -- so the +//! valid cases are driven through the key's `encrypt_out` with a [`FixedSeedRNG`] emitting the +//! vector's IV, and the returned init data is checked against it before any ciphertext is +//! compared. + +use bouncycastle_aria::{ARIA128Key, ARIA192Key, ARIA256Key}; +use bouncycastle_cipher::modes::CBC; +use bouncycastle_cipher::padding::PKCS7; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::symmetric_key::{ + FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey, +}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_hex as hex; +use serde_json::Value; +use std::fs; +use std::path::{Path, PathBuf}; + +const VECTOR_FILE: &str = "aria_cbc_pkcs5_test.json"; + +/// Candidate locations, covering `cargo test` run from the crate root or from the repo root. +const TEST_DATA_PATHS: [&str; 2] = + ["../../../bc-test-data/crypto/wycheproof", "../bc-test-data/crypto/wycheproof"]; + +fn vector_file() -> Option { + for candidate in TEST_DATA_PATHS { + let path = Path::new(candidate).join(VECTOR_FILE); + if path.exists() { + return Some(path); + } + } + println!( + "WARNING: {VECTOR_FILE} not found (looked in {TEST_DATA_PATHS:?}); \ + Wycheproof ARIA-CBC-PKCS5 tests will be skipped" + ); + None +} + +fn key_material(raw: &[u8]) -> KeyMaterial { + assert_eq!(raw.len(), N, "key length should match the group"); + KeyMaterial::::from_bytes_as_type(raw, KeyType::SymmetricCipherKey) + .expect("a valid symmetric cipher key") +} + +fn decode(v: &Value, field: &str, tc: u64) -> Vec { + let s = v.get(field).and_then(Value::as_str).unwrap_or_else(|| panic!("tcId {tc}: no {field}")); + hex::decode(s).unwrap_or_else(|_| panic!("tcId {tc}: bad hex in {field}")) +} + +/// Runs one case for a given key length, under the `CBC` key `make` builds from the +/// case's material. Returns `true` if it was a valid case. +fn run_case(t: &Value, make: impl Fn(&KeyMaterial) -> K) -> bool +where + K: SymmetricEncryptionKey + SymmetricDecryptionKey, +{ + let tc = t.get("tcId").and_then(Value::as_u64).expect("tcId"); + let key = make(&key_material::(&decode(t, "key", tc))); + let iv: [u8; 16] = decode(t, "iv", tc).try_into().expect("a 16-byte IV"); + let msg = decode(t, "msg", tc); + let ct = decode(t, "ct", tc); + let result = t.get("result").and_then(Value::as_str).expect("result"); + let comment = t.get("comment").and_then(Value::as_str).unwrap_or(""); + + let mut out = vec![0u8; ct.len() + 16]; + let decrypted = key.decrypt_out(&iv, &ct, &mut out); + + match result { + "valid" => { + let n = decrypted.unwrap_or_else(|e| { + panic!("tcId {tc} ({comment}): valid case failed to decrypt: {e:?}") + }); + assert_eq!(&out[..n], &msg[..], "tcId {tc} ({comment}): wrong plaintext"); + + // ...and encryption reproduces the ciphertext under the vector's IV. + let mut ct_out = vec![0u8; msg.len() + 16]; + let (written, got_iv) = key + .encrypt_out(&mut FixedSeedRNG::<16>::new(iv), &msg, &mut ct_out) + .unwrap_or_else(|e| panic!("tcId {tc}: encryption failed: {e:?}")); + assert_eq!(got_iv, iv, "tcId {tc}: the pinned RNG should reproduce the vector's IV"); + assert_eq!(&ct_out[..written], &ct[..], "tcId {tc} ({comment}): wrong ciphertext"); + true + } + "invalid" => { + assert!( + decrypted.is_err(), + "tcId {tc} ({comment}): an invalid ciphertext must be rejected, not decrypted" + ); + false + } + other => panic!("tcId {tc}: unexpected result {other}"), + } +} + +#[test] +fn wycheproof_cbc_pkcs5_vectors() { + let Some(path) = vector_file() else { return }; + let doc: Value = + serde_json::from_str(&fs::read_to_string(&path).expect("readable vector file")) + .expect("valid Wycheproof JSON"); + + let mut valid = 0usize; + let mut invalid = 0usize; + + for group in doc.get("testGroups").and_then(Value::as_array).expect("testGroups") { + let key_size = group.get("keySize").and_then(Value::as_u64).expect("keySize"); + let iv_size = group.get("ivSize").and_then(Value::as_u64).expect("ivSize"); + assert_eq!(iv_size, 128, "these vectors should all use a 16-byte IV"); + + for t in group.get("tests").and_then(Value::as_array).expect("tests") { + let was_valid = match key_size { + 128 => run_case::<_, 16>(t, |m| { + ARIA128Key::>::from_key_material(m).expect("a valid key") + }), + 192 => run_case::<_, 24>(t, |m| { + ARIA192Key::>::from_key_material(m).expect("a valid key") + }), + 256 => run_case::<_, 32>(t, |m| { + ARIA256Key::>::from_key_material(m).expect("a valid key") + }), + other => panic!("unexpected key size {other}"), + }; + if was_valid { valid += 1 } else { invalid += 1 } + } + } + + println!("Wycheproof ARIA-CBC-PKCS5: {valid} valid and {invalid} invalid cases checked"); + assert_eq!(valid + invalid, 216, "expected the whole Wycheproof set"); + assert!(invalid > 100, "the invalid cases are the point of this suite; found {invalid}"); +} diff --git a/mem_usage_benches/Cargo.toml b/mem_usage_benches/Cargo.toml index fb55fb094..614066b04 100644 --- a/mem_usage_benches/Cargo.toml +++ b/mem_usage_benches/Cargo.toml @@ -30,3 +30,7 @@ path = "src/bench_ccm_mem_usage.rs" [[bin]] name = "bench_ascon_mem_usage" path = "src/bench_ascon_mem_usage.rs" + +[[bin]] +name = "bench_aria_mem_usage" +path = "src/bench_aria_mem_usage.rs" diff --git a/mem_usage_benches/src/bench_aria_mem_usage.rs b/mem_usage_benches/src/bench_aria_mem_usage.rs new file mode 100644 index 000000000..29d18b9ca --- /dev/null +++ b/mem_usage_benches/src/bench_aria_mem_usage.rs @@ -0,0 +1,117 @@ +//! The purpose of this binary is to perform a single run of the primitive under test so that +//! its peak memory usage can be measured with: +//! +//! ```text +//! valgrind --tool=massif --heap=no --stacks=yes -- target/release/bench_aria_mem_usage > /dev/null +//! +//! ms_print massif.out.* +//! ``` +//! +//! or, shoved all into one line: +//! +//! ```text +//! clear; clear; valgrind --tool=massif --heap=no --stacks=yes -- target/release/bench_aria_mem_usage > /dev/null; ms_print massif.out.*; rm massif.out.* +//! ``` +//! +//! Make sure you build in release mode! +//! +//! Note: print!() is used to force the compiler not to optimize away the actual code. +//! The important stuff for benchmarking goes to stderr so the junk can be piped to /dev/null. +//! +//! Main is at the bottom, and controls which of these actually runs -- measure one at a time, +//! because massif reports the peak across the whole process. +//! +//! # What to expect +//! +//! Like AES, SM4 and Camellia, ARIA has no interesting stack profile: peak usage is a small constant +//! plus the round keys. The numbers worth recording in the crate docs are what `print_struct_sizes` +//! prints -- the persistent 208, 240 or 272 bytes of round keys -- and the confirmation that per-call +//! work is the four-block working state (four 32-bit words per block, 64 bytes), the four class +//! words being substituted (16 bytes), the eight 16-bit S-box planes (16 bytes), one round key and +//! circuit temporaries. There are no lookup tables; a straightforward implementation adds 1 KiB of +//! S-box tables and 256 bytes of diffusion masks on top of these numbers. + +#![allow(dead_code)] +#![allow(unused_imports)] + +use bouncycastle::aria::LANES; +use bouncycastle::aria::hazmat::{ARIA128Internal, ARIA192Internal, ARIA256Internal}; +use bouncycastle::core::hazmat::{BlockPermutation, ElectronicCodeBook}; +use bouncycastle::core::key_material::{KeyMaterial, KeyType}; + +/// This exists so /usr/bin/time can measure the base memory footprint of the harness itself. +fn bench_do_nothing() { + eprintln!("DoNothing"); + + print!("{}", 1 + 1); +} + +/// Prints the in-memory size of each engine, i.e. the persistent cost of holding the subkeys. +fn print_struct_sizes() { + use core::mem::size_of; + + // RFC 5794 Sec 2.2: ek1 .. ek{n+1} for n = 12, 14, 16 rounds -- 13, 15 or 17 keys of 16 bytes. + println!("size_of: {}", size_of::()); + println!("size_of: {}", size_of::()); + println!("size_of: {}", size_of::()); +} + +fn key() -> KeyMaterial { + // A fixed non-zero key: an all-zero buffer would be tagged KeyType::Zeroized and rejected. + let mut bytes = [0u8; KEY_LEN]; + for (i, b) in bytes.iter_mut().enumerate() { + *b = (i as u8).wrapping_mul(7).wrapping_add(1); + } + KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).unwrap() +} + +fn bench_aria128_key_expansion() { + eprintln!("ARIA128Internal::new (key expansion)"); + + let aria = ARIA128Internal::new(&key()).unwrap(); + print!("{aria:?}"); +} + +fn bench_aria256_key_expansion() { + eprintln!("ARIA256Internal::new (key expansion)"); + + let aria = ARIA256Internal::new(&key()).unwrap(); + print!("{aria:?}"); +} + +fn bench_aria128_encrypt_block() { + eprintln!("ARIA128Internal::encrypt_block"); + + let aria = ARIA128Internal::new(&key()).unwrap(); + let mut block = [0x11u8; 16]; + aria.encrypt_block(&mut block); + print!("{block:x?}"); +} + +fn bench_aria256_decrypt_block() { + eprintln!("ARIA256Internal::decrypt_block"); + + let aria = ARIA256Internal::new(&key()).unwrap(); + let mut block = [0x11u8; 16]; + aria.decrypt_block(&mut block); + print!("{block:x?}"); +} + +fn bench_aria256_encrypt_4blocks() { + eprintln!("ARIA256Internal::encrypt_4blocks"); + + let aria = ARIA256Internal::new(&key()).unwrap(); + let mut blocks: [[u8; 16]; LANES] = core::array::from_fn(|i| [0x11 * (i as u8 + 1); 16]); + aria.encrypt_4blocks(&mut blocks); + print!("{blocks:x?}"); +} + +fn main() { + print_struct_sizes() + // bench_do_nothing() + // bench_aria128_key_expansion() + // bench_aria256_key_expansion() + // bench_aria128_encrypt_block() + // bench_aria256_decrypt_block() + // bench_aria256_encrypt_4blocks() +} diff --git a/mem_usage_benches/src/lib.rs b/mem_usage_benches/src/lib.rs index 8c3f55926..b2f7e4e26 100644 --- a/mem_usage_benches/src/lib.rs +++ b/mem_usage_benches/src/lib.rs @@ -1,4 +1,5 @@ mod bench_aes_mem_usage; +mod bench_aria_mem_usage; mod bench_ascon_mem_usage; mod bench_ccm_mem_usage; mod bench_mldsa_mem_usage; diff --git a/src/lib.rs b/src/lib.rs index ae250f332..8e30113ac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub use bouncycastle_aes as aes; +pub use bouncycastle_aria as aria; pub use bouncycastle_ascon as ascon; pub use bouncycastle_base64 as base64; pub use bouncycastle_cipher as cipher;