mirror of
https://github.com/gchq/CyberChef.git
synced 2025-04-22 07:46:16 -04:00
Merge branch 'master' into date-delta
This commit is contained in:
commit
d59ebdd0dc
32 changed files with 2485 additions and 233 deletions
|
@ -95,6 +95,8 @@
|
|||
"RC4",
|
||||
"RC4 Drop",
|
||||
"ChaCha",
|
||||
"Salsa20",
|
||||
"XSalsa20",
|
||||
"Rabbit",
|
||||
"SM4 Encrypt",
|
||||
"SM4 Decrypt",
|
||||
|
@ -231,6 +233,7 @@
|
|||
"VarInt Decode",
|
||||
"JA3 Fingerprint",
|
||||
"JA3S Fingerprint",
|
||||
"JA4 Fingerprint",
|
||||
"HASSH Client Fingerprint",
|
||||
"HASSH Server Fingerprint",
|
||||
"Format MAC addresses",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
166
src/core/lib/JA4.mjs
Normal file
166
src/core/lib/JA4.mjs
Normal file
|
@ -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}`,
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
|
|
144
src/core/lib/Salsa20.mjs
Normal file
144
src/core/lib/Salsa20.mjs
Normal file
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
776
src/core/lib/TLS.mjs
Normal file
776
src/core/lib/TLS.mjs
Normal file
|
@ -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);
|
||||
}
|
|
@ -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";
|
||||
|
||||
/**
|
||||
|
|
73
src/core/operations/JA4Fingerprint.mjs
Normal file
73
src/core/operations/JA4Fingerprint.mjs
Normal file
|
@ -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.<br><br>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;
|
|
@ -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.
|
||||
|
|
154
src/core/operations/Salsa20.mjs
Normal file
154
src/core/operations/Salsa20.mjs
Normal file
|
@ -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.<br><br><b>Key:</b> Salsa20 uses a key of 16 or 32 bytes (128 or 256 bits).<br><br><b>Nonce:</b> Salsa20 uses a nonce of 8 bytes (64 bits).<br><br><b>Counter:</b> 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;
|
|
@ -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";
|
||||
|
||||
/**
|
||||
|
|
156
src/core/operations/XSalsa20.mjs
Normal file
156
src/core/operations/XSalsa20.mjs
Normal file
|
@ -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.<br><br><b>Key:</b> XSalsa20 uses a key of 16 or 32 bytes (128 or 256 bits).<br><br><b>Nonce:</b> XSalsa20 uses a nonce of 24 bytes (192 bits).<br><br><b>Counter:</b> 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;
|
|
@ -46,6 +46,8 @@ class App {
|
|||
this.appLoaded = false;
|
||||
this.workerLoaded = false;
|
||||
this.waitersLoaded = false;
|
||||
|
||||
this.snackbars = [];
|
||||
}
|
||||
|
||||
|
||||
|
@ -500,22 +502,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
|
||||
|
@ -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();
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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"
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
@ -23,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;
|
||||
|
@ -92,22 +95,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"));
|
||||
}
|
||||
|
||||
|
@ -124,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"));
|
||||
}
|
||||
|
@ -221,25 +214,34 @@ class StatusBarPanel {
|
|||
* @param {EditorState} state
|
||||
*/
|
||||
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"]
|
||||
};
|
||||
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 eolName = eolLookup[state.lineBreak];
|
||||
val.textContent = eolName[0];
|
||||
button.setAttribute("title", `End of line sequence:<br>${eolName[1]}`);
|
||||
button.setAttribute("data-original-title", `End of line sequence:<br>${eolName[1]}`);
|
||||
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:<br>${eolName}`);
|
||||
button.setAttribute("data-original-title", `End of line sequence:<br>${eolName}`);
|
||||
this.eolVal = state.lineBreak;
|
||||
}
|
||||
|
||||
|
@ -249,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:<br>${name}`);
|
||||
button.setAttribute("data-original-title", `${this.label} character encoding:<br>${name}`);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,8 @@ class InputWaiter {
|
|||
|
||||
this.inputTextEl = document.getElementById("input-text");
|
||||
this.inputChrEnc = 0;
|
||||
this.eolState = 0; // 0 = unset, 1 = detected, 2 = manual
|
||||
this.encodingState = 0; // 0 = unset, 1 = detected, 2 = manual
|
||||
this.initEditor();
|
||||
|
||||
this.inputWorker = null;
|
||||
|
@ -92,6 +94,7 @@ class InputWaiter {
|
|||
fileDetailsPanel: new Compartment
|
||||
};
|
||||
|
||||
const self = this;
|
||||
const initialState = EditorState.create({
|
||||
doc: null,
|
||||
extensions: [
|
||||
|
@ -114,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
|
||||
|
@ -141,10 +146,21 @@ class InputWaiter {
|
|||
if (e.docChanged && !this.silentInputChange)
|
||||
this.inputChange(e);
|
||||
this.silentInputChange = false;
|
||||
}),
|
||||
|
||||
// Event handlers
|
||||
EditorView.domEventHandlers({
|
||||
paste(event, view) {
|
||||
setTimeout(() => {
|
||||
self.afterPaste(event);
|
||||
});
|
||||
}
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
|
||||
if (this.inputEditorView) this.inputEditorView.destroy();
|
||||
this.inputEditorView = new EditorView({
|
||||
state: initialState,
|
||||
parent: this.inputTextEl
|
||||
|
@ -154,12 +170,23 @@ class InputWaiter {
|
|||
/**
|
||||
* Handler for EOL change events
|
||||
* Sets the line separator
|
||||
* @param {string} eolVal
|
||||
* @param {string} eol
|
||||
* @param {boolean} [manual=false]
|
||||
*/
|
||||
eolChange(eolVal) {
|
||||
const oldInputVal = this.getInput();
|
||||
eolChange(eol, manual=false) {
|
||||
const eolVal = eolCodeToSeq[eol];
|
||||
if (eolVal === undefined) return;
|
||||
|
||||
this.eolState = manual ? 2 : this.eolState;
|
||||
if (this.eolState < 2 && eolVal === this.getEOLSeq()) return;
|
||||
|
||||
if (this.eolState === 1) {
|
||||
// Alert
|
||||
this.app.alert(`Input end of line separator has been detected and 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))
|
||||
});
|
||||
|
@ -176,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();
|
||||
}
|
||||
|
||||
|
@ -195,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
|
||||
|
@ -866,6 +911,55 @@ 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.eolState > 1) 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;
|
||||
|
||||
// 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]) {
|
||||
choice = "CRLF";
|
||||
}
|
||||
|
||||
const eolVal = eolCodeToSeq[choice];
|
||||
if (eolVal === this.getEOLSeq()) return;
|
||||
|
||||
// Setting automatically
|
||||
this.eolState = 1;
|
||||
this.eolChange(choice);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for input dragover events.
|
||||
* Gives the user a visual cue to show that items can be dropped here.
|
||||
|
@ -1199,6 +1293,14 @@ class InputWaiter {
|
|||
this.manager.output.removeAllOutputs();
|
||||
this.manager.output.terminateZipWorker();
|
||||
|
||||
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;
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
@ -38,7 +39,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 +71,8 @@ class OutputWaiter {
|
|||
this.zipWorker = null;
|
||||
this.maxTabs = this.manager.tabs.calcMaxTabs();
|
||||
this.tabTimeout = null;
|
||||
this.eolState = 0; // 0 = unset, 1 = detected, 2 = manual
|
||||
this.encodingState = 0; // 0 = unset, 1 = detected, 2 = manual
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -109,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),
|
||||
|
@ -137,6 +142,7 @@ class OutputWaiter {
|
|||
]
|
||||
});
|
||||
|
||||
if (this.outputEditorView) this.outputEditorView.destroy();
|
||||
this.outputEditorView = new EditorView({
|
||||
state: initialState,
|
||||
parent: this.outputTextEl
|
||||
|
@ -146,9 +152,21 @@ class OutputWaiter {
|
|||
/**
|
||||
* Handler for EOL change events
|
||||
* Sets the line separator
|
||||
* @param {string} eolVal
|
||||
* @param {string} eol
|
||||
* @param {boolean} [manual=false]
|
||||
*/
|
||||
async eolChange(eolVal) {
|
||||
async eolChange(eol, manual=false) {
|
||||
const eolVal = eolCodeToSeq[eol];
|
||||
if (eolVal === undefined) return;
|
||||
|
||||
this.eolState = manual ? 2 : this.eolState;
|
||||
if (this.eolState < 2 && eolVal === this.getEOLSeq()) return;
|
||||
|
||||
if (this.eolState === 1) {
|
||||
// Alert
|
||||
this.app.alert(`Output end of line separator has been detected and changed to ${eolCodeToName[eol]}`, 5000);
|
||||
}
|
||||
|
||||
const currentTabNum = this.manager.tabs.getActiveTab("output");
|
||||
if (currentTabNum >= 0) {
|
||||
this.outputs[currentTabNum].eolSequence = eolVal;
|
||||
|
@ -180,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) {
|
||||
|
@ -195,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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -213,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
|
||||
|
@ -248,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);
|
||||
|
@ -276,6 +320,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 +392,85 @@ class OutputWaiter {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the EOL separator should be updated
|
||||
*
|
||||
* @param {string} data
|
||||
*/
|
||||
detectEOLSequence(data) {
|
||||
// If EOL has been fixed, skip this.
|
||||
if (this.eolState > 1) 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;
|
||||
|
||||
// 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]) {
|
||||
choice = "CRLF";
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the maximum number of tabs to display
|
||||
*/
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue