Skip to content

Commit fba0995

Browse files
committed
stega mode
1 parent 46f75e2 commit fba0995

File tree

6 files changed

+316
-33
lines changed

6 files changed

+316
-33
lines changed

app/index.html

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,22 @@
1111
<div class="container">
1212
<h2>Image cipher</h2>
1313
<p>
14-
This app encode your image to pure noise,
15-
only person with the secret can decode it back.
16-
The code runs entirely in your browser,
17-
so your image and secret are always safe and private :)
14+
This app works as a image cryptography and steganography tool.
15+
<br>
16+
<ul>
17+
<li>Cryptography: It makes your image to pure noise by encrypting it with a secret key. </li>
18+
<li>Steganography: It hides a secret message inside an image.</li>
19+
</ul>
20+
The code runs entirely in your browser, so your data is always safe and private :)
1821
</p>
22+
23+
Mode:
24+
<span class="mode-select-span">
25+
<input type="radio" name="mode" id="cipherModeInput" checked> Cryptography
26+
</span>
27+
<span class="mode-select-span">
28+
<input type="radio" name="mode" id="steganoModeInput"> Steganography
29+
</span>
1930
</div>
2031

2132
<div id="input-container" class="container">
@@ -29,8 +40,15 @@ <h2>Image cipher</h2>
2940
<!-- https://gist.github.com/danawoodman/4788404bc620d5392d111dba98c73873 -->
3041
<input type="file" id="fileInput" accept="image/*;capture=camera" required>
3142
<br>
32-
<label for="secretInput">Secret:</label>
33-
<input id="secretInput" placeholder="Enter your secret here" autocomplete="off">
43+
44+
<div id="msgInput-container" style="display: none; flex-direction: column; margin-bottom: 0.5rem;">
45+
<label for="msgInput">Message:</label>
46+
<textarea id="msgInput" placeholder="Enter your message here" autocomplete="off" rows="3"></textarea>
47+
</div>
48+
<div id="secretInput-container" style="display: flex; flex-direction: column; margin-bottom: 0.5rem;">
49+
<label for="secretInput">Secret:</label>
50+
<input id="secretInput" placeholder="Your secret key for encryption / decryption." autocomplete="off">
51+
</div>
3452

3553
<span style="margin-top: 0.25rem">
3654
<input type="checkbox" id="limitMaxSide" checked>
@@ -43,8 +61,8 @@ <h2>Image cipher</h2>
4361
<div id="button-container">
4462
<button id="encodeButton">Encode</button>
4563
<button id="decodeButton">Decode</button>
46-
as
47-
<div style="display: flex">
64+
<div style="display: block" id="imType-container">
65+
as
4866
<select id="imTypeSelect">
4967
<option value="png">PNG</option>
5068
<option value="jpeg">JPEG</option>

app/script.js

Lines changed: 123 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ if (!window.WebAssembly) {
33
alert("WebAssembly is not supported in your browser. Please use a modern browser.");
44
}
55

6+
const cipherModeInput = document.getElementById('cipherModeInput');
7+
const steganoModeInput = document.getElementById('steganoModeInput');
8+
const msgInputContainer = document.getElementById('msgInput-container');
9+
const msgInput = document.getElementById('msgInput');
610
const secretInput = document.getElementById('secretInput');
711
const fileInput = document.getElementById('fileInput');
812
const limitMaxSideInput = document.getElementById('limitMaxSide');
@@ -15,16 +19,60 @@ const decodeButton = document.getElementById('decodeButton');
1519
const downloadBtn = document.getElementById('downloadBtn');
1620
const errorDiv = document.getElementById('error');
1721
const imTypeSelect = document.getElementById('imTypeSelect');
22+
const imTypeContainer = document.getElementById('imType-container');
1823
const downloadBtnContainer = document.getElementById('downloadBtn-container');
1924
const outputContainer = document.getElementById('output-container');
2025
const footer = document.getElementById('footer');
2126

2227
const worker = new Worker('./worker.js', { type: 'module' });
2328

