cyberchef 10.22.1 → 10.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +182 -0
- package/CONTRIBUTING.md +40 -0
- package/Dockerfile +2 -0
- package/Gruntfile.js +2 -28
- package/README.md +1 -1
- package/babel.config.js +0 -6
- package/package.json +64 -63
- package/src/core/Chef.mjs +8 -1
- package/src/core/Ingredient.mjs +5 -2
- package/src/core/Operation.mjs +6 -1
- package/src/core/Recipe.mjs +10 -5
- package/src/core/config/Categories.json +20 -4
- package/src/core/config/OperationConfig.json +550 -26
- package/src/core/config/modules/Ciphers.mjs +6 -0
- package/src/core/config/modules/Crypto.mjs +6 -0
- package/src/core/config/modules/Default.mjs +8 -0
- package/src/core/config/modules/Shellcode.mjs +2 -0
- package/src/core/config/scripts/fixCryptoApiImports.mjs +54 -0
- package/src/core/config/scripts/fixSnackBarMarkup.mjs +28 -0
- package/src/core/lib/AudioBytes.mjs +103 -0
- package/src/core/lib/AudioMetaSchema.mjs +82 -0
- package/src/core/lib/AudioParsers.mjs +630 -0
- package/src/core/lib/BigIntUtils.mjs +73 -0
- package/src/core/lib/Extract.mjs +5 -0
- package/src/core/lib/Modhex.mjs +2 -0
- package/src/core/lib/ParityBit.mjs +50 -0
- package/src/core/lib/QRCode.mjs +30 -10
- package/src/core/lib/RC6.mjs +625 -0
- package/src/core/operations/A1Z26CipherDecode.mjs +1 -1
- package/src/core/operations/AddTextToImage.mjs +116 -64
- package/src/core/operations/AnalyseUUID.mjs +109 -6
- package/src/core/operations/BlurImage.mjs +10 -12
- package/src/core/operations/ContainImage.mjs +50 -40
- package/src/core/operations/ConvertImageFormat.mjs +33 -39
- package/src/core/operations/CoverImage.mjs +39 -37
- package/src/core/operations/CropImage.mjs +35 -21
- package/src/core/operations/DisassembleARM.mjs +193 -0
- package/src/core/operations/DitherImage.mjs +8 -8
- package/src/core/operations/EscapeUnicodeCharacters.mjs +0 -17
- package/src/core/operations/ExtractAudioMetadata.mjs +175 -0
- package/src/core/operations/ExtractEmailAddresses.mjs +2 -3
- package/src/core/operations/ExtractLSB.mjs +17 -11
- package/src/core/operations/ExtractRGBA.mjs +12 -10
- package/src/core/operations/FlaskSessionDecode.mjs +80 -0
- package/src/core/operations/FlaskSessionSign.mjs +89 -0
- package/src/core/operations/FlaskSessionVerify.mjs +136 -0
- package/src/core/operations/FlipImage.mjs +14 -10
- package/src/core/operations/GenerateImage.mjs +39 -32
- package/src/core/operations/ImageBrightnessContrast.mjs +10 -10
- package/src/core/operations/ImageFilter.mjs +14 -13
- package/src/core/operations/ImageHueSaturationLightness.mjs +22 -20
- package/src/core/operations/ImageOpacity.mjs +6 -8
- package/src/core/operations/InvertImage.mjs +4 -6
- package/src/core/operations/Jq.mjs +12 -4
- package/src/core/operations/NormaliseImage.mjs +5 -7
- package/src/core/operations/OffsetChecker.mjs +1 -1
- package/src/core/operations/ParityBit.mjs +128 -0
- package/src/core/operations/ParseEthernetFrame.mjs +112 -0
- package/src/core/operations/ParseIPv4Header.mjs +23 -6
- package/src/core/operations/ParseQRCode.mjs +13 -13
- package/src/core/operations/PseudoRandomIntegerGenerator.mjs +164 -0
- package/src/core/operations/RC6Decrypt.mjs +119 -0
- package/src/core/operations/RC6Encrypt.mjs +119 -0
- package/src/core/operations/RandomizeColourPalette.mjs +11 -11
- package/src/core/operations/RegularExpression.mjs +2 -1
- package/src/core/operations/RenderMarkdown.mjs +35 -4
- package/src/core/operations/ResizeImage.mjs +30 -23
- package/src/core/operations/RotateImage.mjs +8 -9
- package/src/core/operations/SQLBeautify.mjs +21 -3
- package/src/core/operations/SharpenImage.mjs +94 -62
- package/src/core/operations/SplitColourChannels.mjs +47 -21
- package/src/core/operations/TextIntegerConverter.mjs +123 -0
- package/src/core/operations/UnescapeUnicodeCharacters.mjs +17 -0
- package/src/core/operations/ViewBitPlane.mjs +16 -20
- package/src/core/operations/index.mjs +22 -0
- package/src/node/index.mjs +55 -0
- package/src/web/HTMLIngredient.mjs +24 -43
- package/src/web/HTMLOperation.mjs +8 -2
- package/src/web/Manager.mjs +1 -0
- package/src/web/html/index.html +6 -6
- package/src/web/static/fonts/bmfonts/Roboto72White.fnt +491 -485
- package/src/web/static/fonts/bmfonts/RobotoBlack72White.fnt +494 -488
- package/src/web/static/fonts/bmfonts/RobotoMono72White.fnt +110 -103
- package/src/web/static/fonts/bmfonts/RobotoSlab72White.fnt +498 -492
- package/src/web/stylesheets/layout/_banner.css +30 -0
- package/src/web/stylesheets/layout/_modals.css +5 -0
- package/src/web/stylesheets/utils/_overrides.css +7 -0
- package/src/web/waiters/ControlsWaiter.mjs +82 -0
- package/src/web/waiters/InputWaiter.mjs +12 -6
- package/src/web/waiters/OperationsWaiter.mjs +30 -13
- package/src/web/waiters/RecipeWaiter.mjs +2 -2
- package/tests/browser/02_ops.js +23 -3
- package/tests/node/index.mjs +1 -0
- package/tests/node/tests/lib/BigIntUtils.mjs +150 -0
- package/tests/node/tests/operations.mjs +11 -10
- package/tests/operations/index.mjs +13 -0
- package/tests/operations/tests/A1Z26CipherDecode.mjs +33 -0
- package/tests/operations/tests/AnalyseUUID.mjs +66 -0
- package/tests/operations/tests/DisassembleARM.mjs +377 -0
- package/tests/operations/tests/ExtractAudioMetadata.mjs +287 -0
- package/tests/operations/tests/ExtractEmailAddresses.mjs +38 -12
- package/tests/operations/tests/Fernet.mjs +18 -3
- package/tests/operations/tests/FlaskSession.mjs +246 -0
- package/tests/operations/tests/GenerateQRCode.mjs +67 -0
- package/tests/operations/tests/JWTSign.mjs +83 -8
- package/tests/operations/tests/Jq.mjs +32 -0
- package/tests/operations/tests/Modhex.mjs +20 -0
- package/tests/operations/tests/ParityBit.mjs +147 -0
- package/tests/operations/tests/ParseEthernetFrame.mjs +45 -0
- package/tests/operations/tests/RC6.mjs +487 -0
- package/tests/operations/tests/RegularExpression.mjs +75 -0
- package/tests/operations/tests/RenderMarkdown.mjs +110 -0
- package/tests/operations/tests/SQLBeautify.mjs +54 -0
- package/tests/operations/tests/TextIntegerConverter.mjs +199 -0
- package/tests/samples/Audio.mjs +73 -0
- package/tests/samples/Images.mjs +0 -12
- package/webpack.config.js +10 -7
- package/src/core/lib/ImageManipulation.mjs +0 -251
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @author MedjedThomasXM
|
|
3
|
+
* @copyright Crown Copyright 2024
|
|
4
|
+
* @license Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import Operation from "../Operation.mjs";
|
|
8
|
+
import OperationError from "../errors/OperationError.mjs";
|
|
9
|
+
import { isWorkerEnvironment } from "../Utils.mjs";
|
|
10
|
+
import cs from "@alexaltea/capstone-js/dist/capstone.min.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Disassemble ARM operation
|
|
14
|
+
*/
|
|
15
|
+
class DisassembleARM extends Operation {
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* DisassembleARM constructor
|
|
19
|
+
*/
|
|
20
|
+
constructor() {
|
|
21
|
+
super();
|
|
22
|
+
|
|
23
|
+
this.name = "Disassemble ARM";
|
|
24
|
+
this.module = "Shellcode";
|
|
25
|
+
this.description = "Disassembles ARM machine code into assembly language.<br><br>Supports ARM (32-bit), Thumb, and ARM64 (AArch64) architectures using the Capstone disassembly framework.<br><br>Input should be in hexadecimal.";
|
|
26
|
+
this.infoURL = "https://wikipedia.org/wiki/ARM_architecture_family";
|
|
27
|
+
this.inputType = "string";
|
|
28
|
+
this.outputType = "string";
|
|
29
|
+
this.args = [
|
|
30
|
+
{
|
|
31
|
+
"name": "Architecture",
|
|
32
|
+
"type": "option",
|
|
33
|
+
"value": ["ARM (32-bit)", "ARM64 (AArch64)"]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"name": "Mode",
|
|
37
|
+
"type": "option",
|
|
38
|
+
"value": ["ARM", "Thumb", "Thumb + Cortex-M", "ARMv8"]
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"name": "Endianness",
|
|
42
|
+
"type": "option",
|
|
43
|
+
"value": ["Little Endian", "Big Endian"]
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"name": "Starting address (hex)",
|
|
47
|
+
"type": "number",
|
|
48
|
+
"value": 0
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"name": "Show instruction hex",
|
|
52
|
+
"type": "boolean",
|
|
53
|
+
"value": true
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"name": "Show instruction position",
|
|
57
|
+
"type": "boolean",
|
|
58
|
+
"value": true
|
|
59
|
+
}
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {string} input
|
|
65
|
+
* @param {Object[]} args
|
|
66
|
+
* @returns {string}
|
|
67
|
+
*/
|
|
68
|
+
async run(input, args) {
|
|
69
|
+
const [
|
|
70
|
+
architecture,
|
|
71
|
+
mode,
|
|
72
|
+
endianness,
|
|
73
|
+
startAddress,
|
|
74
|
+
showHex,
|
|
75
|
+
showPosition
|
|
76
|
+
] = args;
|
|
77
|
+
|
|
78
|
+
// Remove whitespace from input
|
|
79
|
+
const hexInput = input.replace(/\s/g, "");
|
|
80
|
+
|
|
81
|
+
// Validate hex input
|
|
82
|
+
if (!/^[0-9a-fA-F]*$/.test(hexInput)) {
|
|
83
|
+
throw new OperationError("Invalid hexadecimal input. Please provide valid hex characters only.");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (hexInput.length === 0) {
|
|
87
|
+
return "";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (hexInput.length % 2 !== 0) {
|
|
91
|
+
throw new OperationError("Invalid hexadecimal input. Length must be even.");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Convert hex string to byte array
|
|
95
|
+
const bytes = [];
|
|
96
|
+
for (let i = 0; i < hexInput.length; i += 2) {
|
|
97
|
+
bytes.push(parseInt(hexInput.substr(i, 2), 16));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Determine architecture constant
|
|
101
|
+
let arch;
|
|
102
|
+
if (architecture === "ARM64 (AArch64)") {
|
|
103
|
+
arch = cs.ARCH_ARM64;
|
|
104
|
+
} else {
|
|
105
|
+
arch = cs.ARCH_ARM;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Determine mode constant
|
|
109
|
+
let modeValue = cs.MODE_LITTLE_ENDIAN;
|
|
110
|
+
|
|
111
|
+
if (architecture === "ARM (32-bit)") {
|
|
112
|
+
switch (mode) {
|
|
113
|
+
case "ARM":
|
|
114
|
+
modeValue = cs.MODE_ARM;
|
|
115
|
+
break;
|
|
116
|
+
case "Thumb":
|
|
117
|
+
modeValue = cs.MODE_THUMB;
|
|
118
|
+
break;
|
|
119
|
+
case "Thumb + Cortex-M":
|
|
120
|
+
modeValue = cs.MODE_THUMB | cs.MODE_MCLASS;
|
|
121
|
+
break;
|
|
122
|
+
case "ARMv8":
|
|
123
|
+
modeValue = cs.MODE_ARM | cs.MODE_V8;
|
|
124
|
+
break;
|
|
125
|
+
default:
|
|
126
|
+
modeValue = cs.MODE_ARM;
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
// ARM64 only has one mode (ARM mode is default for ARM64)
|
|
130
|
+
modeValue = cs.MODE_ARM;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Add endianness
|
|
134
|
+
if (endianness === "Big Endian") {
|
|
135
|
+
modeValue |= cs.MODE_BIG_ENDIAN;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (isWorkerEnvironment()) {
|
|
139
|
+
self.sendStatusMessage("Disassembling...");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let disassembler;
|
|
143
|
+
try {
|
|
144
|
+
disassembler = new cs.Capstone(arch, modeValue);
|
|
145
|
+
} catch (e) {
|
|
146
|
+
throw new OperationError(`Failed to initialise Capstone disassembler: ${e}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let instructions;
|
|
150
|
+
try {
|
|
151
|
+
instructions = disassembler.disasm(bytes, startAddress);
|
|
152
|
+
} catch (e) {
|
|
153
|
+
disassembler.close();
|
|
154
|
+
// Check if it's a "no valid instructions" error (code 0 means OK but nothing decoded)
|
|
155
|
+
if (e && e.includes && e.includes("code 0:")) {
|
|
156
|
+
throw new OperationError(`No valid ${architecture} instructions found in input. The bytes may be for a different architecture or mode.`);
|
|
157
|
+
}
|
|
158
|
+
throw new OperationError(`Disassembly failed: ${e}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Format output
|
|
162
|
+
const output = [];
|
|
163
|
+
for (const insn of instructions) {
|
|
164
|
+
let line = "";
|
|
165
|
+
|
|
166
|
+
if (showPosition) {
|
|
167
|
+
// Format address as hex with 0x prefix
|
|
168
|
+
const addrHex = "0x" + insn.address.toString(16).padStart(8, "0");
|
|
169
|
+
line += addrHex + " ";
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (showHex) {
|
|
173
|
+
// Format instruction bytes as hex
|
|
174
|
+
const bytesHex = insn.bytes.map(b => b.toString(16).padStart(2, "0")).join("");
|
|
175
|
+
line += bytesHex.padEnd(16, " ") + " ";
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
line += insn.mnemonic;
|
|
179
|
+
if (insn.op_str) {
|
|
180
|
+
line += " " + insn.op_str;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
output.push(line);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
disassembler.close();
|
|
187
|
+
|
|
188
|
+
return output.join("\n");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export default DisassembleARM;
|
|
@@ -9,13 +9,12 @@ import OperationError from "../errors/OperationError.mjs";
|
|
|
9
9
|
import { isImage } from "../lib/FileType.mjs";
|
|
10
10
|
import { toBase64 } from "../lib/Base64.mjs";
|
|
11
11
|
import { isWorkerEnvironment } from "../Utils.mjs";
|
|
12
|
-
import Jimp from "jimp
|
|
12
|
+
import { Jimp, JimpMime } from "jimp";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Image Dither operation
|
|
16
16
|
*/
|
|
17
17
|
class DitherImage extends Operation {
|
|
18
|
-
|
|
19
18
|
/**
|
|
20
19
|
* DitherImage constructor
|
|
21
20
|
*/
|
|
@@ -51,17 +50,19 @@ class DitherImage extends Operation {
|
|
|
51
50
|
try {
|
|
52
51
|
if (isWorkerEnvironment())
|
|
53
52
|
self.sendStatusMessage("Applying dither to image...");
|
|
54
|
-
image.
|
|
53
|
+
image.dither();
|
|
55
54
|
|
|
56
55
|
let imageBuffer;
|
|
57
|
-
if (image.
|
|
58
|
-
imageBuffer = await image.
|
|
56
|
+
if (image.mime === "image/gif") {
|
|
57
|
+
imageBuffer = await image.getBuffer(JimpMime.png);
|
|
59
58
|
} else {
|
|
60
|
-
imageBuffer = await image.
|
|
59
|
+
imageBuffer = await image.getBuffer(image.mime);
|
|
61
60
|
}
|
|
62
61
|
return imageBuffer.buffer;
|
|
63
62
|
} catch (err) {
|
|
64
|
-
throw new OperationError(
|
|
63
|
+
throw new OperationError(
|
|
64
|
+
`Error applying dither to image. (${err})`,
|
|
65
|
+
);
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
68
|
|
|
@@ -81,7 +82,6 @@ class DitherImage extends Operation {
|
|
|
81
82
|
|
|
82
83
|
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
|
|
83
84
|
}
|
|
84
|
-
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
export default DitherImage;
|
|
@@ -44,23 +44,6 @@ class EscapeUnicodeCharacters extends Operation {
|
|
|
44
44
|
"value": true
|
|
45
45
|
}
|
|
46
46
|
];
|
|
47
|
-
this.checks = [
|
|
48
|
-
{
|
|
49
|
-
pattern: "\\\\u(?:[\\da-f]{4,6})",
|
|
50
|
-
flags: "i",
|
|
51
|
-
args: ["\\u"]
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
pattern: "%u(?:[\\da-f]{4,6})",
|
|
55
|
-
flags: "i",
|
|
56
|
-
args: ["%u"]
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
pattern: "U\\+(?:[\\da-f]{4,6})",
|
|
60
|
-
flags: "i",
|
|
61
|
-
args: ["U+"]
|
|
62
|
-
}
|
|
63
|
-
];
|
|
64
47
|
}
|
|
65
48
|
|
|
66
49
|
/**
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @author d0s1nt [d0s1nt@cyberchefaudio]
|
|
3
|
+
* @copyright Crown Copyright 2025
|
|
4
|
+
* @license Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import Operation from "../Operation.mjs";
|
|
8
|
+
import OperationError from "../errors/OperationError.mjs";
|
|
9
|
+
import Utils from "../Utils.mjs";
|
|
10
|
+
import { makeEmptyReport, sniffContainer } from "../lib/AudioMetaSchema.mjs";
|
|
11
|
+
import {
|
|
12
|
+
parseMp3, parseRiffWave, parseFlac, parseOgg,
|
|
13
|
+
parseMp4BestEffort, parseAiffBestEffort,
|
|
14
|
+
parseAacAdts, parseAc3, parseWmaAsf,
|
|
15
|
+
} from "../lib/AudioParsers.mjs";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Extract Audio Metadata operation.
|
|
19
|
+
*/
|
|
20
|
+
class ExtractAudioMetadata extends Operation {
|
|
21
|
+
/** Creates the Extract Audio Metadata operation. */
|
|
22
|
+
constructor() {
|
|
23
|
+
super();
|
|
24
|
+
|
|
25
|
+
this.name = "Extract Audio Metadata";
|
|
26
|
+
this.module = "Default";
|
|
27
|
+
this.description =
|
|
28
|
+
"Extract common audio metadata across MP3 (ID3v2/ID3v1/GEOB), WAV/BWF/BW64 (INFO/bext/iXML/axml), FLAC (Vorbis Comment/Picture), OGG (Vorbis/OpusTags), AAC (ADTS), AC3 (Dolby Digital), WMA (ASF), plus best-effort MP4/M4A and AIFF scanning. Outputs normalized JSON.";
|
|
29
|
+
this.infoURL = "https://wikipedia.org/wiki/Audio_file_format";
|
|
30
|
+
this.inputType = "ArrayBuffer";
|
|
31
|
+
this.outputType = "JSON";
|
|
32
|
+
this.presentType = "html";
|
|
33
|
+
|
|
34
|
+
this.args = [
|
|
35
|
+
{ name: "Filename (optional)", type: "string", value: "" },
|
|
36
|
+
{ name: "Max embedded text bytes (iXML/axml/etc)", type: "number", value: 1024 * 512 },
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {ArrayBuffer} input
|
|
42
|
+
* @param {Object[]} args
|
|
43
|
+
* @returns {Object}
|
|
44
|
+
*/
|
|
45
|
+
run(input, args) {
|
|
46
|
+
const filename = (args?.[0] || "").trim() || null;
|
|
47
|
+
const maxTextBytes = Number.isFinite(args?.[1]) ? Math.max(1024, args[1]) : 1024 * 512;
|
|
48
|
+
|
|
49
|
+
if (!(input instanceof ArrayBuffer) || input.byteLength === 0)
|
|
50
|
+
throw new OperationError("No input data. Load an audio file (drag/drop or use the open file button).");
|
|
51
|
+
|
|
52
|
+
const bytes = new Uint8Array(input);
|
|
53
|
+
const container = sniffContainer(bytes);
|
|
54
|
+
const report = makeEmptyReport(filename, bytes.length, container);
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const parsers = {
|
|
58
|
+
mp3: () => parseMp3(bytes, report),
|
|
59
|
+
wav: () => parseRiffWave(bytes, report, maxTextBytes),
|
|
60
|
+
bw64: () => parseRiffWave(bytes, report, maxTextBytes),
|
|
61
|
+
flac: () => parseFlac(bytes, report, maxTextBytes),
|
|
62
|
+
ogg: () => parseOgg(bytes, report),
|
|
63
|
+
opus: () => parseOgg(bytes, report),
|
|
64
|
+
mp4: () => parseMp4BestEffort(bytes, report),
|
|
65
|
+
m4a: () => parseMp4BestEffort(bytes, report),
|
|
66
|
+
aiff: () => parseAiffBestEffort(bytes, report, maxTextBytes),
|
|
67
|
+
aac: () => parseAacAdts(bytes, report),
|
|
68
|
+
ac3: () => parseAc3(bytes, report),
|
|
69
|
+
wma: () => parseWmaAsf(bytes, report),
|
|
70
|
+
};
|
|
71
|
+
if (parsers[container.type]) {
|
|
72
|
+
parsers[container.type]();
|
|
73
|
+
} else {
|
|
74
|
+
report.errors.push({ stage: "sniff", message: "Unknown/unsupported container (best-effort scan not implemented)." });
|
|
75
|
+
}
|
|
76
|
+
} catch (e) {
|
|
77
|
+
report.errors.push({ stage: "parse", message: String(e?.message || e) });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return report;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Renders the extracted metadata as an HTML table. */
|
|
84
|
+
present(data) {
|
|
85
|
+
if (!data || typeof data !== "object") return JSON.stringify(data, null, 4);
|
|
86
|
+
|
|
87
|
+
const esc = Utils.escapeHtml;
|
|
88
|
+
const row = (k, v) => `<tr><td>${esc(String(k))}</td><td>${esc(String(v ?? ""))}</td></tr>\n`;
|
|
89
|
+
const section = (title) => `<tr><th colspan="2" style="background:#e9ecef;text-align:center">${esc(title)}</th></tr>\n`;
|
|
90
|
+
const objRows = (obj, filter = (v) => v !== null) => {
|
|
91
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
92
|
+
if (filter(v)) html += row(k, v);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const objSection = (obj, title, filter) => {
|
|
96
|
+
if (!obj) return;
|
|
97
|
+
html += section(title);
|
|
98
|
+
objRows(obj, filter);
|
|
99
|
+
};
|
|
100
|
+
const listSection = (arr, title, fmt) => {
|
|
101
|
+
if (!arr?.length) return;
|
|
102
|
+
html += section(title);
|
|
103
|
+
for (const item of arr) html += fmt(item);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
let html = `<table class="table table-hover table-sm table-bordered table-nonfluid">\n`;
|
|
107
|
+
|
|
108
|
+
html += section("Artifact");
|
|
109
|
+
html += row("Filename", data.artifact?.filename || "(none)");
|
|
110
|
+
html += row("Size", `${(data.artifact?.byte_length ?? 0).toLocaleString()} bytes`);
|
|
111
|
+
html += row("Container", data.artifact?.container?.type);
|
|
112
|
+
html += row("MIME", data.artifact?.container?.mime);
|
|
113
|
+
if (data.artifact?.container?.brand) html += row("Brand", data.artifact.container.brand);
|
|
114
|
+
|
|
115
|
+
html += section("Detections");
|
|
116
|
+
html += row("Metadata systems", (data.detections?.metadata_systems || []).join(", ") || "None");
|
|
117
|
+
html += row("Provenance systems", (data.detections?.provenance_systems || []).join(", ") || "None");
|
|
118
|
+
|
|
119
|
+
const common = data.tags?.common || {};
|
|
120
|
+
html += section("Common Tags");
|
|
121
|
+
if (Object.values(common).some((v) => v !== null)) {
|
|
122
|
+
for (const [key, val] of Object.entries(common)) {
|
|
123
|
+
if (val !== null) html += row(key.charAt(0).toUpperCase() + key.slice(1), val);
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
html += row("(none)", "No common tags found");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
listSection(data.tags?.raw?.id3v2?.frames, "ID3v2 Frames", (f) => {
|
|
130
|
+
const val = typeof f.decoded === "object" ? JSON.stringify(f.decoded) : (f.decoded ?? `(${f.size} bytes)`);
|
|
131
|
+
return row(f.id + (f.description ? ` \u2014 ${f.description}` : ""), val);
|
|
132
|
+
});
|
|
133
|
+
objSection(data.tags?.raw?.id3v1, "ID3v1", (v) => !!v);
|
|
134
|
+
listSection(data.tags?.raw?.apev2?.items, "APEv2 Tags", (i) => row(i.key, i.value));
|
|
135
|
+
|
|
136
|
+
if (data.tags?.raw?.vorbis_comments?.comments?.length) {
|
|
137
|
+
html += section("Vorbis Comments");
|
|
138
|
+
html += row("Vendor", data.tags.raw.vorbis_comments.vendor);
|
|
139
|
+
for (const c of data.tags.raw.vorbis_comments.comments) html += row(c.key, c.value);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
objSection(data.tags?.raw?.riff?.info, "RIFF INFO", () => true);
|
|
143
|
+
objSection(data.tags?.raw?.riff?.bext, "BWF bext");
|
|
144
|
+
listSection(data.tags?.raw?.riff?.chunks, "RIFF Chunks", (c) => row(c.id, `${c.size} bytes @ offset ${c.offset}`));
|
|
145
|
+
listSection(data.tags?.raw?.flac?.blocks, "FLAC Metadata Blocks", (b) => row(b.type, `${b.length} bytes`));
|
|
146
|
+
|
|
147
|
+
if (data.tags?.raw?.mp4?.top_level_atoms?.length) {
|
|
148
|
+
html += section("MP4 Top-Level Atoms");
|
|
149
|
+
const atoms = data.tags.raw.mp4.top_level_atoms;
|
|
150
|
+
for (const a of atoms.slice(0, 50)) html += row(a.type, `${a.size} bytes @ offset ${a.offset}`);
|
|
151
|
+
if (atoms.length > 50) html += row("...", `${atoms.length - 50} more atoms`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
listSection(data.tags?.raw?.aiff?.chunks, "AIFF Chunks", (c) => row(c.id, c.value));
|
|
155
|
+
objSection(data.tags?.raw?.aac, "AAC ADTS");
|
|
156
|
+
objSection(data.tags?.raw?.ac3, "AC3 (Dolby Digital)");
|
|
157
|
+
objSection(data.tags?.raw?.asf?.content_description, "ASF Content Description", (v) => !!v);
|
|
158
|
+
listSection(data.tags?.raw?.asf?.extended_content, "ASF Extended Content", (d) => row(d.name, d.value));
|
|
159
|
+
listSection(data.embedded, "Embedded Objects", (e) => row(e.id, `${e.content_type || "unknown"} \u2014 ${(e.byte_length ?? 0).toLocaleString()} bytes`));
|
|
160
|
+
|
|
161
|
+
if (data.provenance?.c2pa?.present) {
|
|
162
|
+
html += section("C2PA Provenance");
|
|
163
|
+
html += row("Present", "Yes");
|
|
164
|
+
for (const emb of (data.provenance.c2pa.embedding || []))
|
|
165
|
+
html += row("Carrier", `${emb.carrier} \u2014 ${(emb.byte_length ?? 0).toLocaleString()} bytes`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
listSection(data.errors, "Errors", (e) => row(e.stage, e.message));
|
|
169
|
+
|
|
170
|
+
html += "</table>";
|
|
171
|
+
return html;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export default ExtractAudioMetadata;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import Operation from "../Operation.mjs";
|
|
8
|
-
import { search } from "../lib/Extract.mjs";
|
|
8
|
+
import { EMAIL_REGEX, search } from "../lib/Extract.mjs";
|
|
9
9
|
import { caseInsensitiveSort } from "../lib/Sort.mjs";
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -50,8 +50,7 @@ class ExtractEmailAddresses extends Operation {
|
|
|
50
50
|
*/
|
|
51
51
|
run(input, args) {
|
|
52
52
|
const [displayTotal, sort, unique] = args,
|
|
53
|
-
|
|
54
|
-
regex = /(?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9](?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9-]*[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9])?\.)+[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9](?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9-]*[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\])/ig;
|
|
53
|
+
regex = EMAIL_REGEX;
|
|
55
54
|
|
|
56
55
|
const results = search(
|
|
57
56
|
input,
|
|
@@ -9,13 +9,12 @@ import OperationError from "../errors/OperationError.mjs";
|
|
|
9
9
|
import Utils from "../Utils.mjs";
|
|
10
10
|
import { fromBinary } from "../lib/Binary.mjs";
|
|
11
11
|
import { isImage } from "../lib/FileType.mjs";
|
|
12
|
-
import Jimp from "jimp
|
|
12
|
+
import { Jimp } from "jimp";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Extract LSB operation
|
|
16
16
|
*/
|
|
17
17
|
class ExtractLSB extends Operation {
|
|
18
|
-
|
|
19
18
|
/**
|
|
20
19
|
* ExtractLSB constructor
|
|
21
20
|
*/
|
|
@@ -24,8 +23,10 @@ class ExtractLSB extends Operation {
|
|
|
24
23
|
|
|
25
24
|
this.name = "Extract LSB";
|
|
26
25
|
this.module = "Image";
|
|
27
|
-
this.description =
|
|
28
|
-
|
|
26
|
+
this.description =
|
|
27
|
+
"Extracts the Least Significant Bit data from each pixel in an image. This is a common way to hide data in Steganography.";
|
|
28
|
+
this.infoURL =
|
|
29
|
+
"https://wikipedia.org/wiki/Bit_numbering#Least_significant_bit_in_digital_steganography";
|
|
29
30
|
this.inputType = "ArrayBuffer";
|
|
30
31
|
this.outputType = "byteArray";
|
|
31
32
|
this.args = [
|
|
@@ -57,8 +58,8 @@ class ExtractLSB extends Operation {
|
|
|
57
58
|
{
|
|
58
59
|
name: "Bit",
|
|
59
60
|
type: "number",
|
|
60
|
-
value: 0
|
|
61
|
-
}
|
|
61
|
+
value: 0,
|
|
62
|
+
},
|
|
62
63
|
];
|
|
63
64
|
}
|
|
64
65
|
|
|
@@ -68,21 +69,27 @@ class ExtractLSB extends Operation {
|
|
|
68
69
|
* @returns {byteArray}
|
|
69
70
|
*/
|
|
70
71
|
async run(input, args) {
|
|
71
|
-
if (!isImage(input))
|
|
72
|
+
if (!isImage(input))
|
|
73
|
+
throw new OperationError("Please enter a valid image file.");
|
|
72
74
|
|
|
73
75
|
const bit = 7 - args.pop(),
|
|
74
76
|
pixelOrder = args.pop(),
|
|
75
|
-
colours = args
|
|
77
|
+
colours = args
|
|
78
|
+
.filter((option) => option !== "")
|
|
79
|
+
.map((option) => COLOUR_OPTIONS.indexOf(option)),
|
|
76
80
|
parsedImage = await Jimp.read(input),
|
|
77
81
|
width = parsedImage.bitmap.width,
|
|
78
82
|
height = parsedImage.bitmap.height,
|
|
79
83
|
rgba = parsedImage.bitmap.data;
|
|
80
84
|
|
|
81
85
|
if (bit < 0 || bit > 7) {
|
|
82
|
-
throw new OperationError(
|
|
86
|
+
throw new OperationError(
|
|
87
|
+
"Error: Bit argument must be between 0 and 7",
|
|
88
|
+
);
|
|
83
89
|
}
|
|
84
90
|
|
|
85
|
-
let i,
|
|
91
|
+
let i,
|
|
92
|
+
combinedBinary = "";
|
|
86
93
|
|
|
87
94
|
if (pixelOrder === "Row") {
|
|
88
95
|
for (i = 0; i < rgba.length; i += 4) {
|
|
@@ -106,7 +113,6 @@ class ExtractLSB extends Operation {
|
|
|
106
113
|
|
|
107
114
|
return fromBinary(combinedBinary);
|
|
108
115
|
}
|
|
109
|
-
|
|
110
116
|
}
|
|
111
117
|
|
|
112
118
|
const COLOUR_OPTIONS = ["R", "G", "B", "A"];
|
|
@@ -7,15 +7,14 @@
|
|
|
7
7
|
import Operation from "../Operation.mjs";
|
|
8
8
|
import OperationError from "../errors/OperationError.mjs";
|
|
9
9
|
import { isImage } from "../lib/FileType.mjs";
|
|
10
|
-
import Jimp from "jimp
|
|
10
|
+
import { Jimp } from "jimp";
|
|
11
11
|
|
|
12
|
-
import {RGBA_DELIM_OPTIONS} from "../lib/Delim.mjs";
|
|
12
|
+
import { RGBA_DELIM_OPTIONS } from "../lib/Delim.mjs";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Extract RGBA operation
|
|
16
16
|
*/
|
|
17
17
|
class ExtractRGBA extends Operation {
|
|
18
|
-
|
|
19
18
|
/**
|
|
20
19
|
* ExtractRGBA constructor
|
|
21
20
|
*/
|
|
@@ -24,7 +23,8 @@ class ExtractRGBA extends Operation {
|
|
|
24
23
|
|
|
25
24
|
this.name = "Extract RGBA";
|
|
26
25
|
this.module = "Image";
|
|
27
|
-
this.description =
|
|
26
|
+
this.description =
|
|
27
|
+
"Extracts each pixel's RGBA value in an image. These are sometimes used in Steganography to hide text or data.";
|
|
28
28
|
this.infoURL = "https://wikipedia.org/wiki/RGBA_color_space";
|
|
29
29
|
this.inputType = "ArrayBuffer";
|
|
30
30
|
this.outputType = "string";
|
|
@@ -32,13 +32,13 @@ class ExtractRGBA extends Operation {
|
|
|
32
32
|
{
|
|
33
33
|
name: "Delimiter",
|
|
34
34
|
type: "editableOption",
|
|
35
|
-
value: RGBA_DELIM_OPTIONS
|
|
35
|
+
value: RGBA_DELIM_OPTIONS,
|
|
36
36
|
},
|
|
37
37
|
{
|
|
38
38
|
name: "Include Alpha",
|
|
39
39
|
type: "boolean",
|
|
40
|
-
value: true
|
|
41
|
-
}
|
|
40
|
+
value: true,
|
|
41
|
+
},
|
|
42
42
|
];
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -48,18 +48,20 @@ class ExtractRGBA extends Operation {
|
|
|
48
48
|
* @returns {string}
|
|
49
49
|
*/
|
|
50
50
|
async run(input, args) {
|
|
51
|
-
if (!isImage(input))
|
|
51
|
+
if (!isImage(input))
|
|
52
|
+
throw new OperationError("Please enter a valid image file.");
|
|
52
53
|
|
|
53
54
|
const delimiter = args[0],
|
|
54
55
|
includeAlpha = args[1],
|
|
55
56
|
parsedImage = await Jimp.read(input);
|
|
56
57
|
|
|
57
58
|
let bitmap = parsedImage.bitmap.data;
|
|
58
|
-
bitmap = includeAlpha ?
|
|
59
|
+
bitmap = includeAlpha ?
|
|
60
|
+
bitmap :
|
|
61
|
+
bitmap.filter((val, idx) => idx % 4 !== 3);
|
|
59
62
|
|
|
60
63
|
return bitmap.join(delimiter);
|
|
61
64
|
}
|
|
62
|
-
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
export default ExtractRGBA;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @author ThePlayer372-FR []
|
|
3
|
+
* @license Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import Operation from "../Operation.mjs";
|
|
7
|
+
import OperationError from "../errors/OperationError.mjs";
|
|
8
|
+
import { fromBase64 } from "../lib/Base64.mjs";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Flask Session Decode operation
|
|
12
|
+
*/
|
|
13
|
+
class FlaskSessionDecode extends Operation {
|
|
14
|
+
/**
|
|
15
|
+
* FlaskSessionDecode constructor
|
|
16
|
+
*/
|
|
17
|
+
constructor() {
|
|
18
|
+
super();
|
|
19
|
+
|
|
20
|
+
this.name = "Flask Session Decode";
|
|
21
|
+
this.module = "Crypto";
|
|
22
|
+
this.description = "Decodes the payload of a Flask session cookie (itsdangerous) into JSON.";
|
|
23
|
+
this.inputType = "string";
|
|
24
|
+
this.outputType = "JSON";
|
|
25
|
+
this.args = [
|
|
26
|
+
{
|
|
27
|
+
name: "View TimeStamp",
|
|
28
|
+
type: "boolean",
|
|
29
|
+
value: false
|
|
30
|
+
}
|
|
31
|
+
];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {string} input
|
|
36
|
+
* @param {Object[]} args
|
|
37
|
+
* @returns {Object[]}
|
|
38
|
+
*/
|
|
39
|
+
run(input, args) {
|
|
40
|
+
input = input.trim();
|
|
41
|
+
const parts = input.split(".");
|
|
42
|
+
if (parts.length !== 3) {
|
|
43
|
+
throw new OperationError("Invalid Flask token format. Expected payload.timestamp.signature");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const payloadB64 = parts[0];
|
|
47
|
+
const time = parts[1];
|
|
48
|
+
|
|
49
|
+
const timeB64 = time.replace(/-/g, "+").replace(/_/g, "/");
|
|
50
|
+
const binary = fromBase64(timeB64);
|
|
51
|
+
const bytes = new Uint8Array(4);
|
|
52
|
+
for (let i = 0; i < 4; i++) {
|
|
53
|
+
bytes[i] = binary.charCodeAt(i);
|
|
54
|
+
}
|
|
55
|
+
const view = new DataView(bytes.buffer);
|
|
56
|
+
const timestamp = view.getInt32(0, false);
|
|
57
|
+
|
|
58
|
+
const base64 = payloadB64.replace(/-/g, "+").replace(/_/g, "/");
|
|
59
|
+
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
|
|
60
|
+
let payloadJson;
|
|
61
|
+
try {
|
|
62
|
+
payloadJson = fromBase64(padded);
|
|
63
|
+
} catch (e) {
|
|
64
|
+
throw new OperationError("Invalid Base64 payload");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
let data = JSON.parse(payloadJson);
|
|
69
|
+
|
|
70
|
+
if (args[0]) {
|
|
71
|
+
data = {payload: data, timestamp: timestamp};
|
|
72
|
+
}
|
|
73
|
+
return data;
|
|
74
|
+
} catch (e) {
|
|
75
|
+
throw new OperationError("Unable to decode JSON payload: " + e.message);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export default FlaskSessionDecode;
|