cyberchef 10.22.1 → 10.23.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.
Files changed (101) hide show
  1. package/CHANGELOG.md +131 -0
  2. package/CONTRIBUTING.md +37 -0
  3. package/Dockerfile +2 -0
  4. package/Gruntfile.js +0 -12
  5. package/README.md +1 -1
  6. package/babel.config.js +0 -6
  7. package/package.json +63 -62
  8. package/src/core/Chef.mjs +8 -1
  9. package/src/core/Ingredient.mjs +5 -2
  10. package/src/core/Operation.mjs +6 -1
  11. package/src/core/Recipe.mjs +10 -5
  12. package/src/core/config/Categories.json +18 -3
  13. package/src/core/config/OperationConfig.json +496 -23
  14. package/src/core/config/modules/Ciphers.mjs +6 -0
  15. package/src/core/config/modules/Crypto.mjs +6 -0
  16. package/src/core/config/modules/Default.mjs +6 -0
  17. package/src/core/config/modules/Shellcode.mjs +2 -0
  18. package/src/core/lib/AudioBytes.mjs +103 -0
  19. package/src/core/lib/AudioMetaSchema.mjs +82 -0
  20. package/src/core/lib/AudioParsers.mjs +630 -0
  21. package/src/core/lib/BigIntUtils.mjs +73 -0
  22. package/src/core/lib/Modhex.mjs +2 -0
  23. package/src/core/lib/QRCode.mjs +30 -10
  24. package/src/core/lib/RC6.mjs +625 -0
  25. package/src/core/operations/A1Z26CipherDecode.mjs +1 -1
  26. package/src/core/operations/AddTextToImage.mjs +116 -64
  27. package/src/core/operations/BlurImage.mjs +10 -12
  28. package/src/core/operations/ContainImage.mjs +50 -40
  29. package/src/core/operations/ConvertImageFormat.mjs +33 -39
  30. package/src/core/operations/CoverImage.mjs +39 -37
  31. package/src/core/operations/CropImage.mjs +35 -21
  32. package/src/core/operations/DisassembleARM.mjs +193 -0
  33. package/src/core/operations/DitherImage.mjs +8 -8
  34. package/src/core/operations/EscapeUnicodeCharacters.mjs +0 -17
  35. package/src/core/operations/ExtractAudioMetadata.mjs +175 -0
  36. package/src/core/operations/ExtractLSB.mjs +17 -11
  37. package/src/core/operations/ExtractRGBA.mjs +12 -10
  38. package/src/core/operations/FlaskSessionDecode.mjs +80 -0
  39. package/src/core/operations/FlaskSessionSign.mjs +89 -0
  40. package/src/core/operations/FlaskSessionVerify.mjs +136 -0
  41. package/src/core/operations/FlipImage.mjs +14 -10
  42. package/src/core/operations/GenerateImage.mjs +39 -32
  43. package/src/core/operations/ImageBrightnessContrast.mjs +10 -10
  44. package/src/core/operations/ImageFilter.mjs +14 -13
  45. package/src/core/operations/ImageHueSaturationLightness.mjs +22 -20
  46. package/src/core/operations/ImageOpacity.mjs +6 -8
  47. package/src/core/operations/InvertImage.mjs +4 -6
  48. package/src/core/operations/Jq.mjs +12 -4
  49. package/src/core/operations/NormaliseImage.mjs +5 -7
  50. package/src/core/operations/OffsetChecker.mjs +1 -1
  51. package/src/core/operations/ParseEthernetFrame.mjs +112 -0
  52. package/src/core/operations/ParseIPv4Header.mjs +23 -6
  53. package/src/core/operations/ParseQRCode.mjs +13 -13
  54. package/src/core/operations/PseudoRandomIntegerGenerator.mjs +164 -0
  55. package/src/core/operations/RC6Decrypt.mjs +119 -0
  56. package/src/core/operations/RC6Encrypt.mjs +119 -0
  57. package/src/core/operations/RandomizeColourPalette.mjs +11 -11
  58. package/src/core/operations/ResizeImage.mjs +30 -23
  59. package/src/core/operations/RotateImage.mjs +8 -9
  60. package/src/core/operations/SQLBeautify.mjs +21 -3
  61. package/src/core/operations/SharpenImage.mjs +94 -62
  62. package/src/core/operations/SplitColourChannels.mjs +47 -21
  63. package/src/core/operations/TextIntegerConverter.mjs +123 -0
  64. package/src/core/operations/UnescapeUnicodeCharacters.mjs +17 -0
  65. package/src/core/operations/ViewBitPlane.mjs +16 -20
  66. package/src/core/operations/index.mjs +20 -0
  67. package/src/node/index.mjs +50 -0
  68. package/src/web/HTMLIngredient.mjs +24 -43
  69. package/src/web/Manager.mjs +1 -0
  70. package/src/web/html/index.html +6 -6
  71. package/src/web/static/fonts/bmfonts/Roboto72White.fnt +491 -485
  72. package/src/web/static/fonts/bmfonts/RobotoBlack72White.fnt +494 -488
  73. package/src/web/static/fonts/bmfonts/RobotoMono72White.fnt +110 -103
  74. package/src/web/static/fonts/bmfonts/RobotoSlab72White.fnt +498 -492
  75. package/src/web/stylesheets/layout/_banner.css +30 -0
  76. package/src/web/stylesheets/layout/_modals.css +5 -0
  77. package/src/web/stylesheets/utils/_overrides.css +7 -0
  78. package/src/web/waiters/ControlsWaiter.mjs +82 -0
  79. package/src/web/waiters/InputWaiter.mjs +12 -6
  80. package/src/web/waiters/RecipeWaiter.mjs +2 -2
  81. package/tests/browser/02_ops.js +23 -3
  82. package/tests/node/index.mjs +1 -0
  83. package/tests/node/tests/lib/BigIntUtils.mjs +150 -0
  84. package/tests/node/tests/operations.mjs +9 -7
  85. package/tests/operations/index.mjs +8 -0
  86. package/tests/operations/tests/A1Z26CipherDecode.mjs +33 -0
  87. package/tests/operations/tests/DisassembleARM.mjs +377 -0
  88. package/tests/operations/tests/ExtractAudioMetadata.mjs +287 -0
  89. package/tests/operations/tests/FlaskSession.mjs +246 -0
  90. package/tests/operations/tests/GenerateQRCode.mjs +67 -0
  91. package/tests/operations/tests/JWTSign.mjs +83 -8
  92. package/tests/operations/tests/Jq.mjs +32 -0
  93. package/tests/operations/tests/Modhex.mjs +20 -0
  94. package/tests/operations/tests/ParseEthernetFrame.mjs +45 -0
  95. package/tests/operations/tests/RC6.mjs +487 -0
  96. package/tests/operations/tests/SQLBeautify.mjs +54 -0
  97. package/tests/operations/tests/TextIntegerConverter.mjs +199 -0
  98. package/tests/samples/Audio.mjs +73 -0
  99. package/tests/samples/Images.mjs +0 -12
  100. package/webpack.config.js +10 -7
  101. package/src/core/lib/ImageManipulation.mjs +0 -251
@@ -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;
@@ -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/es/index.js";
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 = "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 = "https://wikipedia.org/wiki/Bit_numbering#Least_significant_bit_in_digital_steganography";
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)) throw new OperationError("Please enter a valid image file.");
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.filter(option => option !== "").map(option => COLOUR_OPTIONS.indexOf(option)),
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("Error: Bit argument must be between 0 and 7");
86
+ throw new OperationError(
87
+ "Error: Bit argument must be between 0 and 7",
88
+ );
83
89
  }
84
90
 
85
- let i, combinedBinary = "";
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/es/index.js";
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 = "Extracts each pixel's RGBA value in an image. These are sometimes used in Steganography to hide text or data.";
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)) throw new OperationError("Please enter a valid image file.");
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 ? bitmap : bitmap.filter((val, idx) => idx % 4 !== 3);
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;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * @author ThePlayer372-FR []
3
+ * @license Apache-2.0
4
+ */
5
+
6
+ import Operation from "../Operation.mjs";
7
+ import CryptoApi from "crypto-api/src/crypto-api.mjs";
8
+ import Utils from "../Utils.mjs";
9
+ import { toBase64 } from "../lib/Base64.mjs";
10
+ import OperationError from "../errors/OperationError.mjs";
11
+
12
+ /**
13
+ * Flask Session Sign operation
14
+ */
15
+ class FlaskSessionSign extends Operation {
16
+ /**
17
+ * FlaskSessionSign constructor
18
+ */
19
+ constructor() {
20
+ super();
21
+
22
+ this.name = "Flask Session Sign";
23
+ this.module = "Crypto";
24
+ this.description = "Signs a JSON payload to produce a Flask session cookie (itsdangerous HMAC).";
25
+ this.inputType = "JSON";
26
+ this.outputType = "string";
27
+ this.args = [
28
+ {
29
+ name: "Key",
30
+ type: "toggleString",
31
+ value: "",
32
+ toggleValues: ["Hex", "Decimal", "Binary", "Base64", "UTF8", "Latin1"]
33
+ },
34
+ {
35
+ name: "Salt",
36
+ type: "toggleString",
37
+ value: "cookie-session",
38
+ toggleValues: ["UTF8", "Hex", "Decimal", "Binary", "Base64", "Latin1"]
39
+ },
40
+ {
41
+ name: "Algorithm",
42
+ type: "option",
43
+ value: ["sha1", "sha256"],
44
+ }
45
+ ];
46
+ }
47
+
48
+ /**
49
+ * @param {string} input
50
+ * @param {Object[]} args
51
+ * @returns {string}
52
+ */
53
+ run(input, args) {
54
+ if (!args[0].string) {
55
+ throw new OperationError("Secret key required");
56
+ }
57
+ const key = Utils.convertToByteString(args[0].string, args[0].option);
58
+ const salt = Utils.convertToByteString(args[1].string || "cookie-session", args[1].option);
59
+ const algorithm = args[2] || "sha1";
60
+
61
+ const payloadB64 = toBase64(Utils.strToByteArray(JSON.stringify(input)));
62
+ const payload = payloadB64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
63
+
64
+ const derivedKey = CryptoApi.getHmac(key, CryptoApi.getHasher(algorithm));
65
+ derivedKey.update(salt);
66
+
67
+ const currentTimeStamp = Math.ceil(Date.now() / 1000);
68
+ const buffer = new ArrayBuffer(4);
69
+ const view = new DataView(buffer);
70
+ view.setInt32(0, currentTimeStamp, false);
71
+ const bytes = new Uint8Array(buffer);
72
+ let binary = "";
73
+ bytes.forEach(b => binary += String.fromCharCode(b));
74
+ const timeB64 = toBase64(Utils.strToByteArray(binary));
75
+ const time = timeB64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
76
+
77
+ const data = Utils.convertToByteString(payload + "." + time, "utf8");
78
+ const sign = CryptoApi.getHmac(derivedKey.finalize(), CryptoApi.getHasher(algorithm));
79
+ sign.update(data);
80
+
81
+ const signB64 = toBase64(sign.finalize());
82
+ const sign64 = signB64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
83
+
84
+ return payload + "." + time + "." + sign64;
85
+ }
86
+ }
87
+
88
+
89
+ export default FlaskSessionSign;
@@ -0,0 +1,136 @@
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 CryptoApi from "crypto-api/src/crypto-api.mjs";
9
+ import Utils from "../Utils.mjs";
10
+ import { toBase64, fromBase64 } from "../lib/Base64.mjs";
11
+
12
+ /**
13
+ * Flask Session Verify operation
14
+ */
15
+ class FlaskSessionVerify extends Operation {
16
+ /**
17
+ * FlaskSessionVerify constructor
18
+ */
19
+ constructor() {
20
+ super();
21
+
22
+ this.name = "Flask Session Verify";
23
+ this.module = "Crypto";
24
+ this.description = "Verifies the HMAC signature of a Flask session cookie (itsdangerous) generated.";
25
+ this.inputType = "string";
26
+ this.outputType = "JSON";
27
+ this.args = [
28
+ {
29
+ name: "Key",
30
+ type: "toggleString",
31
+ value: "",
32
+ toggleValues: ["Hex", "Decimal", "Binary", "Base64", "UTF8", "Latin1"]
33
+ },
34
+ {
35
+ name: "Salt",
36
+ type: "toggleString",
37
+ value: "cookie-session",
38
+ toggleValues: ["UTF8", "Hex", "Decimal", "Binary", "Base64", "Latin1"]
39
+ },
40
+ {
41
+ name: "Algorithm",
42
+ type: "option",
43
+ value: ["sha1", "sha256"],
44
+ },
45
+ {
46
+ name: "View TimeStamp",
47
+ type: "boolean",
48
+ value: true
49
+ }
50
+ ];
51
+ }
52
+
53
+ /**
54
+ * @param {string} input
55
+ * @param {Object[]} args
56
+ * @returns {string}
57
+ */
58
+ run(input, args) {
59
+
60
+ if (!args[0].string) {
61
+ throw new OperationError("Secret key required");
62
+ }
63
+
64
+ const key = Utils.convertToByteString(args[0].string, args[0].option);
65
+ const salt = Utils.convertToByteString(args[1].string || "cookie-session", args[1].option);
66
+ const algorithm = args[2] || "sha1";
67
+
68
+ input = input.trim();
69
+
70
+ const parts = input.split(".");
71
+
72
+ if (parts.length !== 3) {
73
+ throw new OperationError("Invalid Flask token format. Expected payload.timestamp.signature");
74
+ }
75
+
76
+ const data = Utils.convertToByteString(parts[0] + "." + parts[1], "utf8");
77
+
78
+
79
+ const derivedKey = CryptoApi.getHmac(key, CryptoApi.getHasher(algorithm));
80
+ derivedKey.update(salt);
81
+
82
+ const sign = CryptoApi.getHmac(derivedKey.finalize(), CryptoApi.getHasher(algorithm));
83
+ sign.update(data);
84
+
85
+ const payloadB64 = parts[0];
86
+ const base64 = payloadB64.replace(/-/g, "+").replace(/_/g, "/");
87
+ const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
88
+
89
+ const time = parts[1];
90
+
91
+ const timeB64 = time.replace(/-/g, "+").replace(/_/g, "/");
92
+ const binary = fromBase64(timeB64);
93
+ const bytes = new Uint8Array(4);
94
+ for (let i = 0; i < 4; i++) {
95
+ bytes[i] = binary.charCodeAt(i);
96
+ }
97
+ const view = new DataView(bytes.buffer);
98
+ const timestamp = view.getInt32(0, false);
99
+
100
+ let payloadJson;
101
+ try {
102
+ payloadJson = fromBase64(padded);
103
+ } catch (e) {
104
+ throw new OperationError("Invalid Base64 payload");
105
+ }
106
+
107
+ const signB64 = toBase64(sign.finalize());
108
+ const sign64 = signB64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
109
+
110
+ if (sign64 !== parts[2]) {
111
+ throw new OperationError("Invalid signature!");
112
+ }
113
+
114
+ try {
115
+ const decoded = JSON.parse(payloadJson);
116
+ if (!args[3]) {
117
+ return {
118
+ valid: true,
119
+ payload: decoded,
120
+ };
121
+ } else {
122
+ return {
123
+ valid: true,
124
+ payload: decoded,
125
+ timestamp: timestamp
126
+ };
127
+ }
128
+ } catch (e) {
129
+ throw new OperationError("Unable to decode JSON payload: " + e.message);
130
+ }
131
+
132
+ }
133
+ }
134
+
135
+
136
+ export default FlaskSessionVerify;