Introduction
Our Rust Bacon cipher packs a surprising amount of functionality into just a few dozen lines of clean, idiomatic code. At its core are four tiny functions - char_to_bits
, bits_to_char
, u16_to_bits
and bits_to_u16
- that convert between letters, bit-chunks and a 16-bit length header. From there, a painless encode
routine walks a cover text and flips each letter’s case to hide both the header and your message bits, while decode
reverses the process by reading its own header and stopping exactly where the secret ends. No external crates, no global state—just Rust’s standard library, iterators and bit-wise operations working together to deliver a self-terminating steganography tool.
Project Setup
Before we dive into the code, make sure you have Rust installed on your system. You can check this by running:
If Rust is not installed, follow the official installation guide.
Step 1: Creating the Rust Project
Create a new Rust project using Cargo:
cargo new bacon_cipher
cd bacon_cipher
Step 2: Implementing the Bacon Cipher in Rust
Open the src/main.rs
file and follow the steps below.
Step 2.1: Define the letter-bit mapping layer
/// A–Z → [b4,b3,b2,b1,b0]
fn char_to_bits(c: char) -> Option<[u8; 5]> {
if !c.is_ascii_alphabetic() {
return None;
}
let idx = c.to_ascii_uppercase() as u8 - b'A';
let mut bits = [0; 5];
for i in 0..5 {
bits[i] = (idx >> (4 - i)) & 1;
}
Some(bits)
}
/// [b4,b3,b2,b1,b0] → A–Z
fn bits_to_char(bits: &[u8]) -> Option<char> {
if bits.len() != 5 {
return None;
}
let mut idx = 0;
for (i, &b) in bits.iter().enumerate() {
idx |= (b as u8) << (4 - i);
}
Some((b'A' + idx) as char)
}
/// u16 → 16 bits (big-endian)
fn u16_to_bits(n: u16) -> [u8; 16] {
let mut bits = [0; 16];
for i in 0..16 {
bits[i] = ((n >> (15 - i)) & 1) as u8;
}
bits
}
/// 16 bits → u16
fn bits_to_u16(bits: &[u8]) -> u16 {
let mut n = 0;
for &b in bits.iter().take(16) {
n = (n << 1) | (b as u16);
}
n
}
Step 2.2: Encoding a Message
fn encode(msg: &str, cover: &str) -> String {
// build bits (header + message)
let mut bits = u16_to_bits(msg.chars().count() as u16).to_vec();
bits.extend(msg.chars().filter_map(char_to_bits).flat_map(|b| b));
// count how many letters you have available
let letters = cover.chars().filter(|c| c.is_ascii_alphabetic()).count();
if letters < bits.len() {
eprintln!(
"Error: cover has only {} letters but needs {} (16+{}×5).",
letters,
bits.len(),
msg.chars().count()
);
std::process::exit(1);
}
// 1) Header = msg.len() as u16 → 16 bits
let len = msg.chars().count() as u16;
let mut bits = u16_to_bits(len).to_vec();
// 2) Append message bits
bits.extend(msg.chars().filter_map(char_to_bits).flat_map(|b| b));
// 3) Embed via casing (only consume a bit on letters)
let mut it = bits.into_iter();
cover
.chars()
.map(|c| {
if c.is_ascii_alphabetic() {
match it.next() {
Some(1) => c.to_ascii_uppercase(),
Some(0) => c.to_ascii_lowercase(),
Some(_) => c, // Handle unexpected bit values gracefully
None => c,
}
} else {
c
}
})
.collect()
}
Step 2.3: Decoding a Message
fn decode(stego: &str) -> String {
let bits: Vec<u8> = stego
.chars()
.filter_map(|c| {
if c.is_ascii_alphabetic() {
Some(if c.is_ascii_uppercase() { 1 } else { 0 })
} else {
None
}
})
.collect();
if bits.len() < 16 {
eprintln!(
"Error: stego text has only {} bits; need at least 16 for the header.",
bits.len()
);
return String::new();
}
let msg_len = bits_to_u16(&bits[..16]) as usize;
let required = 16 + msg_len * 5;
if bits.len() < required {
eprintln!(
"Error: stego text has {} bits but need {} (header + {}×5).",
bits.len(),
required,
msg_len
);
return String::new();
}
bits[16..required]
.chunks(5)
.filter_map(bits_to_char)
.collect()
}
Step 2.4: Usage / Main Function
fn usage() {
eprintln!("Usage:");
eprintln!(" bacon encode <msg> <cover_text>");
eprintln!(" bacon decode <stego_text>");
}
fn main() {
let args: Vec<String> = std::env::args().collect();
if args.len() < 3 {
usage();
return;
}
match args[1].as_str() {
"encode" => {
if args.len() != 4 {
usage();
return;
}
println!("{}", encode(&args[2], &args[3]));
}
"decode" => {
if args.len() != 3 {
usage();
return;
}
println!("{}", decode(&args[2]));
}
_ => usage(),
}
}
Step 3: Running the Program
Step 3.1: Build the release
Step 3.2: Build the release
./target/release/bacon_cipher encode "HELLO" "This cover text now has way more than forty-one letters in it for our demo!"
You should see output similar to:
this cover text NoW haS WAy mOre tHaN FoRtY-OnE LEtters in it for our demo!
(That’s your “stego” text — the header + bit-stream hidden in letter case.)
Step 3.3: Decode it back
./target/release/bacon_cipher decode "this cover text NoW haS WAy mOre tHaN FoRtY-OnE LEtters in it for our demo!"
You should see output similar to: