HC256 in RustThe HC-256 method was created in 2004 [1], and uses a 256-bit key (K) and a 256-bit nonce (IV) value. Internally it has two secret tables: P and Q, and where each table has 1,024 32-bit words. Basically, these represent state values, and each change of state will update one of the entries of the table, and which uses a non-linear update function. Thus, after 2,048 state changes, every element of the tables will have been updated. We then derive the final has by taking a SHA-256, but where we use the P and Q tables as S-boxes. Overall, it is a fast-stream cipher with just four CPU cycles per byte. |
Theory
The HC-256 method was created in 2004 [1], and uses a 256-bit key (K) and a 256-bit nonce (IV) value. Internally it has two secret tables: P and Q, and where each table has 1,024 32-bit words. Basically, these represent state values, and each change of state will update one of the entries of the table, and which uses a non-linear update function. Thus, after 2,048 state changes, every element of the tables will have been updated. We then derive the final has by taking a SHA-256, but where we use the P and Q tables as S-boxes. Overall, it is a fast-stream cipher with just four CPU cycles per byte.
The basic functions that HC-256 uses are simple and involve either EX-OR, mod, add or bit shift operations:
Figure [here]
As we have a stream cipher, we basically take the key and the IV, and expand into a key stream (s). The cipher is then:
\(c = p \oplus s\)
and where we EX-OR each bit of the plaintext (p) with the bits in the keystream (s), and generate a cipher stream (c ). In order to recover the plaintext, we just EX-OR again with the key stream:
\(p = c \oplus s\)
Key and IV setup
The initialization phase provides an expansion of the 256-bit key into the P and Q tables. With this, we take the key and the IV, and split them up into eight 32-bit values (K_0 … K_7, and IV_0 … IV_7). Next, we compute W_i and then use these values to fill the P and Q tables:
Figure [here]
Keystream expansion
In this stage, we generate the key stream (s). Each step will update one of the entries in either the P table or the Q table:
Figure [here]
This will then create the S-box values for the SHA-256 operation. Once generated, the plaintext and cipher stream is just EX-ORed with the expanded key stream.
Coding
The outline code is:
use hc_256::Hc256; use rand::thread_rng; use rand::Rng; use hc_256::cipher::{KeyIvInit, StreamCipher}; use std::str; use std::env; fn get_random_key32() -> [u8; 32]{ let mut arr = [0u8; 32]; thread_rng().try_fill(&mut arr[..]).expect("Ooops!"); return arr; } fn main() { let key = get_random_key32(); let nonce = get_random_key32(); let args: Vec<String> = env::args().collect(); let mut msg="Test"; if args.len() >1 { msg = args[1].as_str(); } let message = String::from(msg); let plaintext=message.as_bytes(); let len = plaintext.len(); let mut buffer = [0u8; 128]; buffer[..len].copy_from_slice(plaintext); let mut cipher = Hc256::new(&key.into(), &nonce.into()); cipher.apply_keystream(&mut buffer); println!("== HC256 =="); println!("Message: {}",message); println!("Key {}",hex::encode(&key)); println!("Nonce {}",hex::encode(&nonce)); println!("\nEncrypted: {}",hex::encode(&buffer[0..len])); let mut cipher = Hc256::new(&key.into(), &nonce.into()); cipher.apply_keystream(&mut buffer); println!("\nCiphertext: {:?}",str::from_utf8(&buffer[0..len]).unwrap()); }
and Cargo.toml:
[package] name = "hc256" version = "0.1.0" edition = "2021" [dependencies] hc-256 = "0.5.0" hex="0.4.3" hex-literal="0.3.3" rand="0.8.3"
A test run is:
== HC256 == Message: Hello Key e01c659e77c0ef0b91abb3cd9b1e6172a0854b8417a2c7b36b999ee6d19f76cf Nonce 0811c359c9498ff389960d0de31d2bf30a504c219862a028167197cdbf347767 Encrypted: cd896cd27a Ciphertext: "Hello"
References
[1] Wu, H. (2004, February). A new stream cipher HC-256. In International Workshop on Fast Software Encryption (pp. 226-244). Springer, Berlin, Heidelberg.