Add Flask Session operations (Decode, Sign, Verify) (#2208)
This commit is contained in:
parent
0c6454e10c
commit
cbe1d39e06
@ -164,7 +164,10 @@
|
|||||||
"Typex",
|
"Typex",
|
||||||
"Lorenz",
|
"Lorenz",
|
||||||
"Colossus",
|
"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…
x
Reference in New Issue
Block a user