This simple implementation hides text secrets (S) in images by manipulating the least significant bits (lsb) on Pixels.
How it works
When encrypting I take a three pixel "chunk" for each byte. Each bit of the byte is hidden in the RGB (lsb) values of the pixels. The last blue value in the last pixel of each chunk is an exception, it stores a termination flag, more characters need to be encrypted sets this flag to 1 otherwise to 0.
Decryption reverts the above method, by constructing a byte from each three pixel chunk and putting it back to a string.
Implementation
The encrypt function:
/// Hides a secret string in the target image.
///
/// # Arguments
///
/// * `src_img` - Target image the secret will be written to.
/// * `secret` - Secret string which will be hidden in the target image.
///
pub fn encode_secret(src_img: &mut DynamicImage, secret: String) {
let secret_bytes = secret.into_bytes();
// Iterate over each byte in the secret
for (byte_idx, byte) in secret_bytes.iter().enumerate() {
// Index of the first (of three) Pixel
let n_rgb = byte_idx as u32 * 3;
// Get bits for current byte
let bit_buffer = byte.to_bit_buffer();
// Iterate over chunks of three bits
for (bit_chunk_idx, bit_chunk) in bit_buffer.chunks(3).enumerate() {
// Calculate (image) row and column of the current pixel
let n: u32 = n_rgb + bit_chunk_idx as u32;
let col_idx = n / src_img.width();
let row_idx = n % src_img.width();
// Replace the least signbificant bits for R and G values of the Pixel.
let o_pixel = src_img.get_pixel(row_idx, col_idx);
let r_out = o_pixel[0].set_lsb(bit_chunk[0]);
let g_out = o_pixel[1].set_lsb(bit_chunk[1]);
let b_out: u8 = if bit_chunk.len() > 2 {
// More than 2 bits in chunk, not the last chunk. We expect more chunks to come
o_pixel[2].set_lsb(bit_chunk[2])
} else if byte_idx + 1 == secret_bytes.len() {
// This is the last secret char, we set the last byte to be even.
o_pixel[2].set_lsb(false)
} else {
o_pixel[2].set_lsb(true) // Last chunk but not the last secret char, we set the last byte to be odd.
};
// Write pixel back to image
src_img.put_pixel(
row_idx,
col_idx,
image::Rgba([r_out, g_out, b_out, o_pixel[3]]),
);
}
}
}
The decrypt function:
/// Returns a secret string retrieved from the provided image if it exists.
///
/// # Arguments
///
/// * `img` - Image from which a secret will be retrieved.
///
pub fn decode_secret(img: &DynamicImage) -> Option<String> {
let mut result: String = String::new();
let mut byte_array = vec![];
let pixels: Vec<_> = img.pixels().collect();
// Iterate chunks of three pixels
for pixel_chunk in pixels.chunks(3) {
let mut byte: u8 = 0;
// Construct a byte from three pixels
byte = byte.set_bit(0, pixel_chunk[0].2 .0[0].get_lsb());
byte = byte.set_bit(1, pixel_chunk[0].2 .0[1].get_lsb());
byte = byte.set_bit(2, pixel_chunk[0].2 .0[2].get_lsb());
byte = byte.set_bit(3, pixel_chunk[1].2 .0[0].get_lsb());
byte = byte.set_bit(4, pixel_chunk[1].2 .0[1].get_lsb());
byte = byte.set_bit(5, pixel_chunk[1].2 .0[2].get_lsb());
byte = byte.set_bit(6, pixel_chunk[2].2 .0[0].get_lsb());
byte = byte.set_bit(7, pixel_chunk[2].2 .0[1].get_lsb());
byte_array.push(byte);
// Last byte (blue) value of the last pixel in the chunk is even. This is the termination flag.
if pixel_chunk[2].2 .0[2] % 2 == 0 {
break;
}
}
// Try convert the byte array to (secret) string
match String::from_utf8(byte_array) {
Ok(res) => result = format!("{}{}", result, res),
Err(_err) => return None,
}
if result.is_empty() {
return None;
}
Some(result)
}
For the bit operations I use some helper functions:
pub trait BitBuffer {
fn to_bit_buffer(&self) -> Vec<bool>;
}
impl BitBuffer for u8 {
/// Returns a vector of bools representing the single bits of self.
///
fn to_bit_buffer(&self) -> Vec<bool> {
let mut result = vec![];
for i in 0..=7 {
result.push(self.get_bit(i).unwrap());
}
result
}
}
pub trait BitOps {
/// Sets the least significant bit of a number according to the passed value.
///
fn set_lsb(&self, value: bool) -> Self;
/// Returns the bit on the specified position.
///
fn get_bit(&self, n: u8) -> Option<bool>;
/// Sets a bit on the specified position.
///
fn set_bit(&self, n: usize, value: bool) -> u8;
/// Returns the least significant bit.
///
fn get_lsb(&self) -> bool;
}
impl BitOps for u8 {
fn get_bit(&self, n: u8) -> Option<bool> {
if n > 7 {
return None;
}
let result = *self >> n & 1;
if result == 1 {
return Some(true);
} else if result == 0 {
return Some(false);
}
panic!();
}
fn set_bit(&self, n: usize, value: bool) -> u8 {
let mut result = *self;
if n > 7 {
panic!();
}
if value {
result |= 1 << n;
} else {
result &= !(1 << n);
}
result
}
fn set_lsb(&self, value: bool) -> u8 {
let mut result = *self;
if value {
result |= 0b0000_0001;
return result;
}
result &= 0b1111_1110;
result
}
fn get_lsb(&self) -> bool {
self % 2 != 0
}
}
In order to run the above implementation you will need the image crate in your project. Load an image and pass the required parameters to the functions above. Because of the nature of the implementation, your image will need to have a pixel amount which is at least three times the character amount.
What am I (not) looking for in a review
- It is not my goal to implement a highly sophisticated algorithm, just a simple one.
- Am I doing things the rustacean way? Especially in the
encode_secret
function I have a feeling, that I should/could have used moreiter
magic. Still after browsing the docs I ended up using nested loops. - How is the overall code style? Is there any room for performance optimization?