mirror of
https://github.com/gchq/CyberChef.git
synced 2025-04-22 07:46:16 -04:00
Merge branch 'master' into master
This commit is contained in:
commit
f6c5a04088
21 changed files with 1586 additions and 20 deletions
|
@ -168,6 +168,8 @@
|
|||
"Hex to PEM",
|
||||
"Hex to Object Identifier",
|
||||
"Object Identifier to Hex",
|
||||
"PEM to JWK",
|
||||
"JWK to PEM",
|
||||
"Generate PGP Key Pair",
|
||||
"PGP Encrypt",
|
||||
"PGP Decrypt",
|
||||
|
@ -180,7 +182,9 @@
|
|||
"RSA Encrypt",
|
||||
"RSA Decrypt",
|
||||
"Parse SSH Host Key",
|
||||
"Parse CSR"
|
||||
"Parse CSR",
|
||||
"Public Key from Certificate",
|
||||
"Public Key from Private Key"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -238,6 +242,7 @@
|
|||
"JA3 Fingerprint",
|
||||
"JA3S Fingerprint",
|
||||
"JA4 Fingerprint",
|
||||
"JA4Server Fingerprint",
|
||||
"HASSH Client Fingerprint",
|
||||
"HASSH Server Fingerprint",
|
||||
"Format MAC addresses",
|
||||
|
|
|
@ -25,6 +25,9 @@ export function toJA4(bytes) {
|
|||
let tlsr = {};
|
||||
try {
|
||||
tlsr = parseTLSRecord(bytes);
|
||||
if (tlsr.handshake.value.handshakeType.value !== 0x01) {
|
||||
throw new Error();
|
||||
}
|
||||
} catch (err) {
|
||||
throw new OperationError("Data is not a valid TLS Client Hello. QUIC is not yet supported.\n" + err);
|
||||
}
|
||||
|
@ -48,16 +51,7 @@ export function toJA4(bytes) {
|
|||
break;
|
||||
}
|
||||
}
|
||||
switch (version) {
|
||||
case 0x0304: version = "13"; break; // TLS 1.3
|
||||
case 0x0303: version = "12"; break; // TLS 1.2
|
||||
case 0x0302: version = "11"; break; // TLS 1.1
|
||||
case 0x0301: version = "10"; break; // TLS 1.0
|
||||
case 0x0300: version = "s3"; break; // SSL 3.0
|
||||
case 0x0200: version = "s2"; break; // SSL 2.0
|
||||
case 0x0100: version = "s1"; break; // SSL 1.0
|
||||
default: version = "00"; // Unknown
|
||||
}
|
||||
version = tlsVersionMapper(version);
|
||||
|
||||
/* SNI
|
||||
If the SNI extension (0x0000) exists, then the destination of the connection is a domain, or “d” in the fingerprint.
|
||||
|
@ -99,6 +93,7 @@ export function toJA4(bytes) {
|
|||
if (ext.type.value === "application_layer_protocol_negotiation") {
|
||||
alpn = parseFirstALPNValue(ext.value.data);
|
||||
alpn = alpn.charAt(0) + alpn.charAt(alpn.length - 1);
|
||||
if (alpn.charCodeAt(0) > 127) alpn = "99";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -164,3 +159,106 @@ export function toJA4(bytes) {
|
|||
"JA4_ro": `${ptype}${version}${sni}${cipherLen}${extLen}${alpn}_${originalCiphersRaw}_${originalExtensionsRaw}`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Calculate the JA4Server from a given TLS Server Hello Stream
|
||||
* @param {Uint8Array} bytes
|
||||
* @returns {string}
|
||||
*/
|
||||
export function toJA4S(bytes) {
|
||||
let tlsr = {};
|
||||
try {
|
||||
tlsr = parseTLSRecord(bytes);
|
||||
if (tlsr.handshake.value.handshakeType.value !== 0x02) {
|
||||
throw new Error();
|
||||
}
|
||||
} catch (err) {
|
||||
throw new OperationError("Data is not a valid TLS Server Hello. QUIC is not yet supported.\n" + err);
|
||||
}
|
||||
|
||||
/* QUIC
|
||||
“q” or “t”, which denotes whether the hello packet is for QUIC or TCP.
|
||||
TODO: Implement QUIC
|
||||
*/
|
||||
const ptype = "t";
|
||||
|
||||
/* TLS Version
|
||||
TLS version is shown in 3 different places. If extension 0x002b exists (supported_versions), then the version
|
||||
is the highest value in the extension. Remember to ignore GREASE values. If the extension doesn’t exist, then
|
||||
the TLS version is the value of the Protocol Version. Handshake version (located at the top of the packet)
|
||||
should be ignored.
|
||||
*/
|
||||
let version = tlsr.version.value;
|
||||
for (const ext of tlsr.handshake.value.extensions.value) {
|
||||
if (ext.type.value === "supported_versions") {
|
||||
version = parseHighestSupportedVersion(ext.value.data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
version = tlsVersionMapper(version);
|
||||
|
||||
/* Number of Extensions
|
||||
2 character number of cipher suites, so if there’s 6 cipher suites in the hello packet, then the value should be “06”.
|
||||
If there’s > 99, which there should never be, then output “99”.
|
||||
*/
|
||||
let extLen = tlsr.handshake.value.extensions.value.length;
|
||||
extLen = extLen > 99 ? "99" : extLen.toString().padStart(2, "0");
|
||||
|
||||
/* ALPN Extension Chosen Value
|
||||
The first and last characters of the ALPN (Application-Layer Protocol Negotiation) first value.
|
||||
If there are no ALPN values or no ALPN extension then we print “00” as the value in the fingerprint.
|
||||
*/
|
||||
let alpn = "00";
|
||||
for (const ext of tlsr.handshake.value.extensions.value) {
|
||||
if (ext.type.value === "application_layer_protocol_negotiation") {
|
||||
alpn = parseFirstALPNValue(ext.value.data);
|
||||
alpn = alpn.charAt(0) + alpn.charAt(alpn.length - 1);
|
||||
if (alpn.charCodeAt(0) > 127) alpn = "99";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Chosen Cipher
|
||||
The hex value of the chosen cipher suite
|
||||
*/
|
||||
const cipher = toHexFast(tlsr.handshake.value.cipherSuite.data);
|
||||
|
||||
/* Extension hash
|
||||
A 12 character truncated sha256 hash of the list of extensions.
|
||||
The extension list is created using the 4 character hex values of the extensions, lower case, comma delimited.
|
||||
*/
|
||||
const extensionsList = [];
|
||||
for (const ext of tlsr.handshake.value.extensions.value) {
|
||||
extensionsList.push(toHexFast(ext.type.data));
|
||||
}
|
||||
const extensionsRaw = extensionsList.join(",");
|
||||
const extensionsHash = runHash(
|
||||
"sha256",
|
||||
Utils.strToArrayBuffer(extensionsRaw)
|
||||
).substring(0, 12);
|
||||
|
||||
return {
|
||||
"JA4S": `${ptype}${version}${extLen}${alpn}_${cipher}_${extensionsHash}`,
|
||||
"JA4S_r": `${ptype}${version}${extLen}${alpn}_${cipher}_${extensionsRaw}`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Takes a TLS version value and returns a JA4 TLS version string
|
||||
* @param {Uint8Array} version - Two byte array of version number
|
||||
* @returns {string}
|
||||
*/
|
||||
function tlsVersionMapper(version) {
|
||||
switch (version) {
|
||||
case 0x0304: return "13"; // TLS 1.3
|
||||
case 0x0303: return "12"; // TLS 1.2
|
||||
case 0x0302: return "11"; // TLS 1.1
|
||||
case 0x0301: return "10"; // TLS 1.0
|
||||
case 0x0300: return "s3"; // SSL 3.0
|
||||
case 0x0200: return "s2"; // SSL 2.0
|
||||
case 0x0100: return "s1"; // SSL 1.0
|
||||
default: return "00"; // Unknown
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,13 +70,11 @@ function parseHandshake(bytes) {
|
|||
|
||||
// Handshake type
|
||||
h.handshakeType = {
|
||||
description: "Client Hello",
|
||||
description: "Handshake Type",
|
||||
length: 1,
|
||||
data: b.getBytes(1),
|
||||
value: s.readInt(1)
|
||||
};
|
||||
if (h.handshakeType.value !== 0x01)
|
||||
throw new OperationError("Not a Client Hello.");
|
||||
|
||||
// Handshake length
|
||||
h.handshakeLength = {
|
||||
|
@ -86,8 +84,33 @@ function parseHandshake(bytes) {
|
|||
value: s.readInt(3)
|
||||
};
|
||||
if (s.length !== h.handshakeLength.value + 4)
|
||||
throw new OperationError("Not enough data in Client Hello.");
|
||||
throw new OperationError("Not enough data in Handshake message.");
|
||||
|
||||
|
||||
switch (h.handshakeType.value) {
|
||||
case 0x01:
|
||||
h.handshakeType.description = "Client Hello";
|
||||
parseClientHello(s, b, h);
|
||||
break;
|
||||
case 0x02:
|
||||
h.handshakeType.description = "Server Hello";
|
||||
parseServerHello(s, b, h);
|
||||
break;
|
||||
default:
|
||||
throw new OperationError("Not a known handshake message.");
|
||||
}
|
||||
|
||||
return h;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a TLS Client Hello
|
||||
* @param {Stream} s
|
||||
* @param {Stream} b
|
||||
* @param {Object} h
|
||||
* @returns {JSON}
|
||||
*/
|
||||
function parseClientHello(s, b, h) {
|
||||
// Hello version
|
||||
h.helloVersion = {
|
||||
description: "Client Hello Version",
|
||||
|
@ -171,6 +194,79 @@ function parseHandshake(bytes) {
|
|||
return h;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a TLS Server Hello
|
||||
* @param {Stream} s
|
||||
* @param {Stream} b
|
||||
* @param {Object} h
|
||||
* @returns {JSON}
|
||||
*/
|
||||
function parseServerHello(s, b, h) {
|
||||
// Hello version
|
||||
h.helloVersion = {
|
||||
description: "Server Hello Version",
|
||||
length: 2,
|
||||
data: b.getBytes(2),
|
||||
value: s.readInt(2)
|
||||
};
|
||||
|
||||
// Random
|
||||
h.random = {
|
||||
description: "Server Random",
|
||||
length: 32,
|
||||
data: b.getBytes(32),
|
||||
value: s.getBytes(32)
|
||||
};
|
||||
|
||||
// Session ID Length
|
||||
h.sessionIDLength = {
|
||||
description: "Session ID Length",
|
||||
length: 1,
|
||||
data: b.getBytes(1),
|
||||
value: s.readInt(1)
|
||||
};
|
||||
|
||||
// Session ID
|
||||
h.sessionID = {
|
||||
description: "Session ID",
|
||||
length: h.sessionIDLength.value,
|
||||
data: b.getBytes(h.sessionIDLength.value),
|
||||
value: s.getBytes(h.sessionIDLength.value)
|
||||
};
|
||||
|
||||
// Cipher Suite
|
||||
h.cipherSuite = {
|
||||
description: "Selected Cipher Suite",
|
||||
length: 2,
|
||||
data: b.getBytes(2),
|
||||
value: CIPHER_SUITES_LOOKUP[s.readInt(2)] || "Unknown"
|
||||
};
|
||||
|
||||
// Compression Method
|
||||
h.compressionMethod = {
|
||||
description: "Selected Compression Method",
|
||||
length: 1,
|
||||
data: b.getBytes(1),
|
||||
value: s.readInt(1) // TODO: Compression method name here
|
||||
};
|
||||
|
||||
// Extensions Length
|
||||
h.extensionsLength = {
|
||||
description: "Extensions Length",
|
||||
length: 2,
|
||||
data: b.getBytes(2),
|
||||
value: s.readInt(2)
|
||||
};
|
||||
|
||||
// Extensions
|
||||
h.extensions = {
|
||||
description: "Extensions",
|
||||
length: h.extensionsLength.value,
|
||||
data: b.getBytes(h.extensionsLength.value),
|
||||
value: parseExtensions(s.getBytes(h.extensionsLength.value))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Cipher Suites
|
||||
* @param {Uint8Array} bytes
|
||||
|
@ -748,6 +844,11 @@ export const GREASE_VALUES = [
|
|||
export function parseHighestSupportedVersion(bytes) {
|
||||
const s = new Stream(bytes);
|
||||
|
||||
// The Server Hello supported_versions extension simply contains the chosen version
|
||||
if (s.length === 2) {
|
||||
return s.readInt(2);
|
||||
}
|
||||
|
||||
// Length
|
||||
let i = s.readInt(1);
|
||||
|
||||
|
|
66
src/core/operations/JA4ServerFingerprint.mjs
Normal file
66
src/core/operations/JA4ServerFingerprint.mjs
Normal file
|
@ -0,0 +1,66 @@
|
|||
/**
|
||||
* @author n1474335 [n1474335@gmail.com]
|
||||
* @copyright Crown Copyright 2024
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation.mjs";
|
||||
import Utils from "../Utils.mjs";
|
||||
import {toJA4S} from "../lib/JA4.mjs";
|
||||
|
||||
/**
|
||||
* JA4Server Fingerprint operation
|
||||
*/
|
||||
class JA4ServerFingerprint extends Operation {
|
||||
|
||||
/**
|
||||
* JA4ServerFingerprint constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "JA4Server Fingerprint";
|
||||
this.module = "Crypto";
|
||||
this.description = "Generates a JA4Server Fingerprint (JA4S) to help identify TLS servers or sessions based on hashing together values from the Server Hello.<br><br>Input: A hex stream of the TLS or QUIC Server Hello packet application layer.";
|
||||
this.infoURL = "https://medium.com/foxio/ja4-network-fingerprinting-9376fe9ca637";
|
||||
this.inputType = "string";
|
||||
this.outputType = "string";
|
||||
this.args = [
|
||||
{
|
||||
name: "Input format",
|
||||
type: "option",
|
||||
value: ["Hex", "Base64", "Raw"]
|
||||
},
|
||||
{
|
||||
name: "Output format",
|
||||
type: "option",
|
||||
value: ["JA4S", "JA4S Raw", "Both"]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
* @param {Object[]} args
|
||||
* @returns {string}
|
||||
*/
|
||||
run(input, args) {
|
||||
const [inputFormat, outputFormat] = args;
|
||||
input = Utils.convertToByteArray(input, inputFormat);
|
||||
const ja4s = toJA4S(new Uint8Array(input));
|
||||
|
||||
// Output
|
||||
switch (outputFormat) {
|
||||
case "JA4S":
|
||||
return ja4s.JA4S;
|
||||
case "JA4S Raw":
|
||||
return ja4s.JA4S_r;
|
||||
case "Both":
|
||||
default:
|
||||
return `JA4S: ${ja4s.JA4S}\nJA4S_r: ${ja4s.JA4S_r}`;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default JA4ServerFingerprint;
|
80
src/core/operations/JWKToPem.mjs
Normal file
80
src/core/operations/JWKToPem.mjs
Normal file
|
@ -0,0 +1,80 @@
|
|||
/**
|
||||
* @author cplussharp
|
||||
* @copyright Crown Copyright 2021
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import r from "jsrsasign";
|
||||
import Operation from "../Operation.mjs";
|
||||
import OperationError from "../errors/OperationError.mjs";
|
||||
|
||||
/**
|
||||
* PEM to JWK operation
|
||||
*/
|
||||
class PEMToJWK extends Operation {
|
||||
|
||||
/**
|
||||
* PEMToJWK constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "JWK to PEM";
|
||||
this.module = "PublicKey";
|
||||
this.description = "Converts Keys in JSON Web Key format to PEM format (PKCS#8).";
|
||||
this.infoURL = "https://datatracker.ietf.org/doc/html/rfc7517";
|
||||
this.inputType = "string";
|
||||
this.outputType = "string";
|
||||
this.args = [];
|
||||
this.checks = [
|
||||
{
|
||||
"pattern": "\"kty\":\\s*\"(EC|RSA)\"",
|
||||
"flags": "gm",
|
||||
"args": []
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
* @param {Object[]} args
|
||||
* @returns {string}
|
||||
*/
|
||||
run(input, args) {
|
||||
const inputJson = JSON.parse(input);
|
||||
|
||||
let keys = [];
|
||||
if (Array.isArray(inputJson)) {
|
||||
// list of keys => transform all keys
|
||||
keys = inputJson;
|
||||
} else if (Array.isArray(inputJson.keys)) {
|
||||
// JSON Web Key Set => transform all keys
|
||||
keys = inputJson.keys;
|
||||
} else if (typeof inputJson === "object") {
|
||||
// single key
|
||||
keys.push(inputJson);
|
||||
} else {
|
||||
throw new OperationError("Input is not a JSON Web Key");
|
||||
}
|
||||
|
||||
let output = "";
|
||||
for (let i=0; i<keys.length; i++) {
|
||||
const jwk = keys[i];
|
||||
if (typeof jwk.kty !== "string") {
|
||||
throw new OperationError("Invalid JWK format");
|
||||
} else if ("|RSA|EC|".indexOf(jwk.kty) === -1) {
|
||||
throw new OperationError(`Unsupported JWK key type '${inputJson.kty}'`);
|
||||
}
|
||||
|
||||
const key = r.KEYUTIL.getKey(jwk);
|
||||
const pem = key.isPrivate ? r.KEYUTIL.getPEM(key, "PKCS8PRV") : r.KEYUTIL.getPEM(key);
|
||||
|
||||
// PEM ends with '\n', so a new key always starts on a new line
|
||||
output += pem;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
export default PEMToJWK;
|
88
src/core/operations/PEMToJWK.mjs
Normal file
88
src/core/operations/PEMToJWK.mjs
Normal file
|
@ -0,0 +1,88 @@
|
|||
/**
|
||||
* @author cplussharp
|
||||
* @copyright Crown Copyright 2021
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import r from "jsrsasign";
|
||||
import Operation from "../Operation.mjs";
|
||||
import OperationError from "../errors/OperationError.mjs";
|
||||
|
||||
/**
|
||||
* PEM to JWK operation
|
||||
*/
|
||||
class PEMToJWK extends Operation {
|
||||
|
||||
/**
|
||||
* PEMToJWK constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "PEM to JWK";
|
||||
this.module = "PublicKey";
|
||||
this.description = "Converts Keys in PEM format to a JSON Web Key format.";
|
||||
this.infoURL = "https://datatracker.ietf.org/doc/html/rfc7517";
|
||||
this.inputType = "string";
|
||||
this.outputType = "string";
|
||||
this.args = [];
|
||||
this.checks = [
|
||||
{
|
||||
"pattern": "-----BEGIN ((RSA |EC )?(PRIVATE|PUBLIC) KEY|CERTIFICATE)-----",
|
||||
"args": []
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
* @param {Object[]} args
|
||||
* @returns {string}
|
||||
*/
|
||||
run(input, args) {
|
||||
let output = "";
|
||||
let match;
|
||||
const regex = /-----BEGIN ([A-Z][A-Z ]+[A-Z])-----/g;
|
||||
while ((match = regex.exec(input)) !== null) {
|
||||
// find corresponding end tag
|
||||
const indexBase64 = match.index + match[0].length;
|
||||
const header = input.substring(match.index, indexBase64);
|
||||
const footer = `-----END ${match[1]}-----`;
|
||||
const indexFooter = input.indexOf(footer, indexBase64);
|
||||
if (indexFooter === -1) {
|
||||
throw new OperationError(`PEM footer '${footer}' not found`);
|
||||
}
|
||||
|
||||
const pem = input.substring(match.index, indexFooter + footer.length);
|
||||
if (match[1].indexOf("KEY") !== -1) {
|
||||
if (header === "-----BEGIN RSA PUBLIC KEY-----") {
|
||||
throw new OperationError("Unsupported RSA public key format. Only PKCS#8 is supported.");
|
||||
}
|
||||
|
||||
const key = r.KEYUTIL.getKey(pem);
|
||||
if (key.type === "DSA") {
|
||||
throw new OperationError("DSA keys are not supported for JWK");
|
||||
}
|
||||
const jwk = r.KEYUTIL.getJWKFromKey(key);
|
||||
if (output.length > 0) {
|
||||
output += "\n";
|
||||
}
|
||||
output += JSON.stringify(jwk);
|
||||
} else if (match[1] === "CERTIFICATE") {
|
||||
const cert = new r.X509();
|
||||
cert.readCertPEM(pem);
|
||||
const key = cert.getPublicKey();
|
||||
const jwk = r.KEYUTIL.getJWKFromKey(key);
|
||||
if (output.length > 0) {
|
||||
output += "\n";
|
||||
}
|
||||
output += JSON.stringify(jwk);
|
||||
} else {
|
||||
throw new OperationError(`Unsupported PEM type '${match[1]}'`);
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
export default PEMToJWK;
|
68
src/core/operations/PubKeyFromCert.mjs
Normal file
68
src/core/operations/PubKeyFromCert.mjs
Normal file
|
@ -0,0 +1,68 @@
|
|||
/**
|
||||
* @author cplussharp
|
||||
* @copyright Crown Copyright 2023
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import r from "jsrsasign";
|
||||
import Operation from "../Operation.mjs";
|
||||
import OperationError from "../errors/OperationError.mjs";
|
||||
|
||||
/**
|
||||
* Public Key from Certificate operation
|
||||
*/
|
||||
class PubKeyFromCert extends Operation {
|
||||
|
||||
/**
|
||||
* PubKeyFromCert constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Public Key from Certificate";
|
||||
this.module = "PublicKey";
|
||||
this.description = "Extracts the Public Key from a Certificate.";
|
||||
this.infoURL = "https://en.wikipedia.org/wiki/X.509";
|
||||
this.inputType = "string";
|
||||
this.outputType = "string";
|
||||
this.args = [];
|
||||
this.checks = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
* @param {Object[]} args
|
||||
* @returns {string}
|
||||
*/
|
||||
run(input, args) {
|
||||
let output = "";
|
||||
let match;
|
||||
const regex = /-----BEGIN CERTIFICATE-----/g;
|
||||
while ((match = regex.exec(input)) !== null) {
|
||||
// find corresponding end tag
|
||||
const indexBase64 = match.index + match[0].length;
|
||||
const footer = "-----END CERTIFICATE-----";
|
||||
const indexFooter = input.indexOf(footer, indexBase64);
|
||||
if (indexFooter === -1) {
|
||||
throw new OperationError(`PEM footer '${footer}' not found`);
|
||||
}
|
||||
|
||||
const certPem = input.substring(match.index, indexFooter + footer.length);
|
||||
const cert = new r.X509();
|
||||
cert.readCertPEM(certPem);
|
||||
let pubKey;
|
||||
try {
|
||||
pubKey = cert.getPublicKey();
|
||||
} catch {
|
||||
throw new OperationError("Unsupported public key type");
|
||||
}
|
||||
const pubKeyPem = r.KEYUTIL.getPEM(pubKey);
|
||||
|
||||
// PEM ends with '\n', so a new key always starts on a new line
|
||||
output += pubKeyPem;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
export default PubKeyFromCert;
|
82
src/core/operations/PubKeyFromPrivKey.mjs
Normal file
82
src/core/operations/PubKeyFromPrivKey.mjs
Normal file
|
@ -0,0 +1,82 @@
|
|||
/**
|
||||
* @author cplussharp
|
||||
* @copyright Crown Copyright 2023
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import r from "jsrsasign";
|
||||
import Operation from "../Operation.mjs";
|
||||
import OperationError from "../errors/OperationError.mjs";
|
||||
|
||||
/**
|
||||
* Public Key from Private Key operation
|
||||
*/
|
||||
class PubKeyFromPrivKey extends Operation {
|
||||
|
||||
/**
|
||||
* PubKeyFromPrivKey constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Public Key from Private Key";
|
||||
this.module = "PublicKey";
|
||||
this.description = "Extracts the Public Key from a Private Key.";
|
||||
this.infoURL = "https://en.wikipedia.org/wiki/PKCS_8";
|
||||
this.inputType = "string";
|
||||
this.outputType = "string";
|
||||
this.args = [];
|
||||
this.checks = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
* @param {Object[]} args
|
||||
* @returns {string}
|
||||
*/
|
||||
run(input, args) {
|
||||
let output = "";
|
||||
let match;
|
||||
const regex = /-----BEGIN ((RSA |EC |DSA )?PRIVATE KEY)-----/g;
|
||||
while ((match = regex.exec(input)) !== null) {
|
||||
// find corresponding end tag
|
||||
const indexBase64 = match.index + match[0].length;
|
||||
const footer = `-----END ${match[1]}-----`;
|
||||
const indexFooter = input.indexOf(footer, indexBase64);
|
||||
if (indexFooter === -1) {
|
||||
throw new OperationError(`PEM footer '${footer}' not found`);
|
||||
}
|
||||
|
||||
const privKeyPem = input.substring(match.index, indexFooter + footer.length);
|
||||
let privKey;
|
||||
try {
|
||||
privKey = r.KEYUTIL.getKey(privKeyPem);
|
||||
} catch (err) {
|
||||
throw new OperationError(`Unsupported key type: ${err}`);
|
||||
}
|
||||
let pubKey;
|
||||
if (privKey.type && privKey.type === "EC") {
|
||||
pubKey = new r.KJUR.crypto.ECDSA({ curve: privKey.curve });
|
||||
pubKey.setPublicKeyHex(privKey.generatePublicKeyHex());
|
||||
} else if (privKey.type && privKey.type === "DSA") {
|
||||
if (!privKey.y) {
|
||||
throw new OperationError(`DSA Private Key in PKCS#8 is not supported`);
|
||||
}
|
||||
pubKey = new r.KJUR.crypto.DSA();
|
||||
pubKey.setPublic(privKey.p, privKey.q, privKey.g, privKey.y);
|
||||
} else if (privKey.n && privKey.e) {
|
||||
pubKey = new r.RSAKey();
|
||||
pubKey.setPublic(privKey.n, privKey.e);
|
||||
} else {
|
||||
throw new OperationError(`Unsupported key type`);
|
||||
}
|
||||
const pubKeyPem = r.KEYUTIL.getPEM(pubKey);
|
||||
|
||||
// PEM ends with '\n', so a new key always starts on a new line
|
||||
output += pubKeyPem;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
export default PubKeyFromPrivKey;
|
|
@ -85,6 +85,7 @@ class HTMLOperation {
|
|||
<div class="recip-icons">
|
||||
<i class="material-icons breakpoint" title="Set breakpoint" break="false" data-help-title="Setting breakpoints" data-help="Setting a breakpoint on an operation will cause execution of the Recipe to pause when it reaches that operation.">pause</i>
|
||||
<i class="material-icons disable-icon" title="Disable operation" disabled="false" data-help-title="Disabling operations" data-help="Disabling an operation will prevent it from being executed when the Recipe is baked. Execution will skip over the disabled operation and continue with subsequent operations.">not_interested</i>
|
||||
<i class="material-icons hide-args-icon" title="Hide operation's arguments" hide-args="false" data-help-title="Hide operation's arguments" data-help="Hiding an operation's argument will save space in the Recipe window. Execution will still take place with the selected argument options.">keyboard_arrow_up</i>
|
||||
</div>
|
||||
<div class="clearfix"> </div>`;
|
||||
|
||||
|
|
|
@ -139,6 +139,7 @@ class Manager {
|
|||
document.getElementById("load-delete-button").addEventListener("click", this.controls.loadDeleteClick.bind(this.controls));
|
||||
document.getElementById("load-name").addEventListener("change", this.controls.loadNameChange.bind(this.controls));
|
||||
document.getElementById("load-button").addEventListener("click", this.controls.loadButtonClick.bind(this.controls));
|
||||
document.getElementById("hide-icon").addEventListener("click", this.controls.hideRecipeArgsClick.bind(this.recipe));
|
||||
document.getElementById("support").addEventListener("click", this.controls.supportButtonClick.bind(this.controls));
|
||||
this.addMultiEventListeners("#save-texts textarea", "keyup paste", this.controls.saveTextChange, this.controls);
|
||||
|
||||
|
@ -154,6 +155,7 @@ class Manager {
|
|||
// Recipe
|
||||
this.addDynamicListener(".arg:not(select)", "input", this.recipe.ingChange, this.recipe);
|
||||
this.addDynamicListener(".arg[type=checkbox], .arg[type=radio], select.arg", "change", this.recipe.ingChange, this.recipe);
|
||||
this.addDynamicListener(".hide-args-icon", "click", this.recipe.hideArgsClick, this.recipe);
|
||||
this.addDynamicListener(".disable-icon", "click", this.recipe.disableClick, this.recipe);
|
||||
this.addDynamicListener(".breakpoint", "click", this.recipe.breakpointClick, this.recipe);
|
||||
this.addDynamicListener("#rec-list li.operation", "dblclick", this.recipe.operationDblclick, this.recipe);
|
||||
|
|
|
@ -181,6 +181,9 @@
|
|||
<div class="title no-select">
|
||||
Recipe
|
||||
<span class="pane-controls hide-on-maximised-output">
|
||||
<button type="button" aria-label="Hide arguments" class="btn btn-primary bmd-btn-icon" id="hide-icon" data-toggle="tooltip" title="Hide arguments" hide-args="false" data-help-title="Hiding every Operation's argument view in a Recipe" data-help="Clicking 'Hide arguments' will hide all the argument views for every Operation in the Recipe, to save space when you have too many Operation in your Recipe">
|
||||
<i class="material-icons">keyboard_arrow_up</i>
|
||||
</button>
|
||||
<button type="button" aria-label="Save recipe" class="btn btn-primary bmd-btn-icon" id="save" data-toggle="tooltip" title="Save recipe" data-help-title="Saving a recipe" data-help="<p>Recipes can be represented in a few different formats and saved for use at a later date. You can either copy the Recipe configuration and save it somewhere offline for later use, or use your browser's local storage.</p><ul><li><b>Deep link:</b> The easiest way to share a CyberChef Recipe is to copy the deep link, either from the address bar (which is updated as the Recipe or Input changes), or from the 'Save recipe' pane. When you visit this link, the Recipe and Input should be populated from where you left off.</li><li><b>Chef format:</b> This custom format is designed to be compact and easily readable. It is the format used in CyberChef's URL, so it largely uses characters that do not have to be escaped in URL encoding, making it a little easier to understand what a CyberChef URL contains.</li><li><b>Clean JSON:</b> This JSON format uses whitespace and indentation in a way that makes the Recipe easy to read.</li><li><b>Compact JSON:</b> This is the most compact way that the Recipe can be represented in JSON.</li><li><b>Local storage:</b> Alternatively, you can enter a name into the 'Recipe name' field and save to your browser's local storage. The Recipe will then be available to load from the 'Load Recipe' pane as long as you are using the same browser profile. Be aware that if your browser profile is cleaned, you may lose this data.</li></ul>">
|
||||
<i class="material-icons" aria-hidden="true">save</i>
|
||||
</button>
|
||||
|
|
|
@ -345,6 +345,36 @@ class ControlsWaiter {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Hides the arguments for all the operations in the current recipe.
|
||||
*/
|
||||
hideRecipeArgsClick() {
|
||||
const icon = document.getElementById("hide-icon");
|
||||
|
||||
if (icon.getAttribute("hide-args") === "false") {
|
||||
icon.setAttribute("hide-args", "true");
|
||||
icon.setAttribute("data-original-title", "Show arguments");
|
||||
icon.children[0].innerText = "keyboard_arrow_down";
|
||||
Array.from(document.getElementsByClassName("hide-args-icon")).forEach(function(item) {
|
||||
item.setAttribute("hide-args", "true");
|
||||
item.innerText = "keyboard_arrow_down";
|
||||
item.classList.add("hide-args-selected");
|
||||
item.parentNode.previousElementSibling.style.display = "none";
|
||||
});
|
||||
} else {
|
||||
icon.setAttribute("hide-args", "false");
|
||||
icon.setAttribute("data-original-title", "Hide arguments");
|
||||
icon.children[0].innerText = "keyboard_arrow_up";
|
||||
Array.from(document.getElementsByClassName("hide-args-icon")).forEach(function(item) {
|
||||
item.setAttribute("hide-args", "false");
|
||||
item.innerText = "keyboard_arrow_up";
|
||||
item.classList.remove("hide-args-selected");
|
||||
item.parentNode.previousElementSibling.style.display = "grid";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Populates the bug report information box with useful technical info.
|
||||
*
|
||||
|
|
|
@ -215,6 +215,45 @@ class RecipeWaiter {
|
|||
window.dispatchEvent(this.manager.statechange);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for hide-args click events.
|
||||
* Updates the icon status.
|
||||
*
|
||||
* @fires Manager#statechange
|
||||
* @param {event} e
|
||||
*/
|
||||
hideArgsClick(e) {
|
||||
const icon = e.target;
|
||||
|
||||
if (icon.getAttribute("hide-args") === "false") {
|
||||
icon.setAttribute("hide-args", "true");
|
||||
icon.innerText = "keyboard_arrow_down";
|
||||
icon.classList.add("hide-args-selected");
|
||||
icon.parentNode.previousElementSibling.style.display = "none";
|
||||
} else {
|
||||
icon.setAttribute("hide-args", "false");
|
||||
icon.innerText = "keyboard_arrow_up";
|
||||
icon.classList.remove("hide-args-selected");
|
||||
icon.parentNode.previousElementSibling.style.display = "grid";
|
||||
}
|
||||
|
||||
const icons = Array.from(document.getElementsByClassName("hide-args-icon"));
|
||||
if (icons.length > 1) {
|
||||
// Check if ALL the icons are hidden/shown
|
||||
const uniqueIcons = icons.map(function(item) {
|
||||
return item.getAttribute("hide-args");
|
||||
}).unique();
|
||||
|
||||
const controlsIconStatus = document.getElementById("hide-icon").getAttribute("hide-args");
|
||||
|
||||
// If all icons are in the same state and the global icon isn't, fix it
|
||||
if (uniqueIcons.length === 1 && icon.getAttribute("hide-args") !== controlsIconStatus) {
|
||||
this.manager.controls.hideRecipeArgsClick();
|
||||
}
|
||||
}
|
||||
|
||||
window.dispatchEvent(this.manager.statechange);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for disable click events.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue