first commit

This commit is contained in:
2026-08-14 11:44:00 +02:00
commit 4fa600a3a3
990 changed files with 153262 additions and 0 deletions
+936
View File
@@ -0,0 +1,936 @@
const encodeForm =
document.getElementById("encode-form");
const decodeForm =
document.getElementById("decode-form");
const encodeImage =
document.getElementById("encode-image");
const decodeImage =
document.getElementById("decode-image");
const encodeDropzone =
document.getElementById("encode-dropzone");
const decodeDropzone =
document.getElementById("decode-dropzone");
const encodeImageInfo =
document.getElementById("encode-image-info");
const decodeImageInfo =
document.getElementById("decode-image-info");
const messageSection =
document.getElementById("message-section");
const fileSection =
document.getElementById("file-section");
const messageInput =
document.getElementById("message");
const messageSize =
document.getElementById("message-size");
const secretFile =
document.getElementById("secret-file");
const encodePassword =
document.getElementById("encode-password");
const decodePassword =
document.getElementById("decode-password");
const encodeButton =
document.getElementById("encode-button");
const decodeButton =
document.getElementById("decode-button");
const encodeStatus =
document.getElementById("encode-status");
const decodeStatus =
document.getElementById("decode-status");
const decodedMessage =
document.getElementById("decoded-message");
const messageOutput =
document.getElementById("message-output");
const copyMessage =
document.getElementById("copy-message");
function getEncodeMode() {
const selected =
document.querySelector(
'input[name="encode-mode"]:checked'
);
return selected
? selected.value
: "message";
}
function setStatus(
element,
message,
type = ""
) {
element.textContent =
message;
element.className =
`status ${type}`;
}
function setLoading(
button,
loading
) {
if (loading) {
button.disabled =
true;
button.dataset.originalText =
button.textContent;
button.textContent =
"Working...";
} else {
button.disabled =
false;
button.textContent =
button.dataset.originalText ||
button.textContent;
}
}
function formatBytes(bytes) {
if (bytes < 1024) {
return `${bytes} B`;
}
if (bytes < 1024 * 1024) {
return `${(bytes / 1024).toFixed(1)} KB`;
}
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
}
function setSelectedFile(
input,
file
) {
/*
* DataTransfer lets us put a file selected
* through drag-and-drop into the file input.
*/
const dataTransfer =
new DataTransfer();
dataTransfer.items.add(file);
input.files =
dataTransfer.files;
input.dispatchEvent(
new Event(
"change",
{
bubbles: true
}
)
);
}
function setupDropzone(
dropzone,
input
) {
dropzone.addEventListener(
"click",
() => {
input.click();
}
);
dropzone.addEventListener(
"keydown",
(event) => {
if (
event.key === "Enter" ||
event.key === " "
) {
event.preventDefault();
input.click();
}
}
);
[
"dragenter",
"dragover"
].forEach((eventName) => {
dropzone.addEventListener(
eventName,
(event) => {
event.preventDefault();
dropzone.classList.add(
"dragging"
);
}
);
});
[
"dragleave",
"drop"
].forEach((eventName) => {
dropzone.addEventListener(
eventName,
(event) => {
event.preventDefault();
dropzone.classList.remove(
"dragging"
);
}
);
});
dropzone.addEventListener(
"drop",
(event) => {
const file =
event.dataTransfer.files[0];
if (!file) {
return;
}
if (
!file.type.includes("png") &&
!file.name
.toLowerCase()
.endsWith(".png")
) {
alert(
"Please select a PNG image."
);
return;
}
setSelectedFile(
input,
file
);
}
);
input.addEventListener(
"change",
() => {
const file =
input.files[0];
if (!file) {
return;
}
dropzone.classList.add(
"has-file"
);
const strong =
dropzone.querySelector(
"strong"
);
const span =
dropzone.querySelector(
"span"
);
strong.textContent =
file.name;
span.textContent =
formatBytes(file.size);
}
);
}
setupDropzone(
encodeDropzone,
encodeImage
);
setupDropzone(
decodeDropzone,
decodeImage
);
async function getImageInfo(file) {
const form =
new FormData();
form.append(
"image",
file
);
const response =
await fetch(
"/api/info",
{
method: "POST",
body: form
}
);
const data =
await response.json();
if (!response.ok) {
throw new Error(
data.error ||
"Unable to inspect image"
);
}
return data;
}
async function updateImageInfo(
file,
element,
statusElement
) {
element.classList.add(
"hidden"
);
if (!file) {
return;
}
try {
const info =
await getImageInfo(file);
element.innerHTML =
`
<strong>
${info.width} × ${info.height}
</strong>
<span>
${info.format.toUpperCase()}
· ${info.capacityKB} KB capacity
</span>
`;
element.classList.remove(
"hidden"
);
} catch (error) {
setStatus(
statusElement,
error.message,
"error"
);
}
}
encodeImage.addEventListener(
"change",
async () => {
await updateImageInfo(
encodeImage.files[0],
encodeImageInfo,
encodeStatus
);
}
);
messageInput.addEventListener(
"input",
() => {
const count =
messageInput.value.length;
messageSize.textContent =
`${count.toLocaleString()} character${
count === 1
? ""
: "s"
}`;
}
);
document
.querySelectorAll(
'input[name="encode-mode"]'
)
.forEach((radio) => {
radio.addEventListener(
"change",
() => {
const mode =
getEncodeMode();
if (
mode === "message"
) {
messageSection
.classList
.remove("hidden");
fileSection
.classList
.add("hidden");
} else {
messageSection
.classList
.add("hidden");
fileSection
.classList
.remove("hidden");
}
}
);
});
encodeForm.addEventListener(
"submit",
async (event) => {
event.preventDefault();
const image =
encodeImage.files[0];
const password =
encodePassword.value;
const mode =
getEncodeMode();
if (!image) {
setStatus(
encodeStatus,
"Please select a PNG image.",
"error"
);
return;
}
const form =
new FormData();
form.append(
"image",
image
);
/*
* Password is optional.
*/
if (password) {
form.append(
"password",
password
);
}
if (
mode === "message"
) {
if (
!messageInput.value
) {
setStatus(
encodeStatus,
"Please enter a message.",
"error"
);
return;
}
form.append(
"message",
messageInput.value
);
} else {
const file =
secretFile.files[0];
if (!file) {
setStatus(
encodeStatus,
"Please select a file.",
"error"
);
return;
}
form.append(
"file",
file
);
}
setLoading(
encodeButton,
true
);
setStatus(
encodeStatus,
"Encoding..."
);
try {
const response =
await fetch(
"/api/encode",
{
method: "POST",
body: form
}
);
if (!response.ok) {
const data =
await response.json();
throw new Error(
data.error ||
"Encoding failed"
);
}
const blob =
await response.blob();
const url =
URL.createObjectURL(
blob
);
const link =
document.createElement(
"a"
);
link.href =
url;
link.download =
"encoded.png";
document.body.appendChild(
link
);
link.click();
link.remove();
URL.revokeObjectURL(
url
);
setStatus(
encodeStatus,
"Encoded successfully. Download started.",
"success"
);
} catch (error) {
setStatus(
encodeStatus,
error.message,
"error"
);
} finally {
setLoading(
encodeButton,
false
);
}
}
);
decodeForm.addEventListener(
"submit",
async (event) => {
event.preventDefault();
const image =
decodeImage.files[0];
const password =
decodePassword.value;
decodedMessage
.classList
.add("hidden");
if (!image) {
setStatus(
decodeStatus,
"Please select an encoded PNG.",
"error"
);
return;
}
const form =
new FormData();
form.append(
"image",
image
);
/*
* Password is optional.
*/
if (password) {
form.append(
"password",
password
);
}
setLoading(
decodeButton,
true
);
setStatus(
decodeStatus,
"Decoding..."
);
try {
const response =
await fetch(
"/api/decode",
{
method: "POST",
body: form
}
);
if (!response.ok) {
const data =
await response.json();
throw new Error(
data.error ||
"Decoding failed"
);
}
const contentType =
response.headers.get(
"content-type"
);
/*
* Message payload.
*/
if (
contentType &&
contentType.startsWith(
"application/json"
)
) {
const result =
await response.json();
if (
result.type !==
"message"
) {
throw new Error(
"Unknown decoded payload"
);
}
messageOutput.textContent =
result.message;
decodedMessage
.classList
.remove("hidden");
setStatus(
decodeStatus,
"Message decoded successfully.",
"success"
);
return;
}
/*
* File payload.
*/
const blob =
await response.blob();
const url =
URL.createObjectURL(
blob
);
let filename =
"extracted-file";
const disposition =
response.headers.get(
"content-disposition"
);
if (disposition) {
const match =
disposition.match(
/filename="([^"]+)"/
);
if (match) {
filename =
match[1];
}
}
const link =
document.createElement(
"a"
);
link.href =
url;
link.download =
filename;
document.body.appendChild(
link
);
link.click();
link.remove();
URL.revokeObjectURL(
url
);
setStatus(
decodeStatus,
`File extracted: ${filename}`,
"success"
);
} catch (error) {
setStatus(
decodeStatus,
error.message,
"error"
);
} finally {
setLoading(
decodeButton,
false
);
}
}
);
copyMessage.addEventListener(
"click",
async () => {
try {
await navigator.clipboard.writeText(
messageOutput.textContent
);
const original =
copyMessage.textContent;
copyMessage.textContent =
"Copied!";
setTimeout(
() => {
copyMessage.textContent =
original;
},
1500
);
} catch {
setStatus(
decodeStatus,
"Unable to copy message.",
"error"
);
}
}
);
+335
View File
@@ -0,0 +1,335 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
>
<title>Steg Tool</title>
<link
rel="stylesheet"
href="/style.css"
>
</head>
<body>
<main class="container">
<header class="header">
<h1>Steg Tool</h1>
<p>
Hide messages or files inside PNG images.
</p>
</header>
<!-- ENCODE -->
<section class="card">
<div class="card-header">
<h2>Encode</h2>
<p>
Hide a message or file inside a PNG.
</p>
</div>
<form id="encode-form">
<div class="form-group">
<label for="encode-image">
Cover image
</label>
<div
id="encode-dropzone"
class="dropzone"
tabindex="0"
>
<input
id="encode-image"
type="file"
accept="image/png"
hidden
>
<div class="dropzone-icon">
</div>
<strong>
Choose a PNG
</strong>
<span>
or drag and drop one here
</span>
</div>
<div
id="encode-image-info"
class="image-info hidden"
></div>
</div>
<div class="form-group">
<span class="label">
Payload
</span>
<div class="mode-selector">
<label class="mode-option">
<input
type="radio"
name="encode-mode"
value="message"
checked
>
<span>Message</span>
</label>
<label class="mode-option">
<input
type="radio"
name="encode-mode"
value="file"
>
<span>File</span>
</label>
</div>
</div>
<div
id="message-section"
class="form-group"
>
<label for="message">
Message
</label>
<textarea
id="message"
rows="6"
placeholder="Enter your secret message..."
></textarea>
<div
id="message-size"
class="field-hint"
>
0 characters
</div>
</div>
<div
id="file-section"
class="form-group hidden"
>
<label for="secret-file">
Secret file
</label>
<input
id="secret-file"
type="file"
>
</div>
<div class="form-group">
<label for="encode-password">
Password
<span class="optional">
optional
</span>
</label>
<input
id="encode-password"
type="password"
placeholder="Leave blank for no encryption"
autocomplete="new-password"
>
<div class="field-hint">
Without a password, anyone with the PNG
can extract the payload.
</div>
</div>
<button
type="submit"
id="encode-button"
class="button"
>
Encode PNG
</button>
</form>
<div
id="encode-status"
class="status"
></div>
</section>
<!-- DECODE -->
<section class="card">
<div class="card-header">
<h2>Decode</h2>
<p>
Extract a hidden message or file from a PNG.
</p>
</div>
<form id="decode-form">
<div class="form-group">
<label for="decode-image">
Encoded PNG
</label>
<div
id="decode-dropzone"
class="dropzone"
tabindex="0"
>
<input
id="decode-image"
type="file"
accept="image/png"
hidden
>
<div class="dropzone-icon">
</div>
<strong>
Choose an encoded PNG
</strong>
<span>
or drag and drop one here
</span>
</div>
<div
id="decode-image-info"
class="image-info hidden"
></div>
</div>
<div class="form-group">
<label for="decode-password">
Password
<span class="optional">
optional
</span>
</label>
<input
id="decode-password"
type="password"
placeholder="Leave blank if not encrypted"
autocomplete="current-password"
>
</div>
<button
type="submit"
id="decode-button"
class="button"
>
Decode PNG
</button>
</form>
<div
id="decode-status"
class="status"
></div>
<div
id="decoded-message"
class="result hidden"
>
<div class="result-header">
<h3>Decoded message</h3>
<button
id="copy-message"
type="button"
class="secondary-button"
>
Copy
</button>
</div>
<pre id="message-output"></pre>
</div>
</section>
</main>
<script src="/app.js"></script>
</body>
</html>
+685
View File
@@ -0,0 +1,685 @@
* {
box-sizing: border-box;
}
:root {
font-family:
Inter,
ui-sans-serif,
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
sans-serif;
color: #18181b;
background: #f4f4f5;
}
body {
margin: 0;
min-height: 100vh;
background:
radial-gradient(
circle at top,
#ffffff 0,
#f4f4f5 45%,
#e4e4e7 100%
);
}
button,
input,
textarea {
font: inherit;
}
.container {
width:
min(
900px,
calc(100% - 32px)
);
margin:
0 auto;
padding:
56px 0 80px;
}
/* Header */
.header {
margin-bottom: 32px;
}
.header h1 {
margin:
0 0 8px;
font-size:
clamp(
32px,
6vw,
46px
);
letter-spacing:
-0.04em;
}
.header p {
margin: 0;
color: #71717a;
font-size: 16px;
}
/* Cards */
.card {
margin-bottom: 24px;
padding: 28px;
background:
rgba(
255,
255,
255,
0.92
);
border:
1px solid #e4e4e7;
border-radius: 16px;
box-shadow:
0 8px 30px
rgba(
0,
0,
0,
0.05
);
}
.card-header {
margin-bottom: 28px;
}
.card-header h2 {
margin:
0 0 6px;
font-size: 22px;
letter-spacing:
-0.02em;
}
.card-header p {
margin: 0;
color: #71717a;
font-size: 14px;
}
/* Forms */
.form-group {
margin-bottom: 22px;
}
label,
.label {
display: block;
margin-bottom: 9px;
font-size: 14px;
font-weight: 650;
}
.optional {
margin-left: 4px;
color: #a1a1aa;
font-weight: 400;
}
.field-hint {
margin-top: 7px;
color: #71717a;
font-size: 12px;
}
input[type="password"],
input[type="file"],
textarea {
width: 100%;
border:
1px solid #d4d4d8;
border-radius: 8px;
background: #ffffff;
color: #18181b;
}
input[type="password"] {
padding:
11px 12px;
}
textarea {
display: block;
min-height: 130px;
padding: 12px;
resize: vertical;
}
input:focus,
textarea:focus {
outline: none;
border-color: #71717a;
box-shadow:
0 0 0 3px
rgba(
113,
113,
122,
0.12
);
}
/* Dropzone */
.dropzone {
display:
flex;
flex-direction:
column;
align-items:
center;
justify-content:
center;
min-height:
170px;
padding:
28px;
border:
1.5px dashed #a1a1aa;
border-radius:
12px;
background:
#fafafa;
text-align:
center;
cursor:
pointer;
transition:
border-color 120ms ease,
background 120ms ease,
transform 120ms ease;
}
.dropzone:hover,
.dropzone:focus {
outline: none;
border-color:
#52525b;
background:
#f4f4f5;
}
.dropzone.dragging {
border-color:
#18181b;
background:
#e4e4e7;
transform:
scale(1.01);
}
.dropzone.has-file {
border-style:
solid;
border-color:
#a1a1aa;
background:
#fafafa;
}
.dropzone-icon {
display:
flex;
align-items:
center;
justify-content:
center;
width:
42px;
height:
42px;
margin-bottom:
12px;
border-radius:
50%;
background:
#e4e4e7;
font-size:
22px;
font-weight:
700;
}
.dropzone strong {
max-width:
100%;
overflow:
hidden;
text-overflow:
ellipsis;
white-space:
nowrap;
}
.dropzone span {
margin-top:
5px;
color:
#71717a;
font-size:
13px;
}
/* Mode */
.mode-selector {
display:
grid;
grid-template-columns:
1fr 1fr;
gap:
10px;
}
.mode-option {
display:
flex;
align-items:
center;
justify-content:
center;
gap:
8px;
padding:
11px;
margin: 0;
border:
1px solid #d4d4d8;
border-radius:
8px;
background:
#ffffff;
cursor:
pointer;
font-weight:
550;
transition:
background 120ms ease,
border-color 120ms ease;
}
.mode-option:hover {
background:
#f4f4f5;
}
.mode-option:has(
input:checked
) {
border-color:
#18181b;
background:
#f4f4f5;
}
.mode-option input {
margin: 0;
}
/* Image info */
.image-info {
display:
flex;
justify-content:
space-between;
gap:
12px;
margin-top:
10px;
padding:
10px 12px;
border:
1px solid #e4e4e7;
border-radius:
8px;
background:
#fafafa;
color:
#52525b;
font-size:
13px;
}
.image-info strong {
color:
#18181b;
}
/* Buttons */
.button {
width:
100%;
padding:
12px 16px;
border:
0;
border-radius:
8px;
background:
#18181b;
color:
#ffffff;
font-weight:
650;
cursor:
pointer;
transition:
background 120ms ease,
transform 120ms ease;
}
.button:hover {
background:
#27272a;
}
.button:active {
transform:
translateY(1px);
}
.button:disabled {
opacity:
0.5;
cursor:
not-allowed;
transform:
none;
}
.secondary-button {
padding:
7px 11px;
border:
1px solid #d4d4d8;
border-radius:
7px;
background:
#ffffff;
color:
#18181b;
font-size:
13px;
cursor:
pointer;
}
.secondary-button:hover {
background:
#f4f4f5;
}
/* Status */
.status {
min-height:
20px;
margin-top:
14px;
font-size:
14px;
}
.status.success {
color:
#166534;
}
.status.error {
color:
#b91c1c;
}
/* Results */
.result {
margin-top:
24px;
padding-top:
22px;
border-top:
1px solid #e4e4e7;
}
.result-header {
display:
flex;
align-items:
center;
justify-content:
space-between;
gap:
12px;
margin-bottom:
10px;
}
.result-header h3 {
margin:
0;
}
.result pre {
margin:
0;
padding:
16px;
max-height:
400px;
overflow:
auto;
border-radius:
8px;
background:
#f4f4f5;
white-space:
pre-wrap;
overflow-wrap:
anywhere;
font-family:
ui-monospace,
SFMono-Regular,
Menlo,
Monaco,
Consolas,
monospace;
font-size:
13px;
line-height:
1.6;
}
/* Utility */
.hidden {
display:
none !important;
}
/* Mobile */
@media (
max-width: 600px
) {
.container {
width:
calc(100% - 20px);
padding:
28px 0 50px;
}
.card {
padding:
20px;
}
.mode-selector {
grid-template-columns:
1fr;
}
.image-info {
flex-direction:
column;
}
.dropzone {
min-height:
150px;
}
}