diff --git a/package-lock.json b/package-lock.json index 61db7931..ab3f3bbf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,7 +52,7 @@ "highlight.js": "^11.11.1", "ieee754": "^1.2.1", "jimp": "^1.6.0", - "jq-web": "^0.6.2", + "jq-wasm": "^1.1.0-jq-1.8.1", "jquery": "3.7.1", "js-sha3": "^0.9.3", "jsesc": "^3.1.0", @@ -12060,11 +12060,11 @@ "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", "license": "BSD-3-Clause" }, - "node_modules/jq-web": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/jq-web/-/jq-web-0.6.2.tgz", - "integrity": "sha512-+7XvjBYwTx4vP5PYkf6Q6orubO/v+UgMU6By1GritrmShr9QpT3UKa4ANzXWQfhdqtBnQYXsm7ZNbdIHT6tYpQ==", - "license": "ISC" + "node_modules/jq-wasm": { + "version": "1.1.0-jq-1.8.1", + "resolved": "https://registry.npmjs.org/jq-wasm/-/jq-wasm-1.1.0-jq-1.8.1.tgz", + "integrity": "sha512-lWfu34lpDFIygOYcL5TzxhZIApDR9iR5XywcVoyUAZ6jlQrj8HKHOKeCcHgUm2dE9RVdbP3eqNAKGLuj+k4seQ==", + "license": "MIT" }, "node_modules/jquery": { "version": "3.7.1", diff --git a/package.json b/package.json index 1e87c230..1602b1b6 100644 --- a/package.json +++ b/package.json @@ -135,7 +135,7 @@ "highlight.js": "^11.11.1", "ieee754": "^1.2.1", "jimp": "^1.6.0", - "jq-web": "^0.6.2", + "jq-wasm": "^1.1.0-jq-1.8.1", "jquery": "3.7.1", "js-sha3": "^0.9.3", "jsesc": "^3.1.0", diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 6d6b2f39..cee966b0 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -164,7 +164,10 @@ "Typex", "Lorenz", "Colossus", - "SIGABA" + "SIGABA", + "Flask Session Decode", + "Flask Session Sign", + "Flask Session Verify" ] }, { diff --git a/src/core/operations/FlaskSessionDecode.mjs b/src/core/operations/FlaskSessionDecode.mjs new file mode 100644 index 00000000..5486357e --- /dev/null +++ b/src/core/operations/FlaskSessionDecode.mjs @@ -0,0 +1,80 @@ +/** + * @author ThePlayer372-FR [] + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; +import { fromBase64 } from "../lib/Base64.mjs"; + +/** + * Flask Session Decode operation + */ +class FlaskSessionDecode extends Operation { + /** + * FlaskSessionDecode constructor + */ + constructor() { + super(); + + this.name = "Flask Session Decode"; + this.module = "Crypto"; + this.description = "Decodes the payload of a Flask session cookie (itsdangerous) into JSON."; + this.inputType = "string"; + this.outputType = "JSON"; + this.args = [ + { + name: "View TimeStamp", + type: "boolean", + value: false + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {Object[]} + */ + run(input, args) { + input = input.trim(); + const parts = input.split("."); + if (parts.length !== 3) { + throw new OperationError("Invalid Flask token format. Expected payload.timestamp.signature"); + } + + const payloadB64 = parts[0]; + const time = parts[1]; + + const timeB64 = time.replace(/-/g, "+").replace(/_/g, "/"); + const binary = fromBase64(timeB64); + const bytes = new Uint8Array(4); + for (let i = 0; i < 4; i++) { + bytes[i] = binary.charCodeAt(i); + } + const view = new DataView(bytes.buffer); + const timestamp = view.getInt32(0, false); + + const base64 = payloadB64.replace(/-/g, "+").replace(/_/g, "/"); + const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "="); + let payloadJson; + try { + payloadJson = fromBase64(padded); + } catch (e) { + throw new OperationError("Invalid Base64 payload"); + } + + try { + let data = JSON.parse(payloadJson); + + if (args[0]) { + data = {payload: data, timestamp: timestamp}; + } + return data; + } catch (e) { + throw new OperationError("Unable to decode JSON payload: " + e.message); + } + } +} + +export default FlaskSessionDecode; diff --git a/src/core/operations/FlaskSessionSign.mjs b/src/core/operations/FlaskSessionSign.mjs new file mode 100644 index 00000000..01ee8b1d --- /dev/null +++ b/src/core/operations/FlaskSessionSign.mjs @@ -0,0 +1,89 @@ +/** + * @author ThePlayer372-FR [] + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import CryptoApi from "crypto-api/src/crypto-api.mjs"; +import Utils from "../Utils.mjs"; +import { toBase64 } from "../lib/Base64.mjs"; +import OperationError from "../errors/OperationError.mjs"; + +/** + * Flask Session Sign operation + */ +class FlaskSessionSign extends Operation { + /** + * FlaskSessionSign constructor + */ + constructor() { + super(); + + this.name = "Flask Session Sign"; + this.module = "Crypto"; + this.description = "Signs a JSON payload to produce a Flask session cookie (itsdangerous HMAC)."; + this.inputType = "JSON"; + this.outputType = "string"; + this.args = [ + { + name: "Key", + type: "toggleString", + value: "", + toggleValues: ["Hex", "Decimal", "Binary", "Base64", "UTF8", "Latin1"] + }, + { + name: "Salt", + type: "toggleString", + value: "cookie-session", + toggleValues: ["UTF8", "Hex", "Decimal", "Binary", "Base64", "Latin1"] + }, + { + name: "Algorithm", + type: "option", + value: ["sha1", "sha256"], + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + if (!args[0].string) { + throw new OperationError("Secret key required"); + } + const key = Utils.convertToByteString(args[0].string, args[0].option); + const salt = Utils.convertToByteString(args[1].string || "cookie-session", args[1].option); + const algorithm = args[2] || "sha1"; + + const payloadB64 = toBase64(Utils.strToByteArray(JSON.stringify(input))); + const payload = payloadB64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); + + const derivedKey = CryptoApi.getHmac(key, CryptoApi.getHasher(algorithm)); + derivedKey.update(salt); + + const currentTimeStamp = Math.ceil(Date.now() / 1000); + const buffer = new ArrayBuffer(4); + const view = new DataView(buffer); + view.setInt32(0, currentTimeStamp, false); + const bytes = new Uint8Array(buffer); + let binary = ""; + bytes.forEach(b => binary += String.fromCharCode(b)); + const timeB64 = toBase64(Utils.strToByteArray(binary)); + const time = timeB64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); + + const data = Utils.convertToByteString(payload + "." + time, "utf8"); + const sign = CryptoApi.getHmac(derivedKey.finalize(), CryptoApi.getHasher(algorithm)); + sign.update(data); + + const signB64 = toBase64(sign.finalize()); + const sign64 = signB64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); + + return payload + "." + time + "." + sign64; + } +} + + +export default FlaskSessionSign; diff --git a/src/core/operations/FlaskSessionVerify.mjs b/src/core/operations/FlaskSessionVerify.mjs new file mode 100644 index 00000000..7603ba1f --- /dev/null +++ b/src/core/operations/FlaskSessionVerify.mjs @@ -0,0 +1,136 @@ +/** + * @author ThePlayer372-FR [] + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; +import CryptoApi from "crypto-api/src/crypto-api.mjs"; +import Utils from "../Utils.mjs"; +import { toBase64, fromBase64 } from "../lib/Base64.mjs"; + +/** + * Flask Session Verify operation + */ +class FlaskSessionVerify extends Operation { + /** + * FlaskSessionVerify constructor + */ + constructor() { + super(); + + this.name = "Flask Session Verify"; + this.module = "Crypto"; + this.description = "Verifies the HMAC signature of a Flask session cookie (itsdangerous) generated."; + this.inputType = "string"; + this.outputType = "JSON"; + this.args = [ + { + name: "Key", + type: "toggleString", + value: "", + toggleValues: ["Hex", "Decimal", "Binary", "Base64", "UTF8", "Latin1"] + }, + { + name: "Salt", + type: "toggleString", + value: "cookie-session", + toggleValues: ["UTF8", "Hex", "Decimal", "Binary", "Base64", "Latin1"] + }, + { + name: "Algorithm", + type: "option", + value: ["sha1", "sha256"], + }, + { + name: "View TimeStamp", + type: "boolean", + value: true + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + + if (!args[0].string) { + throw new OperationError("Secret key required"); + } + + const key = Utils.convertToByteString(args[0].string, args[0].option); + const salt = Utils.convertToByteString(args[1].string || "cookie-session", args[1].option); + const algorithm = args[2] || "sha1"; + + input = input.trim(); + + const parts = input.split("."); + + if (parts.length !== 3) { + throw new OperationError("Invalid Flask token format. Expected payload.timestamp.signature"); + } + + const data = Utils.convertToByteString(parts[0] + "." + parts[1], "utf8"); + + + const derivedKey = CryptoApi.getHmac(key, CryptoApi.getHasher(algorithm)); + derivedKey.update(salt); + + const sign = CryptoApi.getHmac(derivedKey.finalize(), CryptoApi.getHasher(algorithm)); + sign.update(data); + + const payloadB64 = parts[0]; + const base64 = payloadB64.replace(/-/g, "+").replace(/_/g, "/"); + const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "="); + + const time = parts[1]; + + const timeB64 = time.replace(/-/g, "+").replace(/_/g, "/"); + const binary = fromBase64(timeB64); + const bytes = new Uint8Array(4); + for (let i = 0; i < 4; i++) { + bytes[i] = binary.charCodeAt(i); + } + const view = new DataView(bytes.buffer); + const timestamp = view.getInt32(0, false); + + let payloadJson; + try { + payloadJson = fromBase64(padded); + } catch (e) { + throw new OperationError("Invalid Base64 payload"); + } + + const signB64 = toBase64(sign.finalize()); + const sign64 = signB64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); + + if (sign64 !== parts[2]) { + throw new OperationError("Invalid signature!"); + } + + try { + const decoded = JSON.parse(payloadJson); + if (!args[3]) { + return { + valid: true, + payload: decoded, + }; + } else { + return { + valid: true, + payload: decoded, + timestamp: timestamp + }; + } + } catch (e) { + throw new OperationError("Unable to decode JSON payload: " + e.message); + } + + } +} + + +export default FlaskSessionVerify; diff --git a/src/core/operations/Jq.mjs b/src/core/operations/Jq.mjs index c1e02b34..4584d1a9 100644 --- a/src/core/operations/Jq.mjs +++ b/src/core/operations/Jq.mjs @@ -6,7 +6,7 @@ import Operation from "../Operation.mjs"; import OperationError from "../errors/OperationError.mjs"; -import jq from "jq-web"; +import * as jq from "jq-wasm"; /** * jq operation @@ -40,16 +40,15 @@ class Jq extends Operation { * @returns {string} */ run(input, args) { - const [query] = args; - let result; - - try { - result = jq.json(input, query); - } catch (err) { - throw new OperationError(`Invalid jq expression: ${err.message}`); - } - - return JSON.stringify(result); + return (async () => { + const [query] = args; + try { + const result = await jq.json(input, query); + return JSON.stringify(result); + } catch (err) { + throw new OperationError(`Invalid jq expression: ${err.message}`); + } + })(); } } diff --git a/src/web/stylesheets/utils/_overrides.css b/src/web/stylesheets/utils/_overrides.css index a2f8b029..fec7d857 100755 --- a/src/web/stylesheets/utils/_overrides.css +++ b/src/web/stylesheets/utils/_overrides.css @@ -249,6 +249,13 @@ optgroup { } +/* Bootstrap form inside CodeMirror editor */ + +.cm-panel > .bmd-form-group { + padding-top: 0; +} + + /* CodeMirror */ .ΝΌ2 .cm-specialChar, diff --git a/tests/browser/02_ops.js b/tests/browser/02_ops.js index d0b89c3e..dde84f68 100644 --- a/tests/browser/02_ops.js +++ b/tests/browser/02_ops.js @@ -50,6 +50,14 @@ module.exports = { testOp(browser, "Analyse hash", "0123456789abcdef", /CRC-64/); testOp(browser, "Atbash Cipher", "test input", "gvhg rmkfg"); // testOp(browser, "Avro to JSON", "test input", "test_output"); + testOp(browser, + [ + "From Hex", "Avro to JSON" + ], + "4f626a0104166176726f2e736368656d6196017b2274797065223a227265636f7264222c226e616d65223a22736d616c6c222c226669656c6473223a5b7b226e616d65223a226e616d65222c2274797065223a22737472696e67227d5d7d146176726f2e636f646563086e756c6c004e0247632e3702e5b75cdab9a62f1541020e0c6d796e616d654e0247632e3702e5b75cdab9a62f1541", + '{"name":"myname"}\n', + [[], [false]] + ); testOp(browser, "BLAKE2b", "test input", "33ebdc8f38177f3f3f334eeb117a84e11f061bbca4db6b8923e5cec85103f59f415551a5d5a933fdb6305dc7bf84671c2540b463dbfa08ee1895cfaa5bd780b5", ["512", "Hex", { "option": "UTF8", "string": "pass" }]); testOp(browser, "BLAKE2s", "test input", "defe73d61dfa6e5807e4f9643e159a09ccda6be3c26dcd65f8a9bb38bfc973a7", ["256", "Hex", { "option": "UTF8", "string": "pass" }]); testOp(browser, "BSON deserialise", "\u0011\u0000\u0000\u0000\u0002a\u0000\u0005\u0000\u0000\u0000test\u0000\u0000", '{\u000A "a": "test"\u000A}'); @@ -206,6 +214,7 @@ module.exports = { testOpHtml(browser, "Index of Coincidence", "test input", "", /Index of Coincidence: 0.08333333333333333/); testOpImage(browser, "Invert Image", "files/Hitchhikers_Guide.jpeg"); // testOp(browser, "JPath expression", "test input", "test_output"); + testOp(browser, "Jq", '{"a":{"b":1}}', '{"b":1}', [".a"]); testOpHtml(browser, "JSON Beautify", "{a:1}", ".json-dict .json-literal", "1"); // testOp(browser, "JSON Minify", "test input", "test_output"); // testOp(browser, "JSON to CSV", "test input", "test_output"); diff --git a/tests/operations/tests/FlaskSession.mjs b/tests/operations/tests/FlaskSession.mjs new file mode 100644 index 00000000..7becf400 --- /dev/null +++ b/tests/operations/tests/FlaskSession.mjs @@ -0,0 +1,246 @@ +/** + * Flask Session tests + * + * @author ThePlayer372-FR [] + * + * @license Apache-2.0 + */ + +import TestRegister from "../../lib/TestRegister.mjs"; + +const validTokenSha1 = "eyJyb2xlIjoic3VwZXJ1c2VyIiwidXNlciI6ImFkbWluIn0.aZ-KEw.E_x6bOhA4GU9t72pMinJUjN-O3I"; +const validTokenSha256 = "eyJyb2xlIjoic3VwZXJ1c2VyIiwidXNlciI6ImFkbWluIn0.aab3Ew.Jsx2DOx_H9anZg0YcvhsASxQ11897EFHeQfS2oja4y8"; + +const validKey = "mysecretkey"; +const wrongKey = "notTheKey"; + +const outputObject = { + user: "admin", + role: "superuser", +}; + +const outputVerify = { + valid: true, + payload: outputObject, +}; + +TestRegister.addTests([ + { + name: "Flask Session: Decode", + input: validTokenSha1, + expectedOutput: outputObject, + recipeConfig: [ + { + op: "Flask Session Decode", + args: [ + false + ], + } + ] + }, + { + name: "Flask Session: Verify Sha1", + input: validTokenSha1, + expectedOutput: outputVerify, + recipeConfig: [ + { + op: "Flask Session Verify", + args: [ + { + string: validKey, + option: "UTF8" + }, + { + string: "cookie-session", + option: "UTF8" + }, + "sha1", + false, + ], + } + ] + }, + { + name: "Flask Session: Verify Sha256", + input: validTokenSha256, + expectedOutput: outputVerify, + recipeConfig: [ + { + op: "Flask Session Verify", + args: [ + { + string: validKey, + option: "UTF8" + }, + { + string: "cookie-session", + option: "UTF8" + }, + "sha256", + false, + ], + } + ] + }, + { + name: "Flask Session: Sign Sha1", + input: outputObject, + expectedOutput: outputVerify, + recipeConfig: [ + { + op: "Flask Session Sign", + args: [ + { + string: validKey, + option: "UTF8" + }, + { + string: "cookie-session", + option: "UTF8" + }, + "sha1" + ] + }, + { + op: "Flask Session Verify", + args: [ + { + string: validKey, + option: "UTF8" + }, + { + string: "cookie-session", + option: "UTF8" + }, + "sha1", + false, + ], + } + ] + }, + { + name: "Flask Session: Sign Sha256", + input: outputObject, + expectedOutput: outputVerify, + recipeConfig: [ + { + op: "Flask Session Sign", + args: [ + { + string: validKey, + option: "UTF8" + }, + { + string: "cookie-session", + option: "UTF8" + }, + "sha256" + ] + }, + { + op: "Flask Session Verify", + args: [ + { + string: validKey, + option: "UTF8" + }, + { + string: "cookie-session", + option: "UTF8" + }, + "sha256", + false, + ], + } + ] + }, + { + name: "Flask Session: Verify Sha1 Wrong Key", + input: validTokenSha1, + expectedOutput: "Invalid signature!", + recipeConfig: [ + { + op: "Flask Session Verify", + args: [ + { + string: wrongKey, + option: "UTF8" + }, + { + string: "cookie-session", + option: "UTF8" + }, + "sha1", + false, + ], + } + ] + }, + { + name: "Flask Session: Verify Sha256 Wrong Key", + input: validTokenSha256, + expectedOutput: "Invalid signature!", + recipeConfig: [ + { + op: "Flask Session Verify", + args: [ + { + string: wrongKey, + option: "UTF8" + }, + { + string: "cookie-session", + option: "UTF8" + }, + "sha256", + false, + ], + } + ] + }, + { + name: "Flask Session: Verify Sha1 Wrong Salt", + input: validTokenSha1, + expectedOutput: "Invalid signature!", + recipeConfig: [ + { + op: "Flask Session Verify", + args: [ + { + string: validKey, + option: "UTF8" + }, + { + string: "notTheSalt", + option: "UTF8" + }, + "sha1", + false, + ], + } + ] + }, + { + name: "Flask Session: Verify Sha256 Wrong Salt", + input: validTokenSha256, + expectedOutput: "Invalid signature!", + recipeConfig: [ + { + op: "Flask Session Verify", + args: [ + { + string: validKey, + option: "UTF8" + }, + { + string: "notTheSalt", + option: "UTF8" + }, + "sha256", + false, + ], + } + ] + }, + +]);