Tidied up Steganography operations. FileType and toBase64 functions now accept ArrayBuffers.

This commit is contained in:
n1474335 2019-09-04 13:54:59 +01:00
parent 5bc5c0df90
commit eb769c7fb4
28 changed files with 81 additions and 75 deletions

View file

@ -177,7 +177,7 @@ class Dish {
this.type = type; this.type = type;
if (!this.valid()) { if (!this.valid()) {
const sample = Utils.truncate(JSON.stringify(this.value), 13); const sample = Utils.truncate(JSON.stringify(this.value), 25);
throw new DishError(`Data is not a valid ${Dish.enumLookup(type)}: ${sample}`); throw new DishError(`Data is not a valid ${Dish.enumLookup(type)}: ${sample}`);
} }
} }

View file

@ -369,7 +369,11 @@
"Scan for Embedded Files", "Scan for Embedded Files",
"Extract Files", "Extract Files",
"Remove EXIF", "Remove EXIF",
"Extract EXIF" "Extract EXIF",
"Extract RGBA",
"View Bit Plane",
"Randomize Colour Palette",
"Extract LSB"
] ]
}, },
{ {
@ -380,10 +384,6 @@
"Remove EXIF", "Remove EXIF",
"Extract EXIF", "Extract EXIF",
"Split Colour Channels", "Split Colour Channels",
"Extract RGBA",
"View Bit Plane",
"Randomize Colour Palette",
"Extract LSB",
"Rotate Image", "Rotate Image",
"Resize Image", "Resize Image",
"Blur Image", "Blur Image",

View file

@ -12,7 +12,7 @@ import Utils from "../Utils.mjs";
/** /**
* Base64's the input byte array using the given alphabet, returning a string. * Base64's the input byte array using the given alphabet, returning a string.
* *
* @param {byteArray|Uint8Array|string} data * @param {byteArray|Uint8Array|ArrayBuffer|string} data
* @param {string} [alphabet="A-Za-z0-9+/="] * @param {string} [alphabet="A-Za-z0-9+/="]
* @returns {string} * @returns {string}
* *
@ -25,6 +25,9 @@ import Utils from "../Utils.mjs";
*/ */
export function toBase64(data, alphabet="A-Za-z0-9+/=") { export function toBase64(data, alphabet="A-Za-z0-9+/=") {
if (!data) return ""; if (!data) return "";
if (data instanceof ArrayBuffer) {
data = new Uint8Array(data);
}
if (typeof data == "string") { if (typeof data == "string") {
data = Utils.strToByteArray(data); data = Utils.strToByteArray(data);
} }

View file

@ -72,8 +72,8 @@ export const JOIN_DELIM_OPTIONS = [
{name: "Nothing (join chars)", value: ""} {name: "Nothing (join chars)", value: ""}
]; ];
/* /**
RGBA list delimiters. * RGBA list delimiters.
*/ */
export const RGBA_DELIM_OPTIONS = [ export const RGBA_DELIM_OPTIONS = [
{name: "Comma", value: ","}, {name: "Comma", value: ","},

View file

@ -75,7 +75,7 @@ function bytesMatch(sig, buf, offset=0) {
* Given a buffer, detects magic byte sequences at specific positions and returns the * Given a buffer, detects magic byte sequences at specific positions and returns the
* extension and mime type. * extension and mime type.
* *
* @param {Uint8Array} buf * @param {Uint8Array|ArrayBuffer} buf
* @param {string[]} [categories=All] - Which categories of file to look for * @param {string[]} [categories=All] - Which categories of file to look for
* @returns {Object[]} types * @returns {Object[]} types
* @returns {string} type.name - Name of file type * @returns {string} type.name - Name of file type
@ -84,6 +84,10 @@ function bytesMatch(sig, buf, offset=0) {
* @returns {string} [type.desc] - Description * @returns {string} [type.desc] - Description
*/ */
export function detectFileType(buf, categories=Object.keys(FILE_SIGNATURES)) { export function detectFileType(buf, categories=Object.keys(FILE_SIGNATURES)) {
if (buf instanceof ArrayBuffer) {
buf = new Uint8Array(buf);
}
if (!(buf && buf.length > 1)) { if (!(buf && buf.length > 1)) {
return []; return [];
} }
@ -203,7 +207,7 @@ function locatePotentialSig(buf, sig, offset) {
* Detects whether the given buffer is a file of the type specified. * Detects whether the given buffer is a file of the type specified.
* *
* @param {string|RegExp} type * @param {string|RegExp} type
* @param {Uint8Array} buf * @param {Uint8Array|ArrayBuffer} buf
* @returns {string|false} The mime type or false if the type does not match * @returns {string|false} The mime type or false if the type does not match
*/ */
export function isType(type, buf) { export function isType(type, buf) {
@ -230,7 +234,7 @@ export function isType(type, buf) {
/** /**
* Detects whether the given buffer contains an image file. * Detects whether the given buffer contains an image file.
* *
* @param {Uint8Array} buf * @param {Uint8Array|ArrayBuffer} buf
* @returns {string|false} The mime type or false if the type does not match * @returns {string|false} The mime type or false if the type does not match
*/ */
export function isImage(buf) { export function isImage(buf) {

View file

@ -121,7 +121,7 @@ class AddTextToImage extends Operation {
let xPos = args[3], let xPos = args[3],
yPos = args[4]; yPos = args[4];
if (!isImage(new Uint8Array(input))) { if (!isImage(input)) {
throw new OperationError("Invalid file type."); throw new OperationError("Invalid file type.");
} }

View file

@ -53,7 +53,7 @@ class BlurImage extends Operation {
async run(input, args) { async run(input, args) {
const [blurAmount, blurType] = args; const [blurAmount, blurType] = args;
if (!isImage(new Uint8Array(input))) { if (!isImage(input)) {
throw new OperationError("Invalid file type."); throw new OperationError("Invalid file type.");
} }

View file

@ -107,7 +107,7 @@ class ContainImage extends Operation {
"Bottom": jimp.VERTICAL_ALIGN_BOTTOM "Bottom": jimp.VERTICAL_ALIGN_BOTTOM
}; };
if (!isImage(new Uint8Array(input))) { if (!isImage(input)) {
throw new OperationError("Invalid file type."); throw new OperationError("Invalid file type.");
} }

View file

@ -93,7 +93,7 @@ class ConvertImageFormat extends Operation {
const mime = formatMap[format]; const mime = formatMap[format];
if (!isImage(new Uint8Array(input))) { if (!isImage(input)) {
throw new OperationError("Invalid file format."); throw new OperationError("Invalid file format.");
} }
let image; let image;

View file

@ -102,7 +102,7 @@ class CoverImage extends Operation {
"Bottom": jimp.VERTICAL_ALIGN_BOTTOM "Bottom": jimp.VERTICAL_ALIGN_BOTTOM
}; };
if (!isImage(new Uint8Array(input))) { if (!isImage(input)) {
throw new OperationError("Invalid file type."); throw new OperationError("Invalid file type.");
} }

View file

@ -93,7 +93,7 @@ class CropImage extends Operation {
*/ */
async run(input, args) { async run(input, args) {
const [xPos, yPos, width, height, autocrop, autoTolerance, autoFrames, autoSymmetric, autoBorder] = args; const [xPos, yPos, width, height, autocrop, autoTolerance, autoFrames, autoSymmetric, autoBorder] = args;
if (!isImage(new Uint8Array(input))) { if (!isImage(input)) {
throw new OperationError("Invalid file type."); throw new OperationError("Invalid file type.");
} }

View file

@ -38,7 +38,7 @@ class DitherImage extends Operation {
* @returns {byteArray} * @returns {byteArray}
*/ */
async run(input, args) { async run(input, args) {
if (!isImage(new Uint8Array(input))) { if (!isImage(input)) {
throw new OperationError("Invalid file type."); throw new OperationError("Invalid file type.");
} }

View file

@ -6,8 +6,9 @@
import Operation from "../Operation.mjs"; import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs"; import OperationError from "../errors/OperationError.mjs";
import Utils from "../Utils"; import Utils from "../Utils.mjs";
import { isImage } from "../lib/FileType"; import { fromBinary } from "../lib/Binary.mjs";
import { isImage } from "../lib/FileType.mjs";
import jimp from "jimp"; import jimp from "jimp";
/** /**
@ -24,8 +25,8 @@ class ExtractLSB extends Operation {
this.name = "Extract LSB"; this.name = "Extract LSB";
this.module = "Image"; this.module = "Image";
this.description = "Extracts the Least Significant Bit data from each pixel in an image. This is a common way to hide data in Steganography."; this.description = "Extracts the Least Significant Bit data from each pixel in an image. This is a common way to hide data in Steganography.";
this.infoURL = "https://en.wikipedia.org/wiki/Bit_numbering#Least_significant_bit_in_digital_steganography"; this.infoURL = "https://wikipedia.org/wiki/Bit_numbering#Least_significant_bit_in_digital_steganography";
this.inputType = "byteArray"; this.inputType = "ArrayBuffer";
this.outputType = "byteArray"; this.outputType = "byteArray";
this.args = [ this.args = [
{ {
@ -62,9 +63,9 @@ class ExtractLSB extends Operation {
} }
/** /**
* @param {File} input * @param {ArrayBuffer} input
* @param {Object[]} args * @param {Object[]} args
* @returns {File} * @returns {byteArray}
*/ */
async run(input, args) { async run(input, args) {
if (!isImage(input)) throw new OperationError("Please enter a valid image file."); if (!isImage(input)) throw new OperationError("Please enter a valid image file.");
@ -72,7 +73,7 @@ class ExtractLSB extends Operation {
const bit = 7 - args.pop(), const bit = 7 - args.pop(),
pixelOrder = args.pop(), pixelOrder = args.pop(),
colours = args.filter(option => option !== "").map(option => COLOUR_OPTIONS.indexOf(option)), colours = args.filter(option => option !== "").map(option => COLOUR_OPTIONS.indexOf(option)),
parsedImage = await jimp.read(Buffer.from(input)), parsedImage = await jimp.read(input),
width = parsedImage.bitmap.width, width = parsedImage.bitmap.width,
height = parsedImage.bitmap.height, height = parsedImage.bitmap.height,
rgba = parsedImage.bitmap.data; rgba = parsedImage.bitmap.data;
@ -103,8 +104,7 @@ class ExtractLSB extends Operation {
} }
} }
return Utils.convertToByteArray(combinedBinary, "binary"); return fromBinary(combinedBinary);
} }
} }

View file

@ -6,7 +6,7 @@
import Operation from "../Operation.mjs"; import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs"; import OperationError from "../errors/OperationError.mjs";
import { isImage } from "../lib/FileType"; import { isImage } from "../lib/FileType.mjs";
import jimp from "jimp"; import jimp from "jimp";
import {RGBA_DELIM_OPTIONS} from "../lib/Delim.mjs"; import {RGBA_DELIM_OPTIONS} from "../lib/Delim.mjs";
@ -25,8 +25,8 @@ class ExtractRGBA extends Operation {
this.name = "Extract RGBA"; this.name = "Extract RGBA";
this.module = "Image"; this.module = "Image";
this.description = "Extracts each pixel's RGBA value in an image. These are sometimes used in Steganography to hide text or data."; this.description = "Extracts each pixel's RGBA value in an image. These are sometimes used in Steganography to hide text or data.";
this.infoURL = "https://en.wikipedia.org/wiki/RGBA_color_space"; this.infoURL = "https://wikipedia.org/wiki/RGBA_color_space";
this.inputType = "byteArray"; this.inputType = "ArrayBuffer";
this.outputType = "string"; this.outputType = "string";
this.args = [ this.args = [
{ {
@ -43,7 +43,7 @@ class ExtractRGBA extends Operation {
} }
/** /**
* @param {byteArray} input * @param {ArrayBuffer} input
* @param {Object[]} args * @param {Object[]} args
* @returns {string} * @returns {string}
*/ */
@ -52,7 +52,7 @@ class ExtractRGBA extends Operation {
const delimiter = args[0], const delimiter = args[0],
includeAlpha = args[1], includeAlpha = args[1],
parsedImage = await jimp.read(Buffer.from(input)); parsedImage = await jimp.read(input);
let bitmap = parsedImage.bitmap.data; let bitmap = parsedImage.bitmap.data;
bitmap = includeAlpha ? bitmap : bitmap.filter((val, idx) => idx % 4 !== 3); bitmap = includeAlpha ? bitmap : bitmap.filter((val, idx) => idx % 4 !== 3);

View file

@ -45,7 +45,7 @@ class FlipImage extends Operation {
*/ */
async run(input, args) { async run(input, args) {
const [flipAxis] = args; const [flipAxis] = args;
if (!isImage(new Uint8Array(input))) { if (!isImage(input)) {
throw new OperationError("Invalid input file type."); throw new OperationError("Invalid input file type.");
} }

View file

@ -54,7 +54,7 @@ class ImageBrightnessContrast extends Operation {
*/ */
async run(input, args) { async run(input, args) {
const [brightness, contrast] = args; const [brightness, contrast] = args;
if (!isImage(new Uint8Array(input))) { if (!isImage(input)) {
throw new OperationError("Invalid file type."); throw new OperationError("Invalid file type.");
} }

View file

@ -48,7 +48,7 @@ class ImageFilter extends Operation {
*/ */
async run(input, args) { async run(input, args) {
const [filterType] = args; const [filterType] = args;
if (!isImage(new Uint8Array(input))) { if (!isImage(input)) {
throw new OperationError("Invalid file type."); throw new OperationError("Invalid file type.");
} }

View file

@ -62,7 +62,7 @@ class ImageHueSaturationLightness extends Operation {
async run(input, args) { async run(input, args) {
const [hue, saturation, lightness] = args; const [hue, saturation, lightness] = args;
if (!isImage(new Uint8Array(input))) { if (!isImage(input)) {
throw new OperationError("Invalid file type."); throw new OperationError("Invalid file type.");
} }

View file

@ -47,7 +47,7 @@ class ImageOpacity extends Operation {
*/ */
async run(input, args) { async run(input, args) {
const [opacity] = args; const [opacity] = args;
if (!isImage(new Uint8Array(input))) { if (!isImage(input)) {
throw new OperationError("Invalid file type."); throw new OperationError("Invalid file type.");
} }

View file

@ -38,7 +38,7 @@ class InvertImage extends Operation {
* @returns {byteArray} * @returns {byteArray}
*/ */
async run(input, args) { async run(input, args) {
if (!isImage(new Uint8Array(input))) { if (!isImage(input)) {
throw new OperationError("Invalid input file format."); throw new OperationError("Invalid input file format.");
} }

View file

@ -37,7 +37,7 @@ class NormaliseImage extends Operation {
* @returns {byteArray} * @returns {byteArray}
*/ */
async run(input, args) { async run(input, args) {
if (!isImage(new Uint8Array(input))) { if (!isImage(input)) {
throw new OperationError("Invalid file type."); throw new OperationError("Invalid file type.");
} }

View file

@ -51,7 +51,7 @@ class ParseQRCode extends Operation {
async run(input, args) { async run(input, args) {
const [normalise] = args; const [normalise] = args;
if (!isImage(new Uint8Array(input))) { if (!isImage(input)) {
throw new OperationError("Invalid file type."); throw new OperationError("Invalid file type.");
} }
return await parseQrCode(input, normalise); return await parseQrCode(input, normalise);

View file

@ -6,12 +6,12 @@
import Operation from "../Operation.mjs"; import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs"; import OperationError from "../errors/OperationError.mjs";
import Utils from "../Utils"; import Utils from "../Utils.mjs";
import PseudoRandomNumberGenerator from "./PseudoRandomNumberGenerator.mjs"; import { isImage } from "../lib/FileType.mjs";
import { isImage } from "../lib/FileType";
import { runHash } from "../lib/Hash.mjs"; import { runHash } from "../lib/Hash.mjs";
import { toBase64 } from "../lib/Base64"; import { toBase64 } from "../lib/Base64.mjs";
import jimp from "jimp"; import jimp from "jimp";
import { toHex } from "../lib/Hex.mjs";
/** /**
* Randomize Colour Palette operation * Randomize Colour Palette operation
@ -26,10 +26,10 @@ class RandomizeColourPalette extends Operation {
this.name = "Randomize Colour Palette"; this.name = "Randomize Colour Palette";
this.module = "Image"; this.module = "Image";
this.description = "Randomize's each colour in an image's colour palette. This can often reveal text or symbols that were previously a very similar colour to their surroundings."; this.description = "Randomizes each colour in an image's colour palette. This can often reveal text or symbols that were previously a very similar colour to their surroundings, a technique sometimes used in Steganography.";
this.infoURL = "https://en.wikipedia.org/wiki/Indexed_color"; this.infoURL = "https://wikipedia.org/wiki/Indexed_color";
this.inputType = "byteArray"; this.inputType = "ArrayBuffer";
this.outputType = "byteArray"; this.outputType = "ArrayBuffer";
this.presentType = "html"; this.presentType = "html";
this.args = [ this.args = [
{ {
@ -41,15 +41,15 @@ class RandomizeColourPalette extends Operation {
} }
/** /**
* @param {byteArray} input * @param {ArrayBuffer} input
* @param {Object[]} args * @param {Object[]} args
* @returns {byteArray} * @returns {ArrayBuffer}
*/ */
async run(input, args) { async run(input, args) {
if (!isImage(input)) throw new OperationError("Please enter a valid image file."); if (!isImage(input)) throw new OperationError("Please enter a valid image file.");
const seed = args[0] || (new PseudoRandomNumberGenerator()).run("", [5, "Hex"]), const seed = args[0] || (Math.random().toString().substr(2)),
parsedImage = await jimp.read(Buffer.from(input)), parsedImage = await jimp.read(input),
width = parsedImage.bitmap.width, width = parsedImage.bitmap.width,
height = parsedImage.bitmap.height; height = parsedImage.bitmap.height;
@ -64,16 +64,16 @@ class RandomizeColourPalette extends Operation {
const imageBuffer = await parsedImage.getBufferAsync(jimp.AUTO); const imageBuffer = await parsedImage.getBufferAsync(jimp.AUTO);
return Array.from(imageBuffer); return new Uint8Array(imageBuffer).buffer;
} }
/** /**
* Displays the extracted data as an image for web apps. * Displays the extracted data as an image for web apps.
* @param {byteArray} data * @param {ArrayBuffer} data
* @returns {html} * @returns {html}
*/ */
present(data) { present(data) {
if (!data.length) return ""; if (!data.byteLength) return "";
const type = isImage(data); const type = isImage(data);
return `<img src="data:${type};base64,${toBase64(data)}">`; return `<img src="data:${type};base64,${toBase64(data)}">`;

View file

@ -87,7 +87,7 @@ class ResizeImage extends Operation {
"Bezier": jimp.RESIZE_BEZIER "Bezier": jimp.RESIZE_BEZIER
}; };
if (!isImage(new Uint8Array(input))) { if (!isImage(input)) {
throw new OperationError("Invalid file type."); throw new OperationError("Invalid file type.");
} }

View file

@ -46,7 +46,7 @@ class RotateImage extends Operation {
async run(input, args) { async run(input, args) {
const [degrees] = args; const [degrees] = args;
if (!isImage(new Uint8Array(input))) { if (!isImage(input)) {
throw new OperationError("Invalid file type."); throw new OperationError("Invalid file type.");
} }

View file

@ -62,7 +62,7 @@ class SharpenImage extends Operation {
async run(input, args) { async run(input, args) {
const [radius, amount, threshold] = args; const [radius, amount, threshold] = args;
if (!isImage(new Uint8Array(input))) { if (!isImage(input)) {
throw new OperationError("Invalid file type."); throw new OperationError("Invalid file type.");
} }

View file

@ -40,7 +40,7 @@ class ToBase64 extends Operation {
*/ */
run(input, args) { run(input, args) {
const alphabet = args[0]; const alphabet = args[0];
return toBase64(new Uint8Array(input), alphabet); return toBase64(input, alphabet);
} }
/** /**

View file

@ -4,11 +4,11 @@
* @license Apache-2.0 * @license Apache-2.0
*/ */
import Operation from "../Operation"; import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError"; import OperationError from "../errors/OperationError.mjs";
import Utils from "../Utils"; import Utils from "../Utils.mjs";
import { isImage } from "../lib/FileType"; import { isImage } from "../lib/FileType.mjs";
import { toBase64 } from "../lib/Base64"; import { toBase64 } from "../lib/Base64.mjs";
import jimp from "jimp"; import jimp from "jimp";
/** /**
@ -24,10 +24,10 @@ class ViewBitPlane extends Operation {
this.name = "View Bit Plane"; this.name = "View Bit Plane";
this.module = "Image"; this.module = "Image";
this.description = "Extracts and displays a bit plane of any given image. These show only a single bit from each pixel, and so are often used to hide messages in Steganography."; this.description = "Extracts and displays a bit plane of any given image. These show only a single bit from each pixel, and can be used to hide messages in Steganography.";
this.infoURL = "https://wikipedia.org/wiki/Bit_plane"; this.infoURL = "https://wikipedia.org/wiki/Bit_plane";
this.inputType = "byteArray"; this.inputType = "ArrayBuffer";
this.outputType = "byteArray"; this.outputType = "ArrayBuffer";
this.presentType = "html"; this.presentType = "html";
this.args = [ this.args = [
{ {
@ -44,15 +44,15 @@ class ViewBitPlane extends Operation {
} }
/** /**
* @param {File} input * @param {ArrayBuffer} input
* @param {Object[]} args * @param {Object[]} args
* @returns {File} * @returns {ArrayBuffer}
*/ */
async run(input, args) { async run(input, args) {
if (!isImage(input)) throw new OperationError("Please enter a valid image file."); if (!isImage(input)) throw new OperationError("Please enter a valid image file.");
const [colour, bit] = args, const [colour, bit] = args,
parsedImage = await jimp.read(Buffer.from(input)), parsedImage = await jimp.read(input),
width = parsedImage.bitmap.width, width = parsedImage.bitmap.width,
height = parsedImage.bitmap.height, height = parsedImage.bitmap.height,
colourIndex = COLOUR_OPTIONS.indexOf(colour), colourIndex = COLOUR_OPTIONS.indexOf(colour),
@ -62,7 +62,6 @@ class ViewBitPlane extends Operation {
throw new OperationError("Error: Bit argument must be between 0 and 7"); throw new OperationError("Error: Bit argument must be between 0 and 7");
} }
let pixel, bin, newPixelValue; let pixel, bin, newPixelValue;
parsedImage.scan(0, 0, width, height, function(x, y, idx) { parsedImage.scan(0, 0, width, height, function(x, y, idx) {
@ -81,12 +80,12 @@ class ViewBitPlane extends Operation {
const imageBuffer = await parsedImage.getBufferAsync(jimp.AUTO); const imageBuffer = await parsedImage.getBufferAsync(jimp.AUTO);
return Array.from(imageBuffer); return new Uint8Array(imageBuffer).buffer;
} }
/** /**
* Displays the extracted data as an image for web apps. * Displays the extracted data as an image for web apps.
* @param {byteArray} data * @param {ArrayBuffer} data
* @returns {html} * @returns {html}
*/ */
present(data) { present(data) {