Hamming code steganography
⚓ Rust 📅 2026-05-29 👤 surdeus 👁️ 1Here is an implementation of steganography using Hamming codes and an xor cipher:
use std::io::Write;
use std::fs::File;
use std::io;
use rand::{rng, RngExt};
static WITHOUT_POWERS_OF_TWO: &[u8] = &[0, 3, 5, 6, 7, 9, 10, 11, 12, 13, 14, 15];
fn string_to_fmt_u16s(s: String) -> Vec<u16> {
let string = s.chars().filter(|c| *c == '0' || *c == '1').collect::<String>();
let mut slices = Vec::new();
let out_len = string.len() / 16;
for i in 0..out_len {
slices.push(u16::from_str_radix(&string[i*16..(i+1)*16], 2).expect("Input not a valid binary number."));
}
slices
}
fn string_to_fmt_bytes(s: String) -> Vec<u8> {
let string = s.chars().filter(|c| *c == '0' || *c == '1').collect::<String>();
let mut slices = Vec::new();
let out_len = string.len() / 8;
for i in 0..out_len {
slices.push(u8::from_str_radix(&string[i*8..(i+1)*8], 2).expect("Input not a valid binary number."));
}
slices
}
const fn get_bit_at(v: u16, i: u8) -> u8 {
((v >> i) & 1) as u8
}
fn set_bit_at(v: &mut u16, i: u8, val: bool) {
let cleared_bit = *v & !(1 << i);
if val {
*v = cleared_bit | (1 << i)
} else {
*v = cleared_bit
}
}
fn toggle_bit_at(v: u16, i: u8) -> u16 {
v ^ (1 << i)
}
fn bools_to_u8(b: [bool; 8]) -> u8 {
let mut answer: u8 = 0;
for (i, boolean) in b.iter().enumerate() {
answer |= (*boolean as u8) << (7 - i as u8)
};
answer
}
fn arr_bool_to_arr_u8(bools: Vec<bool>) -> Vec<u8> {
let mut bools_copy = bools.clone();
let len_u8s = bools_copy.len() / 8 + if bools_copy.len() % 8 > 0 { 1 } else { 0 };
let mut false_bools_space = vec![false; bools_copy.len() % 8];
bools_copy.append(&mut false_bools_space);
let mut answer = Vec::new();
for i in 0..len_u8s {
answer.push(bools_to_u8(bools_copy[i*8..(i+1)*8].try_into().unwrap()));
};
answer.into()
}
fn remove_by_value<T: PartialEq>(xs: &mut Vec<T>, value: T) {
let index = match xs.iter().position(|x| *x == value) {
Some(i) => i,
None => return
};
xs.remove(index);
}
fn spread_u8_to_u16(bits: u8, how_to_spread: &[u8]) -> u16 {
let mut ans = 0;
for i in 0..how_to_spread.len() {
ans |= ((bits >> i & 1) as u16) << how_to_spread[i] as u16;
}
ans
}
fn unspread_u16_to_u8(bits: u16, how_to_unspread: &[u8]) -> u8 {
let mut ans = 0;
for i in 0..how_to_unspread.len() {
ans |= get_bit_at(bits, how_to_unspread[i]) << i;
}
ans
}
fn hamming(data_block: u16) -> u16 {
let mut code = data_block;
// We calculate 4 parity bits (1, 2, 4, 8)
for i in 0..4 {
let parity_bit_pos = 1 << i;
let mut parity = 0;
for j in 1..16 {
if (j & parity_bit_pos) != 0 {
parity ^= get_bit_at(code, j as u8);
}
}
if parity != 0 {
code = toggle_bit_at(code, parity_bit_pos as u8);
}
}
code
}
fn reverse_hamming(received: u16) -> u8 {
let mut error_pos = 0;
for i in 0..4 {
let parity_bit_pos = 1 << i;
let mut parity = 0;
for j in 1..16 {
if (j & parity_bit_pos) != 0 {
parity ^= get_bit_at(received, j as u8);
}
}
if parity != 0 {
error_pos += parity_bit_pos;
}
}
error_pos as u8
}
fn hamming_encrypt(string: &str, bytes_flag: bool) -> Vec<u16> {
let mut rng = rng();
let bytes = if bytes_flag { &string_to_fmt_bytes(string.into()) } else { string.as_bytes() };
let mut error_bits = Vec::new();
let mut key = Vec::new();
for _ in 0..bytes.len() {
error_bits.push(rng.random_range(0..16));
key.push(rng.random::<bool>());
}
let key_u8 = arr_bool_to_arr_u8(key.repeat(8));
let encrypted: Vec<_> = bytes.iter().enumerate().map(|(i, b)| *b ^ key_u8[i]).collect();
let encrypted_u16 = encrypted.iter().enumerate().map(|(i, b)| {
let mut spread_indexes = WITHOUT_POWERS_OF_TWO.to_vec();
remove_by_value(&mut spread_indexes, error_bits[i]);
let mut b = spread_u8_to_u16(*b, &spread_indexes[..8]);
set_bit_at(&mut b, error_bits[i], !key[i]);
b
}).collect::<Vec<_>>();
let hamminged_encrypted_u16 = encrypted_u16.iter().enumerate().map(|(i, b)| {
let hamminged = hamming(*b);
toggle_bit_at(if (get_bit_at(hamminged, error_bits[i]) != 0) == key[i] {
hamming(hamminged | 1 << 15)
} else {
hamminged
}, error_bits[i])
}).collect::<Vec<_>>();
hamminged_encrypted_u16
}
fn hamming_decrypt(encrypted: Vec<u16>, bytes_flag: bool) -> String {
let hamming_key_locations = encrypted.iter().map(|x| reverse_hamming(*x)).collect::<Vec<_>>();
let unspread_bits = encrypted.iter().enumerate().map(|(i, x)| {
let mut unspread_indexes = WITHOUT_POWERS_OF_TWO.to_vec();
remove_by_value(&mut unspread_indexes, hamming_key_locations[i]);
unspread_u16_to_u8(*x, &unspread_indexes[..8])
}).collect::<Vec<_>>();
let key = encrypted.iter().enumerate().map(|(i, x)| get_bit_at(*x, hamming_key_locations[i]) != 0).collect::<Vec<_>>();
let key_u8 = arr_bool_to_arr_u8(key.repeat(8));
let bytes = unspread_bits.iter().enumerate().map(|(i, x)| x ^ key_u8[i]).collect::<Vec<_>>();
if bytes_flag {
bytes.iter().map(|&b| format!("{:08b}", b)).collect::<Vec<_>>().join(" ")
} else {
match String::from_utf8(bytes.clone()) {
Ok(string) => string,
Err(_) => {
let mut err_str = String::from("Failed to convert to string, raw bytes provided below:\n");
err_str.push_str(&*bytes.iter().map(|&b| format!("{:08b}", b)).collect::<Vec<_>>().join(" "));
err_str
}
}
}
}
enum Output {
File(File),
Stdout,
}
impl Write for Output {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self {
Output::File(f) => f.write(buf),
Output::Stdout => io::stdout().write(buf),
}
}
fn flush(&mut self) -> io::Result<()> {
match self {
Output::File(f) => f.flush(),
Output::Stdout => io::stdout().flush(),
}
}
}
fn main() {
let args = std::env::args().collect::<Vec<_>>();
if args.len() < 3 {
eprintln!("Usage: hamming [bytes] encrypt/decrypt <text> [<output file>]");
} else {
let mut output = if (args.len() == 4 && args[1] != "bytes") || (args.len() == 5 && args[1] == "bytes"){ Output::File(File::create(&args[args.len()-1]).expect("Your choice of file is invalid. Please choose another file."))} else { Output::Stdout };
match args[1].as_str() {
"bytes" => match args[2].as_str() {
"encrypt" => writeln!(output, "{}", hamming_encrypt(&args[3], true).iter().map(|x| format!("{:016b}", x).trim_start_matches("0b").to_string() ).collect::<Vec<_>>().join(" ")).expect("Could not write to specified file."),
"decrypt" => writeln!(output, "{}", hamming_decrypt(string_to_fmt_u16s(args[3].clone()), true)).expect("Could not write to specified file."),
_ => eprintln!("Usage: hamming [bytes] encrypt/decrypt <text> [<output file>]"),
}
"encrypt" => writeln!(output, "{}", hamming_encrypt(&args[2], false).iter().map(|x| format!("{:016b}", x).trim_start_matches("0b").to_string() ).collect::<Vec<_>>().join(" ")).expect("Could not write to specified file."),
"decrypt" => writeln!(output, "{}", hamming_decrypt(string_to_fmt_u16s(args[2].clone()), false)).expect("Could not write to specified file."),
_ => eprintln!("Usage: hamming [bytes] encrypt/decrypt <text> [<output file>]"),
}
}
}
It uses a CLI and supports bytes and text as well as an output file.
3 posts - 3 participants
🏷️ Rust_feed