1use image::RgbImage;
2use rand::RngCore;
3use uuid::Uuid;
4
5use crate::{
6 crypto::{derive_base_key, derive_message_key, encrypt, sha256},
7 error::{Result, StegoError},
8 formats::{ChunkHeader, CHUNK_HEADER_SIZE},
9};
10
11use super::lsb::{LsbReader, LsbWriter};
12
13pub struct EncoderBuilder {
19 bits_per_channel: u8,
20}
21
22impl Default for EncoderBuilder {
23 fn default() -> Self {
24 Self {
25 bits_per_channel: 1,
26 }
27 }
28}
29
30impl EncoderBuilder {
31 pub fn bits_per_channel(mut self, bpc: u8) -> Self {
33 self.bits_per_channel = bpc;
34 self
35 }
36
37 pub fn build(self) -> Result<StegoEncoder> {
39 if self.bits_per_channel == 0 || self.bits_per_channel > 8 {
40 return Err(StegoError::InvalidBitsPerChannel(self.bits_per_channel));
41 }
42 Ok(StegoEncoder {
43 bits_per_channel: self.bits_per_channel,
44 })
45 }
46}
47
48#[derive(Debug)]
56pub struct StegoEncoder {
57 bits_per_channel: u8,
58}
59
60pub fn scan_used_channels(img: &RgbImage) -> usize {
64 let total = img.width() as usize * img.height() as usize * 3;
65 let mut channel_idx = 0;
66 loop {
67 if channel_idx + CHUNK_HEADER_SIZE * 8 > total {
69 break;
70 }
71 let mut reader = LsbReader::new_at(img, channel_idx);
72 let Ok(header_bytes) = reader.read_bits(CHUNK_HEADER_SIZE, 1) else {
73 break;
74 };
75 let Ok(header) = ChunkHeader::from_bytes(&header_bytes) else {
76 break;
77 };
78 let header_channels = CHUNK_HEADER_SIZE * 8;
80 let payload_channels = (header.payload_length as usize * 8)
81 .div_ceil(header.bits_per_channel as usize);
82 channel_idx += header_channels + payload_channels;
83 if channel_idx >= total {
84 break;
85 }
86 }
87 channel_idx
88}
89
90impl StegoEncoder {
91 pub fn builder() -> EncoderBuilder {
93 EncoderBuilder::default()
94 }
95
96 pub(crate) fn split_message(
101 message: &[u8],
102 images: &[RgbImage],
103 bpc: u8,
104 ) -> Result<Vec<Vec<u8>>> {
105 if images.is_empty() {
106 return Err(StegoError::InvalidConfig("no cover images provided".into()));
107 }
108
109 const GCM_TAG: usize = 16;
110
111 let mut chunks: Vec<Vec<u8>> = Vec::new();
112 let mut remaining = message;
113
114 for img in images {
115 if remaining.is_empty() {
116 break;
117 }
118 let total_channels = img.width() as usize * img.height() as usize * 3;
119 let header_channels = CHUNK_HEADER_SIZE * 8;
121 let payload_channels = total_channels.saturating_sub(header_channels);
122 let payload_bytes = payload_channels * bpc as usize / 8;
123 let plaintext_cap = payload_bytes.saturating_sub(GCM_TAG);
124
125 let take = remaining.len().min(plaintext_cap);
126 chunks.push(remaining[..take].to_vec());
127 remaining = &remaining[take..];
128 }
129
130 if !remaining.is_empty() {
131 let total_available: usize = images
132 .iter()
133 .map(|img| {
134 let total_channels = img.width() as usize * img.height() as usize * 3;
135 let payload_channels = total_channels.saturating_sub(CHUNK_HEADER_SIZE * 8);
136 payload_channels * bpc as usize / 8
137 })
138 .sum::<usize>()
139 .saturating_sub(images.len() * GCM_TAG);
140 return Err(StegoError::InsufficientCapacity {
141 needed: message.len(),
142 available: total_available,
143 });
144 }
145
146 if chunks.is_empty() {
147 chunks.push(Vec::new());
148 }
149
150 Ok(chunks)
151 }
152
153 pub fn encode(
158 self,
159 cover_images: Vec<RgbImage>,
160 message: &[u8],
161 password: &[u8],
162 ) -> Result<Vec<RgbImage>> {
163 if cover_images.is_empty() {
164 return Err(StegoError::InvalidConfig("no cover images provided".into()));
165 }
166 if cover_images.len() > 255 {
167 return Err(StegoError::InvalidConfig(
168 "too many cover images (max 255)".into(),
169 ));
170 }
171
172 let bpc = self.bits_per_channel;
173 let chunks = Self::split_message(message, &cover_images, bpc)?;
174 let total_chunks = chunks.len() as u8;
175
176 let message_id: [u8; 16] = *Uuid::new_v4().as_bytes();
177 let payload_hash = sha256(message);
178
179 let mut result_images = Vec::with_capacity(chunks.len());
180
181 for (i, chunk) in chunks.iter().enumerate() {
182 let cover = &cover_images[i];
183
184 let mut argon2_salt = [0u8; 16];
185 let mut aes_nonce = [0u8; 12];
186 rand::thread_rng().fill_bytes(&mut argon2_salt);
187 rand::thread_rng().fill_bytes(&mut aes_nonce);
188
189 let base_key = derive_base_key(password, &argon2_salt)?;
190 let aes_key = derive_message_key(&base_key, &message_id)?;
191
192 let ciphertext_len = chunk.len() + 16; let header = ChunkHeader {
195 message_id,
196 chunk_index: i as u8,
197 total_chunks,
198 payload_length: ciphertext_len as u64,
199 argon2_salt,
200 aes_nonce,
201 payload_hash,
202 bits_per_channel: bpc,
203 reserved: 0,
204 };
205 let header_bytes = header.to_bytes();
206
207 let ciphertext = encrypt(&aes_key, &aes_nonce, chunk, &header_bytes)?;
208
209 let total_channels = cover.width() as usize * cover.height() as usize * 3;
211 let header_channels = CHUNK_HEADER_SIZE * 8; let payload_channels_needed = (ciphertext.len() * 8).div_ceil(bpc as usize);
213 if header_channels + payload_channels_needed > total_channels {
214 return Err(StegoError::InsufficientCapacity {
215 needed: CHUNK_HEADER_SIZE + ciphertext.len(),
216 available: total_channels * bpc as usize / 8,
217 });
218 }
219
220 let mut writer = LsbWriter::new(cover.clone());
221 writer.write_bits(&header_bytes, 1)?;
222 writer.write_bits(&ciphertext, bpc)?;
223 result_images.push(writer.into_image());
224 }
225
226 Ok(result_images)
227 }
228
229 pub fn encode_many(
235 self,
236 mut cover_images: Vec<RgbImage>,
237 messages: &[&[u8]],
238 password: &[u8],
239 ) -> Result<Vec<RgbImage>> {
240 if cover_images.is_empty() {
241 return Err(StegoError::InvalidConfig("no cover images provided".into()));
242 }
243 if cover_images.len() > 255 {
244 return Err(StegoError::InvalidConfig(
245 "too many cover images (max 255)".into(),
246 ));
247 }
248
249 let bpc = self.bits_per_channel;
250 let mut modified: Vec<bool> = vec![false; cover_images.len()];
251
252 for message in messages {
253 let message_id: [u8; 16] = *uuid::Uuid::new_v4().as_bytes();
256 let payload_hash = crate::crypto::sha256(message);
257
258 let mut placed = false;
259
260 for (img_idx, img) in cover_images.iter_mut().enumerate() {
261 let start_channel = scan_used_channels(img);
262 let total_channels = img.width() as usize * img.height() as usize * 3;
263
264 let remaining_channels = total_channels.saturating_sub(start_channel);
266 let header_channels = CHUNK_HEADER_SIZE * 8; if remaining_channels <= header_channels {
268 continue;
269 }
270 let payload_channels = remaining_channels - header_channels;
271 let payload_bytes = payload_channels * bpc as usize / 8;
272 let plaintext_cap = payload_bytes.saturating_sub(16); if message.len() > plaintext_cap {
275 continue;
276 }
277
278 let mut argon2_salt = [0u8; 16];
280 let mut aes_nonce = [0u8; 12];
281 rand::thread_rng().fill_bytes(&mut argon2_salt);
282 rand::thread_rng().fill_bytes(&mut aes_nonce);
283
284 let base_key = crate::crypto::derive_base_key(password, &argon2_salt)?;
285 let aes_key = crate::crypto::derive_message_key(&base_key, &message_id)?;
286
287 let ciphertext_len = message.len() + 16;
288 let header = ChunkHeader {
289 message_id,
290 chunk_index: 0,
291 total_chunks: 1,
292 payload_length: ciphertext_len as u64,
293 argon2_salt,
294 aes_nonce,
295 payload_hash,
296 bits_per_channel: bpc,
297 reserved: 0,
298 };
299 let header_bytes = header.to_bytes();
300 let ciphertext = crate::crypto::encrypt(&aes_key, &aes_nonce, message, &header_bytes)?;
301
302 let mut writer = LsbWriter::new_at(img.clone(), start_channel);
303 writer.write_bits(&header_bytes, 1)?;
304 writer.write_bits(&ciphertext, bpc)?;
305 *img = writer.into_image();
306 modified[img_idx] = true;
307 placed = true;
308 break;
309 }
310
311 if !placed {
312 return Err(StegoError::InsufficientCapacity {
313 needed: message.len(),
314 available: 0,
315 });
316 }
317 }
318
319 Ok(cover_images
320 .into_iter()
321 .zip(modified.iter())
322 .filter_map(|(img, &m)| if m { Some(img) } else { None })
323 .collect())
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330 use image::RgbImage;
331
332 fn blank(w: u32, h: u32) -> RgbImage {
333 RgbImage::new(w, h)
334 }
335
336 #[test]
337 fn builder_default_succeeds() {
338 assert!(StegoEncoder::builder().build().is_ok());
339 }
340
341 #[test]
342 fn builder_bpc0_fails() {
343 let err = StegoEncoder::builder()
344 .bits_per_channel(0)
345 .build()
346 .unwrap_err();
347 assert!(matches!(err, StegoError::InvalidBitsPerChannel(0)));
348 }
349
350 #[test]
351 fn builder_bpc2_canonical() {
352 assert!(StegoEncoder::builder().bits_per_channel(2).build().is_ok());
353 }
354
355 #[test]
356 fn split_300_bytes_fits_in_one_100x100() {
357 let images: Vec<RgbImage> = vec![blank(100, 100)];
358 let msg: Vec<u8> = (0..300).map(|b| b as u8).collect();
359 let chunks = StegoEncoder::split_message(&msg, &images, 1).unwrap();
360 assert_eq!(chunks.len(), 1);
361 assert_eq!(chunks[0].len(), 300);
362 }
363
364 #[test]
365 fn split_50_bytes_3_images_single_chunk() {
366 let images: Vec<RgbImage> = (0..3).map(|_| blank(100, 100)).collect();
367 let msg = vec![0u8; 50];
368 let chunks = StegoEncoder::split_message(&msg, &images, 1).unwrap();
369 assert_eq!(chunks.len(), 1);
370 assert_eq!(chunks[0].len(), 50);
371 }
372
373 #[test]
374 fn encode_10_bytes_100x100_succeeds() {
375 let cover = vec![blank(100, 100)];
376 let enc = StegoEncoder::builder().build().unwrap();
377 let out = enc.encode(cover, b"hello enc!", b"pass").unwrap();
378 assert_eq!(out.len(), 1);
379 assert_eq!(out[0].width(), 100);
380 }
381
382 #[test]
383 fn encode_1mb_into_10x10_insufficient_capacity() {
384 let cover = vec![blank(10, 10)];
385 let enc = StegoEncoder::builder().build().unwrap();
386 let msg = vec![0u8; 1024 * 1024];
387 let err = enc.encode(cover, &msg, b"pass").unwrap_err();
388 assert!(matches!(err, StegoError::InsufficientCapacity { .. }));
389 }
390
391 #[test]
392 fn scan_used_channels_empty_image_returns_zero() {
393 let img = blank(100, 100);
394 assert_eq!(scan_used_channels(&img), 0);
396 }
397
398 #[test]
399 fn scan_used_channels_after_one_encode() {
400 let cover = vec![blank(100, 100)];
401 let enc = StegoEncoder::builder().build().unwrap();
402 let stego = enc.encode(cover, b"hello", b"pass").unwrap();
403 let used = scan_used_channels(&stego[0]);
404 assert!(used > CHUNK_HEADER_SIZE * 8);
406 }
407
408 #[test]
409 fn scan_used_channels_zero_for_tiny_image() {
410 let img = blank(1, 1); assert_eq!(scan_used_channels(&img), 0);
413 }
414
415 #[test]
416 fn encode_many_two_messages_one_image() {
417 let cover = vec![blank(200, 200)];
418 let enc = StegoEncoder::builder().build().unwrap();
419 let msgs: &[&[u8]] = &[b"first message", b"second message"];
420 let result = enc.encode_many(cover, msgs, b"pass");
421 assert!(result.is_ok());
422 let imgs = result.unwrap();
423 assert_eq!(imgs.len(), 1);
424 let used = scan_used_channels(&imgs[0]);
426 assert!(used > CHUNK_HEADER_SIZE * 8 * 2);
427 }
428
429 #[test]
430 fn encode_many_overflow_to_second_image() {
431 let covers = vec![blank(100, 100), blank(100, 100)];
435 let enc = StegoEncoder::builder().build().unwrap();
436 let big_msg = vec![0u8; 3646]; let small_msg = b"second";
438 let msgs: &[&[u8]] = &[&big_msg, small_msg];
439 let imgs = enc.encode_many(covers, msgs, b"pass").unwrap();
440 assert_eq!(imgs.len(), 2);
442 }
443
444 #[test]
445 fn encode_many_no_space_returns_error() {
446 let cover = vec![blank(10, 10)]; let enc = StegoEncoder::builder().build().unwrap();
448 let msgs: &[&[u8]] = &[b"hello"];
449 let err = enc.encode_many(cover, msgs, b"pass").unwrap_err();
450 assert!(matches!(err, StegoError::InsufficientCapacity { .. }));
451 }
452}