Merge remote-tracking branch 'upstream/master' into bump-dependencies
This commit is contained in:
commit
b002e54cbd
8
package-lock.json
generated
8
package-lock.json
generated
@ -61,7 +61,7 @@
|
||||
"jsonwebtoken": "9.0.3",
|
||||
"jsqr": "^1.4.0",
|
||||
"jsrsasign": "^11.1.0",
|
||||
"kbpgp": "2.1.15",
|
||||
"kbpgp": "^2.1.17",
|
||||
"libbzip2-wasm": "0.0.4",
|
||||
"libyara-wasm": "^1.2.1",
|
||||
"lodash": "^4.17.21",
|
||||
@ -12459,9 +12459,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/kbpgp": {
|
||||
"version": "2.1.15",
|
||||
"resolved": "https://registry.npmjs.org/kbpgp/-/kbpgp-2.1.15.tgz",
|
||||
"integrity": "sha512-iFdQT+m2Mi2DB14kEFydF2joNe9x3E2VZCGZUt7UXsiZnQx5TtSl4KofP7EPtjHvf7weCxNKlEPSYiiCNMZ2jA==",
|
||||
"version": "2.1.17",
|
||||
"resolved": "https://registry.npmjs.org/kbpgp/-/kbpgp-2.1.17.tgz",
|
||||
"integrity": "sha512-pnjH7amyg6dZLXyF42BKbCTST0l0r1ErunqtFRrJCkHkGJb83cZZmx1pnqNFr+d/ls+5gvcHrZLPfUG5q7oRYw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"bn": "^1.0.5",
|
||||
|
||||
@ -147,7 +147,7 @@
|
||||
"jsonwebtoken": "9.0.3",
|
||||
"jsqr": "^1.4.0",
|
||||
"jsrsasign": "^11.1.0",
|
||||
"kbpgp": "2.1.15",
|
||||
"kbpgp": "^2.1.17",
|
||||
"libbzip2-wasm": "0.0.4",
|
||||
"libyara-wasm": "^1.2.1",
|
||||
"lodash": "^4.17.21",
|
||||
|
||||
@ -177,7 +177,7 @@ class Utils {
|
||||
*/
|
||||
static printable(str, preserveWs=false, onlyAscii=false) {
|
||||
if (onlyAscii) {
|
||||
return str.replace(/[^\x20-\x7f]/g, ".");
|
||||
return str.replace(/[^\x20-\x7e]/g, ".");
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-misleading-character-class
|
||||
|
||||
@ -26,6 +26,8 @@
|
||||
"From Base45",
|
||||
"To Base58",
|
||||
"From Base58",
|
||||
"To Bech32",
|
||||
"From Bech32",
|
||||
"To Base62",
|
||||
"From Base62",
|
||||
"To Base64",
|
||||
|
||||
371
src/core/lib/Bech32.mjs
Normal file
371
src/core/lib/Bech32.mjs
Normal file
@ -0,0 +1,371 @@
|
||||
/**
|
||||
* Pure JavaScript implementation of Bech32 and Bech32m encoding.
|
||||
*
|
||||
* Bech32 is defined in BIP-0173: https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki
|
||||
* Bech32m is defined in BIP-0350: https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki
|
||||
*
|
||||
* @author Medjedtxm
|
||||
* @copyright Crown Copyright 2025
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import OperationError from "../errors/OperationError.mjs";
|
||||
|
||||
/** Bech32 character set (32 characters, excludes 1, b, i, o) */
|
||||
const CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
||||
|
||||
/** Reverse lookup table for decoding */
|
||||
const CHARSET_REV = {};
|
||||
for (let i = 0; i < CHARSET.length; i++) {
|
||||
CHARSET_REV[CHARSET[i]] = i;
|
||||
}
|
||||
|
||||
/** Checksum constant for Bech32 (BIP-0173) */
|
||||
const BECH32_CONST = 1;
|
||||
|
||||
/** Checksum constant for Bech32m (BIP-0350) */
|
||||
const BECH32M_CONST = 0x2bc830a3;
|
||||
|
||||
/** Generator polynomial coefficients for checksum */
|
||||
const GENERATOR = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
|
||||
|
||||
/**
|
||||
* Compute the polymod checksum
|
||||
* @param {number[]} values - Array of 5-bit values
|
||||
* @returns {number} - Checksum value
|
||||
*/
|
||||
function polymod(values) {
|
||||
let chk = 1;
|
||||
for (const v of values) {
|
||||
const top = chk >> 25;
|
||||
chk = ((chk & 0x1ffffff) << 5) ^ v;
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if ((top >> i) & 1) {
|
||||
chk ^= GENERATOR[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
return chk;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand HRP for checksum computation
|
||||
* @param {string} hrp - Human-readable part (lowercase)
|
||||
* @returns {number[]} - Expanded values
|
||||
*/
|
||||
function hrpExpand(hrp) {
|
||||
const result = [];
|
||||
for (let i = 0; i < hrp.length; i++) {
|
||||
result.push(hrp.charCodeAt(i) >> 5);
|
||||
}
|
||||
result.push(0);
|
||||
for (let i = 0; i < hrp.length; i++) {
|
||||
result.push(hrp.charCodeAt(i) & 31);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify checksum of a Bech32/Bech32m string
|
||||
* @param {string} hrp - Human-readable part (lowercase)
|
||||
* @param {number[]} data - Data including checksum (5-bit values)
|
||||
* @param {string} encoding - "Bech32" or "Bech32m"
|
||||
* @returns {boolean} - True if checksum is valid
|
||||
*/
|
||||
function verifyChecksum(hrp, data, encoding) {
|
||||
const constant = encoding === "Bech32m" ? BECH32M_CONST : BECH32_CONST;
|
||||
return polymod(hrpExpand(hrp).concat(data)) === constant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create checksum for Bech32/Bech32m encoding
|
||||
* @param {string} hrp - Human-readable part (lowercase)
|
||||
* @param {number[]} data - Data values (5-bit)
|
||||
* @param {string} encoding - "Bech32" or "Bech32m"
|
||||
* @returns {number[]} - 6 checksum values
|
||||
*/
|
||||
function createChecksum(hrp, data, encoding) {
|
||||
const constant = encoding === "Bech32m" ? BECH32M_CONST : BECH32_CONST;
|
||||
const values = hrpExpand(hrp).concat(data).concat([0, 0, 0, 0, 0, 0]);
|
||||
const mod = polymod(values) ^ constant;
|
||||
const result = [];
|
||||
for (let i = 0; i < 6; i++) {
|
||||
result.push((mod >> (5 * (5 - i))) & 31);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert 8-bit bytes to 5-bit words
|
||||
* @param {number[]|Uint8Array} data - Input bytes
|
||||
* @returns {number[]} - 5-bit words
|
||||
*/
|
||||
export function toWords(data) {
|
||||
let value = 0;
|
||||
let bits = 0;
|
||||
const result = [];
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
value = (value << 8) | data[i];
|
||||
bits += 8;
|
||||
|
||||
while (bits >= 5) {
|
||||
bits -= 5;
|
||||
result.push((value >> bits) & 31);
|
||||
}
|
||||
}
|
||||
|
||||
// Pad remaining bits
|
||||
if (bits > 0) {
|
||||
result.push((value << (5 - bits)) & 31);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert 5-bit words to 8-bit bytes
|
||||
* @param {number[]} words - 5-bit words
|
||||
* @returns {number[]} - Output bytes
|
||||
*/
|
||||
export function fromWords(words) {
|
||||
let value = 0;
|
||||
let bits = 0;
|
||||
const result = [];
|
||||
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
value = (value << 5) | words[i];
|
||||
bits += 5;
|
||||
|
||||
while (bits >= 8) {
|
||||
bits -= 8;
|
||||
result.push((value >> bits) & 255);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for invalid padding per BIP-0173
|
||||
// Condition 1: Cannot have 5+ bits remaining (would indicate incomplete byte)
|
||||
if (bits >= 5) {
|
||||
throw new OperationError("Invalid padding: too many bits remaining");
|
||||
}
|
||||
// Condition 2: Remaining padding bits must all be zero
|
||||
if (bits > 0) {
|
||||
const paddingValue = (value << (8 - bits)) & 255;
|
||||
if (paddingValue !== 0) {
|
||||
throw new OperationError("Invalid padding: non-zero bits in padding");
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode data to Bech32/Bech32m string
|
||||
*
|
||||
* @param {string} hrp - Human-readable part
|
||||
* @param {number[]|Uint8Array} data - Data bytes to encode
|
||||
* @param {string} encoding - "Bech32" or "Bech32m"
|
||||
* @param {boolean} segwit - If true, treat first byte as witness version (for Bitcoin SegWit)
|
||||
* @returns {string} - Encoded Bech32/Bech32m string
|
||||
*/
|
||||
export function encode(hrp, data, encoding = "Bech32", segwit = false) {
|
||||
// Validate HRP
|
||||
if (!hrp || hrp.length === 0) {
|
||||
throw new OperationError("Human-Readable Part (HRP) cannot be empty.");
|
||||
}
|
||||
|
||||
// Check HRP characters (ASCII 33-126)
|
||||
for (let i = 0; i < hrp.length; i++) {
|
||||
const c = hrp.charCodeAt(i);
|
||||
if (c < 33 || c > 126) {
|
||||
throw new OperationError(`HRP contains invalid character at position ${i}. Only printable ASCII characters (33-126) are allowed.`);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert HRP to lowercase
|
||||
const hrpLower = hrp.toLowerCase();
|
||||
|
||||
let words;
|
||||
if (segwit && data.length >= 2) {
|
||||
// SegWit encoding: first byte is witness version (0-16), rest is witness program
|
||||
const witnessVersion = data[0];
|
||||
if (witnessVersion > 16) {
|
||||
throw new OperationError(`Invalid witness version: ${witnessVersion}. Must be 0-16.`);
|
||||
}
|
||||
const witnessProgram = Array.prototype.slice.call(data, 1);
|
||||
|
||||
// Validate witness program length per BIP-0141
|
||||
if (witnessProgram.length < 2 || witnessProgram.length > 40) {
|
||||
throw new OperationError(`Invalid witness program length: ${witnessProgram.length}. Must be 2-40 bytes.`);
|
||||
}
|
||||
if (witnessVersion === 0 && witnessProgram.length !== 20 && witnessProgram.length !== 32) {
|
||||
throw new OperationError(`Invalid witness program length for v0: ${witnessProgram.length}. Must be 20 or 32 bytes.`);
|
||||
}
|
||||
|
||||
// Witness version is kept as single 5-bit value, program is converted
|
||||
words = [witnessVersion].concat(toWords(witnessProgram));
|
||||
} else {
|
||||
// Generic encoding: convert all bytes to 5-bit words
|
||||
words = toWords(data);
|
||||
}
|
||||
|
||||
// Create checksum
|
||||
const checksum = createChecksum(hrpLower, words, encoding);
|
||||
|
||||
// Build result string
|
||||
let result = hrpLower + "1";
|
||||
for (const w of words.concat(checksum)) {
|
||||
result += CHARSET[w];
|
||||
}
|
||||
|
||||
// Check maximum length (90 characters)
|
||||
if (result.length > 90) {
|
||||
throw new OperationError(`Encoded string exceeds maximum length of 90 characters (got ${result.length}). Consider using smaller input data.`);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a Bech32/Bech32m string
|
||||
*
|
||||
* @param {string} str - Bech32/Bech32m encoded string
|
||||
* @param {string} encoding - "Bech32", "Bech32m", or "Auto-detect"
|
||||
* @returns {{hrp: string, data: number[]}} - Decoded HRP and data bytes
|
||||
*/
|
||||
export function decode(str, encoding = "Auto-detect") {
|
||||
// Check for empty input
|
||||
if (!str || str.length === 0) {
|
||||
throw new OperationError("Input cannot be empty.");
|
||||
}
|
||||
|
||||
// Check maximum length
|
||||
if (str.length > 90) {
|
||||
throw new OperationError(`Invalid Bech32 string: exceeds maximum length of 90 characters (got ${str.length}).`);
|
||||
}
|
||||
|
||||
// Check for mixed case
|
||||
const hasUpper = /[A-Z]/.test(str);
|
||||
const hasLower = /[a-z]/.test(str);
|
||||
if (hasUpper && hasLower) {
|
||||
throw new OperationError("Invalid Bech32 string: mixed case is not allowed. Use all uppercase or all lowercase.");
|
||||
}
|
||||
|
||||
// Convert to lowercase for processing
|
||||
str = str.toLowerCase();
|
||||
|
||||
// Find separator (last occurrence of '1')
|
||||
const sepIndex = str.lastIndexOf("1");
|
||||
if (sepIndex === -1) {
|
||||
throw new OperationError("Invalid Bech32 string: no separator '1' found.");
|
||||
}
|
||||
|
||||
if (sepIndex === 0) {
|
||||
throw new OperationError("Invalid Bech32 string: Human-Readable Part (HRP) cannot be empty.");
|
||||
}
|
||||
|
||||
if (sepIndex + 7 > str.length) {
|
||||
throw new OperationError("Invalid Bech32 string: data part is too short (minimum 6 characters for checksum).");
|
||||
}
|
||||
|
||||
// Extract HRP and data part
|
||||
const hrp = str.substring(0, sepIndex);
|
||||
const dataPart = str.substring(sepIndex + 1);
|
||||
|
||||
// Validate HRP characters
|
||||
for (let i = 0; i < hrp.length; i++) {
|
||||
const c = hrp.charCodeAt(i);
|
||||
if (c < 33 || c > 126) {
|
||||
throw new OperationError(`HRP contains invalid character at position ${i}.`);
|
||||
}
|
||||
}
|
||||
|
||||
// Decode data characters to 5-bit values
|
||||
const data = [];
|
||||
for (let i = 0; i < dataPart.length; i++) {
|
||||
const c = dataPart[i];
|
||||
if (CHARSET_REV[c] === undefined) {
|
||||
throw new OperationError(`Invalid character '${c}' at position ${sepIndex + 1 + i}.`);
|
||||
}
|
||||
data.push(CHARSET_REV[c]);
|
||||
}
|
||||
|
||||
// Verify checksum
|
||||
let usedEncoding;
|
||||
if (encoding === "Bech32") {
|
||||
if (!verifyChecksum(hrp, data, "Bech32")) {
|
||||
throw new OperationError("Invalid Bech32 checksum.");
|
||||
}
|
||||
usedEncoding = "Bech32";
|
||||
} else if (encoding === "Bech32m") {
|
||||
if (!verifyChecksum(hrp, data, "Bech32m")) {
|
||||
throw new OperationError("Invalid Bech32m checksum.");
|
||||
}
|
||||
usedEncoding = "Bech32m";
|
||||
} else {
|
||||
// Auto-detect: try Bech32 first, then Bech32m
|
||||
if (verifyChecksum(hrp, data, "Bech32")) {
|
||||
usedEncoding = "Bech32";
|
||||
} else if (verifyChecksum(hrp, data, "Bech32m")) {
|
||||
usedEncoding = "Bech32m";
|
||||
} else {
|
||||
throw new OperationError("Invalid Bech32/Bech32m string: checksum verification failed.");
|
||||
}
|
||||
}
|
||||
|
||||
// Remove checksum (last 6 values)
|
||||
const words = data.slice(0, data.length - 6);
|
||||
|
||||
// Check if this is likely a SegWit address (Bitcoin, Litecoin, etc.)
|
||||
// For SegWit, the first 5-bit word is the witness version (0-16)
|
||||
// and should be extracted separately, not bit-converted with the rest
|
||||
const segwitHrps = ["bc", "tb", "ltc", "tltc", "bcrt"];
|
||||
const couldBeSegWit = segwitHrps.includes(hrp) && words.length > 0 && words[0] <= 16;
|
||||
|
||||
let bytes;
|
||||
let witnessVersion = null;
|
||||
|
||||
if (couldBeSegWit) {
|
||||
// Try SegWit decode first
|
||||
try {
|
||||
witnessVersion = words[0];
|
||||
const programWords = words.slice(1);
|
||||
const programBytes = fromWords(programWords);
|
||||
|
||||
// Validate SegWit witness program length (20 or 32 bytes for v0, 2-40 for others)
|
||||
const validV0 = witnessVersion === 0 && (programBytes.length === 20 || programBytes.length === 32);
|
||||
const validOther = witnessVersion !== 0 && programBytes.length >= 2 && programBytes.length <= 40;
|
||||
|
||||
if (validV0 || validOther) {
|
||||
// Valid SegWit address
|
||||
bytes = [witnessVersion, ...programBytes];
|
||||
} else {
|
||||
// Not valid SegWit, fall back to generic decode
|
||||
witnessVersion = null;
|
||||
bytes = fromWords(words);
|
||||
}
|
||||
} catch (e) {
|
||||
// SegWit decode failed, try generic decode
|
||||
witnessVersion = null;
|
||||
try {
|
||||
bytes = fromWords(words);
|
||||
} catch (e2) {
|
||||
throw new OperationError(`Failed to decode data: ${e2.message}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Generic Bech32: convert all words
|
||||
try {
|
||||
bytes = fromWords(words);
|
||||
} catch (e) {
|
||||
throw new OperationError(`Failed to decode data: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
hrp: hrp,
|
||||
data: bytes,
|
||||
encoding: usedEncoding,
|
||||
witnessVersion: witnessVersion
|
||||
};
|
||||
}
|
||||
@ -91,9 +91,7 @@ export function toJA4(bytes) {
|
||||
let alpn = "00";
|
||||
for (const ext of tlsr.handshake.value.extensions.value) {
|
||||
if (ext.type.value === "application_layer_protocol_negotiation") {
|
||||
alpn = parseFirstALPNValue(ext.value.data);
|
||||
alpn = alpn.charAt(0) + alpn.charAt(alpn.length - 1);
|
||||
if (alpn.charCodeAt(0) > 127) alpn = "99";
|
||||
alpn = alpnFingerprint(parseFirstALPNValue(ext.value.data));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -212,9 +210,7 @@ export function toJA4S(bytes) {
|
||||
let alpn = "00";
|
||||
for (const ext of tlsr.handshake.value.extensions.value) {
|
||||
if (ext.type.value === "application_layer_protocol_negotiation") {
|
||||
alpn = parseFirstALPNValue(ext.value.data);
|
||||
alpn = alpn.charAt(0) + alpn.charAt(alpn.length - 1);
|
||||
if (alpn.charCodeAt(0) > 127) alpn = "99";
|
||||
alpn = alpnFingerprint(parseFirstALPNValue(ext.value.data));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -262,3 +258,33 @@ function tlsVersionMapper(version) {
|
||||
default: return "00"; // Unknown
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a byte is ASCII alphanumeric (0-9, A-Z, a-z).
|
||||
* @param {number} byte
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isAlphanumeric(byte) {
|
||||
return (byte >= 0x30 && byte <= 0x39) ||
|
||||
(byte >= 0x41 && byte <= 0x5A) ||
|
||||
(byte >= 0x61 && byte <= 0x7A);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the 2-character ALPN fingerprint from raw ALPN bytes.
|
||||
* If both first and last bytes are ASCII alphanumeric, returns their characters.
|
||||
* Otherwise, returns first hex digit of first byte + last hex digit of last byte.
|
||||
* @param {Uint8Array|null} rawBytes
|
||||
* @returns {string}
|
||||
*/
|
||||
function alpnFingerprint(rawBytes) {
|
||||
if (!rawBytes || rawBytes.length === 0) return "00";
|
||||
const firstByte = rawBytes[0];
|
||||
const lastByte = rawBytes[rawBytes.length - 1];
|
||||
if (isAlphanumeric(firstByte) && isAlphanumeric(lastByte)) {
|
||||
return String.fromCharCode(firstByte) + String.fromCharCode(lastByte);
|
||||
}
|
||||
const firstHex = firstByte.toString(16).padStart(2, "0");
|
||||
const lastHex = lastByte.toString(16).padStart(2, "0");
|
||||
return firstHex[0] + lastHex[1];
|
||||
}
|
||||
|
||||
@ -863,15 +863,15 @@ export function parseHighestSupportedVersion(bytes) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the application_layer_protocol_negotiation extension and returns the first value.
|
||||
* Parses the application_layer_protocol_negotiation extension and returns the first value as raw bytes.
|
||||
* @param {Uint8Array} bytes
|
||||
* @returns {number}
|
||||
* @returns {Uint8Array|null}
|
||||
*/
|
||||
export function parseFirstALPNValue(bytes) {
|
||||
const s = new Stream(bytes);
|
||||
const alpnExtLen = s.readInt(2);
|
||||
if (alpnExtLen < 3) return "00";
|
||||
if (alpnExtLen < 2) return null;
|
||||
const strLen = s.readInt(1);
|
||||
if (strLen < 2) return "00";
|
||||
return s.readString(strLen);
|
||||
if (strLen < 1) return null;
|
||||
return s.getBytes(strLen);
|
||||
}
|
||||
|
||||
149
src/core/operations/FromBech32.mjs
Normal file
149
src/core/operations/FromBech32.mjs
Normal file
@ -0,0 +1,149 @@
|
||||
/**
|
||||
* @author Medjedtxm
|
||||
* @copyright Crown Copyright 2025
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation.mjs";
|
||||
import { decode } from "../lib/Bech32.mjs";
|
||||
import { toHex } from "../lib/Hex.mjs";
|
||||
|
||||
/**
|
||||
* From Bech32 operation
|
||||
*/
|
||||
class FromBech32 extends Operation {
|
||||
|
||||
/**
|
||||
* FromBech32 constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "From Bech32";
|
||||
this.module = "Default";
|
||||
this.description = "Bech32 is an encoding scheme primarily used for Bitcoin SegWit addresses (BIP-0173). It uses a 32-character alphabet that excludes easily confused characters (1, b, i, o) and includes a checksum for error detection.<br><br>Bech32m (BIP-0350) is an updated version used for Bitcoin Taproot addresses.<br><br>Auto-detect will attempt Bech32 first, then Bech32m if the checksum fails.<br><br>Output format options allow you to see the Human-Readable Part (HRP) along with the decoded data.";
|
||||
this.infoURL = "https://wikipedia.org/wiki/Bech32";
|
||||
this.inputType = "string";
|
||||
this.outputType = "string";
|
||||
this.args = [
|
||||
{
|
||||
"name": "Encoding",
|
||||
"type": "option",
|
||||
"value": ["Auto-detect", "Bech32", "Bech32m"]
|
||||
},
|
||||
{
|
||||
"name": "Output Format",
|
||||
"type": "option",
|
||||
"value": ["Raw", "Hex", "Bitcoin scriptPubKey", "HRP: Hex", "JSON"]
|
||||
}
|
||||
];
|
||||
this.checks = [
|
||||
{
|
||||
// Bitcoin mainnet SegWit/Taproot addresses
|
||||
pattern: "^bc1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{6,87}$",
|
||||
flags: "i",
|
||||
args: ["Auto-detect", "Hex"]
|
||||
},
|
||||
{
|
||||
// Bitcoin testnet addresses
|
||||
pattern: "^tb1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{6,87}$",
|
||||
flags: "i",
|
||||
args: ["Auto-detect", "Hex"]
|
||||
},
|
||||
{
|
||||
// AGE public keys
|
||||
pattern: "^age1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{6,87}$",
|
||||
flags: "i",
|
||||
args: ["Auto-detect", "HRP: Hex"]
|
||||
},
|
||||
{
|
||||
// AGE secret keys
|
||||
pattern: "^AGE-SECRET-KEY-1[QPZRY9X8GF2TVDW0S3JN54KHCE6MUA7L]{6,87}$",
|
||||
flags: "",
|
||||
args: ["Auto-detect", "HRP: Hex"]
|
||||
},
|
||||
{
|
||||
// Litecoin mainnet addresses
|
||||
pattern: "^ltc1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{6,87}$",
|
||||
flags: "i",
|
||||
args: ["Auto-detect", "Hex"]
|
||||
},
|
||||
{
|
||||
// Generic bech32 pattern
|
||||
pattern: "^[a-z]{1,83}1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{6,}$",
|
||||
flags: "i",
|
||||
args: ["Auto-detect", "Hex"]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
* @param {Object[]} args
|
||||
* @returns {string}
|
||||
*/
|
||||
run(input, args) {
|
||||
const encoding = args[0];
|
||||
const outputFormat = args[1];
|
||||
|
||||
input = input.trim();
|
||||
|
||||
if (input.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const decoded = decode(input, encoding);
|
||||
|
||||
// Format output based on selected option
|
||||
switch (outputFormat) {
|
||||
case "Raw":
|
||||
return decoded.data.map(b => String.fromCharCode(b)).join("");
|
||||
|
||||
case "Hex":
|
||||
return toHex(decoded.data, "");
|
||||
|
||||
case "Bitcoin scriptPubKey": {
|
||||
// Convert to Bitcoin scriptPubKey format as shown in BIP-0173/BIP-0350
|
||||
// Format: [OP_version][length][witness_program]
|
||||
// OP_0 = 0x00, OP_1-OP_16 = 0x51-0x60
|
||||
if (decoded.witnessVersion === null || decoded.data.length < 2) {
|
||||
// Not a SegWit address, fall back to hex
|
||||
return toHex(decoded.data, "");
|
||||
}
|
||||
const witnessVersion = decoded.data[0];
|
||||
const witnessProgram = decoded.data.slice(1);
|
||||
|
||||
// Convert witness version to OP code
|
||||
let opCode;
|
||||
if (witnessVersion === 0) {
|
||||
opCode = 0x00; // OP_0
|
||||
} else if (witnessVersion >= 1 && witnessVersion <= 16) {
|
||||
opCode = 0x50 + witnessVersion; // OP_1 = 0x51, ..., OP_16 = 0x60
|
||||
} else {
|
||||
// Invalid witness version, fall back to hex
|
||||
return toHex(decoded.data, "");
|
||||
}
|
||||
|
||||
// Build scriptPubKey: [OP_version][length][program]
|
||||
const scriptPubKey = [opCode, witnessProgram.length, ...witnessProgram];
|
||||
return toHex(scriptPubKey, "");
|
||||
}
|
||||
|
||||
case "HRP: Hex":
|
||||
return `${decoded.hrp}: ${toHex(decoded.data, "")}`;
|
||||
|
||||
case "JSON":
|
||||
return JSON.stringify({
|
||||
hrp: decoded.hrp,
|
||||
encoding: decoded.encoding,
|
||||
data: toHex(decoded.data, "")
|
||||
}, null, 2);
|
||||
|
||||
default:
|
||||
return toHex(decoded.data, "");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default FromBech32;
|
||||
@ -33,7 +33,7 @@ class ToBase85 extends Operation {
|
||||
value: ALPHABET_OPTIONS
|
||||
},
|
||||
{
|
||||
name: "Include delimeter",
|
||||
name: "Include delimiter",
|
||||
type: "boolean",
|
||||
value: false
|
||||
}
|
||||
|
||||
92
src/core/operations/ToBech32.mjs
Normal file
92
src/core/operations/ToBech32.mjs
Normal file
@ -0,0 +1,92 @@
|
||||
/**
|
||||
* @author Medjedtxm
|
||||
* @copyright Crown Copyright 2025
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation.mjs";
|
||||
import { encode } from "../lib/Bech32.mjs";
|
||||
import { fromHex } from "../lib/Hex.mjs";
|
||||
|
||||
/**
|
||||
* To Bech32 operation
|
||||
*/
|
||||
class ToBech32 extends Operation {
|
||||
|
||||
/**
|
||||
* ToBech32 constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "To Bech32";
|
||||
this.module = "Default";
|
||||
this.description = "Bech32 is an encoding scheme primarily used for Bitcoin SegWit addresses (BIP-0173). It uses a 32-character alphabet that excludes easily confused characters (1, b, i, o) and includes a checksum for error detection.<br><br>Bech32m (BIP-0350) is an updated version that fixes a weakness in the original Bech32 checksum and is used for Bitcoin Taproot addresses.<br><br>The Human-Readable Part (HRP) identifies the network or purpose (e.g., 'bc' for Bitcoin mainnet, 'tb' for testnet, 'age' for AGE encryption keys).<br><br>Maximum output length is 90 characters as per specification.";
|
||||
this.infoURL = "https://wikipedia.org/wiki/Bech32";
|
||||
this.inputType = "ArrayBuffer";
|
||||
this.outputType = "string";
|
||||
this.args = [
|
||||
{
|
||||
"name": "Human-Readable Part (HRP)",
|
||||
"type": "string",
|
||||
"value": "bc"
|
||||
},
|
||||
{
|
||||
"name": "Encoding",
|
||||
"type": "option",
|
||||
"value": ["Bech32", "Bech32m"]
|
||||
},
|
||||
{
|
||||
"name": "Input Format",
|
||||
"type": "option",
|
||||
"value": ["Raw bytes", "Hex"]
|
||||
},
|
||||
{
|
||||
"name": "Mode",
|
||||
"type": "option",
|
||||
"value": ["Generic", "Bitcoin SegWit"]
|
||||
},
|
||||
{
|
||||
"name": "Witness Version",
|
||||
"type": "number",
|
||||
"value": 0,
|
||||
"hint": "SegWit witness version (0-16). Only used in Bitcoin SegWit mode."
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ArrayBuffer} input
|
||||
* @param {Object[]} args
|
||||
* @returns {string}
|
||||
*/
|
||||
run(input, args) {
|
||||
const hrp = args[0];
|
||||
const encoding = args[1];
|
||||
const inputFormat = args[2];
|
||||
const mode = args[3];
|
||||
const witnessVersion = args[4];
|
||||
|
||||
let inputArray;
|
||||
if (inputFormat === "Hex") {
|
||||
// Convert hex string to bytes
|
||||
const hexStr = new TextDecoder().decode(new Uint8Array(input)).replace(/\s/g, "");
|
||||
inputArray = fromHex(hexStr);
|
||||
} else {
|
||||
inputArray = new Uint8Array(input);
|
||||
}
|
||||
|
||||
if (mode === "Bitcoin SegWit") {
|
||||
// Prepend witness version to the input data
|
||||
const withVersion = new Uint8Array(inputArray.length + 1);
|
||||
withVersion[0] = witnessVersion;
|
||||
withVersion.set(inputArray, 1);
|
||||
return encode(hrp, withVersion, encoding, true);
|
||||
}
|
||||
|
||||
return encode(hrp, inputArray, encoding, false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default ToBech32;
|
||||
@ -20,4 +20,10 @@ TestRegister.addApiTests([
|
||||
assert.equal(Utils.parseEscapedChars("\\\\\\'"), "\\'");
|
||||
}),
|
||||
|
||||
it("Utils: should replace delete character", () => {
|
||||
assert.equal(
|
||||
Utils.printable("\x7e\x7f\x80\xa7", false, true),
|
||||
"\x7e...",
|
||||
);
|
||||
}),
|
||||
]);
|
||||
|
||||
@ -26,6 +26,7 @@ import "./tests/Base64.mjs";
|
||||
import "./tests/Base85.mjs";
|
||||
import "./tests/Base92.mjs";
|
||||
import "./tests/BCD.mjs";
|
||||
import "./tests/Bech32.mjs";
|
||||
import "./tests/BitwiseOp.mjs";
|
||||
import "./tests/BLAKE2b.mjs";
|
||||
import "./tests/BLAKE2s.mjs";
|
||||
|
||||
702
tests/operations/tests/Bech32.mjs
Normal file
702
tests/operations/tests/Bech32.mjs
Normal file
@ -0,0 +1,702 @@
|
||||
/**
|
||||
* Bech32 tests.
|
||||
*
|
||||
* Test vectors from official BIP specifications:
|
||||
* BIP-0173: https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki
|
||||
* BIP-0350: https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki
|
||||
*
|
||||
* AGE key test vectors from:
|
||||
* https://asecuritysite.com/age/go_age5
|
||||
*
|
||||
* @author Medjedtxm
|
||||
* @copyright Crown Copyright 2025
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import TestRegister from "../../lib/TestRegister.mjs";
|
||||
|
||||
TestRegister.addTests([
|
||||
// ============= To Bech32 Tests =============
|
||||
{
|
||||
name: "To Bech32: empty input",
|
||||
input: "",
|
||||
expectedOutput: "bc1gmk9yu",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "To Bech32",
|
||||
"args": ["bc", "Bech32", "Raw bytes", "Generic", 0]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "To Bech32: single byte",
|
||||
input: "A",
|
||||
expectedOutput: "bc1gyufle22",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "To Bech32",
|
||||
"args": ["bc", "Bech32", "Raw bytes", "Generic", 0]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "To Bech32: Hello",
|
||||
input: "Hello",
|
||||
expectedOutput: "bc1fpjkcmr0gzsgcg",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "To Bech32",
|
||||
"args": ["bc", "Bech32", "Raw bytes", "Generic", 0]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "To Bech32: custom HRP",
|
||||
input: "test",
|
||||
expectedOutput: "custom1w3jhxaq593qur",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "To Bech32",
|
||||
"args": ["custom", "Bech32", "Raw bytes", "Generic", 0]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "To Bech32: testnet HRP",
|
||||
input: "data",
|
||||
expectedOutput: "tb1v3shgcg3x07jr",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "To Bech32",
|
||||
"args": ["tb", "Bech32", "Raw bytes", "Generic", 0]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "To Bech32m: empty input",
|
||||
input: "",
|
||||
expectedOutput: "bc1a8xfp7",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "To Bech32",
|
||||
"args": ["bc", "Bech32m", "Raw bytes", "Generic", 0]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "To Bech32m: single byte",
|
||||
input: "A",
|
||||
expectedOutput: "bc1gyf4040g",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "To Bech32",
|
||||
"args": ["bc", "Bech32m", "Raw bytes", "Generic", 0]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "To Bech32m: Hello",
|
||||
input: "Hello",
|
||||
expectedOutput: "bc1fpjkcmr0a7qya2",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "To Bech32",
|
||||
"args": ["bc", "Bech32m", "Raw bytes", "Generic", 0]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "To Bech32: empty HRP error",
|
||||
input: "test",
|
||||
expectedOutput: "Human-Readable Part (HRP) cannot be empty.",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "To Bech32",
|
||||
"args": ["", "Bech32", "Raw bytes", "Generic", 0]
|
||||
}
|
||||
],
|
||||
},
|
||||
|
||||
// ============= From Bech32 Tests (Raw output) =============
|
||||
{
|
||||
name: "From Bech32: decode single byte (Raw)",
|
||||
input: "bc1gyufle22",
|
||||
expectedOutput: "A",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Bech32", "Raw"]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "From Bech32: decode Hello (Raw)",
|
||||
input: "bc1fpjkcmr0gzsgcg",
|
||||
expectedOutput: "Hello",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Bech32", "Raw"]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "From Bech32: auto-detect Bech32 (Raw)",
|
||||
input: "bc1fpjkcmr0gzsgcg",
|
||||
expectedOutput: "Hello",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Auto-detect", "Raw"]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "From Bech32m: decode Hello (Raw)",
|
||||
input: "bc1fpjkcmr0a7qya2",
|
||||
expectedOutput: "Hello",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Bech32m", "Raw"]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "From Bech32: auto-detect Bech32m (Raw)",
|
||||
input: "bc1fpjkcmr0a7qya2",
|
||||
expectedOutput: "Hello",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Auto-detect", "Raw"]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "From Bech32: uppercase input (Raw)",
|
||||
input: "BC1FPJKCMR0GZSGCG",
|
||||
expectedOutput: "Hello",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Auto-detect", "Raw"]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "From Bech32: custom HRP (Raw)",
|
||||
input: "custom1w3jhxaq593qur",
|
||||
expectedOutput: "test",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Bech32", "Raw"]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "From Bech32: empty input",
|
||||
input: "",
|
||||
expectedOutput: "",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Auto-detect", "Hex"]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "From Bech32: empty data part (Hex)",
|
||||
input: "bc1gmk9yu",
|
||||
expectedOutput: "",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Bech32", "Hex"]
|
||||
}
|
||||
],
|
||||
},
|
||||
|
||||
// ============= From Bech32 HRP Output Tests =============
|
||||
{
|
||||
name: "From Bech32: HRP: Hex output format",
|
||||
input: "bc1fpjkcmr0gzsgcg",
|
||||
expectedOutput: "bc: 48656c6c6f",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Bech32", "HRP: Hex"]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "From Bech32: JSON output format",
|
||||
input: "bc1fpjkcmr0gzsgcg",
|
||||
expectedOutput: "{\n \"hrp\": \"bc\",\n \"encoding\": \"Bech32\",\n \"data\": \"48656c6c6f\"\n}",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Bech32", "JSON"]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "From Bech32: Hex output format",
|
||||
input: "bc1fpjkcmr0gzsgcg",
|
||||
expectedOutput: "48656c6c6f",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Bech32", "Hex"]
|
||||
}
|
||||
],
|
||||
},
|
||||
|
||||
// ============= AGE Key Test Vectors =============
|
||||
// From: https://asecuritysite.com/age/go_age5
|
||||
{
|
||||
name: "From Bech32: AGE public key 1 (HRP: Hex)",
|
||||
input: "age1kk86t4lr4s9uwvnqjzp2e35rflvcpnjt33q99547ct23xzk0ssss3ma49j",
|
||||
expectedOutput: "age: b58fa5d7e3ac0bc732609082acc6834fd980ce4b8c4052d2bec2d5130acf8421",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Auto-detect", "HRP: Hex"]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "From Bech32: AGE private key 1 (HRP: Hex)",
|
||||
input: "AGE-SECRET-KEY-1Z5N23X54Y4E9NLMPNH6EZDQQX9V883TMKJ3ZJF5QXXMKNZ2RPFXQUQF74G",
|
||||
expectedOutput: "age-secret-key-: 1526a89a95257259ff619df5913400315873c57bb4a229268031b76989430a4c",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Auto-detect", "HRP: Hex"]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "From Bech32: AGE public key 2 (HRP: Hex)",
|
||||
input: "age1nwt7gkq7udvalagqn7l8a4jgju7wtenkg925pvuqvn7cfcry6u2qkae4ad",
|
||||
expectedOutput: "age: 9b97e4581ee359dff5009fbe7ed648973ce5e676415540b38064fd84e064d714",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Auto-detect", "HRP: Hex"]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "From Bech32: AGE private key 2 (HRP: Hex)",
|
||||
input: "AGE-SECRET-KEY-137M0YVE3CL6M8C4ET9L2KU67FPQHJZTW547QD5CK0R5A5T09ZGJSQGR9LX",
|
||||
expectedOutput: "age-secret-key-: 8fb6f23331c7f5b3e2b9597eab735e484179096ea57c06d31678e9da2de51225",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Auto-detect", "HRP: Hex"]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "From Bech32: AGE public key 1 (JSON)",
|
||||
input: "age1kk86t4lr4s9uwvnqjzp2e35rflvcpnjt33q99547ct23xzk0ssss3ma49j",
|
||||
expectedOutput: "{\n \"hrp\": \"age\",\n \"encoding\": \"Bech32\",\n \"data\": \"b58fa5d7e3ac0bc732609082acc6834fd980ce4b8c4052d2bec2d5130acf8421\"\n}",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Auto-detect", "JSON"]
|
||||
}
|
||||
],
|
||||
},
|
||||
|
||||
// ============= Error Cases =============
|
||||
{
|
||||
name: "From Bech32: mixed case error",
|
||||
input: "bc1FpjKcmr0gzsgcg",
|
||||
expectedOutput: "Invalid Bech32 string: mixed case is not allowed. Use all uppercase or all lowercase.",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Auto-detect", "Hex"]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "From Bech32: no separator error",
|
||||
input: "noseparator",
|
||||
expectedOutput: "Invalid Bech32 string: no separator '1' found.",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Auto-detect", "Hex"]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "From Bech32: empty HRP error",
|
||||
input: "1qqqqqqqqqqqqqqqq",
|
||||
expectedOutput: "Invalid Bech32 string: Human-Readable Part (HRP) cannot be empty.",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Auto-detect", "Hex"]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "From Bech32: invalid checksum",
|
||||
input: "bc1fpjkcmr0gzsgcx",
|
||||
expectedOutput: "Invalid Bech32/Bech32m string: checksum verification failed.",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Auto-detect", "Hex"]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "From Bech32: data too short",
|
||||
input: "bc1abc",
|
||||
expectedOutput: "Invalid Bech32 string: data part is too short (minimum 6 characters for checksum).",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Auto-detect", "Hex"]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "From Bech32: wrong encoding specified",
|
||||
input: "bc1fpjkcmr0gzsgcg",
|
||||
expectedOutput: "Invalid Bech32m checksum.",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Bech32m", "Hex"]
|
||||
}
|
||||
],
|
||||
},
|
||||
|
||||
// ============= BIP-0173 Test Vectors (Bech32) =============
|
||||
{
|
||||
name: "From Bech32: BIP-0173 A12UEL5L (empty data)",
|
||||
input: "A12UEL5L",
|
||||
expectedOutput: "",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Bech32", "Hex"]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "From Bech32: BIP-0173 a12uel5l lowercase",
|
||||
input: "a12uel5l",
|
||||
expectedOutput: "",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Bech32", "Hex"]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "From Bech32: BIP-0173 long HRP with bio",
|
||||
input: "an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1tt5tgs",
|
||||
expectedOutput: "",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Bech32", "Hex"]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "From Bech32: BIP-0173 abcdef with data",
|
||||
input: "abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw",
|
||||
expectedOutput: "abcdef: 00443214c74254b635cf84653a56d7c675be77df",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Bech32", "HRP: Hex"]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "From Bech32: BIP-0173 split HRP",
|
||||
input: "split1checkupstagehandshakeupstreamerranterredcaperred2y9e3w",
|
||||
expectedOutput: "split: c5f38b70305f519bf66d85fb6cf03058f3dde463ecd7918f2dc743918f2d",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Bech32", "HRP: Hex"]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "From Bech32: BIP-0173 question mark HRP",
|
||||
input: "?1ezyfcl",
|
||||
expectedOutput: "",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Bech32", "Hex"]
|
||||
}
|
||||
],
|
||||
},
|
||||
|
||||
// ============= BIP-0350 Test Vectors (Bech32m) =============
|
||||
{
|
||||
name: "From Bech32m: BIP-0350 A1LQFN3A (empty data)",
|
||||
input: "A1LQFN3A",
|
||||
expectedOutput: "",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Bech32m", "Hex"]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "From Bech32m: BIP-0350 a1lqfn3a lowercase",
|
||||
input: "a1lqfn3a",
|
||||
expectedOutput: "",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Bech32m", "Hex"]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "From Bech32m: BIP-0350 long HRP",
|
||||
input: "an83characterlonghumanreadablepartthatcontainsthetheexcludedcharactersbioandnumber11sg7hg6",
|
||||
expectedOutput: "",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Bech32m", "Hex"]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "From Bech32m: BIP-0350 abcdef with data",
|
||||
input: "abcdef1l7aum6echk45nj3s0wdvt2fg8x9yrzpqzd3ryx",
|
||||
expectedOutput: "abcdef: ffbbcdeb38bdab49ca307b9ac5a928398a418820",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Bech32m", "HRP: Hex"]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "From Bech32m: BIP-0350 split HRP",
|
||||
input: "split1checkupstagehandshakeupstreamerranterredcaperredlc445v",
|
||||
expectedOutput: "split: c5f38b70305f519bf66d85fb6cf03058f3dde463ecd7918f2dc743918f2d",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Bech32m", "HRP: Hex"]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "From Bech32m: BIP-0350 question mark HRP",
|
||||
input: "?1v759aa",
|
||||
expectedOutput: "",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Bech32m", "Hex"]
|
||||
}
|
||||
],
|
||||
},
|
||||
|
||||
// ============= Bitcoin scriptPubKey Output Format Tests =============
|
||||
// Test vectors from BIP-0173 and BIP-0350
|
||||
{
|
||||
name: "From Bech32: Bitcoin scriptPubKey v0 P2WPKH",
|
||||
input: "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4",
|
||||
expectedOutput: "0014751e76e8199196d454941c45d1b3a323f1433bd6",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Auto-detect", "Bitcoin scriptPubKey"]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "From Bech32: Bitcoin scriptPubKey v0 P2WSH",
|
||||
input: "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7",
|
||||
expectedOutput: "00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Auto-detect", "Bitcoin scriptPubKey"]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "From Bech32: Bitcoin scriptPubKey v1 Taproot (Bech32m)",
|
||||
input: "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0",
|
||||
expectedOutput: "512079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Auto-detect", "Bitcoin scriptPubKey"]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "From Bech32: Bitcoin scriptPubKey v16",
|
||||
input: "BC1SW50QGDZ25J",
|
||||
expectedOutput: "6002751e",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Auto-detect", "Bitcoin scriptPubKey"]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "From Bech32: Bitcoin scriptPubKey v2",
|
||||
input: "bc1zw508d6qejxtdg4y5r3zarvaryvaxxpcs",
|
||||
expectedOutput: "5210751e76e8199196d454941c45d1b3a323",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Auto-detect", "Bitcoin scriptPubKey"]
|
||||
}
|
||||
],
|
||||
},
|
||||
|
||||
// ============= Bitcoin SegWit Encoding Tests =============
|
||||
{
|
||||
name: "To Bech32: Bitcoin SegWit v0 P2WPKH",
|
||||
input: "751e76e8199196d454941c45d1b3a323f1433bd6",
|
||||
expectedOutput: "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "To Bech32",
|
||||
"args": ["bc", "Bech32", "Hex", "Bitcoin SegWit", 0]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "To Bech32: Bitcoin SegWit v0 P2WSH testnet",
|
||||
input: "1863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262",
|
||||
expectedOutput: "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "To Bech32",
|
||||
"args": ["tb", "Bech32", "Hex", "Bitcoin SegWit", 0]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "To Bech32m: Bitcoin Taproot v1",
|
||||
input: "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
|
||||
expectedOutput: "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "To Bech32",
|
||||
"args": ["bc", "Bech32m", "Hex", "Bitcoin SegWit", 1]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "To Bech32m: Bitcoin SegWit v16",
|
||||
input: "751e",
|
||||
expectedOutput: "bc1sw50qgdz25j",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "To Bech32",
|
||||
"args": ["bc", "Bech32m", "Hex", "Bitcoin SegWit", 16]
|
||||
}
|
||||
],
|
||||
},
|
||||
|
||||
// ============= Round-trip Tests =============
|
||||
{
|
||||
name: "Bech32: encode then decode round-trip",
|
||||
input: "The quick brown fox jumps over the lazy dog",
|
||||
expectedOutput: "The quick brown fox jumps over the lazy dog",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "To Bech32",
|
||||
"args": ["test", "Bech32", "Raw bytes", "Generic", 0]
|
||||
},
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Bech32", "Raw"]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Bech32m: encode then decode round-trip",
|
||||
input: "The quick brown fox jumps over the lazy dog",
|
||||
expectedOutput: "The quick brown fox jumps over the lazy dog",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "To Bech32",
|
||||
"args": ["test", "Bech32m", "Raw bytes", "Generic", 0]
|
||||
},
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Bech32m", "Raw"]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Bech32: binary data round-trip",
|
||||
input: "0001020304050607",
|
||||
expectedOutput: "0001020304050607",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Hex",
|
||||
"args": ["Auto"]
|
||||
},
|
||||
{
|
||||
"op": "To Bech32",
|
||||
"args": ["bc", "Bech32", "Raw bytes", "Generic", 0]
|
||||
},
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Bech32", "Hex"]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Bech32: auto-detect round-trip",
|
||||
input: "CyberChef Bech32 Test",
|
||||
expectedOutput: "CyberChef Bech32 Test",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "To Bech32",
|
||||
"args": ["cyberchef", "Bech32", "Raw bytes", "Generic", 0]
|
||||
},
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Auto-detect", "Raw"]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Bech32m: auto-detect round-trip",
|
||||
input: "CyberChef Bech32m Test",
|
||||
expectedOutput: "CyberChef Bech32m Test",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "To Bech32",
|
||||
"args": ["cyberchef", "Bech32m", "Raw bytes", "Generic", 0]
|
||||
},
|
||||
{
|
||||
"op": "From Bech32",
|
||||
"args": ["Auto-detect", "Raw"]
|
||||
}
|
||||
],
|
||||
},
|
||||
]);
|
||||
@ -336,8 +336,7 @@ TestRegister.addTests([
|
||||
]
|
||||
}
|
||||
],
|
||||
expectedMatch: /^Invalid JPath expression: jsonPath: Cannot read properties of {2}\(reading 'constructor'\): /
|
||||
},
|
||||
expectedMatch: /^Invalid JPath expression: jsonPath: Cannot read properties of {2}\(reading 'constructor'\): / },
|
||||
{
|
||||
name: "CSS selector",
|
||||
input: '<div id="test">\n<p class="a">hello</p>\n<p>world</p>\n<p class="a">again</p>\n</div>',
|
||||
|
||||
@ -30,6 +30,28 @@ TestRegister.addTests([
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "JA4 Fingerprint: TLS 1.3 with whitespace-only ALPN",
|
||||
input: "1603010200010001fc0303ed338a18e711d670cdc472ff570a5b59f1ace12e5365918bf68bf845019147b6207e4437bfb062d98a4aeb753be8f09022a9dc9413d7694dad4db57fcdcf076e820024130213031301c02cc030c02bc02fcca9cca8c024c028c023c027009f009e006b006700ff0100018f0000001800160000136465762e636f6e74656e74677261622e6e6574000b000403000102000a00160014001d0017001e00190018010001010102010301040023000000100004000201200016000000170000000d002a0028040305030603080708080809080a080b080408050806040105010601030303010302040205020602002b00050403040303002d00020101003300260024001d00207af053336d5e2c1675aa4c6ce78de5e5fdbd296538113f051ea17ccb64289f22001500d2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
||||
expectedOutput: "t13d181220_85036bcba153_d41ae481755e",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "JA4 Fingerprint",
|
||||
"args": ["Hex", "JA4"]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "JA4 Fingerprint: TLS 1.3 with ALPN containing a whitespace",
|
||||
input: "1603010200010001fc0303273682a603be3f64dd025df4ad0f4d2d13043c3a233405a68bb29b865808749a20f4dfc40242b2fce38fae26c516ef9bef20a1b9349eba3c003780168d72471f5c0024130213031301c02cc030c02bc02fcca9cca8c024c028c023c027009f009e006b006700ff0100018f0000001800160000136465762e636f6e74656e74677261622e6e6574000b000403000102000a00160014001d0017001e0019001801000101010201030104002300000010000500030261200016000000170000000d002a0028040305030603080708080809080a080b080408050806040105010601030303010302040205020602002b00050403040303002d00020101003300260024001d0020f4dd1567bd858d3a9f1d88db1fee6a10ab0ea1aa6afe96ffb6a7c4d79dea4075001500d10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
||||
expectedOutput: "t13d181260_85036bcba153_d41ae481755e",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "JA4 Fingerprint",
|
||||
"args": ["Hex", "JA4"]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "JA4 Fingerprint: TLS 1.2",
|
||||
input: "1603010200010001fc0303ecb2691addb2bf6c599c7aaae23de5f42561cc04eb41029acc6fc050a16ac1d22046f8617b580ac9358e2aa44e306d52466bcc989c87c8ca64309f5faf50ba7b4d0022130113031302c02bc02fcca9cca8c02cc030c00ac009c013c014009c009d002f00350100019100000021001f00001c636f6e74696c652e73657276696365732e6d6f7a696c6c612e636f6d00170000ff01000100000a000e000c001d00170018001901000101000b00020100002300000010000e000c02683208687474702f312e310005000501000000000022000a000804030503060302030033006b0069001d00208909858fbeb6ed2f1248ba5b9e2978bead0e840110192c61daed0096798b184400170041044d183d91f5eed35791fa982464e3b0214aaa5f5d1b78616d9b9fbebc22d11f535b2f94c686143136aa795e6e5a875d6c08064ad5b76d44caad766e2483012748002b00050403040303000d0018001604030503060308040805080604010501060102030201002d00020101001c000240010015007a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user