From 9068b6c17afef72a3c19655dc5400028f13ce30d Mon Sep 17 00:00:00 2001 From: Joost Rijneveld Date: Sun, 10 Mar 2024 16:42:33 +0100 Subject: [PATCH 01/22] Add Salsa20 and XSalsa20 operation --- src/core/config/Categories.json | 2 + src/core/lib/Salsa20.mjs | 144 +++++++++++++++++++++++++ src/core/operations/Salsa20.mjs | 154 +++++++++++++++++++++++++++ src/core/operations/XSalsa20.mjs | 156 ++++++++++++++++++++++++++++ tests/operations/index.mjs | 2 + tests/operations/tests/Salsa20.mjs | 76 ++++++++++++++ tests/operations/tests/XSalsa20.mjs | 61 +++++++++++ 7 files changed, 595 insertions(+) create mode 100644 src/core/lib/Salsa20.mjs create mode 100644 src/core/operations/Salsa20.mjs create mode 100644 src/core/operations/XSalsa20.mjs create mode 100644 tests/operations/tests/Salsa20.mjs create mode 100644 tests/operations/tests/XSalsa20.mjs diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 8e300c76..fd34d5ef 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -95,6 +95,8 @@ "RC4", "RC4 Drop", "ChaCha", + "Salsa20", + "XSalsa20", "Rabbit", "SM4 Encrypt", "SM4 Decrypt", diff --git a/src/core/lib/Salsa20.mjs b/src/core/lib/Salsa20.mjs new file mode 100644 index 00000000..d72831bf --- /dev/null +++ b/src/core/lib/Salsa20.mjs @@ -0,0 +1,144 @@ +/** + * @author joostrijneveld [joost@joostrijneveld.nl] + * @copyright Crown Copyright 2024 + * @license Apache-2.0 + */ + +import Utils from "../Utils.mjs"; + +/** + * Computes the Salsa20 permute function + * + * @param {byteArray} x + * @param {integer} rounds + */ +function salsa20Permute(x, rounds) { + /** + * Macro to compute a 32-bit rotate-left operation + * + * @param {integer} x + * @param {integer} n + * @returns {integer} + */ + function ROL32(x, n) { + return ((x << n) & 0xFFFFFFFF) | (x >>> (32 - n)); + } + + /** + * Macro to compute a single Salsa20 quarterround operation + * + * @param {integer} x + * @param {integer} a + * @param {integer} b + * @param {integer} c + * @param {integer} d + * @returns {integer} + */ + function quarterround(x, a, b, c, d) { + x[b] ^= ROL32((x[a] + x[d]) & 0xFFFFFFFF, 7); + x[c] ^= ROL32((x[b] + x[a]) & 0xFFFFFFFF, 9); + x[d] ^= ROL32((x[c] + x[b]) & 0xFFFFFFFF, 13); + x[a] ^= ROL32((x[d] + x[c]) & 0xFFFFFFFF, 18); + } + + for (let i = 0; i < rounds / 2; i++) { + quarterround(x, 0, 4, 8, 12); + quarterround(x, 5, 9, 13, 1); + quarterround(x, 10, 14, 2, 6); + quarterround(x, 15, 3, 7, 11); + quarterround(x, 0, 1, 2, 3); + quarterround(x, 5, 6, 7, 4); + quarterround(x, 10, 11, 8, 9); + quarterround(x, 15, 12, 13, 14); + } +} + +/** + * Computes the Salsa20 block function + * + * @param {byteArray} key + * @param {byteArray} nonce + * @param {byteArray} counter + * @param {integer} rounds + * @returns {byteArray} + */ +export function salsa20Block(key, nonce, counter, rounds) { + const tau = "expand 16-byte k"; + const sigma = "expand 32-byte k"; + let state, c; + if (key.length === 16) { + c = Utils.strToByteArray(tau); + key = key.concat(key); + } else { + c = Utils.strToByteArray(sigma); + } + + state = c.slice(0, 4); + state = state.concat(key.slice(0, 16)); + state = state.concat(c.slice(4, 8)); + state = state.concat(nonce); + state = state.concat(counter); + state = state.concat(c.slice(8, 12)); + state = state.concat(key.slice(16, 32)); + state = state.concat(c.slice(12, 16)); + + const x = Array(); + for (let i = 0; i < 64; i += 4) { + x.push(Utils.byteArrayToInt(state.slice(i, i + 4), "little")); + } + const a = [...x]; + + salsa20Permute(x, rounds); + + for (let i = 0; i < 16; i++) { + x[i] = (x[i] + a[i]) & 0xFFFFFFFF; + } + + let output = Array(); + for (let i = 0; i < 16; i++) { + output = output.concat(Utils.intToByteArray(x[i], 4, "little")); + } + return output; +} + +/** + * Computes the hSalsa20 function + * + * @param {byteArray} key + * @param {byteArray} nonce + * @param {integer} rounds + * @returns {byteArray} + */ +export function hsalsa20(key, nonce, rounds) { + const tau = "expand 16-byte k"; + const sigma = "expand 32-byte k"; + let state, c; + if (key.length === 16) { + c = Utils.strToByteArray(tau); + key = key.concat(key); + } else { + c = Utils.strToByteArray(sigma); + } + + state = c.slice(0, 4); + state = state.concat(key.slice(0, 16)); + state = state.concat(c.slice(4, 8)); + state = state.concat(nonce); + state = state.concat(c.slice(8, 12)); + state = state.concat(key.slice(16, 32)); + state = state.concat(c.slice(12, 16)); + + const x = Array(); + for (let i = 0; i < 64; i += 4) { + x.push(Utils.byteArrayToInt(state.slice(i, i + 4), "little")); + } + + salsa20Permute(x, rounds); + + let output = Array(); + const idx = [0, 5, 10, 15, 6, 7, 8, 9]; + for (let i = 0; i < 8; i++) { + output = output.concat(Utils.intToByteArray(x[idx[i]], 4, "little")); + } + return output; +} diff --git a/src/core/operations/Salsa20.mjs b/src/core/operations/Salsa20.mjs new file mode 100644 index 00000000..a95dd5d3 --- /dev/null +++ b/src/core/operations/Salsa20.mjs @@ -0,0 +1,154 @@ +/** + * @author joostrijneveld [joost@joostrijneveld.nl] + * @copyright Crown Copyright 2024 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; +import Utils from "../Utils.mjs"; +import { toHex } from "../lib/Hex.mjs"; +import { salsa20Block } from "../lib/Salsa20.mjs"; + +/** + * Salsa20 operation + */ +class Salsa20 extends Operation { + + /** + * Salsa20 constructor + */ + constructor() { + super(); + + this.name = "Salsa20"; + this.module = "Default"; + this.description = "Salsa20 is a stream cipher designed by Daniel J. Bernstein and submitted to the eSTREAM project; Salsa20/8 and Salsa20/12 are round-reduced variants. It is closely related to the ChaCha stream cipher.

Key: Salsa20 uses a key of 16 or 32 bytes (128 or 256 bits).

Nonce: Salsa20 uses a nonce of 8 bytes (64 bits).

Counter: Salsa uses a counter of 8 bytes (64 bits). The counter starts at zero at the start of the keystream, and is incremented at every 64 bytes."; + this.infoURL = "https://wikipedia.org/wiki/Salsa20"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + "name": "Key", + "type": "toggleString", + "value": "", + "toggleValues": ["Hex", "UTF8", "Latin1", "Base64"] + }, + { + "name": "Nonce", + "type": "toggleString", + "value": "", + "toggleValues": ["Hex", "UTF8", "Latin1", "Base64", "Integer"] + }, + { + "name": "Counter", + "type": "number", + "value": 0, + "min": 0 + }, + { + "name": "Rounds", + "type": "option", + "value": ["20", "12", "8"] + }, + { + "name": "Input", + "type": "option", + "value": ["Hex", "Raw"] + }, + { + "name": "Output", + "type": "option", + "value": ["Raw", "Hex"] + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const key = Utils.convertToByteArray(args[0].string, args[0].option), + nonceType = args[1].option, + rounds = parseInt(args[3], 10), + inputType = args[4], + outputType = args[5]; + + if (key.length !== 16 && key.length !== 32) { + throw new OperationError(`Invalid key length: ${key.length} bytes. + +Salsa20 uses a key of 16 or 32 bytes (128 or 256 bits).`); + } + + let counter, nonce; + if (nonceType === "Integer") { + nonce = Utils.intToByteArray(parseInt(args[1].string, 10), 8, "little"); + } else { + nonce = Utils.convertToByteArray(args[1].string, args[1].option); + if (!(nonce.length === 8)) { + throw new OperationError(`Invalid nonce length: ${nonce.length} bytes. + +Salsa20 uses a nonce of 8 bytes (64 bits).`); + } + } + counter = Utils.intToByteArray(args[2], 8, "little"); + + const output = []; + input = Utils.convertToByteArray(input, inputType); + + let counterAsInt = Utils.byteArrayToInt(counter, "little"); + for (let i = 0; i < input.length; i += 64) { + counter = Utils.intToByteArray(counterAsInt, 8, "little"); + const stream = salsa20Block(key, nonce, counter, rounds); + for (let j = 0; j < 64 && i + j < input.length; j++) { + output.push(input[i + j] ^ stream[j]); + } + counterAsInt++; + } + + if (outputType === "Hex") { + return toHex(output); + } else { + return Utils.arrayBufferToStr(Uint8Array.from(output).buffer); + } + } + + /** + * Highlight Salsa20 + * + * @param {Object[]} pos + * @param {number} pos[].start + * @param {number} pos[].end + * @param {Object[]} args + * @returns {Object[]} pos + */ + highlight(pos, args) { + const inputType = args[4], + outputType = args[5]; + if (inputType === "Raw" && outputType === "Raw") { + return pos; + } + } + + /** + * Highlight Salsa20 in reverse + * + * @param {Object[]} pos + * @param {number} pos[].start + * @param {number} pos[].end + * @param {Object[]} args + * @returns {Object[]} pos + */ + highlightReverse(pos, args) { + const inputType = args[4], + outputType = args[5]; + if (inputType === "Raw" && outputType === "Raw") { + return pos; + } + } + +} + +export default Salsa20; diff --git a/src/core/operations/XSalsa20.mjs b/src/core/operations/XSalsa20.mjs new file mode 100644 index 00000000..32e8a0ba --- /dev/null +++ b/src/core/operations/XSalsa20.mjs @@ -0,0 +1,156 @@ +/** + * @author joostrijneveld [joost@joostrijneveld.nl] + * @copyright Crown Copyright 2024 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; +import Utils from "../Utils.mjs"; +import { toHex } from "../lib/Hex.mjs"; +import { salsa20Block, hsalsa20 } from "../lib/Salsa20.mjs"; + +/** + * XSalsa20 operation + */ +class XSalsa20 extends Operation { + + /** + * XSalsa20 constructor + */ + constructor() { + super(); + + this.name = "XSalsa20"; + this.module = "Default"; + this.description = "XSalsa20 is a variant of the Salsa20 stream cipher designed by Daniel J. Bernstein; XSalsa uses longer nonces.

Key: XSalsa20 uses a key of 16 or 32 bytes (128 or 256 bits).

Nonce: XSalsa20 uses a nonce of 24 bytes (192 bits).

Counter: XSalsa uses a counter of 8 bytes (64 bits). The counter starts at zero at the start of the keystream, and is incremented at every 64 bytes."; + this.infoURL = "https://en.wikipedia.org/wiki/Salsa20#XSalsa20_with_192-bit_nonce"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + "name": "Key", + "type": "toggleString", + "value": "", + "toggleValues": ["Hex", "UTF8", "Latin1", "Base64"] + }, + { + "name": "Nonce", + "type": "toggleString", + "value": "", + "toggleValues": ["Hex", "UTF8", "Latin1", "Base64", "Integer"] + }, + { + "name": "Counter", + "type": "number", + "value": 0, + "min": 0 + }, + { + "name": "Rounds", + "type": "option", + "value": ["20", "12", "8"] + }, + { + "name": "Input", + "type": "option", + "value": ["Hex", "Raw"] + }, + { + "name": "Output", + "type": "option", + "value": ["Raw", "Hex"] + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const key = Utils.convertToByteArray(args[0].string, args[0].option), + nonceType = args[1].option, + rounds = parseInt(args[3], 10), + inputType = args[4], + outputType = args[5]; + + if (key.length !== 16 && key.length !== 32) { + throw new OperationError(`Invalid key length: ${key.length} bytes. + +XSalsa20 uses a key of 16 or 32 bytes (128 or 256 bits).`); + } + + let counter, nonce; + if (nonceType === "Integer") { + nonce = Utils.intToByteArray(parseInt(args[1].string, 10), 8, "little"); + } else { + nonce = Utils.convertToByteArray(args[1].string, args[1].option); + if (!(nonce.length === 24)) { + throw new OperationError(`Invalid nonce length: ${nonce.length} bytes. + +XSalsa20 uses a nonce of 24 bytes (192 bits).`); + } + } + counter = Utils.intToByteArray(args[2], 8, "little"); + + const xsalsaKey = hsalsa20(key, nonce.slice(0, 16), rounds); + + const output = []; + input = Utils.convertToByteArray(input, inputType); + + let counterAsInt = Utils.byteArrayToInt(counter, "little"); + for (let i = 0; i < input.length; i += 64) { + counter = Utils.intToByteArray(counterAsInt, 8, "little"); + const stream = salsa20Block(xsalsaKey, nonce.slice(16, 24), counter, rounds); + for (let j = 0; j < 64 && i + j < input.length; j++) { + output.push(input[i + j] ^ stream[j]); + } + counterAsInt++; + } + + if (outputType === "Hex") { + return toHex(output); + } else { + return Utils.arrayBufferToStr(Uint8Array.from(output).buffer); + } + } + + /** + * Highlight XSalsa20 + * + * @param {Object[]} pos + * @param {number} pos[].start + * @param {number} pos[].end + * @param {Object[]} args + * @returns {Object[]} pos + */ + highlight(pos, args) { + const inputType = args[4], + outputType = args[5]; + if (inputType === "Raw" && outputType === "Raw") { + return pos; + } + } + + /** + * Highlight XSalsa20 in reverse + * + * @param {Object[]} pos + * @param {number} pos[].start + * @param {number} pos[].end + * @param {Object[]} args + * @returns {Object[]} pos + */ + highlightReverse(pos, args) { + const inputType = args[4], + outputType = args[5]; + if (inputType === "Raw" && outputType === "Raw") { + return pos; + } + } + +} + +export default XSalsa20; diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs index e85b6ad3..c03bf451 100644 --- a/tests/operations/index.mjs +++ b/tests/operations/index.mjs @@ -124,6 +124,8 @@ import "./tests/Register.mjs"; import "./tests/RisonEncodeDecode.mjs"; import "./tests/Rotate.mjs"; import "./tests/RSA.mjs"; +import "./tests/Salsa20.mjs"; +import "./tests/XSalsa20.mjs"; import "./tests/SeqUtils.mjs"; import "./tests/SetDifference.mjs"; import "./tests/SetIntersection.mjs"; diff --git a/tests/operations/tests/Salsa20.mjs b/tests/operations/tests/Salsa20.mjs new file mode 100644 index 00000000..2b9cbe29 --- /dev/null +++ b/tests/operations/tests/Salsa20.mjs @@ -0,0 +1,76 @@ +/** + * Salsa20 tests. + * + * @author joostrijneveld [joost@joostrijneveld.nl] + * @copyright Crown Copyright 2024 + * @license Apache-2.0 + */ + +import TestRegister from "../../lib/TestRegister.mjs"; + +TestRegister.addTests([ + { + name: "Salsa20: no key", + input: "", + expectedOutput: `Invalid key length: 0 bytes. + +Salsa20 uses a key of 16 or 32 bytes (128 or 256 bits).`, + recipeConfig: [ + { + "op": "Salsa20", + "args": [ + {"option": "Hex", "string": ""}, + {"option": "Hex", "string": ""}, + 0, "20", "Hex", "Hex", + ] + } + ], + }, + { + name: "Salsa20: no nonce", + input: "", + expectedOutput: `Invalid nonce length: 0 bytes. + +Salsa20 uses a nonce of 8 bytes (64 bits).`, + recipeConfig: [ + { + "op": "Salsa20", + "args": [ + {"option": "Hex", "string": "00000000000000000000000000000000"}, + {"option": "Hex", "string": ""}, + 0, "20", "Hex", "Hex", + ] + } + ], + }, + { + name: "Salsa20: ECRYPT Set 1 vector# 0", + input: "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00", + expectedOutput: "e3 be 8f dd 8b ec a2 e3 ea 8e f9 47 5b 29 a6 e7 00 39 51 e1 09 7a 5c 38 d2 3b 7a 5f ad 9f 68 44 b2 2c 97 55 9e 27 23 c7 cb bd 3f e4 fc 8d 9a 07 44 65 2a 83 e7 2a 9c 46 18 76 af 4d 7e f1 a1 17 8d a2 b7 4e ef 1b 62 83 e7 e2 01 66 ab ca e5 38 e9 71 6e 46 69 e2 81 6b 6b 20 c5 c3 56 80 20 01 cc 14 03 a9 a1 17 d1 2a 26 69 f4 56 36 6d 6e bb 0f 12 46 f1 26 51 50 f7 93 cd b4 b2 53 e3 48 ae 20 3d 89 bc 02 5e 80 2a 7e 0e 00 62 1d 70 aa 36 b7 e0 7c b1 e7 d5 b3 8d 5e 22 2b 8b 0e 4b 84 07 01 42 b1 e2 95 04 76 7d 76 82 48 50 32 0b 53 68 12 9f dd 74 e8 61 b4 98 e3 be 8d 16 f2 d7 d1 69 57 be 81 f4 7b 17 d9 ae 7c 4f f1 54 29 a7 3e 10 ac f2 50 ed 3a 90 a9 3c 71 13 08 a7 4c 62 16 a9 ed 84 cd 12 6d a7 f2 8e 8a bf 8b b6 35 17 e1 ca 98 e7 12 f4 fb 2e 1a 6a ed 9f dc 73 29 1f aa 17 95 82 11 c4 ba 2e bd 58 38 c6 35 ed b8 1f 51 3a 91 a2 94 e1 94 f1 c0 39 ae ec 65 7d ce 40 aa 7e 7c 0a f5 7c ac ef a4 0c 9f 14 b7 1a 4b 34 56 a6 3e 16 2e c7 d8 d1 0b 8f fb 18 10 d7 10 01 b6 18 2f 9f 73 da 53 b8 54 05 c1 1f 7b 2d 89 0f a8 ae 0c 7f 2e 92 6d 8a 98 c7 ec 4e 91 b6 51 20 e9 88 34 96 31 a7 00 c6 fa ce c3 47 1c b0 41 36 56 e7 5e 30 94 56 58 40 84 d7 e1 2c 5b 43 a4 1c 43 ed 9a 04 8a bd 9b 88 0d a6 5f 6a 66 5a 20 fe 7b 77 cd 29 2f e6 2c ae 64 4b 7f 7d f6 9f 32 bd b3 31 90 3e 65 05 ce 44 fd c2 93 92 0c 6a 9e c7 05 7e 23 df 7d ad 29 8f 82 dd f4 ef b7 fd c7 bf c6 22 69 6a fc fd 0c dd cc 83 c7 e7 7f 11 a6 49 d7 9a cd c3 35 4e 96 35 ff 13 7e 92 99 33 a0 bd 6f 53 77 ef a1 05 a3 a4 26 6b 7c 0d 08 9d 08 f1 e8 55 cc 32 b1 5b 93 78 4a 36 e5 6a 76 cc 64 bc 84 77", + recipeConfig: [ + { + "op": "Salsa20", + "args": [ + {"option": "Hex", "string": "80:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00"}, + {"option": "Hex", "string": "00:00:00:00:00:00:00:00"}, + 0, "20", "Hex", "Hex", + ] + } + ], + }, + { + name: "Salsa20: ECRYPT Set 6 vector# 3", + input: "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00", + expectedOutput: "71 da ee 51 42 d0 72 8b 41 b6 59 79 33 eb f4 67 e4 32 79 e3 09 78 67 70 78 94 16 02 62 9c bf 68 b7 3d 6b d2 c9 5f 11 8d 2b 3e 6e c9 55 da bb 6d c6 1c 41 43 bc 9a 9b 32 b9 9d be 68 66 16 6d c0 86 31 b7 d6 55 30 50 30 3d 72 52 c2 64 d3 a9 0d 26 c8 53 63 48 13 e0 9a d7 54 5a 6c e7 e8 4a 5d fc 75 ec 43 43 12 07 d5 31 99 70 b0 fa ad b0 e1 51 06 25 bb 54 37 2c 85 15 e2 8e 2a cc f0 a9 93 0a d1 5f 43 18 74 92 3d 2a 59 e2 0d 9f 2a 53 67 db a6 05 15 64 f1 50 28 7d eb b1 db 53 6f f9 b0 9a d9 81 f2 5e 50 10 d8 5d 76 ee 0c 30 5f 75 5b 25 e6 f0 93 41 e0 81 2f 95 c9 4f 42 ee ad 34 6e 81 f3 9c 58 c5 fa a2 c8 89 53 dc 0c ac 90 46 9d b2 06 3c b5 cd b2 2c 9e ae 22 af bf 05 06 fc a4 1d c7 10 b8 46 fb df e3 c4 68 83 dd 11 8f 3a 5e 8b 11 b6 af d9 e7 16 80 d8 66 65 57 30 1a 2d aa fb 94 96 c5 59 78 4d 35 a0 35 36 08 85 f9 b1 7b d7 19 19 77 de ea 93 2b 98 1e bd b2 90 57 ae 3c 92 cf ef f5 e6 c5 d0 cb 62 f2 09 ce 34 2d 4e 35 c6 96 46 cc d1 4e 53 35 0e 48 8b b3 10 a3 2f 8b 02 48 e7 0a cc 5b 47 3d f5 37 ce d3 f8 1a 01 4d 40 83 93 2b ed d6 2e d0 e4 47 b6 76 6c d2 60 4b 70 6e 9b 34 6c 44 68 be b4 6a 34 ec f1 61 0e bd 38 33 1d 52 bf 33 34 6a fe c1 5e ef b2 a7 69 9e 87 59 db 5a 1f 63 6a 48 a0 39 68 8e 39 de 34 d9 95 df 9f 27 ed 9e dc 8d d7 95 e3 9e 53 d9 d9 25 b2 78 01 05 65 ff 66 52 69 04 2f 05 09 6d 94 da 34 33 d9 57 ec 13 d2 fd 82 a0 06 62 83 d0 d1 ee b8 1b f0 ef 13 3b 7f d9 02 48 b8 ff b4 99 b2 41 4c d4 fa 00 30 93 ff 08 64 57 5a 43 74 9b f5 96 02 f2 6c 71 7f a9 6b 1d 05 76 97 db 08 eb c3 fa 66 4a 01 6a 67 dc ef 88 07 57 7c c3 a0 93 85 d3", + recipeConfig: [ + { + "op": "Salsa20", + "args": [ + {"option": "Hex", "string": "0F:62:B5:08:5B:AE:01:54:A7:FA:4D:A0:F3:46:99:EC"}, + {"option": "Hex", "string": "28:8F:F6:5D:C4:2B:92:F9"}, + 0, "20", "Hex", "Hex", + ] + } + ], + }, +]); diff --git a/tests/operations/tests/XSalsa20.mjs b/tests/operations/tests/XSalsa20.mjs new file mode 100644 index 00000000..0d26af97 --- /dev/null +++ b/tests/operations/tests/XSalsa20.mjs @@ -0,0 +1,61 @@ +/** + * XSalsa20 tests. + * + * @author joostrijneveld [joost@joostrijneveld.nl] + * @copyright Crown Copyright 2024 + * @license Apache-2.0 + */ + +import TestRegister from "../../lib/TestRegister.mjs"; + +TestRegister.addTests([ + { + name: "XSalsa20: no key", + input: "", + expectedOutput: `Invalid key length: 0 bytes. + +XSalsa20 uses a key of 16 or 32 bytes (128 or 256 bits).`, + recipeConfig: [ + { + "op": "XSalsa20", + "args": [ + {"option": "Hex", "string": ""}, + {"option": "Hex", "string": ""}, + 0, "20", "Hex", "Hex", + ] + } + ], + }, + { + name: "XSalsa20: no nonce", + input: "", + expectedOutput: `Invalid nonce length: 0 bytes. + +XSalsa20 uses a nonce of 24 bytes (192 bits).`, + recipeConfig: [ + { + "op": "XSalsa20", + "args": [ + {"option": "Hex", "string": "00000000000000000000000000000000"}, + {"option": "Hex", "string": ""}, + 0, "20", "Hex", "Hex", + ] + } + ], + }, + { + name: "XSalsa20 custom vector", + input: "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00", + expectedOutput: "7c b6 60 af dd 9e c6 46 8f 57 dd 6d 24 33 f9 34 28 fd 82 cd 73 86 c5 47 1a 24 d8 ad 2a 52 5b 6e 5e ff 38 4f c7 ca a2 10 bb 3c 8f 3e 68 8f 4a 97 52 a5 46 df 8c 25 3f ef 17 a2 67 94 55 c7 a1 e1 83 db f5 d5 45 b0 f5 02 b9 8d e0 99 7a 66 ab 43 23 41 68 9f f3 97 dc 4f bc 1f 27 bd 1a 61 97 f5 dc 80 ff 19 05 16 c9 ed 14 f2 81 d1 ca 73 88 82 f6 d3 d2 fb 92 1e 2e f8 99 38 9e 0a 22 3b e7 ae 81 5a 04 86 5f 82 52 68 2f 6a d1 4f 98 ff 5f 08 23 cc 22 9d d2 22 9e 69 9d c2 1a 81 7d c9 54 bb 9b c9 0d ec 3b 9b d3 bf 20 9b 82 da f7 89 34 8a 5e 14 ec 54 2b 6d ee 8b 60 1e 7e 6d e3 c2 8a 2d 57 b6 25 e7 ea b3 43 d8 eb 20 85 b6 f6 82 09 58 99 35 20 44 22 60 60 61 d2 8d e9 8b ea 58 af bf ba ad 70 03 98 19 a0 c3 9a a8 63 94 47 5c d0 61 94 b0 17 ab c4 bb 28 b7 56 6d 3c 66 1c 76 f4 8a d3 a3 a2 9e d3 36 df 1f c6 8b 4f 44 2f 06 a3 58 0b ae c8 06 e2 e6 5d 39 ab 18 28 fe 80 18 12 69 2c 60 34 b5 0b f5 f3 3c 51 fc 0c fb 43 82 1e 3e 92 d6 b8 06 cf 00 16 e3 49 a0 34 83 20 f9 b0 53 7e ad ac 4a c1 36 5f cc fb be e2 ba 5a ad 1d 29 74 07 19 34 61 0e 9d ce 84 60 24 6a e6 8d ed 50 e0 20 44 26 d8 76 6d f2 da 4b 12 72 5a 85 c2 b1 07 04 f5 10 2e 3c 67 1c 5a fc 5b 46 0e 4d fb 39 b6 10 73 22 47 84 10 93 df 5f c8 92 7e 87 c3 0d 24 3a 48 b2 ad c2 56 3d a2 22 e9 02 9c 58 64 c6 d5 a5 f8 c6 54 99 1c 0f 6b f3 db ed 81 16 85 28 17 b0 eb 11 c7 05 9f f9 d8 fc 4a 1c 36 db 16 fd 38 d8 32 34 5b 8c 80 c6 51 21 1d 91 01 c5 8a 60 ad a4 39 33 d5 32 9a c1 f5 b2 ab 20 46 75 db 63 e0 bd d2 97 c0 e9 fc 1c d9 17 4a d1 3a db ea c2 8c 46 22 21 c3 5a bf 6c 1e cf 28 9c 8c 2f b2 0f", + recipeConfig: [ + { + "op": "XSalsa20", + "args": [ + {"option": "Hex", "string": "00:01:02:03:04:05:06:07:08:09:0A:0B:0C:0D:0E:0F:10:11:12:13:14:15:16:17:18:19:1A:1B:1C:1D:1E:1F"}, + {"option": "Hex", "string": "00:01:02:03:04:05:06:07:08:09:0A:0B:0C:0D:0E:0F:10:11:12:13:14:15:16:17"}, + 0, "20", "Hex", "Hex", + ] + } + ], + } +]); From e1c73a64ad9dde36bde56a64bbfb61855e61128a Mon Sep 17 00:00:00 2001 From: Chris White Date: Wed, 13 Mar 2024 09:51:22 -0700 Subject: [PATCH 02/22] Updated xmldom package to new namespace for vuln remediation --- package-lock.json | 17 +++++++++-------- package.json | 2 +- src/core/operations/CSSSelector.mjs | 2 +- src/core/operations/XPathExpression.mjs | 2 +- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5e6671a5..3f32cf86 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@babel/polyfill": "^7.12.1", "@blu3r4y/lzma": "^2.3.3", "@wavesenterprise/crypto-gost-js": "^2.1.0-RC1", + "@xmldom/xmldom": "^0.8.0", "argon2-browser": "^1.18.0", "arrive": "^2.4.1", "avsc": "^5.7.7", @@ -92,7 +93,6 @@ "unorm": "^1.6.0", "utf8": "^3.0.0", "vkbeautify": "^0.99.3", - "xmldom": "^0.6.0", "xpath": "0.0.34", "xregexp": "^5.1.1", "zlibjs": "^0.3.1" @@ -3174,6 +3174,14 @@ "@xtuc/long": "4.2.2" } }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -14835,13 +14843,6 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true }, - "node_modules/xmldom": { - "version": "0.6.0", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/xpath": { "version": "0.0.34", "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.34.tgz", diff --git a/package.json b/package.json index 334d88b5..cd100eda 100644 --- a/package.json +++ b/package.json @@ -174,7 +174,7 @@ "unorm": "^1.6.0", "utf8": "^3.0.0", "vkbeautify": "^0.99.3", - "xmldom": "^0.6.0", + "@xmldom/xmldom": "^0.8.0", "xpath": "0.0.34", "xregexp": "^5.1.1", "zlibjs": "^0.3.1" diff --git a/src/core/operations/CSSSelector.mjs b/src/core/operations/CSSSelector.mjs index d6b8da11..639726c4 100644 --- a/src/core/operations/CSSSelector.mjs +++ b/src/core/operations/CSSSelector.mjs @@ -6,7 +6,7 @@ import Operation from "../Operation.mjs"; import OperationError from "../errors/OperationError.mjs"; -import xmldom from "xmldom"; +import xmldom from "@xmldom/xmldom"; import nwmatcher from "nwmatcher"; /** diff --git a/src/core/operations/XPathExpression.mjs b/src/core/operations/XPathExpression.mjs index 7bfe3ee1..c850104b 100644 --- a/src/core/operations/XPathExpression.mjs +++ b/src/core/operations/XPathExpression.mjs @@ -6,7 +6,7 @@ import Operation from "../Operation.mjs"; import OperationError from "../errors/OperationError.mjs"; -import xmldom from "xmldom"; +import xmldom from "@xmldom/xmldom"; import xpath from "xpath"; /** From ef5ff5bec656044700977d7625d05d40f2adbc9e Mon Sep 17 00:00:00 2001 From: Chris White Date: Wed, 13 Mar 2024 10:26:23 -0700 Subject: [PATCH 03/22] Updated jsonwebtoken dependency to 9+ updated JWTSign operation for backwards compatibility with insecure keys and invalid asym key types --- package-lock.json | 44 ++++++++++++++++++++++++--------- package.json | 2 +- src/core/operations/JWTSign.mjs | 7 +++++- 3 files changed, 40 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5e6671a5..7a69fc4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,7 +52,7 @@ "jsesc": "^3.0.2", "json5": "^2.2.3", "jsonpath-plus": "^8.0.0", - "jsonwebtoken": "8.5.1", + "jsonwebtoken": "^9.0.0", "jsqr": "^1.4.0", "jsrsasign": "^11.1.0", "kbpgp": "2.1.15", @@ -9612,9 +9612,9 @@ } }, "node_modules/jsonwebtoken": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", - "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", @@ -9625,21 +9625,43 @@ "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", - "semver": "^5.6.0" + "semver": "^7.5.4" }, "engines": { - "node": ">=4", - "npm": ">=1.4.28" + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" } }, "node_modules/jsonwebtoken/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dependencies": { + "lru-cache": "^6.0.0" + }, "bin": { - "semver": "bin/semver" + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, + "node_modules/jsonwebtoken/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, "node_modules/jsqr": { "version": "1.4.0", "license": "Apache-2.0" diff --git a/package.json b/package.json index 334d88b5..5597afe4 100644 --- a/package.json +++ b/package.json @@ -134,7 +134,7 @@ "jsesc": "^3.0.2", "json5": "^2.2.3", "jsonpath-plus": "^8.0.0", - "jsonwebtoken": "8.5.1", + "jsonwebtoken": "^9.0.0", "jsqr": "^1.4.0", "jsrsasign": "^11.1.0", "kbpgp": "2.1.15", diff --git a/src/core/operations/JWTSign.mjs b/src/core/operations/JWTSign.mjs index af46908e..e4756c2b 100644 --- a/src/core/operations/JWTSign.mjs +++ b/src/core/operations/JWTSign.mjs @@ -50,7 +50,12 @@ class JWTSign extends Operation { try { return jwt.sign(input, key, { - algorithm: algorithm === "None" ? "none" : algorithm + algorithm: algorithm === "None" ? "none" : algorithm, + + // To utilize jsonwebtoken 9+ library and maintain backwards compatibility for regression tests + // This could be turned into operation args in a future PR + allowInsecureKeySizes: true, + allowInvalidAsymmetricKeyTypes: true }); } catch (err) { throw new OperationError(`Error: Have you entered the key correctly? The key should be either the secret for HMAC algorithms or the PEM-encoded private key for RSA and ECDSA. From 16dfb3fac6cbce710c0c56a043be4bc7ebd65593 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 21 Jul 2023 17:30:39 +0100 Subject: [PATCH 04/22] Automatically detect EOL from paste events and output setting --- src/web/utils/editorUtils.mjs | 39 +++++++++++++ src/web/utils/statusBar.mjs | 39 ++++--------- src/web/waiters/ControlsWaiter.mjs | 9 +-- src/web/waiters/InputWaiter.mjs | 88 ++++++++++++++++++++++++++++-- src/web/waiters/OutputWaiter.mjs | 76 +++++++++++++++++++++++++- tests/browser/00_nightwatch.js | 1 + tests/browser/01_io.js | 6 +- 7 files changed, 215 insertions(+), 43 deletions(-) diff --git a/src/web/utils/editorUtils.mjs b/src/web/utils/editorUtils.mjs index e02e692b..fe1b6749 100644 --- a/src/web/utils/editorUtils.mjs +++ b/src/web/utils/editorUtils.mjs @@ -95,3 +95,42 @@ export function escapeControlChars(str, preserveWs=false, lineBreak="\n") { return n.outerHTML; }); } + +/** + * Convert and EOL sequence to its name + */ +export const eolSeqToCode = { + "\u000a": "LF", + "\u000b": "VT", + "\u000c": "FF", + "\u000d": "CR", + "\u000d\u000a": "CRLF", + "\u0085": "NEL", + "\u2028": "LS", + "\u2029": "PS" +}; + +/** + * Convert an EOL name to its sequence + */ +export const eolCodeToSeq = { + "LF": "\u000a", + "VT": "\u000b", + "FF": "\u000c", + "CR": "\u000d", + "CRLF": "\u000d\u000a", + "NEL": "\u0085", + "LS": "\u2028", + "PS": "\u2029" +}; + +export const eolCodeToName = { + "LF": "Line Feed", + "VT": "Vertical Tab", + "FF": "Form Feed", + "CR": "Carriage Return", + "CRLF": "Carriage Return + Line Feed", + "NEL": "Next Line", + "LS": "Line Separator", + "PS": "Paragraph Separator" +}; diff --git a/src/web/utils/statusBar.mjs b/src/web/utils/statusBar.mjs index 6469379a..69c4dd51 100644 --- a/src/web/utils/statusBar.mjs +++ b/src/web/utils/statusBar.mjs @@ -6,6 +6,7 @@ import {showPanel} from "@codemirror/view"; import {CHR_ENC_SIMPLE_LOOKUP, CHR_ENC_SIMPLE_REVERSE_LOOKUP} from "../../core/lib/ChrEnc.mjs"; +import { eolCodeToName, eolSeqToCode } from "./editorUtils.mjs"; /** * A Status bar extension for CodeMirror @@ -92,22 +93,12 @@ class StatusBarPanel { // preventDefault is required to stop the URL being modified and popState being triggered e.preventDefault(); - const eolLookup = { - "LF": "\u000a", - "VT": "\u000b", - "FF": "\u000c", - "CR": "\u000d", - "CRLF": "\u000d\u000a", - "NEL": "\u0085", - "LS": "\u2028", - "PS": "\u2029" - }; - const eolval = eolLookup[e.target.getAttribute("data-val")]; - - if (eolval === undefined) return; + const eolCode = e.target.getAttribute("data-val"); + if (!eolCode) return; // Call relevant EOL change handler - this.eolHandler(eolval); + this.eolHandler(e.target.getAttribute("data-val"), true); + hideElement(e.target.closest(".cm-status-bar-select-content")); } @@ -223,23 +214,13 @@ class StatusBarPanel { updateEOL(state) { if (state.lineBreak === this.eolVal) return; - const eolLookup = { - "\u000a": ["LF", "Line Feed"], - "\u000b": ["VT", "Vertical Tab"], - "\u000c": ["FF", "Form Feed"], - "\u000d": ["CR", "Carriage Return"], - "\u000d\u000a": ["CRLF", "Carriage Return + Line Feed"], - "\u0085": ["NEL", "Next Line"], - "\u2028": ["LS", "Line Separator"], - "\u2029": ["PS", "Paragraph Separator"] - }; - const val = this.dom.querySelector(".eol-value"); const button = val.closest(".cm-status-bar-select-btn"); - const eolName = eolLookup[state.lineBreak]; - val.textContent = eolName[0]; - button.setAttribute("title", `End of line sequence:
${eolName[1]}`); - button.setAttribute("data-original-title", `End of line sequence:
${eolName[1]}`); + const eolCode = eolSeqToCode[state.lineBreak]; + const eolName = eolCodeToName[eolCode]; + val.textContent = eolCode; + button.setAttribute("title", `End of line sequence:
${eolName}`); + button.setAttribute("data-original-title", `End of line sequence:
${eolName}`); this.eolVal = state.lineBreak; } diff --git a/src/web/waiters/ControlsWaiter.mjs b/src/web/waiters/ControlsWaiter.mjs index 6a0ef6f2..660a7ec5 100755 --- a/src/web/waiters/ControlsWaiter.mjs +++ b/src/web/waiters/ControlsWaiter.mjs @@ -5,6 +5,7 @@ */ import Utils from "../../core/Utils.mjs"; +import { eolSeqToCode } from "../utils/editorUtils.mjs"; /** @@ -140,16 +141,16 @@ class ControlsWaiter { const inputChrEnc = this.manager.input.getChrEnc(); const outputChrEnc = this.manager.output.getChrEnc(); - const inputEOLSeq = this.manager.input.getEOLSeq(); - const outputEOLSeq = this.manager.output.getEOLSeq(); + const inputEOL = eolSeqToCode[this.manager.input.getEOLSeq()]; + const outputEOL = eolSeqToCode[this.manager.output.getEOLSeq()]; const params = [ includeRecipe ? ["recipe", recipeStr] : undefined, includeInput && input.length ? ["input", Utils.escapeHtml(input)] : undefined, inputChrEnc !== 0 ? ["ienc", inputChrEnc] : undefined, outputChrEnc !== 0 ? ["oenc", outputChrEnc] : undefined, - inputEOLSeq !== "\n" ? ["ieol", inputEOLSeq] : undefined, - outputEOLSeq !== "\n" ? ["oeol", outputEOLSeq] : undefined + inputEOL !== "LF" ? ["ieol", inputEOL] : undefined, + outputEOL !== "LF" ? ["oeol", outputEOL] : undefined ]; const hash = params diff --git a/src/web/waiters/InputWaiter.mjs b/src/web/waiters/InputWaiter.mjs index 25c1629d..ad8eb38c 100644 --- a/src/web/waiters/InputWaiter.mjs +++ b/src/web/waiters/InputWaiter.mjs @@ -42,7 +42,7 @@ import { import {statusBar} from "../utils/statusBar.mjs"; import {fileDetailsPanel} from "../utils/fileDetails.mjs"; -import {renderSpecialChar} from "../utils/editorUtils.mjs"; +import {eolCodeToSeq, eolCodeToName, renderSpecialChar} from "../utils/editorUtils.mjs"; /** @@ -62,6 +62,7 @@ class InputWaiter { this.inputTextEl = document.getElementById("input-text"); this.inputChrEnc = 0; + this.eolSetManually = false; this.initEditor(); this.inputWorker = null; @@ -92,6 +93,7 @@ class InputWaiter { fileDetailsPanel: new Compartment }; + const self = this; const initialState = EditorState.create({ doc: null, extensions: [ @@ -141,6 +143,15 @@ class InputWaiter { if (e.docChanged && !this.silentInputChange) this.inputChange(e); this.silentInputChange = false; + }), + + // Event handlers + EditorView.domEventHandlers({ + paste(event, view) { + setTimeout(() => { + self.afterPaste(event); + }); + } }) ] }); @@ -154,12 +165,35 @@ class InputWaiter { /** * Handler for EOL change events * Sets the line separator - * @param {string} eolVal + * @param {string} eol + * @param {boolean} manual - a flag for whether this was set by the user or automatically */ - eolChange(eolVal) { - const oldInputVal = this.getInput(); + eolChange(eol, manual=false) { + const eolVal = eolCodeToSeq[eol]; + if (eolVal === undefined) return; + + const eolBtn = document.querySelector("#input-text .eol-value"); + if (manual) { + this.eolSetManually = true; + eolBtn.classList.remove("font-italic"); + } else { + eolBtn.classList.add("font-italic"); + } + + if (eolVal === this.getEOLSeq()) return; + + if (!manual) { + // Pulse + eolBtn.classList.add("pulse"); + setTimeout(() => { + eolBtn.classList.remove("pulse"); + }, 2000); + // Alert + this.app.alert(`Input EOL separator has been changed to ${eolCodeToName[eol]}`, 5000); + } // Update the EOL value + const oldInputVal = this.getInput(); this.inputEditorView.dispatch({ effects: this.inputEditorConf.eol.reconfigure(EditorState.lineSeparator.of(eolVal)) }); @@ -866,6 +900,49 @@ class InputWaiter { }, delay, "inputChange", this, [e])(); } + /** + * Handler that fires just after input paste events. + * Checks whether the EOL separator or character encoding should be updated. + * + * @param {event} e + */ + afterPaste(e) { + // If EOL has been fixed, skip this. + if (this.eolSetManually) return; + + const inputText = this.getInput(); + + // Detect most likely EOL sequence + const eolCharCounts = { + "LF": inputText.count("\u000a"), + "VT": inputText.count("\u000b"), + "FF": inputText.count("\u000c"), + "CR": inputText.count("\u000d"), + "CRLF": inputText.count("\u000d\u000a"), + "NEL": inputText.count("\u0085"), + "LS": inputText.count("\u2028"), + "PS": inputText.count("\u2029") + }; + + // If all zero, leave alone + const total = Object.values(eolCharCounts).reduce((acc, curr) => { + return acc + curr; + }, 0); + if (total === 0) return; + + // If CRLF not zero and more than half the highest alternative, choose CRLF + const highest = Object.entries(eolCharCounts).reduce((acc, curr) => { + return curr[1] > acc[1] ? curr : acc; + }, ["LF", 0]); + if ((eolCharCounts.CRLF * 2) > highest[1]) { + this.eolChange("CRLF"); + return; + } + + // Else choose max + this.eolChange(highest[0]); + } + /** * Handler for input dragover events. * Gives the user a visual cue to show that items can be dropped here. @@ -1199,6 +1276,9 @@ class InputWaiter { this.manager.output.removeAllOutputs(); this.manager.output.terminateZipWorker(); + this.eolSetManually = false; + this.manager.output.eolSetManually = false; + const tabsList = document.getElementById("input-tabs"); const tabsListChildren = tabsList.children; diff --git a/src/web/waiters/OutputWaiter.mjs b/src/web/waiters/OutputWaiter.mjs index dae27f3e..6acd6752 100755 --- a/src/web/waiters/OutputWaiter.mjs +++ b/src/web/waiters/OutputWaiter.mjs @@ -38,7 +38,7 @@ import { import {statusBar} from "../utils/statusBar.mjs"; import {htmlPlugin} from "../utils/htmlWidget.mjs"; import {copyOverride} from "../utils/copyOverride.mjs"; -import {renderSpecialChar} from "../utils/editorUtils.mjs"; +import {eolCodeToSeq, eolCodeToName, renderSpecialChar} from "../utils/editorUtils.mjs"; /** @@ -70,6 +70,7 @@ class OutputWaiter { this.zipWorker = null; this.maxTabs = this.manager.tabs.calcMaxTabs(); this.tabTimeout = null; + this.eolSetManually = false; } /** @@ -146,9 +147,33 @@ class OutputWaiter { /** * Handler for EOL change events * Sets the line separator - * @param {string} eolVal + * @param {string} eol + * @param {boolean} manual - a flag for whether this was set by the user or automatically */ - async eolChange(eolVal) { + async eolChange(eol, manual=false) { + const eolVal = eolCodeToSeq[eol]; + if (eolVal === undefined) return; + + const eolBtn = document.querySelector("#output-text .eol-value"); + if (manual) { + this.eolSetManually = true; + eolBtn.classList.remove("font-italic"); + } else { + eolBtn.classList.add("font-italic"); + } + + if (eolVal === this.getEOLSeq()) return; + + if (!manual) { + // Pulse + eolBtn.classList.add("pulse"); + setTimeout(() => { + eolBtn.classList.remove("pulse"); + }, 2000); + // Alert + this.app.alert(`Output EOL separator has been changed to ${eolCodeToName[eol]}`, 5000); + } + const currentTabNum = this.manager.tabs.getActiveTab("output"); if (currentTabNum >= 0) { this.outputs[currentTabNum].eolSequence = eolVal; @@ -276,6 +301,9 @@ class OutputWaiter { // If turning word wrap off, do it before we populate the editor for performance reasons if (!wrap) this.setWordWrap(wrap); + // Detect suitable EOL sequence + this.detectEOLSequence(data); + // We use setTimeout here to delay the editor dispatch until the next event cycle, // ensuring all async actions have completed before attempting to set the contents // of the editor. This is mainly with the above call to setWordWrap() in mind. @@ -345,6 +373,48 @@ class OutputWaiter { }); } + /** + * Checks whether the EOL separator should be updated + * + * @param {string} data + */ + detectEOLSequence(data) { + // If EOL has been fixed, skip this. + if (this.eolSetManually) return; + // If data is too long, skip this. + if (data.length > 1000000) return; + + // Detect most likely EOL sequence + const eolCharCounts = { + "LF": data.count("\u000a"), + "VT": data.count("\u000b"), + "FF": data.count("\u000c"), + "CR": data.count("\u000d"), + "CRLF": data.count("\u000d\u000a"), + "NEL": data.count("\u0085"), + "LS": data.count("\u2028"), + "PS": data.count("\u2029") + }; + + // If all zero, leave alone + const total = Object.values(eolCharCounts).reduce((acc, curr) => { + return acc + curr; + }, 0); + if (total === 0) return; + + // If CRLF not zero and more than half the highest alternative, choose CRLF + const highest = Object.entries(eolCharCounts).reduce((acc, curr) => { + return curr[1] > acc[1] ? curr : acc; + }, ["LF", 0]); + if ((eolCharCounts.CRLF * 2) > highest[1]) { + this.eolChange("CRLF"); + return; + } + + // Else choose max + this.eolChange(highest[0]); + } + /** * Calculates the maximum number of tabs to display */ diff --git a/tests/browser/00_nightwatch.js b/tests/browser/00_nightwatch.js index 3ba2a865..2e5688d7 100644 --- a/tests/browser/00_nightwatch.js +++ b/tests/browser/00_nightwatch.js @@ -230,6 +230,7 @@ module.exports = { // Alert bar shows and contains correct content browser + .waitForElementNotVisible("#snackbar-container") .click("#copy-output") .waitForElementVisible("#snackbar-container") .waitForElementVisible("#snackbar-container .snackbar-content") diff --git a/tests/browser/01_io.js b/tests/browser/01_io.js index 67d1fdff..6791c88e 100644 --- a/tests/browser/01_io.js +++ b/tests/browser/01_io.js @@ -545,8 +545,8 @@ module.exports = { browser.expect.element("#output-text .cm-status-bar .stats-lines-value").text.to.equal("2"); /* Line endings appear in the URL */ - browser.assert.urlContains("ieol=%0D%0A"); - browser.assert.urlContains("oeol=%0D"); + browser.assert.urlContains("ieol=CRLF"); + browser.assert.urlContains("oeol=CR"); /* Preserved when changing tabs */ browser @@ -643,7 +643,7 @@ module.exports = { "Loading from URL": browser => { /* Complex deep link populates the input correctly (encoding, eol, input) */ browser - .urlHash("recipe=To_Base64('A-Za-z0-9%2B/%3D')&input=VGhlIHNoaXBzIGh1bmcgaW4gdGhlIHNreSBpbiBtdWNoIHRoZSBzYW1lIHdheSB0aGF0IGJyaWNrcyBkb24ndC4M&ienc=21866&oenc=1201&ieol=%0C&oeol=%E2%80%A9") + .urlHash("recipe=To_Base64('A-Za-z0-9%2B/%3D')&input=VGhlIHNoaXBzIGh1bmcgaW4gdGhlIHNreSBpbiBtdWNoIHRoZSBzYW1lIHdheSB0aGF0IGJyaWNrcyBkb24ndC4M&ienc=21866&oenc=1201&ieol=FF&oeol=PS") .waitForElementVisible("#rec-list li.operation"); browser.expect.element(`#input-text .cm-content`).to.have.property("textContent").match(/^.{65}$/); From 65ffd8d65d88eb369f6f61a5d1d0f807179bffb7 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Tue, 26 Mar 2024 13:33:00 +0000 Subject: [PATCH 05/22] Automatically detect UTF8 character encoding in output --- src/core/lib/ChrEnc.mjs | 81 ++++++++++++- src/core/lib/Magic.mjs | 79 +----------- src/web/App.mjs | 8 +- src/web/stylesheets/components/_operation.css | 6 +- src/web/utils/statusBar.mjs | 51 +++++++- src/web/waiters/InputWaiter.mjs | 78 +++++++----- src/web/waiters/OutputWaiter.mjs | 114 +++++++++++++----- 7 files changed, 270 insertions(+), 147 deletions(-) diff --git a/src/core/lib/ChrEnc.mjs b/src/core/lib/ChrEnc.mjs index 6879d736..55fe3761 100644 --- a/src/core/lib/ChrEnc.mjs +++ b/src/core/lib/ChrEnc.mjs @@ -224,8 +224,85 @@ export function chrEncWidth(page) { * @copyright Crown Copyright 2019 * @license Apache-2.0 */ +export const UNICODE_NORMALISATION_FORMS = ["NFD", "NFC", "NFKD", "NFKC"]; + /** - * Character encoding format mappings. + * Detects whether the input buffer is valid UTF8. + * + * @param {ArrayBuffer} data + * @returns {number} - 0 = not UTF8, 1 = ASCII, 2 = UTF8 */ -export const UNICODE_NORMALISATION_FORMS = ["NFD", "NFC", "NFKD", "NFKC"]; +export function isUTF8(data) { + const bytes = new Uint8Array(data); + let i = 0; + let onlyASCII = true; + while (i < bytes.length) { + if (( // ASCII + bytes[i] === 0x09 || + bytes[i] === 0x0A || + bytes[i] === 0x0D || + (0x20 <= bytes[i] && bytes[i] <= 0x7E) + )) { + i += 1; + continue; + } + + onlyASCII = false; + + if (( // non-overlong 2-byte + (0xC2 <= bytes[i] && bytes[i] <= 0xDF) && + (0x80 <= bytes[i+1] && bytes[i+1] <= 0xBF) + )) { + i += 2; + continue; + } + + if (( // excluding overlongs + bytes[i] === 0xE0 && + (0xA0 <= bytes[i + 1] && bytes[i + 1] <= 0xBF) && + (0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF) + ) || + ( // straight 3-byte + ((0xE1 <= bytes[i] && bytes[i] <= 0xEC) || + bytes[i] === 0xEE || + bytes[i] === 0xEF) && + (0x80 <= bytes[i + 1] && bytes[i+1] <= 0xBF) && + (0x80 <= bytes[i+2] && bytes[i+2] <= 0xBF) + ) || + ( // excluding surrogates + bytes[i] === 0xED && + (0x80 <= bytes[i+1] && bytes[i+1] <= 0x9F) && + (0x80 <= bytes[i+2] && bytes[i+2] <= 0xBF) + )) { + i += 3; + continue; + } + + if (( // planes 1-3 + bytes[i] === 0xF0 && + (0x90 <= bytes[i + 1] && bytes[i + 1] <= 0xBF) && + (0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF) && + (0x80 <= bytes[i + 3] && bytes[i + 3] <= 0xBF) + ) || + ( // planes 4-15 + (0xF1 <= bytes[i] && bytes[i] <= 0xF3) && + (0x80 <= bytes[i + 1] && bytes[i + 1] <= 0xBF) && + (0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF) && + (0x80 <= bytes[i + 3] && bytes[i + 3] <= 0xBF) + ) || + ( // plane 16 + bytes[i] === 0xF4 && + (0x80 <= bytes[i + 1] && bytes[i + 1] <= 0x8F) && + (0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF) && + (0x80 <= bytes[i + 3] && bytes[i + 3] <= 0xBF) + )) { + i += 4; + continue; + } + + return 0; + } + + return onlyASCII ? 1 : 2; +} diff --git a/src/core/lib/Magic.mjs b/src/core/lib/Magic.mjs index 921fc3f6..14111ec7 100644 --- a/src/core/lib/Magic.mjs +++ b/src/core/lib/Magic.mjs @@ -3,6 +3,7 @@ import Utils, { isWorkerEnvironment } from "../Utils.mjs"; import Recipe from "../Recipe.mjs"; import Dish from "../Dish.mjs"; import {detectFileType, isType} from "./FileType.mjs"; +import {isUTF8} from "./ChrEnc.mjs"; import chiSquared from "chi-squared"; /** @@ -111,82 +112,6 @@ class Magic { }; } - /** - * Detects whether the input buffer is valid UTF8. - * - * @returns {boolean} - */ - isUTF8() { - const bytes = new Uint8Array(this.inputBuffer); - let i = 0; - while (i < bytes.length) { - if (( // ASCII - bytes[i] === 0x09 || - bytes[i] === 0x0A || - bytes[i] === 0x0D || - (0x20 <= bytes[i] && bytes[i] <= 0x7E) - )) { - i += 1; - continue; - } - - if (( // non-overlong 2-byte - (0xC2 <= bytes[i] && bytes[i] <= 0xDF) && - (0x80 <= bytes[i+1] && bytes[i+1] <= 0xBF) - )) { - i += 2; - continue; - } - - if (( // excluding overlongs - bytes[i] === 0xE0 && - (0xA0 <= bytes[i + 1] && bytes[i + 1] <= 0xBF) && - (0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF) - ) || - ( // straight 3-byte - ((0xE1 <= bytes[i] && bytes[i] <= 0xEC) || - bytes[i] === 0xEE || - bytes[i] === 0xEF) && - (0x80 <= bytes[i + 1] && bytes[i+1] <= 0xBF) && - (0x80 <= bytes[i+2] && bytes[i+2] <= 0xBF) - ) || - ( // excluding surrogates - bytes[i] === 0xED && - (0x80 <= bytes[i+1] && bytes[i+1] <= 0x9F) && - (0x80 <= bytes[i+2] && bytes[i+2] <= 0xBF) - )) { - i += 3; - continue; - } - - if (( // planes 1-3 - bytes[i] === 0xF0 && - (0x90 <= bytes[i + 1] && bytes[i + 1] <= 0xBF) && - (0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF) && - (0x80 <= bytes[i + 3] && bytes[i + 3] <= 0xBF) - ) || - ( // planes 4-15 - (0xF1 <= bytes[i] && bytes[i] <= 0xF3) && - (0x80 <= bytes[i + 1] && bytes[i + 1] <= 0xBF) && - (0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF) && - (0x80 <= bytes[i + 3] && bytes[i + 3] <= 0xBF) - ) || - ( // plane 16 - bytes[i] === 0xF4 && - (0x80 <= bytes[i + 1] && bytes[i + 1] <= 0x8F) && - (0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF) && - (0x80 <= bytes[i + 3] && bytes[i + 3] <= 0xBF) - )) { - i += 4; - continue; - } - - return false; - } - - return true; - } - /** * Calculates the Shannon entropy of the input data. * @@ -336,7 +261,7 @@ class Magic { data: this.inputStr.slice(0, 100), languageScores: this.detectLanguage(extLang), fileType: this.detectFileType(), - isUTF8: this.isUTF8(), + isUTF8: !!isUTF8(this.inputBuffer), entropy: this.calcEntropy(), matchingOps: matchingOps, useful: useful, diff --git a/src/web/App.mjs b/src/web/App.mjs index cce91b1e..eeae264f 100755 --- a/src/web/App.mjs +++ b/src/web/App.mjs @@ -500,22 +500,22 @@ class App { // Input Character Encoding // Must be set before the input is loaded if (this.uriParams.ienc) { - this.manager.input.chrEncChange(parseInt(this.uriParams.ienc, 10)); + this.manager.input.chrEncChange(parseInt(this.uriParams.ienc, 10), true); } // Output Character Encoding if (this.uriParams.oenc) { - this.manager.output.chrEncChange(parseInt(this.uriParams.oenc, 10)); + this.manager.output.chrEncChange(parseInt(this.uriParams.oenc, 10), true); } // Input EOL sequence if (this.uriParams.ieol) { - this.manager.input.eolChange(this.uriParams.ieol); + this.manager.input.eolChange(this.uriParams.ieol, true); } // Output EOL sequence if (this.uriParams.oeol) { - this.manager.output.eolChange(this.uriParams.oeol); + this.manager.output.eolChange(this.uriParams.oeol, true); } // Read in input data from URI params diff --git a/src/web/stylesheets/components/_operation.css b/src/web/stylesheets/components/_operation.css index 685a368a..a97fed70 100755 --- a/src/web/stylesheets/components/_operation.css +++ b/src/web/stylesheets/components/_operation.css @@ -69,6 +69,10 @@ select.arg { min-width: 100px; } +select.arg.form-control:not([size]):not([multiple]), select.custom-file-control:not([size]):not([multiple]) { + height: 100% !important; +} + textarea.arg { min-height: 74px; resize: vertical; @@ -80,7 +84,7 @@ div.toggle-string { input.toggle-string { border-top-right-radius: 0 !important; - height: 42px !important; + height: 100%; } .operation [class^='bmd-label'], diff --git a/src/web/utils/statusBar.mjs b/src/web/utils/statusBar.mjs index 69c4dd51..1adcd5be 100644 --- a/src/web/utils/statusBar.mjs +++ b/src/web/utils/statusBar.mjs @@ -24,6 +24,8 @@ class StatusBarPanel { this.eolHandler = opts.eolHandler; this.chrEncHandler = opts.chrEncHandler; this.chrEncGetter = opts.chrEncGetter; + this.getEncodingState = opts.getEncodingState; + this.getEOLState = opts.getEOLState; this.htmlOutput = opts.htmlOutput; this.eolVal = null; @@ -115,7 +117,7 @@ class StatusBarPanel { if (isNaN(chrEncVal)) return; - this.chrEncHandler(chrEncVal); + this.chrEncHandler(chrEncVal, true); this.updateCharEnc(chrEncVal); hideElement(e.target.closest(".cm-status-bar-select-content")); } @@ -212,12 +214,31 @@ class StatusBarPanel { * @param {EditorState} state */ updateEOL(state) { - if (state.lineBreak === this.eolVal) return; + if (this.getEOLState() < 2 && state.lineBreak === this.eolVal) return; const val = this.dom.querySelector(".eol-value"); const button = val.closest(".cm-status-bar-select-btn"); - const eolCode = eolSeqToCode[state.lineBreak]; - const eolName = eolCodeToName[eolCode]; + let eolCode = eolSeqToCode[state.lineBreak]; + let eolName = eolCodeToName[eolCode]; + + switch (this.getEOLState()) { + case 1: // Detected + val.classList.add("font-italic"); + eolCode += " (detected)"; + eolName += " (detected)"; + // Pulse + val.classList.add("pulse"); + setTimeout(() => { + val.classList.remove("pulse"); + }, 2000); + break; + case 0: // Unset + case 2: // Manually set + default: + val.classList.remove("font-italic"); + break; + } + val.textContent = eolCode; button.setAttribute("title", `End of line sequence:
${eolName}`); button.setAttribute("data-original-title", `End of line sequence:
${eolName}`); @@ -230,12 +251,30 @@ class StatusBarPanel { */ updateCharEnc() { const chrEncVal = this.chrEncGetter(); - if (chrEncVal === this.chrEncVal) return; + if (this.getEncodingState() < 2 && chrEncVal === this.chrEncVal) return; - const name = CHR_ENC_SIMPLE_REVERSE_LOOKUP[chrEncVal] ? CHR_ENC_SIMPLE_REVERSE_LOOKUP[chrEncVal] : "Raw Bytes"; + let name = CHR_ENC_SIMPLE_REVERSE_LOOKUP[chrEncVal] ? CHR_ENC_SIMPLE_REVERSE_LOOKUP[chrEncVal] : "Raw Bytes"; const val = this.dom.querySelector(".chr-enc-value"); const button = val.closest(".cm-status-bar-select-btn"); + + switch (this.getEncodingState()) { + case 1: // Detected + val.classList.add("font-italic"); + name += " (detected)"; + // Pulse + val.classList.add("pulse"); + setTimeout(() => { + val.classList.remove("pulse"); + }, 2000); + break; + case 0: // Unset + case 2: // Manually set + default: + val.classList.remove("font-italic"); + break; + } + val.textContent = name; button.setAttribute("title", `${this.label} character encoding:
${name}`); button.setAttribute("data-original-title", `${this.label} character encoding:
${name}`); diff --git a/src/web/waiters/InputWaiter.mjs b/src/web/waiters/InputWaiter.mjs index ad8eb38c..bffca98c 100644 --- a/src/web/waiters/InputWaiter.mjs +++ b/src/web/waiters/InputWaiter.mjs @@ -62,7 +62,8 @@ class InputWaiter { this.inputTextEl = document.getElementById("input-text"); this.inputChrEnc = 0; - this.eolSetManually = false; + this.eolState = 0; // 0 = unset, 1 = detected, 2 = manual + this.encodingState = 0; // 0 = unset, 1 = detected, 2 = manual this.initEditor(); this.inputWorker = null; @@ -116,7 +117,9 @@ class InputWaiter { label: "Input", eolHandler: this.eolChange.bind(this), chrEncHandler: this.chrEncChange.bind(this), - chrEncGetter: this.getChrEnc.bind(this) + chrEncGetter: this.getChrEnc.bind(this), + getEncodingState: this.getEncodingState.bind(this), + getEOLState: this.getEOLState.bind(this) }), // Mutable state @@ -156,6 +159,8 @@ class InputWaiter { ] }); + + if (this.inputEditorView) this.inputEditorView.destroy(); this.inputEditorView = new EditorView({ state: initialState, parent: this.inputTextEl @@ -166,30 +171,18 @@ class InputWaiter { * Handler for EOL change events * Sets the line separator * @param {string} eol - * @param {boolean} manual - a flag for whether this was set by the user or automatically + * @param {boolean} [manual=false] */ eolChange(eol, manual=false) { const eolVal = eolCodeToSeq[eol]; if (eolVal === undefined) return; - const eolBtn = document.querySelector("#input-text .eol-value"); - if (manual) { - this.eolSetManually = true; - eolBtn.classList.remove("font-italic"); - } else { - eolBtn.classList.add("font-italic"); - } + this.eolState = manual ? 2 : this.eolState; + if (this.eolState < 2 && eolVal === this.getEOLSeq()) return; - if (eolVal === this.getEOLSeq()) return; - - if (!manual) { - // Pulse - eolBtn.classList.add("pulse"); - setTimeout(() => { - eolBtn.classList.remove("pulse"); - }, 2000); + if (this.eolState === 1) { // Alert - this.app.alert(`Input EOL separator has been changed to ${eolCodeToName[eol]}`, 5000); + this.app.alert(`Input end of line separator has been detected and changed to ${eolCodeToName[eol]}`, 5000); } // Update the EOL value @@ -210,14 +203,24 @@ class InputWaiter { return this.inputEditorView.state.lineBreak; } + /** + * Returns whether the input EOL sequence was set manually or has been detected automatically + * @returns {number} - 0 = unset, 1 = detected, 2 = manual + */ + getEOLState() { + return this.eolState; + } + /** * Handler for Chr Enc change events * Sets the input character encoding * @param {number} chrEncVal + * @param {boolean} [manual=false] */ - chrEncChange(chrEncVal) { + chrEncChange(chrEncVal, manual=false) { if (typeof chrEncVal !== "number") return; this.inputChrEnc = chrEncVal; + this.encodingState = manual ? 2 : this.encodingState; this.inputChange(); } @@ -229,6 +232,14 @@ class InputWaiter { return this.inputChrEnc; } + /** + * Returns whether the input character encoding was set manually or has been detected automatically + * @returns {number} - 0 = unset, 1 = detected, 2 = manual + */ + getEncodingState() { + return this.encodingState; + } + /** * Sets word wrap on the input editor * @param {boolean} wrap @@ -908,7 +919,7 @@ class InputWaiter { */ afterPaste(e) { // If EOL has been fixed, skip this. - if (this.eolSetManually) return; + if (this.eolState > 1) return; const inputText = this.getInput(); @@ -930,17 +941,23 @@ class InputWaiter { }, 0); if (total === 0) return; - // If CRLF not zero and more than half the highest alternative, choose CRLF + // Find most prevalent line ending sequence const highest = Object.entries(eolCharCounts).reduce((acc, curr) => { return curr[1] > acc[1] ? curr : acc; }, ["LF", 0]); + let choice = highest[0]; + + // If CRLF not zero and more than half the highest alternative, choose CRLF if ((eolCharCounts.CRLF * 2) > highest[1]) { - this.eolChange("CRLF"); - return; + choice = "CRLF"; } - // Else choose max - this.eolChange(highest[0]); + const eolVal = eolCodeToSeq[choice]; + if (eolVal === this.getEOLSeq()) return; + + // Setting automatically + this.eolState = 1; + this.eolChange(choice); } /** @@ -1276,8 +1293,13 @@ class InputWaiter { this.manager.output.removeAllOutputs(); this.manager.output.terminateZipWorker(); - this.eolSetManually = false; - this.manager.output.eolSetManually = false; + this.eolState = 0; + this.encodingState = 0; + this.manager.output.eolState = 0; + this.manager.output.encodingState = 0; + + this.initEditor(); + this.manager.output.initEditor(); const tabsList = document.getElementById("input-tabs"); const tabsListChildren = tabsList.children; diff --git a/src/web/waiters/OutputWaiter.mjs b/src/web/waiters/OutputWaiter.mjs index 6acd6752..190d2ad9 100755 --- a/src/web/waiters/OutputWaiter.mjs +++ b/src/web/waiters/OutputWaiter.mjs @@ -7,6 +7,7 @@ import Utils, {debounce} from "../../core/Utils.mjs"; import Dish from "../../core/Dish.mjs"; +import {isUTF8, CHR_ENC_SIMPLE_REVERSE_LOOKUP} from "../../core/lib/ChrEnc.mjs"; import {detectFileType} from "../../core/lib/FileType.mjs"; import FileSaver from "file-saver"; import ZipWorker from "worker-loader?inline=no-fallback!../workers/ZipWorker.mjs"; @@ -70,7 +71,8 @@ class OutputWaiter { this.zipWorker = null; this.maxTabs = this.manager.tabs.calcMaxTabs(); this.tabTimeout = null; - this.eolSetManually = false; + this.eolState = 0; // 0 = unset, 1 = detected, 2 = manual + this.encodingState = 0; // 0 = unset, 1 = detected, 2 = manual } /** @@ -110,6 +112,8 @@ class OutputWaiter { eolHandler: this.eolChange.bind(this), chrEncHandler: this.chrEncChange.bind(this), chrEncGetter: this.getChrEnc.bind(this), + getEncodingState: this.getEncodingState.bind(this), + getEOLState: this.getEOLState.bind(this), htmlOutput: this.htmlOutput }), htmlPlugin(this.htmlOutput), @@ -138,6 +142,7 @@ class OutputWaiter { ] }); + if (this.outputEditorView) this.outputEditorView.destroy(); this.outputEditorView = new EditorView({ state: initialState, parent: this.outputTextEl @@ -148,30 +153,18 @@ class OutputWaiter { * Handler for EOL change events * Sets the line separator * @param {string} eol - * @param {boolean} manual - a flag for whether this was set by the user or automatically + * @param {boolean} [manual=false] */ async eolChange(eol, manual=false) { const eolVal = eolCodeToSeq[eol]; if (eolVal === undefined) return; - const eolBtn = document.querySelector("#output-text .eol-value"); - if (manual) { - this.eolSetManually = true; - eolBtn.classList.remove("font-italic"); - } else { - eolBtn.classList.add("font-italic"); - } + this.eolState = manual ? 2 : this.eolState; + if (this.eolState < 2 && eolVal === this.getEOLSeq()) return; - if (eolVal === this.getEOLSeq()) return; - - if (!manual) { - // Pulse - eolBtn.classList.add("pulse"); - setTimeout(() => { - eolBtn.classList.remove("pulse"); - }, 2000); + if (this.eolState === 1) { // Alert - this.app.alert(`Output EOL separator has been changed to ${eolCodeToName[eol]}`, 5000); + this.app.alert(`Output end of line separator has been detected and changed to ${eolCodeToName[eol]}`, 5000); } const currentTabNum = this.manager.tabs.getActiveTab("output"); @@ -205,13 +198,23 @@ class OutputWaiter { return this.outputs[currentTabNum].eolSequence; } + /** + * Returns whether the output EOL sequence was set manually or has been detected automatically + * @returns {number} - 0 = unset, 1 = detected, 2 = manual + */ + getEOLState() { + return this.eolState; + } + /** * Handler for Chr Enc change events * Sets the output character encoding * @param {number} chrEncVal + * @param {boolean} [manual=false] */ - async chrEncChange(chrEncVal) { + async chrEncChange(chrEncVal, manual=false) { if (typeof chrEncVal !== "number") return; + const currentEnc = this.getChrEnc(); const currentTabNum = this.manager.tabs.getActiveTab("output"); if (currentTabNum >= 0) { @@ -220,10 +223,17 @@ class OutputWaiter { throw new Error(`Cannot change output ${currentTabNum} chrEnc to ${chrEncVal}`); } - // Reset the output, forcing it to re-decode the data with the new character encoding - await this.setOutput(this.currentOutputCache, true); - // Update the URL manually since we aren't firing a statechange event - this.app.updateURL(true); + this.encodingState = manual ? 2 : this.encodingState; + + if (this.encodingState > 1) { + // Reset the output, forcing it to re-decode the data with the new character encoding + await this.setOutput(this.currentOutputCache, true); + // Update the URL manually since we aren't firing a statechange event + this.app.updateURL(true); + } else if (currentEnc !== chrEncVal) { + // Alert + this.app.alert(`Output character encoding has been detected and changed to ${CHR_ENC_SIMPLE_REVERSE_LOOKUP[chrEncVal] || "Raw Bytes"}`, 5000); + } } /** @@ -238,6 +248,14 @@ class OutputWaiter { return this.outputs[currentTabNum].encoding; } + /** + * Returns whether the output character encoding was set manually or has been detected automatically + * @returns {number} - 0 = unset, 1 = detected, 2 = manual + */ + getEncodingState() { + return this.encodingState; + } + /** * Sets word wrap on the output editor * @param {boolean} wrap @@ -273,6 +291,7 @@ class OutputWaiter { const tabNum = this.manager.tabs.getActiveTab("output"); this.manager.timing.recordTime("outputDecodingStart", tabNum); if (data instanceof ArrayBuffer) { + await this.detectEncoding(data); data = await this.bufferToStr(data); } this.manager.timing.recordTime("outputDecodingEnd", tabNum); @@ -380,7 +399,7 @@ class OutputWaiter { */ detectEOLSequence(data) { // If EOL has been fixed, skip this. - if (this.eolSetManually) return; + if (this.eolState > 1) return; // If data is too long, skip this. if (data.length > 1000000) return; @@ -402,17 +421,54 @@ class OutputWaiter { }, 0); if (total === 0) return; - // If CRLF not zero and more than half the highest alternative, choose CRLF + // Find most prevalent line ending sequence const highest = Object.entries(eolCharCounts).reduce((acc, curr) => { return curr[1] > acc[1] ? curr : acc; }, ["LF", 0]); + let choice = highest[0]; + + // If CRLF not zero and more than half the highest alternative, choose CRLF if ((eolCharCounts.CRLF * 2) > highest[1]) { - this.eolChange("CRLF"); - return; + choice = "CRLF"; } - // Else choose max - this.eolChange(highest[0]); + const eolVal = eolCodeToSeq[choice]; + if (eolVal === this.getEOLSeq()) return; + + // Setting automatically + this.eolState = 1; + this.eolChange(choice); + } + + /** + * Checks whether the character encoding should be updated. + * + * @param {ArrayBuffer} data + */ + async detectEncoding(data) { + // If encoding has been fixed, skip this. + if (this.encodingState > 1) return; + // If data is too long, skip this. + if (data.byteLength > 1000000) return; + + const enc = isUTF8(data); // 0 = not UTF8, 1 = ASCII, 2 = UTF8 + + switch (enc) { + case 0: // not UTF8 + // Set to Raw Bytes + this.encodingState = 1; + await this.chrEncChange(0, false); + break; + case 2: // UTF8 + // Set to UTF8 + this.encodingState = 1; + await this.chrEncChange(65001, false); + break; + case 1: // ASCII + default: + // Ignore + break; + } } /** From e4077fb63b587c74df6af31b828a8518d8aeb9bc Mon Sep 17 00:00:00 2001 From: n1474335 Date: Tue, 26 Mar 2024 14:38:27 +0000 Subject: [PATCH 06/22] Lint and dependency update --- package-lock.json | 317 ++++++++++++++++++++++++++------ package.json | 2 +- tests/operations/tests/RAKE.mjs | 2 +- 3 files changed, 260 insertions(+), 61 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5e6671a5..c273f189 100644 --- a/package-lock.json +++ b/package-lock.json @@ -114,7 +114,7 @@ "babel-plugin-dynamic-import-node": "^2.3.3", "babel-plugin-transform-builtin-extend": "1.1.2", "base64-loader": "^1.0.0", - "chromedriver": "^121.0.0", + "chromedriver": "^123.0.0", "cli-progress": "^3.12.0", "colors": "^1.4.0", "copy-webpack-plugin": "^12.0.2", @@ -2830,6 +2830,12 @@ "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true + }, "node_modules/@types/body-parser": { "version": "1.19.2", "dev": true, @@ -3242,15 +3248,15 @@ } }, "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", "dev": true, "dependencies": { - "debug": "4" + "debug": "^4.3.4" }, "engines": { - "node": ">= 6.0.0" + "node": ">= 14" } }, "node_modules/ajv": { @@ -3601,6 +3607,18 @@ "node": "*" } }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/async": { "version": "3.2.3", "dev": true, @@ -3976,6 +3994,15 @@ "node": ">= 0.8" } }, + "node_modules/basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/batch": { "version": "0.6.1", "dev": true, @@ -4708,17 +4735,17 @@ } }, "node_modules/chromedriver": { - "version": "121.0.0", - "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-121.0.0.tgz", - "integrity": "sha512-ZIKEdZrQAfuzT/RRofjl8/EZR99ghbdBXNTOcgJMKGP6N/UL6lHUX4n6ONWBV18pDvDFfQJ0x58h5AdOaXIOMw==", + "version": "123.0.0", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-123.0.0.tgz", + "integrity": "sha512-OE9mpxXwbFy5LncAisqXm1aEzuLPtEMORIxyYIn9uT7N8rquJWyoip6w9Rytub3o2gnynW9+PFOTPVTldaYrtw==", "dev": true, "hasInstallScript": true, "dependencies": { "@testim/chrome-version": "^1.1.4", - "axios": "^1.6.5", + "axios": "^1.6.7", "compare-versions": "^6.1.0", "extract-zip": "^2.0.1", - "https-proxy-agent": "^5.0.1", + "proxy-agent": "^6.4.0", "proxy-from-env": "^1.1.0", "tcp-port-used": "^1.0.2" }, @@ -5817,6 +5844,15 @@ "node": ">=12" } }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, "node_modules/data-urls": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", @@ -5975,6 +6011,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dev": true, + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/delaunator": { "version": "5.0.0", "license": "ISC", @@ -7408,6 +7458,29 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "dev": true }, + "node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs-extra/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/fs-monkey": { "version": "1.0.3", "dev": true, @@ -7511,6 +7584,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-uri": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz", + "integrity": "sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==", + "dev": true, + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4", + "fs-extra": "^11.2.0" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/getobject": { "version": "1.0.2", "dev": true, @@ -8603,9 +8691,9 @@ } }, "node_modules/http-proxy-agent": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", - "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, "dependencies": { "agent-base": "^7.1.0", @@ -8615,18 +8703,6 @@ "node": ">= 14" } }, - "node_modules/http-proxy-agent/node_modules/agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", - "dev": true, - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/http-proxy-middleware": { "version": "2.0.4", "dev": true, @@ -8667,16 +8743,16 @@ "license": "MIT" }, "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", "dev": true, "dependencies": { - "agent-base": "6", + "agent-base": "^7.0.2", "debug": "4" }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/human-signals": { @@ -8875,6 +8951,19 @@ "loose-envify": "^1.0.0" } }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dev": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/ip-regex": { "version": "4.3.0", "dev": true, @@ -9502,6 +9591,12 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true + }, "node_modules/jsdom": { "version": "23.2.0", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-23.2.0.tgz", @@ -9542,31 +9637,6 @@ } } }, - "node_modules/jsdom/node_modules/agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", - "dev": true, - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/jsdom/node_modules/https-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", - "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", - "dev": true, - "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/jsesc": { "version": "3.0.2", "license": "MIT", @@ -9603,6 +9673,27 @@ "node": ">=6" } }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonfile/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/jsonpath-plus": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-8.0.0.tgz", @@ -10696,6 +10787,15 @@ "dev": true, "license": "MIT" }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/ngeohash": { "version": "0.6.3", "license": "MIT", @@ -11488,6 +11588,38 @@ "node": ">= 4" } }, + "node_modules/pac-proxy-agent": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.1.tgz", + "integrity": "sha512-ASV8yU4LLKBAjqIPMbrgtaKIvxQri/yh2OpI+S6hVa9JRkUI3Y3NPFbfngDtY7oFtSMD3w31Xns89mDa3Feo5A==", + "dev": true, + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.2", + "pac-resolver": "^7.0.0", + "socks-proxy-agent": "^8.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dev": true, + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/pad-stream": { "version": "2.0.0", "dev": true, @@ -12259,6 +12391,34 @@ "node": ">= 0.10" } }, + "node_modules/proxy-agent": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -13192,6 +13352,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, "node_modules/snackbarjs": { "version": "1.1.0", "license": "ISC" @@ -13217,6 +13387,34 @@ "node": ">=0.8.0" } }, + "node_modules/socks": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.1.tgz", + "integrity": "sha512-B6w7tkwNid7ToxjZ08rQMT8M9BJAf8DKx8Ft4NivzH0zBUfd6jldGcisJn/RLgxcX3FPNDdNQCUEMMT79b+oCQ==", + "dev": true, + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.2.tgz", + "integrity": "sha512-8zuqoLv1aP/66PHF5TqwJ7Czm3Yv32urJQHrVyhD7mmA6d61Zv8cIXQYPTWwmg6qlupnPvs/QKDmfa4P/qct2g==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "socks": "^2.7.1" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/sortablejs": { "version": "1.15.2", "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.2.tgz", @@ -13288,9 +13486,10 @@ } }, "node_modules/sprintf-js": { - "version": "1.1.2", - "dev": true, - "license": "BSD-3-Clause" + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true }, "node_modules/ssdeep.js": { "version": "0.0.3" diff --git a/package.json b/package.json index 334d88b5..65cbdc49 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "babel-plugin-dynamic-import-node": "^2.3.3", "babel-plugin-transform-builtin-extend": "1.1.2", "base64-loader": "^1.0.0", - "chromedriver": "^121.0.0", + "chromedriver": "^123.0.0", "cli-progress": "^3.12.0", "colors": "^1.4.0", "copy-webpack-plugin": "^12.0.2", diff --git a/tests/operations/tests/RAKE.mjs b/tests/operations/tests/RAKE.mjs index 8164ca01..fe718076 100644 --- a/tests/operations/tests/RAKE.mjs +++ b/tests/operations/tests/RAKE.mjs @@ -1,6 +1,6 @@ /** * RAKE, Rapid Automatic Keyword Extraction tests. - * + * * @author sw5678 * @copyright Crown Copyright 2024 * @license Apache-2.0 From 70ff3a52ca0fb78b1bd818bb3a7cc5ab292176aa Mon Sep 17 00:00:00 2001 From: n1474335 Date: Tue, 26 Mar 2024 14:40:33 +0000 Subject: [PATCH 07/22] Updated CHANGELOG --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 145d1b14..d23d8d0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ All major and minor version changes will be documented in this file. Details of ## Details +### [10.9.0] - 2024-03-26 +- Line ending sequences and UTF-8 character encoding are now detected automatically [@n1474335] | [65ffd8d] + ### [10.8.0] - 2024-02-13 - Add official Docker images [@AshCorr] | [#1699] @@ -386,6 +389,7 @@ All major and minor version changes will be documented in this file. Details of ## [4.0.0] - 2016-11-28 - Initial open source commit [@n1474335] | [b1d73a72](https://github.com/gchq/CyberChef/commit/b1d73a725dc7ab9fb7eb789296efd2b7e4b08306) +[10.9.0]: https://github.com/gchq/CyberChef/releases/tag/v10.9.0 [10.8.0]: https://github.com/gchq/CyberChef/releases/tag/v10.7.0 [10.7.0]: https://github.com/gchq/CyberChef/releases/tag/v10.7.0 [10.6.0]: https://github.com/gchq/CyberChef/releases/tag/v10.6.0 @@ -561,6 +565,7 @@ All major and minor version changes will be documented in this file. Details of [a895d1d]: https://github.com/gchq/CyberChef/commit/a895d1d82a2f92d440a0c5eca2bc7c898107b737 [31a7f83]: https://github.com/gchq/CyberChef/commit/31a7f83b82e78927f89689f323fcb9185144d6ff [760eff4]: https://github.com/gchq/CyberChef/commit/760eff49b5307aaa3104c5e5b437ffe62299acd1 +[65ffd8d]: https://github.com/gchq/CyberChef/commit/65ffd8d65d88eb369f6f61a5d1d0f807179bffb7 [#95]: https://github.com/gchq/CyberChef/pull/299 [#173]: https://github.com/gchq/CyberChef/pull/173 From 762cf3ca41ca83a52bc218f282130bc078a249df Mon Sep 17 00:00:00 2001 From: n1474335 Date: Tue, 26 Mar 2024 14:40:39 +0000 Subject: [PATCH 08/22] 10.9.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index c273f189..e7a2990d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cyberchef", - "version": "10.8.2", + "version": "10.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cyberchef", - "version": "10.8.2", + "version": "10.9.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 65cbdc49..148abdc2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cyberchef", - "version": "10.8.2", + "version": "10.9.0", "description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.", "author": "n1474335 ", "homepage": "https://gchq.github.io/CyberChef", From 64111b8b7b548ed96f879ec07a3e7a8dfad94fde Mon Sep 17 00:00:00 2001 From: n1474335 Date: Tue, 26 Mar 2024 14:57:58 +0000 Subject: [PATCH 09/22] Downgrade chromedriver version for GitHub Actions --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 148abdc2..ca2924c8 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "babel-plugin-dynamic-import-node": "^2.3.3", "babel-plugin-transform-builtin-extend": "1.1.2", "base64-loader": "^1.0.0", - "chromedriver": "^123.0.0", + "chromedriver": "^122.0.0", "cli-progress": "^3.12.0", "colors": "^1.4.0", "copy-webpack-plugin": "^12.0.2", From a5f9a8726bff7bbde2b9fe53ed858f0efe8548fb Mon Sep 17 00:00:00 2001 From: n1474335 Date: Tue, 26 Mar 2024 15:19:35 +0000 Subject: [PATCH 10/22] Fixed erroring test --- package-lock.json | 8 ++++---- tests/browser/00_nightwatch.js | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index e7a2990d..658aef50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -114,7 +114,7 @@ "babel-plugin-dynamic-import-node": "^2.3.3", "babel-plugin-transform-builtin-extend": "1.1.2", "base64-loader": "^1.0.0", - "chromedriver": "^123.0.0", + "chromedriver": "^122.0.0", "cli-progress": "^3.12.0", "colors": "^1.4.0", "copy-webpack-plugin": "^12.0.2", @@ -4735,9 +4735,9 @@ } }, "node_modules/chromedriver": { - "version": "123.0.0", - "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-123.0.0.tgz", - "integrity": "sha512-OE9mpxXwbFy5LncAisqXm1aEzuLPtEMORIxyYIn9uT7N8rquJWyoip6w9Rytub3o2gnynW9+PFOTPVTldaYrtw==", + "version": "122.0.6", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-122.0.6.tgz", + "integrity": "sha512-Q0r+QlUtiJWMQ5HdYaFa0CtBmLFq3n5JWfmq9mOC00UMBvWxku09gUkvBt457QnYfTM/XHqY/HTFOxHvATnTmA==", "dev": true, "hasInstallScript": true, "dependencies": { diff --git a/tests/browser/00_nightwatch.js b/tests/browser/00_nightwatch.js index 2e5688d7..65f0aa23 100644 --- a/tests/browser/00_nightwatch.js +++ b/tests/browser/00_nightwatch.js @@ -232,7 +232,6 @@ module.exports = { browser .waitForElementNotVisible("#snackbar-container") .click("#copy-output") - .waitForElementVisible("#snackbar-container") .waitForElementVisible("#snackbar-container .snackbar-content") .expect.element("#snackbar-container .snackbar-content").text.to.equal("Copied raw output successfully."); From 1f316a2f32b13897e34a8105fd5f2e03854d532a Mon Sep 17 00:00:00 2001 From: n1474335 Date: Tue, 26 Mar 2024 15:37:18 +0000 Subject: [PATCH 11/22] More test tweaking --- tests/browser/00_nightwatch.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/browser/00_nightwatch.js b/tests/browser/00_nightwatch.js index 65f0aa23..6872dead 100644 --- a/tests/browser/00_nightwatch.js +++ b/tests/browser/00_nightwatch.js @@ -232,7 +232,7 @@ module.exports = { browser .waitForElementNotVisible("#snackbar-container") .click("#copy-output") - .waitForElementVisible("#snackbar-container .snackbar-content") + .waitForElementVisible("#snackbar-container .snackbar-content", 5000, 100) .expect.element("#snackbar-container .snackbar-content").text.to.equal("Copied raw output successfully."); // Alert bar disappears after the correct amount of time From f1dcc339b3e175d3d0bc3ddf847b9074647fab09 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Tue, 26 Mar 2024 15:42:21 +0000 Subject: [PATCH 12/22] More test tweaking --- tests/browser/00_nightwatch.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/browser/00_nightwatch.js b/tests/browser/00_nightwatch.js index 6872dead..89ab7eea 100644 --- a/tests/browser/00_nightwatch.js +++ b/tests/browser/00_nightwatch.js @@ -232,7 +232,7 @@ module.exports = { browser .waitForElementNotVisible("#snackbar-container") .click("#copy-output") - .waitForElementVisible("#snackbar-container .snackbar-content", 5000, 100) + .waitForElementVisible("#snackbar-container .snackbar-content", 5000, 100, false) .expect.element("#snackbar-container .snackbar-content").text.to.equal("Copied raw output successfully."); // Alert bar disappears after the correct amount of time From ee77e0a1e4486aab42074a7ef17d6fe77f1e361a Mon Sep 17 00:00:00 2001 From: n1474335 Date: Tue, 26 Mar 2024 16:10:44 +0000 Subject: [PATCH 13/22] More test tweaking --- tests/browser/00_nightwatch.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/browser/00_nightwatch.js b/tests/browser/00_nightwatch.js index 89ab7eea..e64b476b 100644 --- a/tests/browser/00_nightwatch.js +++ b/tests/browser/00_nightwatch.js @@ -194,6 +194,9 @@ module.exports = { // Open category browser + .useCss() + .waitForElementNotVisible("#snackbar-container", 10000) + .useXpath() .click(otherCat) .expect.element(genUUID).to.be.visible; @@ -232,7 +235,7 @@ module.exports = { browser .waitForElementNotVisible("#snackbar-container") .click("#copy-output") - .waitForElementVisible("#snackbar-container .snackbar-content", 5000, 100, false) + .waitForElementVisible("#snackbar-container .snackbar-content") .expect.element("#snackbar-container .snackbar-content").text.to.equal("Copied raw output successfully."); // Alert bar disappears after the correct amount of time From 0026d77b7b5ad8079e1fd58fddbdbc0e6428897c Mon Sep 17 00:00:00 2001 From: n1474335 Date: Tue, 26 Mar 2024 16:34:36 +0000 Subject: [PATCH 14/22] More test tweaking --- src/web/App.mjs | 8 +++++--- tests/browser/browserUtils.js | 2 ++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/web/App.mjs b/src/web/App.mjs index eeae264f..53070e26 100755 --- a/src/web/App.mjs +++ b/src/web/App.mjs @@ -46,6 +46,8 @@ class App { this.appLoaded = false; this.workerLoaded = false; this.waitersLoaded = false; + + this.snackbars = []; } @@ -708,14 +710,14 @@ class App { log.info("[" + time.toLocaleString() + "] " + str); if (silent) return; - this.currentSnackbar = $.snackbar({ + this.snackbars.push($.snackbar({ content: str, timeout: timeout, htmlAllowed: true, onClose: () => { - this.currentSnackbar.remove(); + this.snackbars.shift().remove(); } - }); + })); } diff --git a/tests/browser/browserUtils.js b/tests/browser/browserUtils.js index b73dca91..4a559758 100644 --- a/tests/browser/browserUtils.js +++ b/tests/browser/browserUtils.js @@ -65,6 +65,7 @@ function setChrEnc(browser, io, enc) { io = `#${io}-text`; browser .useCss() + .waitForElementNotVisible("#snackbar-container", 6000) .click(io + " .chr-enc-value") .waitForElementVisible(io + " .chr-enc-select .cm-status-bar-select-scroll") .click("link text", enc) @@ -83,6 +84,7 @@ function setEOLSeq(browser, io, eol) { io = `#${io}-text`; browser .useCss() + .waitForElementNotVisible("#snackbar-container", 6000) .click(io + " .eol-value") .waitForElementVisible(io + " .eol-select .cm-status-bar-select-content") .click(`${io} .cm-status-bar-select-content a[data-val=${eol}]`) From 953861ab30222535fb3ad8a6cc80d7f339c530c7 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Tue, 26 Mar 2024 16:26:17 -0700 Subject: [PATCH 15/22] File signatures for heic/heif, refs #1613 --- src/core/lib/FileSignatures.mjs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/core/lib/FileSignatures.mjs b/src/core/lib/FileSignatures.mjs index 4cba4bc7..69157a1b 100644 --- a/src/core/lib/FileSignatures.mjs +++ b/src/core/lib/FileSignatures.mjs @@ -72,6 +72,27 @@ export const FILE_SIGNATURES = { }, extractor: extractWEBP }, + { + name: "High Efficiency Image File Format", + extension: "heic,heif", + mime: "image/heif", + description: "", + signature: { + 0: 0x00, + 1: 0x00, + 2: 0x00, + // 3 could be 0x24 or 0x18, so skip it + 4: 0x66, // ftypheic + 5: 0x74, + 6: 0x79, + 7: 0x70, + 8: 0x68, + 9: 0x65, + 10: 0x69, + 11: 0x63 + }, + extractor: null + }, { name: "Camera Image File Format", extension: "crw", From ef59634c15ad0fe35e7d631c355709d0184ca600 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Wed, 27 Mar 2024 18:02:17 +0000 Subject: [PATCH 16/22] Added 'JA4 Fingerprint' operation --- src/core/config/Categories.json | 1 + src/core/lib/JA4.mjs | 166 +++++ src/core/lib/Stream.mjs | 17 +- src/core/lib/TLS.mjs | 776 ++++++++++++++++++++++ src/core/operations/JA4Fingerprint.mjs | 73 ++ tests/operations/index.mjs | 1 + tests/operations/tests/JA4Fingerprint.mjs | 55 ++ 7 files changed, 1086 insertions(+), 3 deletions(-) create mode 100644 src/core/lib/JA4.mjs create mode 100644 src/core/lib/TLS.mjs create mode 100644 src/core/operations/JA4Fingerprint.mjs create mode 100644 tests/operations/tests/JA4Fingerprint.mjs diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 8e300c76..9a9e7804 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -231,6 +231,7 @@ "VarInt Decode", "JA3 Fingerprint", "JA3S Fingerprint", + "JA4 Fingerprint", "HASSH Client Fingerprint", "HASSH Server Fingerprint", "Format MAC addresses", diff --git a/src/core/lib/JA4.mjs b/src/core/lib/JA4.mjs new file mode 100644 index 00000000..b0b423a2 --- /dev/null +++ b/src/core/lib/JA4.mjs @@ -0,0 +1,166 @@ +/** + * JA4 resources. + * + * @author n1474335 [n1474335@gmail.com] + * @copyright Crown Copyright 2024 + * @license Apache-2.0 + * + * JA4 Copyright 2023 FoxIO, LLC. + * @license BSD-3-Clause + */ + +import OperationError from "../errors/OperationError.mjs"; +import { parseTLSRecord, parseHighestSupportedVersion, parseFirstALPNValue } from "./TLS.mjs"; +import { toHexFast } from "./Hex.mjs"; +import { runHash } from "./Hash.mjs"; +import Utils from "../Utils.mjs"; + + +/** + * Calculate the JA4 from a given TLS Client Hello Stream + * @param {Uint8Array} bytes + * @returns {string} + */ +export function toJA4(bytes) { + let tlsr = {}; + try { + tlsr = parseTLSRecord(bytes); + } catch (err) { + throw new OperationError("Data is not a valid TLS Client 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; + } + } + 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 + } + + /* SNI + If the SNI extension (0x0000) exists, then the destination of the connection is a domain, or “d” in the fingerprint. + If the SNI does not exist, then the destination is an IP address, or “i”. + */ + let sni = "i"; + for (const ext of tlsr.handshake.value.extensions.value) { + if (ext.type.value === "server_name") { + sni = "d"; + break; + } + } + + /* Number of Ciphers + 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”. Remember, ignore GREASE values. They don’t count. + */ + let cipherLen = 0; + for (const cs of tlsr.handshake.value.cipherSuites.value) { + if (cs.value !== "GREASE") cipherLen++; + } + cipherLen = cipherLen > 99 ? "99" : cipherLen.toString().padStart(2, "0"); + + /* Number of Extensions + Same as counting ciphers. Ignore GREASE. Include SNI and ALPN. + */ + let extLen = 0; + for (const ext of tlsr.handshake.value.extensions.value) { + if (ext.type.value !== "GREASE") extLen++; + } + extLen = extLen > 99 ? "99" : extLen.toString().padStart(2, "0"); + + /* ALPN Extension 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); + break; + } + } + + /* Cipher hash + A 12 character truncated sha256 hash of the list of ciphers sorted in hex order, first 12 characters. + The list is created using the 4 character hex values of the ciphers, lower case, comma delimited, ignoring GREASE. + */ + const originalCiphersList = []; + for (const cs of tlsr.handshake.value.cipherSuites.value) { + if (cs.value !== "GREASE") { + originalCiphersList.push(toHexFast(cs.data)); + } + } + const sortedCiphersList = [...originalCiphersList].sort(); + const sortedCiphersRaw = sortedCiphersList.join(","); + const originalCiphersRaw = originalCiphersList.join(","); + const sortedCiphers = runHash( + "sha256", + Utils.strToArrayBuffer(sortedCiphersRaw) + ).substring(0, 12); + const originalCiphers = runHash( + "sha256", + Utils.strToArrayBuffer(originalCiphersRaw) + ).substring(0, 12); + + /* Extension hash + A 12 character truncated sha256 hash of the list of extensions, sorted by hex value, followed by the list of signature + algorithms, in the order that they appear (not sorted). + The extension list is created using the 4 character hex values of the extensions, lower case, comma delimited, sorted + (not in the order they appear). Ignore the SNI extension (0000) and the ALPN extension (0010) as we’ve already captured + them in the a section of the fingerprint. These values are omitted so that the same application would have the same b + section of the fingerprint regardless of if it were going to a domain, IP, or changing ALPNs. + */ + const originalExtensionsList = []; + let signatureAlgorithms = ""; + for (const ext of tlsr.handshake.value.extensions.value) { + if (ext.type.value !== "GREASE") { + originalExtensionsList.push(toHexFast(ext.type.data)); + } + if (ext.type.value === "signature_algorithms") { + signatureAlgorithms = toHexFast(ext.value.data.slice(2)); + signatureAlgorithms = signatureAlgorithms.replace(/(.{4})/g, "$1,"); + signatureAlgorithms = signatureAlgorithms.substring(0, signatureAlgorithms.length - 1); + } + } + const sortedExtensionsList = [...originalExtensionsList].filter(e => e !== "0000" && e !== "0010").sort(); + const sortedExtensionsRaw = sortedExtensionsList.join(",") + "_" + signatureAlgorithms; + const originalExtensionsRaw = originalExtensionsList.join(",") + "_" + signatureAlgorithms; + const sortedExtensions = runHash( + "sha256", + Utils.strToArrayBuffer(sortedExtensionsRaw) + ).substring(0, 12); + const originalExtensions = runHash( + "sha256", + Utils.strToArrayBuffer(originalExtensionsRaw) + ).substring(0, 12); + + return { + "JA4": `${ptype}${version}${sni}${cipherLen}${extLen}${alpn}_${sortedCiphers}_${sortedExtensions}`, + "JA4_o": `${ptype}${version}${sni}${cipherLen}${extLen}${alpn}_${originalCiphers}_${originalExtensions}`, + "JA4_r": `${ptype}${version}${sni}${cipherLen}${extLen}${alpn}_${sortedCiphersRaw}_${sortedExtensionsRaw}`, + "JA4_ro": `${ptype}${version}${sni}${cipherLen}${extLen}${alpn}_${originalCiphersRaw}_${originalExtensionsRaw}`, + }; +} diff --git a/src/core/lib/Stream.mjs b/src/core/lib/Stream.mjs index 18ce71c3..8253c4cf 100644 --- a/src/core/lib/Stream.mjs +++ b/src/core/lib/Stream.mjs @@ -18,12 +18,23 @@ export default class Stream { * Stream constructor. * * @param {Uint8Array} input + * @param {number} pos + * @param {number} bitPos */ - constructor(input) { + constructor(input, pos=0, bitPos=0) { this.bytes = input; this.length = this.bytes.length; - this.position = 0; - this.bitPos = 0; + this.position = pos; + this.bitPos = bitPos; + } + + /** + * Clone this Stream returning a new identical Stream. + * + * @returns {Stream} + */ + clone() { + return new Stream(this.bytes, this.position, this.bitPos); } /** diff --git a/src/core/lib/TLS.mjs b/src/core/lib/TLS.mjs new file mode 100644 index 00000000..e3f18eb3 --- /dev/null +++ b/src/core/lib/TLS.mjs @@ -0,0 +1,776 @@ +/** + * TLS resources. + * + * @author n1474335 [n1474335@gmail.com] + * @copyright Crown Copyright 2024 + * @license Apache-2.0 + */ + +import OperationError from "../errors/OperationError.mjs"; +import Stream from "../lib/Stream.mjs"; + +/** + * Parse a TLS Record + * @param {Uint8Array} bytes + * @returns {JSON} + */ +export function parseTLSRecord(bytes) { + const s = new Stream(bytes); + const b = s.clone(); + const r = {}; + + // Content type + r.contentType = { + description: "Content Type", + length: 1, + data: b.getBytes(1), + value: s.readInt(1) + }; + if (r.contentType.value !== 0x16) + throw new OperationError("Not handshake data."); + + // Version + r.version = { + description: "Protocol Version", + length: 2, + data: b.getBytes(2), + value: s.readInt(2) + }; + + // Length + r.length = { + description: "Record Length", + length: 2, + data: b.getBytes(2), + value: s.readInt(2) + }; + if (s.length !== r.length.value + 5) + throw new OperationError("Incorrect handshake length."); + + // Handshake + r.handshake = { + description: "Handshake", + length: r.length.value, + data: b.getBytes(r.length.value), + value: parseHandshake(s.getBytes(r.length.value)) + }; + + return r; +} + +/** + * Parse a TLS Handshake + * @param {Uint8Array} bytes + * @returns {JSON} + */ +function parseHandshake(bytes) { + const s = new Stream(bytes); + const b = s.clone(); + const h = {}; + + // Handshake type + h.handshakeType = { + description: "Client Hello", + 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 = { + description: "Handshake Length", + length: 3, + data: b.getBytes(3), + value: s.readInt(3) + }; + if (s.length !== h.handshakeLength.value + 4) + throw new OperationError("Not enough data in Client Hello."); + + // Hello version + h.helloVersion = { + description: "Client Hello Version", + length: 2, + data: b.getBytes(2), + value: s.readInt(2) + }; + + // Random + h.random = { + description: "Client 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 Suites Length + h.cipherSuitesLength = { + description: "Cipher Suites Length", + length: 2, + data: b.getBytes(2), + value: s.readInt(2) + }; + + // Cipher Suites + h.cipherSuites = { + description: "Cipher Suites", + length: h.cipherSuitesLength.value, + data: b.getBytes(h.cipherSuitesLength.value), + value: parseCipherSuites(s.getBytes(h.cipherSuitesLength.value)) + }; + + // Compression Methods Length + h.compressionMethodsLength = { + description: "Compression Methods Length", + length: 1, + data: b.getBytes(1), + value: s.readInt(1) + }; + + // Compression Methods + h.compressionMethods = { + description: "Compression Methods", + length: h.compressionMethodsLength.value, + data: b.getBytes(h.compressionMethodsLength.value), + value: parseCompressionMethods(s.getBytes(h.compressionMethodsLength.value)) + }; + + // 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)) + }; + + return h; +} + +/** + * Parse Cipher Suites + * @param {Uint8Array} bytes + * @returns {JSON} + */ +function parseCipherSuites(bytes) { + const s = new Stream(bytes); + const b = s.clone(); + const cs = []; + + while (s.hasMore()) { + cs.push({ + description: "Cipher Suite", + length: 2, + data: b.getBytes(2), + value: CIPHER_SUITES_LOOKUP[s.readInt(2)] || "Unknown" + }); + } + return cs; +} + +/** + * Parse Compression Methods + * @param {Uint8Array} bytes + * @returns {JSON} + */ +function parseCompressionMethods(bytes) { + const s = new Stream(bytes); + const b = s.clone(); + const cm = []; + + while (s.hasMore()) { + cm.push({ + description: "Compression Method", + length: 1, + data: b.getBytes(1), + value: s.readInt(1) // TODO: Compression method name here + }); + } + return cm; +} + +/** + * Parse Extensions + * @param {Uint8Array} bytes + * @returns {JSON} + */ +function parseExtensions(bytes) { + const s = new Stream(bytes); + const b = s.clone(); + + const exts = []; + while (s.hasMore()) { + const ext = {}; + + // Type + ext.type = { + description: "Extension Type", + length: 2, + data: b.getBytes(2), + value: EXTENSION_LOOKUP[s.readInt(2)] || "unknown" + }; + + // Length + ext.length = { + description: "Extension Length", + length: 2, + data: b.getBytes(2), + value: s.readInt(2) + }; + + // Value + ext.value = { + description: "Extension Value", + length: ext.length.value, + data: b.getBytes(ext.length.value), + value: s.getBytes(ext.length.value) + }; + + exts.push(ext); + } + + return exts; +} + +/** + * Extension type lookup table + */ +const EXTENSION_LOOKUP = { + 0: "server_name", + 1: "max_fragment_length", + 2: "client_certificate_url", + 3: "trusted_ca_keys", + 4: "truncated_hmac", + 5: "status_request", + 6: "user_mapping", + 7: "client_authz", + 8: "server_authz", + 9: "cert_type", + 10: "supported_groups", + 11: "ec_point_formats", + 12: "srp", + 13: "signature_algorithms", + 14: "use_srtp", + 15: "heartbeat", + 16: "application_layer_protocol_negotiation", + 17: "status_request_v2", + 18: "signed_certificate_timestamp", + 19: "client_certificate_type", + 20: "server_certificate_type", + 21: "padding", + 22: "encrypt_then_mac", + 23: "extended_master_secret", + 24: "token_binding", + 25: "cached_info", + 26: "tls_lts", + 27: "compress_certificate", + 28: "record_size_limit", + 29: "pwd_protect", + 30: "pwd_clear", + 31: "password_salt", + 32: "ticket_pinning", + 33: "tls_cert_with_extern_psk", + 34: "delegated_credential", + 35: "session_ticket", + 36: "TLMSP", + 37: "TLMSP_proxying", + 38: "TLMSP_delegate", + 39: "supported_ekt_ciphers", + 40: "Reserved", + 41: "pre_shared_key", + 42: "early_data", + 43: "supported_versions", + 44: "cookie", + 45: "psk_key_exchange_modes", + 46: "Reserved", + 47: "certificate_authorities", + 48: "oid_filters", + 49: "post_handshake_auth", + 50: "signature_algorithms_cert", + 51: "key_share", + 52: "transparency_info", + 53: "connection_id (deprecated)", + 54: "connection_id", + 55: "external_id_hash", + 56: "external_session_id", + 57: "quic_transport_parameters", + 58: "ticket_request", + 59: "dnssec_chain", + 60: "sequence_number_encryption_algorithms", + 61: "rrc", + 2570: "GREASE", + 6682: "GREASE", + 10794: "GREASE", + 14906: "GREASE", + 17513: "application_settings", + 19018: "GREASE", + 23130: "GREASE", + 27242: "GREASE", + 31354: "GREASE", + 35466: "GREASE", + 39578: "GREASE", + 43690: "GREASE", + 47802: "GREASE", + 51914: "GREASE", + 56026: "GREASE", + 60138: "GREASE", + 64250: "GREASE", + 64768: "ech_outer_extensions", + 65037: "encrypted_client_hello", + 65281: "renegotiation_info" +}; + +/** + * Cipher suites lookup table + */ +const CIPHER_SUITES_LOOKUP = { + 0x0000: "TLS_NULL_WITH_NULL_NULL", + 0x0001: "TLS_RSA_WITH_NULL_MD5", + 0x0002: "TLS_RSA_WITH_NULL_SHA", + 0x0003: "TLS_RSA_EXPORT_WITH_RC4_40_MD5", + 0x0004: "TLS_RSA_WITH_RC4_128_MD5", + 0x0005: "TLS_RSA_WITH_RC4_128_SHA", + 0x0006: "TLS_RSA_EXPORT_WITH_RC2_CBC_40_MD5", + 0x0007: "TLS_RSA_WITH_IDEA_CBC_SHA", + 0x0008: "TLS_RSA_EXPORT_WITH_DES40_CBC_SHA", + 0x0009: "TLS_RSA_WITH_DES_CBC_SHA", + 0x000A: "TLS_RSA_WITH_3DES_EDE_CBC_SHA", + 0x000B: "TLS_DH_DSS_EXPORT_WITH_DES40_CBC_SHA", + 0x000C: "TLS_DH_DSS_WITH_DES_CBC_SHA", + 0x000D: "TLS_DH_DSS_WITH_3DES_EDE_CBC_SHA", + 0x000E: "TLS_DH_RSA_EXPORT_WITH_DES40_CBC_SHA", + 0x000F: "TLS_DH_RSA_WITH_DES_CBC_SHA", + 0x0010: "TLS_DH_RSA_WITH_3DES_EDE_CBC_SHA", + 0x0011: "TLS_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA", + 0x0012: "TLS_DHE_DSS_WITH_DES_CBC_SHA", + 0x0013: "TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA", + 0x0014: "TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA", + 0x0015: "TLS_DHE_RSA_WITH_DES_CBC_SHA", + 0x0016: "TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA", + 0x0017: "TLS_DH_anon_EXPORT_WITH_RC4_40_MD5", + 0x0018: "TLS_DH_anon_WITH_RC4_128_MD5", + 0x0019: "TLS_DH_anon_EXPORT_WITH_DES40_CBC_SHA", + 0x001A: "TLS_DH_anon_WITH_DES_CBC_SHA", + 0x001B: "TLS_DH_anon_WITH_3DES_EDE_CBC_SHA", + 0x001E: "TLS_KRB5_WITH_DES_CBC_SHA", + 0x001F: "TLS_KRB5_WITH_3DES_EDE_CBC_SHA", + 0x0020: "TLS_KRB5_WITH_RC4_128_SHA", + 0x0021: "TLS_KRB5_WITH_IDEA_CBC_SHA", + 0x0022: "TLS_KRB5_WITH_DES_CBC_MD5", + 0x0023: "TLS_KRB5_WITH_3DES_EDE_CBC_MD5", + 0x0024: "TLS_KRB5_WITH_RC4_128_MD5", + 0x0025: "TLS_KRB5_WITH_IDEA_CBC_MD5", + 0x0026: "TLS_KRB5_EXPORT_WITH_DES_CBC_40_SHA", + 0x0027: "TLS_KRB5_EXPORT_WITH_RC2_CBC_40_SHA", + 0x0028: "TLS_KRB5_EXPORT_WITH_RC4_40_SHA", + 0x0029: "TLS_KRB5_EXPORT_WITH_DES_CBC_40_MD5", + 0x002A: "TLS_KRB5_EXPORT_WITH_RC2_CBC_40_MD5", + 0x002B: "TLS_KRB5_EXPORT_WITH_RC4_40_MD5", + 0x002C: "TLS_PSK_WITH_NULL_SHA", + 0x002D: "TLS_DHE_PSK_WITH_NULL_SHA", + 0x002E: "TLS_RSA_PSK_WITH_NULL_SHA", + 0x002F: "TLS_RSA_WITH_AES_128_CBC_SHA", + 0x0030: "TLS_DH_DSS_WITH_AES_128_CBC_SHA", + 0x0031: "TLS_DH_RSA_WITH_AES_128_CBC_SHA", + 0x0032: "TLS_DHE_DSS_WITH_AES_128_CBC_SHA", + 0x0033: "TLS_DHE_RSA_WITH_AES_128_CBC_SHA", + 0x0034: "TLS_DH_anon_WITH_AES_128_CBC_SHA", + 0x0035: "TLS_RSA_WITH_AES_256_CBC_SHA", + 0x0036: "TLS_DH_DSS_WITH_AES_256_CBC_SHA", + 0x0037: "TLS_DH_RSA_WITH_AES_256_CBC_SHA", + 0x0038: "TLS_DHE_DSS_WITH_AES_256_CBC_SHA", + 0x0039: "TLS_DHE_RSA_WITH_AES_256_CBC_SHA", + 0x003A: "TLS_DH_anon_WITH_AES_256_CBC_SHA", + 0x003B: "TLS_RSA_WITH_NULL_SHA256", + 0x003C: "TLS_RSA_WITH_AES_128_CBC_SHA256", + 0x003D: "TLS_RSA_WITH_AES_256_CBC_SHA256", + 0x003E: "TLS_DH_DSS_WITH_AES_128_CBC_SHA256", + 0x003F: "TLS_DH_RSA_WITH_AES_128_CBC_SHA256", + 0x0040: "TLS_DHE_DSS_WITH_AES_128_CBC_SHA256", + 0x0041: "TLS_RSA_WITH_CAMELLIA_128_CBC_SHA", + 0x0042: "TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA", + 0x0043: "TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA", + 0x0044: "TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA", + 0x0045: "TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA", + 0x0046: "TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA", + 0x0067: "TLS_DHE_RSA_WITH_AES_128_CBC_SHA256", + 0x0068: "TLS_DH_DSS_WITH_AES_256_CBC_SHA256", + 0x0069: "TLS_DH_RSA_WITH_AES_256_CBC_SHA256", + 0x006A: "TLS_DHE_DSS_WITH_AES_256_CBC_SHA256", + 0x006B: "TLS_DHE_RSA_WITH_AES_256_CBC_SHA256", + 0x006C: "TLS_DH_anon_WITH_AES_128_CBC_SHA256", + 0x006D: "TLS_DH_anon_WITH_AES_256_CBC_SHA256", + 0x0084: "TLS_RSA_WITH_CAMELLIA_256_CBC_SHA", + 0x0085: "TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA", + 0x0086: "TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA", + 0x0087: "TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA", + 0x0088: "TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA", + 0x0089: "TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA", + 0x008A: "TLS_PSK_WITH_RC4_128_SHA", + 0x008B: "TLS_PSK_WITH_3DES_EDE_CBC_SHA", + 0x008C: "TLS_PSK_WITH_AES_128_CBC_SHA", + 0x008D: "TLS_PSK_WITH_AES_256_CBC_SHA", + 0x008E: "TLS_DHE_PSK_WITH_RC4_128_SHA", + 0x008F: "TLS_DHE_PSK_WITH_3DES_EDE_CBC_SHA", + 0x0090: "TLS_DHE_PSK_WITH_AES_128_CBC_SHA", + 0x0091: "TLS_DHE_PSK_WITH_AES_256_CBC_SHA", + 0x0092: "TLS_RSA_PSK_WITH_RC4_128_SHA", + 0x0093: "TLS_RSA_PSK_WITH_3DES_EDE_CBC_SHA", + 0x0094: "TLS_RSA_PSK_WITH_AES_128_CBC_SHA", + 0x0095: "TLS_RSA_PSK_WITH_AES_256_CBC_SHA", + 0x0096: "TLS_RSA_WITH_SEED_CBC_SHA", + 0x0097: "TLS_DH_DSS_WITH_SEED_CBC_SHA", + 0x0098: "TLS_DH_RSA_WITH_SEED_CBC_SHA", + 0x0099: "TLS_DHE_DSS_WITH_SEED_CBC_SHA", + 0x009A: "TLS_DHE_RSA_WITH_SEED_CBC_SHA", + 0x009B: "TLS_DH_anon_WITH_SEED_CBC_SHA", + 0x009C: "TLS_RSA_WITH_AES_128_GCM_SHA256", + 0x009D: "TLS_RSA_WITH_AES_256_GCM_SHA384", + 0x009E: "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", + 0x009F: "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", + 0x00A0: "TLS_DH_RSA_WITH_AES_128_GCM_SHA256", + 0x00A1: "TLS_DH_RSA_WITH_AES_256_GCM_SHA384", + 0x00A2: "TLS_DHE_DSS_WITH_AES_128_GCM_SHA256", + 0x00A3: "TLS_DHE_DSS_WITH_AES_256_GCM_SHA384", + 0x00A4: "TLS_DH_DSS_WITH_AES_128_GCM_SHA256", + 0x00A5: "TLS_DH_DSS_WITH_AES_256_GCM_SHA384", + 0x00A6: "TLS_DH_anon_WITH_AES_128_GCM_SHA256", + 0x00A7: "TLS_DH_anon_WITH_AES_256_GCM_SHA384", + 0x00A8: "TLS_PSK_WITH_AES_128_GCM_SHA256", + 0x00A9: "TLS_PSK_WITH_AES_256_GCM_SHA384", + 0x00AA: "TLS_DHE_PSK_WITH_AES_128_GCM_SHA256", + 0x00AB: "TLS_DHE_PSK_WITH_AES_256_GCM_SHA384", + 0x00AC: "TLS_RSA_PSK_WITH_AES_128_GCM_SHA256", + 0x00AD: "TLS_RSA_PSK_WITH_AES_256_GCM_SHA384", + 0x00AE: "TLS_PSK_WITH_AES_128_CBC_SHA256", + 0x00AF: "TLS_PSK_WITH_AES_256_CBC_SHA384", + 0x00B0: "TLS_PSK_WITH_NULL_SHA256", + 0x00B1: "TLS_PSK_WITH_NULL_SHA384", + 0x00B2: "TLS_DHE_PSK_WITH_AES_128_CBC_SHA256", + 0x00B3: "TLS_DHE_PSK_WITH_AES_256_CBC_SHA384", + 0x00B4: "TLS_DHE_PSK_WITH_NULL_SHA256", + 0x00B5: "TLS_DHE_PSK_WITH_NULL_SHA384", + 0x00B6: "TLS_RSA_PSK_WITH_AES_128_CBC_SHA256", + 0x00B7: "TLS_RSA_PSK_WITH_AES_256_CBC_SHA384", + 0x00B8: "TLS_RSA_PSK_WITH_NULL_SHA256", + 0x00B9: "TLS_RSA_PSK_WITH_NULL_SHA384", + 0x00BA: "TLS_RSA_WITH_CAMELLIA_128_CBC_SHA256", + 0x00BB: "TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA256", + 0x00BC: "TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA256", + 0x00BD: "TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA256", + 0x00BE: "TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256", + 0x00BF: "TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA256", + 0x00C0: "TLS_RSA_WITH_CAMELLIA_256_CBC_SHA256", + 0x00C1: "TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA256", + 0x00C2: "TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA256", + 0x00C3: "TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA256", + 0x00C4: "TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256", + 0x00C5: "TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA256", + 0x00C6: "TLS_SM4_GCM_SM3", + 0x00C7: "TLS_SM4_CCM_SM3", + 0x00FF: "TLS_EMPTY_RENEGOTIATION_INFO_SCSV", + 0x0A0A: "GREASE", + 0x1301: "TLS_AES_128_GCM_SHA256", + 0x1302: "TLS_AES_256_GCM_SHA384", + 0x1303: "TLS_CHACHA20_POLY1305_SHA256", + 0x1304: "TLS_AES_128_CCM_SHA256", + 0x1305: "TLS_AES_128_CCM_8_SHA256", + 0x1306: "TLS_AEGIS_256_SHA512", + 0x1307: "TLS_AEGIS_128L_SHA256", + 0x1A1A: "GREASE", + 0x2A2A: "GREASE", + 0x3A3A: "GREASE", + 0x4A4A: "GREASE", + 0x5600: "TLS_FALLBACK_SCSV", + 0x5A5A: "GREASE", + 0x6A6A: "GREASE", + 0x7A7A: "GREASE", + 0x8A8A: "GREASE", + 0x9A9A: "GREASE", + 0xAAAA: "GREASE", + 0xBABA: "GREASE", + 0xC001: "TLS_ECDH_ECDSA_WITH_NULL_SHA", + 0xC002: "TLS_ECDH_ECDSA_WITH_RC4_128_SHA", + 0xC003: "TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA", + 0xC004: "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA", + 0xC005: "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA", + 0xC006: "TLS_ECDHE_ECDSA_WITH_NULL_SHA", + 0xC007: "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA", + 0xC008: "TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA", + 0xC009: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", + 0xC00A: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", + 0xC00B: "TLS_ECDH_RSA_WITH_NULL_SHA", + 0xC00C: "TLS_ECDH_RSA_WITH_RC4_128_SHA", + 0xC00D: "TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA", + 0xC00E: "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA", + 0xC00F: "TLS_ECDH_RSA_WITH_AES_256_CBC_SHA", + 0xC010: "TLS_ECDHE_RSA_WITH_NULL_SHA", + 0xC011: "TLS_ECDHE_RSA_WITH_RC4_128_SHA", + 0xC012: "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA", + 0xC013: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + 0xC014: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + 0xC015: "TLS_ECDH_anon_WITH_NULL_SHA", + 0xC016: "TLS_ECDH_anon_WITH_RC4_128_SHA", + 0xC017: "TLS_ECDH_anon_WITH_3DES_EDE_CBC_SHA", + 0xC018: "TLS_ECDH_anon_WITH_AES_128_CBC_SHA", + 0xC019: "TLS_ECDH_anon_WITH_AES_256_CBC_SHA", + 0xC01A: "TLS_SRP_SHA_WITH_3DES_EDE_CBC_SHA", + 0xC01B: "TLS_SRP_SHA_RSA_WITH_3DES_EDE_CBC_SHA", + 0xC01C: "TLS_SRP_SHA_DSS_WITH_3DES_EDE_CBC_SHA", + 0xC01D: "TLS_SRP_SHA_WITH_AES_128_CBC_SHA", + 0xC01E: "TLS_SRP_SHA_RSA_WITH_AES_128_CBC_SHA", + 0xC01F: "TLS_SRP_SHA_DSS_WITH_AES_128_CBC_SHA", + 0xC020: "TLS_SRP_SHA_WITH_AES_256_CBC_SHA", + 0xC021: "TLS_SRP_SHA_RSA_WITH_AES_256_CBC_SHA", + 0xC022: "TLS_SRP_SHA_DSS_WITH_AES_256_CBC_SHA", + 0xC023: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", + 0xC024: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384", + 0xC025: "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256", + 0xC026: "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384", + 0xC027: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", + 0xC028: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", + 0xC029: "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256", + 0xC02A: "TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384", + 0xC02B: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + 0xC02C: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + 0xC02D: "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256", + 0xC02E: "TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384", + 0xC02F: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + 0xC030: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + 0xC031: "TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256", + 0xC032: "TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384", + 0xC033: "TLS_ECDHE_PSK_WITH_RC4_128_SHA", + 0xC034: "TLS_ECDHE_PSK_WITH_3DES_EDE_CBC_SHA", + 0xC035: "TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA", + 0xC036: "TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA", + 0xC037: "TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256", + 0xC038: "TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA384", + 0xC039: "TLS_ECDHE_PSK_WITH_NULL_SHA", + 0xC03A: "TLS_ECDHE_PSK_WITH_NULL_SHA256", + 0xC03B: "TLS_ECDHE_PSK_WITH_NULL_SHA384", + 0xC03C: "TLS_RSA_WITH_ARIA_128_CBC_SHA256", + 0xC03D: "TLS_RSA_WITH_ARIA_256_CBC_SHA384", + 0xC03E: "TLS_DH_DSS_WITH_ARIA_128_CBC_SHA256", + 0xC03F: "TLS_DH_DSS_WITH_ARIA_256_CBC_SHA384", + 0xC040: "TLS_DH_RSA_WITH_ARIA_128_CBC_SHA256", + 0xC041: "TLS_DH_RSA_WITH_ARIA_256_CBC_SHA384", + 0xC042: "TLS_DHE_DSS_WITH_ARIA_128_CBC_SHA256", + 0xC043: "TLS_DHE_DSS_WITH_ARIA_256_CBC_SHA384", + 0xC044: "TLS_DHE_RSA_WITH_ARIA_128_CBC_SHA256", + 0xC045: "TLS_DHE_RSA_WITH_ARIA_256_CBC_SHA384", + 0xC046: "TLS_DH_anon_WITH_ARIA_128_CBC_SHA256", + 0xC047: "TLS_DH_anon_WITH_ARIA_256_CBC_SHA384", + 0xC048: "TLS_ECDHE_ECDSA_WITH_ARIA_128_CBC_SHA256", + 0xC049: "TLS_ECDHE_ECDSA_WITH_ARIA_256_CBC_SHA384", + 0xC04A: "TLS_ECDH_ECDSA_WITH_ARIA_128_CBC_SHA256", + 0xC04B: "TLS_ECDH_ECDSA_WITH_ARIA_256_CBC_SHA384", + 0xC04C: "TLS_ECDHE_RSA_WITH_ARIA_128_CBC_SHA256", + 0xC04D: "TLS_ECDHE_RSA_WITH_ARIA_256_CBC_SHA384", + 0xC04E: "TLS_ECDH_RSA_WITH_ARIA_128_CBC_SHA256", + 0xC04F: "TLS_ECDH_RSA_WITH_ARIA_256_CBC_SHA384", + 0xC050: "TLS_RSA_WITH_ARIA_128_GCM_SHA256", + 0xC051: "TLS_RSA_WITH_ARIA_256_GCM_SHA384", + 0xC052: "TLS_DHE_RSA_WITH_ARIA_128_GCM_SHA256", + 0xC053: "TLS_DHE_RSA_WITH_ARIA_256_GCM_SHA384", + 0xC054: "TLS_DH_RSA_WITH_ARIA_128_GCM_SHA256", + 0xC055: "TLS_DH_RSA_WITH_ARIA_256_GCM_SHA384", + 0xC056: "TLS_DHE_DSS_WITH_ARIA_128_GCM_SHA256", + 0xC057: "TLS_DHE_DSS_WITH_ARIA_256_GCM_SHA384", + 0xC058: "TLS_DH_DSS_WITH_ARIA_128_GCM_SHA256", + 0xC059: "TLS_DH_DSS_WITH_ARIA_256_GCM_SHA384", + 0xC05A: "TLS_DH_anon_WITH_ARIA_128_GCM_SHA256", + 0xC05B: "TLS_DH_anon_WITH_ARIA_256_GCM_SHA384", + 0xC05C: "TLS_ECDHE_ECDSA_WITH_ARIA_128_GCM_SHA256", + 0xC05D: "TLS_ECDHE_ECDSA_WITH_ARIA_256_GCM_SHA384", + 0xC05E: "TLS_ECDH_ECDSA_WITH_ARIA_128_GCM_SHA256", + 0xC05F: "TLS_ECDH_ECDSA_WITH_ARIA_256_GCM_SHA384", + 0xC060: "TLS_ECDHE_RSA_WITH_ARIA_128_GCM_SHA256", + 0xC061: "TLS_ECDHE_RSA_WITH_ARIA_256_GCM_SHA384", + 0xC062: "TLS_ECDH_RSA_WITH_ARIA_128_GCM_SHA256", + 0xC063: "TLS_ECDH_RSA_WITH_ARIA_256_GCM_SHA384", + 0xC064: "TLS_PSK_WITH_ARIA_128_CBC_SHA256", + 0xC065: "TLS_PSK_WITH_ARIA_256_CBC_SHA384", + 0xC066: "TLS_DHE_PSK_WITH_ARIA_128_CBC_SHA256", + 0xC067: "TLS_DHE_PSK_WITH_ARIA_256_CBC_SHA384", + 0xC068: "TLS_RSA_PSK_WITH_ARIA_128_CBC_SHA256", + 0xC069: "TLS_RSA_PSK_WITH_ARIA_256_CBC_SHA384", + 0xC06A: "TLS_PSK_WITH_ARIA_128_GCM_SHA256", + 0xC06B: "TLS_PSK_WITH_ARIA_256_GCM_SHA384", + 0xC06C: "TLS_DHE_PSK_WITH_ARIA_128_GCM_SHA256", + 0xC06D: "TLS_DHE_PSK_WITH_ARIA_256_GCM_SHA384", + 0xC06E: "TLS_RSA_PSK_WITH_ARIA_128_GCM_SHA256", + 0xC06F: "TLS_RSA_PSK_WITH_ARIA_256_GCM_SHA384", + 0xC070: "TLS_ECDHE_PSK_WITH_ARIA_128_CBC_SHA256", + 0xC071: "TLS_ECDHE_PSK_WITH_ARIA_256_CBC_SHA384", + 0xC072: "TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_CBC_SHA256", + 0xC073: "TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_CBC_SHA384", + 0xC074: "TLS_ECDH_ECDSA_WITH_CAMELLIA_128_CBC_SHA256", + 0xC075: "TLS_ECDH_ECDSA_WITH_CAMELLIA_256_CBC_SHA384", + 0xC076: "TLS_ECDHE_RSA_WITH_CAMELLIA_128_CBC_SHA256", + 0xC077: "TLS_ECDHE_RSA_WITH_CAMELLIA_256_CBC_SHA384", + 0xC078: "TLS_ECDH_RSA_WITH_CAMELLIA_128_CBC_SHA256", + 0xC079: "TLS_ECDH_RSA_WITH_CAMELLIA_256_CBC_SHA384", + 0xC07A: "TLS_RSA_WITH_CAMELLIA_128_GCM_SHA256", + 0xC07B: "TLS_RSA_WITH_CAMELLIA_256_GCM_SHA384", + 0xC07C: "TLS_DHE_RSA_WITH_CAMELLIA_128_GCM_SHA256", + 0xC07D: "TLS_DHE_RSA_WITH_CAMELLIA_256_GCM_SHA384", + 0xC07E: "TLS_DH_RSA_WITH_CAMELLIA_128_GCM_SHA256", + 0xC07F: "TLS_DH_RSA_WITH_CAMELLIA_256_GCM_SHA384", + 0xC080: "TLS_DHE_DSS_WITH_CAMELLIA_128_GCM_SHA256", + 0xC081: "TLS_DHE_DSS_WITH_CAMELLIA_256_GCM_SHA384", + 0xC082: "TLS_DH_DSS_WITH_CAMELLIA_128_GCM_SHA256", + 0xC083: "TLS_DH_DSS_WITH_CAMELLIA_256_GCM_SHA384", + 0xC084: "TLS_DH_anon_WITH_CAMELLIA_128_GCM_SHA256", + 0xC085: "TLS_DH_anon_WITH_CAMELLIA_256_GCM_SHA384", + 0xC086: "TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_GCM_SHA256", + 0xC087: "TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_GCM_SHA384", + 0xC088: "TLS_ECDH_ECDSA_WITH_CAMELLIA_128_GCM_SHA256", + 0xC089: "TLS_ECDH_ECDSA_WITH_CAMELLIA_256_GCM_SHA384", + 0xC08A: "TLS_ECDHE_RSA_WITH_CAMELLIA_128_GCM_SHA256", + 0xC08B: "TLS_ECDHE_RSA_WITH_CAMELLIA_256_GCM_SHA384", + 0xC08C: "TLS_ECDH_RSA_WITH_CAMELLIA_128_GCM_SHA256", + 0xC08D: "TLS_ECDH_RSA_WITH_CAMELLIA_256_GCM_SHA384", + 0xC08E: "TLS_PSK_WITH_CAMELLIA_128_GCM_SHA256", + 0xC08F: "TLS_PSK_WITH_CAMELLIA_256_GCM_SHA384", + 0xC090: "TLS_DHE_PSK_WITH_CAMELLIA_128_GCM_SHA256", + 0xC091: "TLS_DHE_PSK_WITH_CAMELLIA_256_GCM_SHA384", + 0xC092: "TLS_RSA_PSK_WITH_CAMELLIA_128_GCM_SHA256", + 0xC093: "TLS_RSA_PSK_WITH_CAMELLIA_256_GCM_SHA384", + 0xC094: "TLS_PSK_WITH_CAMELLIA_128_CBC_SHA256", + 0xC095: "TLS_PSK_WITH_CAMELLIA_256_CBC_SHA384", + 0xC096: "TLS_DHE_PSK_WITH_CAMELLIA_128_CBC_SHA256", + 0xC097: "TLS_DHE_PSK_WITH_CAMELLIA_256_CBC_SHA384", + 0xC098: "TLS_RSA_PSK_WITH_CAMELLIA_128_CBC_SHA256", + 0xC099: "TLS_RSA_PSK_WITH_CAMELLIA_256_CBC_SHA384", + 0xC09A: "TLS_ECDHE_PSK_WITH_CAMELLIA_128_CBC_SHA256", + 0xC09B: "TLS_ECDHE_PSK_WITH_CAMELLIA_256_CBC_SHA384", + 0xC09C: "TLS_RSA_WITH_AES_128_CCM", + 0xC09D: "TLS_RSA_WITH_AES_256_CCM", + 0xC09E: "TLS_DHE_RSA_WITH_AES_128_CCM", + 0xC09F: "TLS_DHE_RSA_WITH_AES_256_CCM", + 0xC0A0: "TLS_RSA_WITH_AES_128_CCM_8", + 0xC0A1: "TLS_RSA_WITH_AES_256_CCM_8", + 0xC0A2: "TLS_DHE_RSA_WITH_AES_128_CCM_8", + 0xC0A3: "TLS_DHE_RSA_WITH_AES_256_CCM_8", + 0xC0A4: "TLS_PSK_WITH_AES_128_CCM", + 0xC0A5: "TLS_PSK_WITH_AES_256_CCM", + 0xC0A6: "TLS_DHE_PSK_WITH_AES_128_CCM", + 0xC0A7: "TLS_DHE_PSK_WITH_AES_256_CCM", + 0xC0A8: "TLS_PSK_WITH_AES_128_CCM_8", + 0xC0A9: "TLS_PSK_WITH_AES_256_CCM_8", + 0xC0AA: "TLS_PSK_DHE_WITH_AES_128_CCM_8", + 0xC0AB: "TLS_PSK_DHE_WITH_AES_256_CCM_8", + 0xC0AC: "TLS_ECDHE_ECDSA_WITH_AES_128_CCM", + 0xC0AD: "TLS_ECDHE_ECDSA_WITH_AES_256_CCM", + 0xC0AE: "TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8", + 0xC0AF: "TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8", + 0xC0B0: "TLS_ECCPWD_WITH_AES_128_GCM_SHA256", + 0xC0B1: "TLS_ECCPWD_WITH_AES_256_GCM_SHA384", + 0xC0B2: "TLS_ECCPWD_WITH_AES_128_CCM_SHA256", + 0xC0B3: "TLS_ECCPWD_WITH_AES_256_CCM_SHA384", + 0xC0B4: "TLS_SHA256_SHA256", + 0xC0B5: "TLS_SHA384_SHA384", + 0xC100: "TLS_GOSTR341112_256_WITH_KUZNYECHIK_CTR_OMAC", + 0xC101: "TLS_GOSTR341112_256_WITH_MAGMA_CTR_OMAC", + 0xC102: "TLS_GOSTR341112_256_WITH_28147_CNT_IMIT", + 0xC103: "TLS_GOSTR341112_256_WITH_KUZNYECHIK_MGM_L", + 0xC104: "TLS_GOSTR341112_256_WITH_MAGMA_MGM_L", + 0xC105: "TLS_GOSTR341112_256_WITH_KUZNYECHIK_MGM_S", + 0xC106: "TLS_GOSTR341112_256_WITH_MAGMA_MGM_S", + 0xCACA: "GREASE", + 0xCCA8: "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + 0xCCA9: "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + 0xCCAA: "TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + 0xCCAB: "TLS_PSK_WITH_CHACHA20_POLY1305_SHA256", + 0xCCAC: "TLS_ECDHE_PSK_WITH_CHACHA20_POLY1305_SHA256", + 0xCCAD: "TLS_DHE_PSK_WITH_CHACHA20_POLY1305_SHA256", + 0xCCAE: "TLS_RSA_PSK_WITH_CHACHA20_POLY1305_SHA256", + 0xD001: "TLS_ECDHE_PSK_WITH_AES_128_GCM_SHA256", + 0xD002: "TLS_ECDHE_PSK_WITH_AES_256_GCM_SHA384", + 0xD003: "TLS_ECDHE_PSK_WITH_AES_128_CCM_8_SHA256", + 0xD005: "TLS_ECDHE_PSK_WITH_AES_128_CCM_SHA256", + 0xDADA: "GREASE", + 0xEAEA: "GREASE", + 0xFAFA: "GREASE", +}; + +/** + * GREASE values + */ +export const GREASE_VALUES = [ + 0x0a0a, + 0x1a1a, + 0x2a2a, + 0x3a3a, + 0x4a4a, + 0x5a5a, + 0x6a6a, + 0x7a7a, + 0x8a8a, + 0x9a9a, + 0xaaaa, + 0xbaba, + 0xcaca, + 0xdada, + 0xeaea, + 0xfafa +]; + +/** + * Parses the supported_versions extension and returns the highest supported version. + * @param {Uint8Array} bytes + * @returns {number} + */ +export function parseHighestSupportedVersion(bytes) { + const s = new Stream(bytes); + + // Length + let i = s.readInt(1); + + let highestVersion = 0; + while (s.hasMore() && i-- > 0) { + const v = s.readInt(2); + if (GREASE_VALUES.includes(v)) continue; + if (v > highestVersion) highestVersion = v; + } + + return highestVersion; +} + +/** + * Parses the application_layer_protocol_negotiation extension and returns the first value. + * @param {Uint8Array} bytes + * @returns {number} + */ +export function parseFirstALPNValue(bytes) { + const s = new Stream(bytes); + const alpnExtLen = s.readInt(2); + if (alpnExtLen < 3) return "00"; + const strLen = s.readInt(1); + if (strLen < 2) return "00"; + return s.readString(strLen); +} diff --git a/src/core/operations/JA4Fingerprint.mjs b/src/core/operations/JA4Fingerprint.mjs new file mode 100644 index 00000000..31f57525 --- /dev/null +++ b/src/core/operations/JA4Fingerprint.mjs @@ -0,0 +1,73 @@ +/** + * @author n1474335 [n1474335@gmail.com] + * @copyright Crown Copyright 2024 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import Utils from "../Utils.mjs"; +import {toJA4} from "../lib/JA4.mjs"; + +/** + * JA4 Fingerprint operation + */ +class JA4Fingerprint extends Operation { + + /** + * JA4Fingerprint constructor + */ + constructor() { + super(); + + this.name = "JA4 Fingerprint"; + this.module = "Crypto"; + this.description = "Generates a JA4 fingerprint to help identify TLS clients based on hashing together values from the Client Hello.

Input: A hex stream of the TLS or QUIC Client 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: ["JA4", "JA4 Original Rendering", "JA4 Raw", "JA4 Raw Original Rendering", "All"] + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const [inputFormat, outputFormat] = args; + input = Utils.convertToByteArray(input, inputFormat); + const ja4 = toJA4(new Uint8Array(input)); + + // Output + switch (outputFormat) { + case "JA4": + return ja4.JA4; + case "JA4 Original Rendering": + return ja4.JA4_o; + case "JA4 Raw": + return ja4.JA4_r; + case "JA4 Raw Original Rendering": + return ja4.JA4_ro; + case "All": + default: + return `JA4: ${ja4.JA4} +JA4_o: ${ja4.JA4_o} +JA4_r: ${ja4.JA4_r} +JA4_ro: ${ja4.JA4_ro}`; + } + } + +} + +export default JA4Fingerprint; diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs index e85b6ad3..ef3ef41c 100644 --- a/tests/operations/index.mjs +++ b/tests/operations/index.mjs @@ -81,6 +81,7 @@ import "./tests/HKDF.mjs"; import "./tests/Image.mjs"; import "./tests/IndexOfCoincidence.mjs"; import "./tests/JA3Fingerprint.mjs"; +import "./tests/JA4Fingerprint.mjs"; import "./tests/JA3SFingerprint.mjs"; import "./tests/JSONBeautify.mjs"; import "./tests/JSONMinify.mjs"; diff --git a/tests/operations/tests/JA4Fingerprint.mjs b/tests/operations/tests/JA4Fingerprint.mjs new file mode 100644 index 00000000..dba84a38 --- /dev/null +++ b/tests/operations/tests/JA4Fingerprint.mjs @@ -0,0 +1,55 @@ +/** + * JA4Fingerprint tests. + * + * @author n1474335 [n1474335@gmail.com] + * @copyright Crown Copyright 2024 + * @license Apache-2.0 + */ +import TestRegister from "../../lib/TestRegister.mjs"; + +TestRegister.addTests([ + { + name: "JA4 Fingerprint: TLS 1.3", + input: "1603010200010001fc0303b2c03e7ba990ef540c316a665d4d925f8e9079ac4b15687e587dc99016e75a6c20d0b0099243c9296a0c84153ea4ada7d87ad017f4211c2ea1350b0b3cc5514d5f00205a5a130113021303c02bc02fc02cc030cca9cca8c013c014009c009d002f003501000193fafa000000000024002200001f636f6e74656e742d6175746f66696c6c2e676f6f676c65617069732e636f6d0033002b00293a3a000100001d0020fb2cd8ef3d605b96ab03119ec4f30a6e2088cb1af86c41a81feace8706068c50000d001200100403080404010503080505010806060100230000000b00020100ff01000100000a000a00083a3a001d00170018001b000302000244690005000302683200120000002d000201010010000e000c02683208687474702f312e31000500050100000000002b0007060a0a03040303001700001a1a000100001500b800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + expectedOutput: "t13d1516h2_8daaf6152771_e5627efa2ab1", + recipeConfig: [ + { + "op": "JA4 Fingerprint", + "args": ["Hex", "JA4"] + } + ], + }, + { + name: "JA4 Fingerprint: TLS 1.3 Original Rendering", + input: "1603010200010001fc0303b2c03e7ba990ef540c316a665d4d925f8e9079ac4b15687e587dc99016e75a6c20d0b0099243c9296a0c84153ea4ada7d87ad017f4211c2ea1350b0b3cc5514d5f00205a5a130113021303c02bc02fc02cc030cca9cca8c013c014009c009d002f003501000193fafa000000000024002200001f636f6e74656e742d6175746f66696c6c2e676f6f676c65617069732e636f6d0033002b00293a3a000100001d0020fb2cd8ef3d605b96ab03119ec4f30a6e2088cb1af86c41a81feace8706068c50000d001200100403080404010503080505010806060100230000000b00020100ff01000100000a000a00083a3a001d00170018001b000302000244690005000302683200120000002d000201010010000e000c02683208687474702f312e31000500050100000000002b0007060a0a03040303001700001a1a000100001500b800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + expectedOutput: "t13d1516h2_acb858a92679_5276cb03a33b", + recipeConfig: [ + { + "op": "JA4 Fingerprint", + "args": ["Hex", "JA4 Original Rendering"] + } + ], + }, + { + name: "JA4 Fingerprint: TLS 1.2", + input: "1603010200010001fc0303ecb2691addb2bf6c599c7aaae23de5f42561cc04eb41029acc6fc050a16ac1d22046f8617b580ac9358e2aa44e306d52466bcc989c87c8ca64309f5faf50ba7b4d0022130113031302c02bc02fcca9cca8c02cc030c00ac009c013c014009c009d002f00350100019100000021001f00001c636f6e74696c652e73657276696365732e6d6f7a696c6c612e636f6d00170000ff01000100000a000e000c001d00170018001901000101000b00020100002300000010000e000c02683208687474702f312e310005000501000000000022000a000804030503060302030033006b0069001d00208909858fbeb6ed2f1248ba5b9e2978bead0e840110192c61daed0096798b184400170041044d183d91f5eed35791fa982464e3b0214aaa5f5d1b78616d9b9fbebc22d11f535b2f94c686143136aa795e6e5a875d6c08064ad5b76d44caad766e2483012748002b00050403040303000d0018001604030503060308040805080604010501060102030201002d00020101001c000240010015007a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + expectedOutput: "t13d1715h2_5b57614c22b0_3d5424432f57", + recipeConfig: [ + { + "op": "JA4 Fingerprint", + "args": ["Hex", "JA4"] + } + ], + }, + { + name: "JA4 Fingerprint: TLS 1.2 Original Rendering", + input: "1603010200010001fc0303ecb2691addb2bf6c599c7aaae23de5f42561cc04eb41029acc6fc050a16ac1d22046f8617b580ac9358e2aa44e306d52466bcc989c87c8ca64309f5faf50ba7b4d0022130113031302c02bc02fcca9cca8c02cc030c00ac009c013c014009c009d002f00350100019100000021001f00001c636f6e74696c652e73657276696365732e6d6f7a696c6c612e636f6d00170000ff01000100000a000e000c001d00170018001901000101000b00020100002300000010000e000c02683208687474702f312e310005000501000000000022000a000804030503060302030033006b0069001d00208909858fbeb6ed2f1248ba5b9e2978bead0e840110192c61daed0096798b184400170041044d183d91f5eed35791fa982464e3b0214aaa5f5d1b78616d9b9fbebc22d11f535b2f94c686143136aa795e6e5a875d6c08064ad5b76d44caad766e2483012748002b00050403040303000d0018001604030503060308040805080604010501060102030201002d00020101001c000240010015007a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + expectedOutput: "t13d1715h2_5b234860e130_014157ec0da2", + recipeConfig: [ + { + "op": "JA4 Fingerprint", + "args": ["Hex", "JA4 Original Rendering"] + } + ], + }, +]); From 943d01c208c3a2db47c0498245ede9d3ddd50faf Mon Sep 17 00:00:00 2001 From: n1474335 Date: Wed, 27 Mar 2024 18:28:41 +0000 Subject: [PATCH 17/22] Updated CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d23d8d0a..eb352c28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ All major and minor version changes will be documented in this file. Details of ## Details +### [10.10.0] - 2024-03-27 +- Added 'JA4 Fingerprint' operation [@n1474335] | [#1759] + ### [10.9.0] - 2024-03-26 - Line ending sequences and UTF-8 character encoding are now detected automatically [@n1474335] | [65ffd8d] @@ -389,6 +392,7 @@ All major and minor version changes will be documented in this file. Details of ## [4.0.0] - 2016-11-28 - Initial open source commit [@n1474335] | [b1d73a72](https://github.com/gchq/CyberChef/commit/b1d73a725dc7ab9fb7eb789296efd2b7e4b08306) +[10.10.0]: https://github.com/gchq/CyberChef/releases/tag/v10.10.0 [10.9.0]: https://github.com/gchq/CyberChef/releases/tag/v10.9.0 [10.8.0]: https://github.com/gchq/CyberChef/releases/tag/v10.7.0 [10.7.0]: https://github.com/gchq/CyberChef/releases/tag/v10.7.0 From 862cfdf0ae2eeafec1ee7b14099d44246677defc Mon Sep 17 00:00:00 2001 From: n1474335 Date: Wed, 27 Mar 2024 18:29:13 +0000 Subject: [PATCH 18/22] 10.10.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 658aef50..e9e03543 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cyberchef", - "version": "10.9.0", + "version": "10.10.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cyberchef", - "version": "10.9.0", + "version": "10.10.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index ca2924c8..4092451f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cyberchef", - "version": "10.9.0", + "version": "10.10.0", "description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.", "author": "n1474335 ", "homepage": "https://gchq.github.io/CyberChef", From 27b7e3c4d654e797141413d8c45e23a22b304180 Mon Sep 17 00:00:00 2001 From: a3957273 <89583054+a3957273@users.noreply.github.com> Date: Fri, 29 Mar 2024 02:02:00 +0000 Subject: [PATCH 19/22] Release v10.11.0 --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb352c28..6cd3fbc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,11 @@ All major and minor version changes will be documented in this file. Details of ## Details +### [10.11.0] - 2024-03-29 +- Add HEIC/HEIF file signatures [simonw] | [#1757] +- Update xmldom to fix medium security vulnerability [chriswhite199] | [#1752] +- Update JSONWebToken to fix medium security vulnerability [chriswhite199] | [#1753] + ### [10.10.0] - 2024-03-27 - Added 'JA4 Fingerprint' operation [@n1474335] | [#1759] @@ -392,6 +397,7 @@ All major and minor version changes will be documented in this file. Details of ## [4.0.0] - 2016-11-28 - Initial open source commit [@n1474335] | [b1d73a72](https://github.com/gchq/CyberChef/commit/b1d73a725dc7ab9fb7eb789296efd2b7e4b08306) +[10.11.0]: https://github.com/gchq/CyberChef/releases/tag/v10.11.0 [10.10.0]: https://github.com/gchq/CyberChef/releases/tag/v10.10.0 [10.9.0]: https://github.com/gchq/CyberChef/releases/tag/v10.9.0 [10.8.0]: https://github.com/gchq/CyberChef/releases/tag/v10.7.0 @@ -559,6 +565,8 @@ All major and minor version changes will be documented in this file. Details of [@sg5506844]: https://github.com/sg5506844 [@AliceGrey]: https://github.com/AliceGrey [@AshCorr]: https://github.com/AshCorr +[@simonw]: https://github.com/simonw +[@chriswhite199]: https://github.com/chriswhite199 [8ad18b]: https://github.com/gchq/CyberChef/commit/8ad18bc7db6d9ff184ba3518686293a7685bf7b7 @@ -687,3 +695,6 @@ All major and minor version changes will be documented in this file. Details of [#1555]: https://github.com/gchq/CyberChef/issues/1555 [#1694]: https://github.com/gchq/CyberChef/issues/1694 [#1699]: https://github.com/gchq/CyberChef/issues/1694 +[#1757]: https://github.com/gchq/CyberChef/issues/1757 +[#1752]: https://github.com/gchq/CyberChef/issues/1752 +[#1753]: https://github.com/gchq/CyberChef/issues/1753 From 877c83eae7603e49979d96c767f69cedbe862b8e Mon Sep 17 00:00:00 2001 From: a3957273 <89583054+a3957273@users.noreply.github.com> Date: Fri, 29 Mar 2024 02:03:21 +0000 Subject: [PATCH 20/22] Fix changelog links --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cd3fbc6..01492ec0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,9 +14,9 @@ All major and minor version changes will be documented in this file. Details of ## Details ### [10.11.0] - 2024-03-29 -- Add HEIC/HEIF file signatures [simonw] | [#1757] -- Update xmldom to fix medium security vulnerability [chriswhite199] | [#1752] -- Update JSONWebToken to fix medium security vulnerability [chriswhite199] | [#1753] +- Add HEIC/HEIF file signatures [@simonw] | [#1757] +- Update xmldom to fix medium security vulnerability [@chriswhite199] | [#1752] +- Update JSONWebToken to fix medium security vulnerability [@chriswhite199] | [#1753] ### [10.10.0] - 2024-03-27 - Added 'JA4 Fingerprint' operation [@n1474335] | [#1759] From 4fdea84534c8ad934618e1b288fca62d8f42647d Mon Sep 17 00:00:00 2001 From: a3957273 <89583054+a3957273@users.noreply.github.com> Date: Fri, 29 Mar 2024 02:34:49 +0000 Subject: [PATCH 21/22] 10.12.0 --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01492ec0..c0a53c48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ All major and minor version changes will be documented in this file. Details of ## Details +### [10.12.0] - 2024-03-29 +- Added 'Salsa20' and 'XSalsa20' operation [@joostrijneveld] | [#1750] + ### [10.11.0] - 2024-03-29 - Add HEIC/HEIF file signatures [@simonw] | [#1757] - Update xmldom to fix medium security vulnerability [@chriswhite199] | [#1752] @@ -397,6 +400,7 @@ All major and minor version changes will be documented in this file. Details of ## [4.0.0] - 2016-11-28 - Initial open source commit [@n1474335] | [b1d73a72](https://github.com/gchq/CyberChef/commit/b1d73a725dc7ab9fb7eb789296efd2b7e4b08306) +[10.12.0]: https://github.com/gchq/CyberChef/releases/tag/v10.12.0 [10.11.0]: https://github.com/gchq/CyberChef/releases/tag/v10.11.0 [10.10.0]: https://github.com/gchq/CyberChef/releases/tag/v10.10.0 [10.9.0]: https://github.com/gchq/CyberChef/releases/tag/v10.9.0 @@ -698,3 +702,4 @@ All major and minor version changes will be documented in this file. Details of [#1757]: https://github.com/gchq/CyberChef/issues/1757 [#1752]: https://github.com/gchq/CyberChef/issues/1752 [#1753]: https://github.com/gchq/CyberChef/issues/1753 +[#1750]: https://github.com/gchq/CyberChef/issues/1750 From acce7ca717017bfba97cbac943502f522effc382 Mon Sep 17 00:00:00 2001 From: a3957273 <89583054+a3957273@users.noreply.github.com> Date: Fri, 29 Mar 2024 02:36:56 +0000 Subject: [PATCH 22/22] Bump package version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 308b5f5f..c77548ca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cyberchef", - "version": "10.10.0", + "version": "10.12.1", "description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.", "author": "n1474335 ", "homepage": "https://gchq.github.io/CyberChef",