24-
const urlParams = new URLSearchParams(window.location.search);
25-
const secretFromUrl = urlParams.get('secret') || urlParams.get('s');
26-
if (secretFromUrl) {
27-
secretInput.value = decodeURIComponent(secretFromUrl);
29+
function onModeChange(mode) {
30+
if (mode === 'cipher') {
31+
msgInputContainer.style.display = 'none';
32+
imTypeSelect.disabled = false;
33+
imTypeContainer.style.display = 'block';
34+
} else if (mode === 'stegano') {
35+
msgInputContainer.style.display = 'flex';
36+
imTypeSelect.value = 'png';
37+
imTypeSelect.dispatchEvent(new Event('change'));
38+
imTypeSelect.disabled = true;
39+
imTypeContainer.style.display = 'none';
40+
}
41+
}
42+
43+
// handle initial mode setup
44+
{
45+
const urlParams = new URLSearchParams(window.location.search);
46+
const secretFromUrl = urlParams.get('secret') || urlParams.get('s');
47+
if (secretFromUrl) {
48+
secretInput.value = decodeURIComponent(secretFromUrl);
49+
}
50+
const modeFromUrl = urlParams.get('mode') || urlParams.get('m');
51+
switch (modeFromUrl) {
52+
case 'stega':
53+
case 'stegano':
54+
case 'steganography':
55+
cipherModeInput.checked = false;
56+
steganoModeInput.checked = true;
57+
onModeChange('stegano');
58+
break;
59+
case 'cipher':
60+
case 'cryptography':
61+
default:
62+
cipherModeInput.checked = true;
63+
steganoModeInput.checked = false;
64+
onModeChange('cipher');
65+
break;
66+
}
67+
}
68+
69+
function getMode() {
70+
if (cipherModeInput.checked) {
71+
return 'cipher';
72+
} else if (steganoModeInput.checked) {
73+
return 'stegano';
74+
}
75+
throw new Error("No mode selected");
2876
}
2977

3078
async function getInputBlob() {
@@ -100,13 +148,28 @@ async function encode_image() {
100148
hintLabel.textContent = `Encoding...`;
101149

102150
ensureOutput();
103-
worker.postMessage({
104-
type: 'encode',
105-
buffer: inputBlob,
106-
secret: secretInput.value,
107-
maxSide: limitMaxSideInput.checked ? parseInt(maxSideInput.value) : -1,
108-
outputAs: imTypeSelect.value
109-
});
151+
switch (getMode()) {
152+
case 'cipher':
153+
worker.postMessage({
154+
type: 'encode',
155+
buffer: inputBlob,
156+
secret: secretInput.value,
157+
maxSide: limitMaxSideInput.checked ? parseInt(maxSideInput.value) : -1,
158+
outputAs: imTypeSelect.value
159+
});
160+
break;
161+
case 'stegano':
162+
worker.postMessage({
163+
type: 'stega_encode',
164+
buffer: inputBlob,
165+
secret: secretInput.value,
166+
message: msgInput.value,
167+
maxSide: limitMaxSideInput.checked ? parseInt(maxSideInput.value) : -1,
168+
});
169+
break;
170+
default:
171+
throw new Error("Unknown mode");
172+
}
110173
}
111174

112175
async function decode_image() {
@@ -117,13 +180,27 @@ async function decode_image() {
117180
hintLabel.textContent = `Decoding...`;
118181

119182
ensureOutput();
120-
worker.postMessage({
121-
type: 'decode',
122-
buffer: inputBlob,
123-
secret: secretInput.value,
124-
maxSide: limitMaxSideInput.checked ? parseInt(maxSideInput.value) : -1,
125-
outputAs: imTypeSelect.value
126-
});
183+
switch (getMode()) {
184+
case 'cipher':
185+
worker.postMessage({
186+
type: 'decode',
187+
buffer: inputBlob,
188+
secret: secretInput.value,
189+
maxSide: limitMaxSideInput.checked ? parseInt(maxSideInput.value) : -1,
190+
outputAs: imTypeSelect.value
191+
});
192+
break;
193+
case 'stegano':
194+
worker.postMessage({
195+
type: 'stega_decode',
196+
buffer: inputBlob,
197+
secret: secretInput.value,
198+
maxSide: limitMaxSideInput.checked ? parseInt(maxSideInput.value) : -1,
199+
});
200+
break;
201+
default:
202+
throw new Error("Unknown mode");
203+
}
127204
}
128205

129206
worker.onmessage = async (event) => {
@@ -133,18 +210,41 @@ worker.onmessage = async (event) => {
133210
hintLabel.textContent = '';
134211
return;
135212
}
136-
await showImage(
137-
event.data.type,
138-
event.data.buffer,
139-
event.data.format
140-
);
213+
if (event.data.buffer) {
214+
await showImage(
215+
event.data.type,
216+
event.data.buffer,
217+
event.data.format
218+
);
219+
}
220+
if (event.data.message) {
221+
resetOutput();
222+
ensureOutput();
223+
hintLabel.textContent = '';
224+
const messageElem = document.createElement('pre');
225+
messageElem.textContent = event.data.message;
226+
outputDiv.appendChild(messageElem);
227+
messageElem.style.whiteSpace = 'pre-wrap';
228+
downloadBtnContainer.style.display = 'none';
229+
}
141230
};
142231

143232
// Event listeners for inputs
144233
{
145234
encodeButton.addEventListener('click', encode_image);
146235
decodeButton.addEventListener('click', decode_image);
147236

237+
steganoModeInput.addEventListener('change', () => {
238+
if (steganoModeInput.checked) {
239+
onModeChange('stegano');
240+
}
241+
})
242+
cipherModeInput.addEventListener('change', () => {
243+
if (cipherModeInput.checked) {
244+
onModeChange('cipher');
245+
}
246+
})
247+
148248
imTypeSelect.addEventListener('change', () => {
149249
if (imTypeSelect.value === 'jpeg') {
150250
encodeButton.disabled = true;

app/style.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,11 @@ footer {
8181
align-items: center;
8282
font-size: 0.75rem;
8383
color: #666;
84+
}
85+
86+
textarea {
87+
width: 100%;
88+
min-height: 3rem;
89+
resize: vertical;
90+
height: fit-content;
8491
}

app/worker.js

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import init, { encode, decode } from './pkg/chaotic_enc.js';
1+
import init, { encode, decode, stega_encode, stega_decode } from './pkg/chaotic_enc.js';
22

33
await init();
44
self.onmessage = async function(event) {
5-
const { type, buffer, secret, maxSide, outputAs } = event.data;
5+
const { type, buffer, message, secret, maxSide, outputAs } = event.data;
66

77
console.log(
88
'Worker received args:',
@@ -26,6 +26,24 @@ self.onmessage = async function(event) {
2626
format: outputAs
2727
});
2828
}
29+
30+
if (type === 'stega_encode') {
31+
const encoded = stega_encode(buffer, message, secret, maxSide);
32+
self.postMessage({
33+
type: 'encoded_stega',
34+
buffer: encoded,
35+
format: 'png',
36+
});
37+
}
38+
39+
if (type === 'stega_decode') {
40+
const decoded = stega_decode(buffer, secret, maxSide);
41+
self.postMessage({
42+
type: 'decoded_stega',
43+
message: decoded,
44+
});
45+
}
46+
2947
}
3048
catch (error) {
3149
console.error('Worker error:', error);

src/lib.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
mod logistic_map;
2+
mod stega;
23

34
use std::collections::hash_map::DefaultHasher;
45
use std::hash::{Hash, Hasher};
@@ -138,4 +139,49 @@ pub fn decode(im: &[u8], secret: &str, max_side: i32, as_type: &str) -> Result<B
138139
"jpeg" => ImageType::Jpeg,
139140
_ => panic!("Unsupported image type: {}", as_type),
140141
})
142+
}
143+
144+
#[wasm_bindgen]
145+
pub fn stega_encode(
146+
im: &[u8],
147+
message: &str,
148+
secret: &str,
149+
max_side: i32,
150+
) -> Result<Box<[u8]>, String>
151+
{
152+
153+
console_log!("Encoding stega image");
154+
let max_side = if max_side < 1 { None } else { Some(max_side as u32) };
155+
let seed: Option<f64> = match secret {
156+
"" => None,
157+
s => {
158+
console_log!("Seed: {}", s);
159+
Some(str2f(s))
160+
},
161+
};
162+
163+
let (mut im_v, im_opt) = img2vec(im, max_side)?;
164+
stega::inject_lsb(&mut im_v[..], message, seed)?;
165+
166+
vec2imblob(&im_v, im_opt, None, ImageType::Png)
167+
}
168+
169+
#[wasm_bindgen]
170+
pub fn stega_decode(
171+
im: &[u8],
172+
secret: &str,
173+
max_side: i32,
174+
) -> Result<String, String> {
175+
console_log!("Decoding stega image");
176+
let max_side = if max_side < 1 { None } else { Some(max_side as u32) };
177+
let seed: Option<f64> = match secret {
178+
"" => None,
179+
s => {
180+
console_log!("Seed: {}", s);
181+
Some(str2f(s))
182+
},
183+
};
184+
185+
let (im_v, _) = img2vec(im, max_side)?;
186+
stega::extract_lsb(&im_v[..], seed)
141187
}

0 commit comments

Comments
 (0)