514 lines
10 KiB
JavaScript
514 lines
10 KiB
JavaScript
const test = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
|
|
const stego = require("../src/stego/engine");
|
|
|
|
function createPixels(width, height) {
|
|
const pixels =
|
|
Buffer.alloc(
|
|
width * height * 4
|
|
);
|
|
|
|
// Give the test image valid RGBA values.
|
|
for (
|
|
let i = 0;
|
|
i < pixels.length;
|
|
i += 4
|
|
) {
|
|
pixels[i] = 128;
|
|
pixels[i + 1] = 128;
|
|
pixels[i + 2] = 128;
|
|
pixels[i + 3] = 255;
|
|
}
|
|
|
|
return pixels;
|
|
}
|
|
|
|
|
|
test(
|
|
"encodes and decodes a text message",
|
|
async () => {
|
|
const width = 100;
|
|
const height = 100;
|
|
|
|
const pixels =
|
|
createPixels(
|
|
width,
|
|
height
|
|
);
|
|
|
|
const originalMessage =
|
|
"kokiiii is the best :3";
|
|
|
|
const password =
|
|
"kokithebestie";
|
|
|
|
const encoded =
|
|
await stego.encode({
|
|
pixels,
|
|
width,
|
|
height,
|
|
message:
|
|
originalMessage,
|
|
password
|
|
});
|
|
|
|
const decoded =
|
|
await stego.decode({
|
|
pixels:
|
|
encoded.pixels,
|
|
password
|
|
});
|
|
|
|
assert.equal(
|
|
decoded.type,
|
|
"message"
|
|
);
|
|
|
|
assert.equal(
|
|
decoded.message,
|
|
originalMessage
|
|
);
|
|
}
|
|
);
|
|
|
|
|
|
test(
|
|
"encodes and decodes a file",
|
|
async () => {
|
|
const width = 200;
|
|
const height = 200;
|
|
|
|
const pixels =
|
|
createPixels(
|
|
width,
|
|
height
|
|
);
|
|
|
|
const originalFile =
|
|
Buffer.from(
|
|
"This is a secret file.\n"
|
|
);
|
|
|
|
const encoded =
|
|
await stego.encode({
|
|
pixels,
|
|
width,
|
|
height,
|
|
password:
|
|
"test-password",
|
|
file: {
|
|
originalname:
|
|
"secret.txt",
|
|
mimetype:
|
|
"text/plain",
|
|
buffer:
|
|
originalFile
|
|
}
|
|
});
|
|
|
|
const decoded =
|
|
await stego.decode({
|
|
pixels:
|
|
encoded.pixels,
|
|
password:
|
|
"test-password"
|
|
});
|
|
|
|
assert.equal(
|
|
decoded.type,
|
|
"file"
|
|
);
|
|
|
|
assert.equal(
|
|
decoded.filename,
|
|
"secret.txt"
|
|
);
|
|
|
|
assert.equal(
|
|
decoded.mimetype,
|
|
"text/plain"
|
|
);
|
|
|
|
assert.deepEqual(
|
|
decoded.file,
|
|
originalFile
|
|
);
|
|
}
|
|
);
|
|
|
|
|
|
test(
|
|
"rejects an incorrect password",
|
|
async () => {
|
|
const pixels =
|
|
createPixels(
|
|
100,
|
|
100
|
|
);
|
|
|
|
const encoded =
|
|
await stego.encode({
|
|
pixels,
|
|
width: 100,
|
|
height: 100,
|
|
message:
|
|
"secret message",
|
|
password:
|
|
"correct-password"
|
|
});
|
|
|
|
await assert.rejects(
|
|
() =>
|
|
stego.decode({
|
|
pixels:
|
|
encoded.pixels,
|
|
password:
|
|
"wrong-password"
|
|
}),
|
|
{
|
|
message:
|
|
"Invalid password or corrupted payload"
|
|
}
|
|
);
|
|
}
|
|
);
|
|
|
|
|
|
test(
|
|
"requires a password for encrypted payloads",
|
|
async () => {
|
|
const pixels =
|
|
createPixels(
|
|
100,
|
|
100
|
|
);
|
|
|
|
const encoded =
|
|
await stego.encode({
|
|
pixels,
|
|
width: 100,
|
|
height: 100,
|
|
message:
|
|
"encrypted secret",
|
|
password:
|
|
"secret-password"
|
|
});
|
|
|
|
await assert.rejects(
|
|
() =>
|
|
stego.decode({
|
|
pixels:
|
|
encoded.pixels
|
|
}),
|
|
{
|
|
message:
|
|
"Password is required for this payload"
|
|
}
|
|
);
|
|
}
|
|
);
|
|
|
|
|
|
test(
|
|
"rejects a missing password during encrypted encoding",
|
|
async () => {
|
|
/*
|
|
* This test intentionally verifies that
|
|
* passwordless encoding is now allowed.
|
|
*
|
|
* It should NOT reject.
|
|
*/
|
|
const pixels =
|
|
createPixels(
|
|
100,
|
|
100
|
|
);
|
|
|
|
const encoded =
|
|
await stego.encode({
|
|
pixels,
|
|
width: 100,
|
|
height: 100,
|
|
message:
|
|
"no password"
|
|
});
|
|
|
|
assert.ok(
|
|
encoded.pixels
|
|
);
|
|
}
|
|
);
|
|
|
|
|
|
test(
|
|
"encodes and decodes a message without a password",
|
|
async () => {
|
|
const pixels =
|
|
createPixels(
|
|
100,
|
|
100
|
|
);
|
|
|
|
const encoded =
|
|
await stego.encode({
|
|
pixels,
|
|
width: 100,
|
|
height: 100,
|
|
message:
|
|
"passwordless secret"
|
|
});
|
|
|
|
const decoded =
|
|
await stego.decode({
|
|
pixels:
|
|
encoded.pixels
|
|
});
|
|
|
|
assert.equal(
|
|
decoded.type,
|
|
"message"
|
|
);
|
|
|
|
assert.equal(
|
|
decoded.message,
|
|
"passwordless secret"
|
|
);
|
|
}
|
|
);
|
|
|
|
|
|
test(
|
|
"encodes and decodes a file without a password",
|
|
async () => {
|
|
const pixels =
|
|
createPixels(
|
|
200,
|
|
200
|
|
);
|
|
|
|
const originalFile =
|
|
Buffer.from(
|
|
"passwordless file"
|
|
);
|
|
|
|
const encoded =
|
|
await stego.encode({
|
|
pixels,
|
|
width: 200,
|
|
height: 200,
|
|
file: {
|
|
originalname:
|
|
"secret.txt",
|
|
mimetype:
|
|
"text/plain",
|
|
buffer:
|
|
originalFile
|
|
}
|
|
});
|
|
|
|
const decoded =
|
|
await stego.decode({
|
|
pixels:
|
|
encoded.pixels
|
|
});
|
|
|
|
assert.equal(
|
|
decoded.type,
|
|
"file"
|
|
);
|
|
|
|
assert.equal(
|
|
decoded.filename,
|
|
"secret.txt"
|
|
);
|
|
|
|
assert.equal(
|
|
decoded.mimetype,
|
|
"text/plain"
|
|
);
|
|
|
|
assert.deepEqual(
|
|
decoded.file,
|
|
originalFile
|
|
);
|
|
}
|
|
);
|
|
|
|
|
|
test(
|
|
"rejects when neither message nor file is supplied",
|
|
async () => {
|
|
const pixels =
|
|
createPixels(
|
|
100,
|
|
100
|
|
);
|
|
|
|
await assert.rejects(
|
|
() =>
|
|
stego.encode({
|
|
pixels,
|
|
width: 100,
|
|
height: 100,
|
|
password:
|
|
"password"
|
|
}),
|
|
{
|
|
message:
|
|
"Message or file is required"
|
|
}
|
|
);
|
|
}
|
|
);
|
|
|
|
|
|
test(
|
|
"rejects a payload that is too large",
|
|
async () => {
|
|
const pixels =
|
|
createPixels(
|
|
10,
|
|
10
|
|
);
|
|
|
|
const hugeMessage =
|
|
"A".repeat(10000);
|
|
|
|
await assert.rejects(
|
|
() =>
|
|
stego.encode({
|
|
pixels,
|
|
width: 10,
|
|
height: 10,
|
|
message:
|
|
hugeMessage,
|
|
password:
|
|
"password"
|
|
}),
|
|
{
|
|
message:
|
|
/Payload too large/
|
|
}
|
|
);
|
|
}
|
|
);
|
|
|
|
|
|
test(
|
|
"reports image capacity",
|
|
() => {
|
|
const capacity =
|
|
stego.getCapacity(
|
|
100,
|
|
100
|
|
);
|
|
|
|
assert.equal(
|
|
capacity,
|
|
3740
|
|
);
|
|
}
|
|
);
|
|
|
|
|
|
test(
|
|
"rejects a tampered image",
|
|
async () => {
|
|
const pixels =
|
|
createPixels(
|
|
100,
|
|
100
|
|
);
|
|
|
|
const encoded =
|
|
await stego.encode({
|
|
pixels,
|
|
width: 100,
|
|
height: 100,
|
|
message:
|
|
"secret message",
|
|
password:
|
|
"test-password"
|
|
});
|
|
|
|
const tampered =
|
|
Buffer.from(
|
|
encoded.pixels
|
|
);
|
|
|
|
/*
|
|
* Flip a bit inside the embedded
|
|
* payload.
|
|
*/
|
|
tampered[100] ^= 1;
|
|
|
|
await assert.rejects(
|
|
() =>
|
|
stego.decode({
|
|
pixels:
|
|
tampered,
|
|
password:
|
|
"test-password"
|
|
}),
|
|
{
|
|
message:
|
|
"Invalid password or corrupted payload"
|
|
}
|
|
);
|
|
}
|
|
);
|
|
|
|
|
|
test(
|
|
"rejects a corrupted payload",
|
|
async () => {
|
|
const pixels =
|
|
createPixels(
|
|
100,
|
|
100
|
|
);
|
|
|
|
const encoded =
|
|
await stego.encode({
|
|
pixels,
|
|
width: 100,
|
|
height: 100,
|
|
message:
|
|
"secret message",
|
|
password:
|
|
"test-password"
|
|
});
|
|
|
|
const corrupted =
|
|
Buffer.from(
|
|
encoded.pixels
|
|
);
|
|
|
|
/*
|
|
* Corrupt several embedded bits
|
|
* in the payload area.
|
|
*/
|
|
for (
|
|
let i = 100;
|
|
i < 110;
|
|
i++
|
|
) {
|
|
corrupted[i] ^= 1;
|
|
}
|
|
|
|
await assert.rejects(
|
|
() =>
|
|
stego.decode({
|
|
pixels:
|
|
corrupted,
|
|
password:
|
|
"test-password"
|
|
}),
|
|
{
|
|
message:
|
|
"Invalid password or corrupted payload"
|
|
}
|
|
);
|
|
}
|
|
); |