mirror of
https://github.com/gchq/CyberChef.git
synced 2026-03-13 10:20:47 -07:00
Add Flask Session operations (Decode, Sign, Verify) (#2208)
This commit is contained in:
parent
0c6454e10c
commit
cbe1d39e06
5 changed files with 555 additions and 1 deletions
|
|
@ -164,7 +164,10 @@
|
|||
"Typex",
|
||||
"Lorenz",
|
||||
"Colossus",
|
||||
"SIGABA"
|
||||
"SIGABA",
|
||||
"Flask Session Decode",
|
||||
"Flask Session Sign",
|
||||
"Flask Session Verify"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
|||
80
src/core/operations/FlaskSessionDecode.mjs
Normal file
80
src/core/operations/FlaskSessionDecode.mjs
Normal file
|
|
@ -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;
|
||||
89
src/core/operations/FlaskSessionSign.mjs
Normal file
89
src/core/operations/FlaskSessionSign.mjs
Normal file
|
|
@ -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;
|
||||
136
src/core/operations/FlaskSessionVerify.mjs
Normal file
136
src/core/operations/FlaskSessionVerify.mjs
Normal file
|
|
@ -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;
|
||||
246
tests/operations/tests/FlaskSession.mjs
Normal file
246
tests/operations/tests/FlaskSession.mjs
Normal file
|
|
@ -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,
|
||||
],
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
]);
|
||||
Loading…
Add table
Add a link
Reference in a new issue