diff --git a/CHANGELOG.md b/CHANGELOG.md index 008ccece..2fdf33e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file. The format ## Table of Contents - [Unreleased](#unreleased) +- [1.9.28 - 2025-12-11](#1928---2025-12-11) - [1.9.27 - 2025-12-11](#1927---2025-12-11) - [1.9.26 - 2025-12-10](#1926---2025-12-10) - [1.9.25 - 2025-12-09](#1925---2025-12-09) @@ -200,6 +201,20 @@ All notable changes to this project will be documented in this file. The format --- +## [1.9.28] - 2025-12-11 + +### Added +- Add getBytes64 helper for 64-bit length fields. +- Added long ciphertext test case. + +### Changed +- Changed AESGCM to use Uint8Arrays instead of number[] for all inputs and outputs for optimization. + +### Fixed +- Use 64-bit length encoding for GHASH inputs. + +--- + ## [1.9.27] - 2025-12-11 ### Fixed diff --git a/docs/reference/primitives.md b/docs/reference/primitives.md index ac7e4d3b..25ca9deb 100644 --- a/docs/reference/primitives.md +++ b/docs/reference/primitives.md @@ -1756,6 +1756,7 @@ export default class Point extends BasePoint { x: BigNumber | null; y: BigNumber | null; inf: boolean; + static _assertOnCurve(p: Point): Point static fromDER(bytes: number[]): Point static fromString(str: string): Point static fromX(x: BigNumber | number | number[] | string, odd: boolean): Point @@ -5031,9 +5032,9 @@ encoding, and changing it would render previously encrypted data undecryptable by newer versions of the library. ```ts -export function AESGCM(plainText: number[], initializationVector: number[], key: number[]): { - result: number[]; - authenticationTag: number[]; +export function AESGCM(plainText: Bytes, initializationVector: Bytes, key: Bytes): { + result: Bytes; + authenticationTag: Bytes; } ``` @@ -5043,7 +5044,7 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions]( ### Function: AESGCMDecrypt ```ts -export function AESGCMDecrypt(cipherText: number[], initializationVector: number[], authenticationTag: number[], key: number[]): number[] | null +export function AESGCMDecrypt(cipherText: Bytes, initializationVector: Bytes, authenticationTag: Bytes, key: Bytes): Bytes | null ``` Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables) @@ -5070,7 +5071,7 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions]( ### Function: ghash ```ts -export function ghash(input: number[], hashSubKey: number[]): number[] +export function ghash(input: Bytes, hashSubKey: Bytes): Bytes ``` Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables) @@ -5299,24 +5300,24 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions]( | | | | | --- | --- | --- | -| [BI_EIGHT](#variable-bi_eight) | [biModSqrt](#variable-bimodsqrt) | [multiply](#variable-multiply) | -| [BI_FOUR](#variable-bi_four) | [biModSub](#variable-bimodsub) | [rightShift](#variable-rightshift) | -| [BI_ONE](#variable-bi_one) | [checkBit](#variable-checkbit) | [ripemd160](#variable-ripemd160) | -| [BI_THREE](#variable-bi_three) | [encode](#variable-encode) | [scalarMultiplyWNAF](#variable-scalarmultiplywnaf) | -| [BI_TWO](#variable-bi_two) | [exclusiveOR](#variable-exclusiveor) | [sha1](#variable-sha1) | -| [BI_ZERO](#variable-bi_zero) | [fromBase58](#variable-frombase58) | [sha256](#variable-sha256) | -| [GX_BIGINT](#variable-gx_bigint) | [fromBase58Check](#variable-frombase58check) | [sha256hmac](#variable-sha256hmac) | -| [GY_BIGINT](#variable-gy_bigint) | [getBytes](#variable-getbytes) | [sha512](#variable-sha512) | -| [MASK_256](#variable-mask_256) | [hash160](#variable-hash160) | [sha512hmac](#variable-sha512hmac) | -| [N_BIGINT](#variable-n_bigint) | [hash256](#variable-hash256) | [sign](#variable-sign) | -| [P_BIGINT](#variable-p_bigint) | [incrementLeastSignificantThirtyTwoBits](#variable-incrementleastsignificantthirtytwobits) | [toArray](#variable-toarray) | -| [P_PLUS1_DIV4](#variable-p_plus1_div4) | [jpAdd](#variable-jpadd) | [toBase58](#variable-tobase58) | -| [biMod](#variable-bimod) | [jpDouble](#variable-jpdouble) | [toBase58Check](#variable-tobase58check) | -| [biModAdd](#variable-bimodadd) | [jpNeg](#variable-jpneg) | [toHex](#variable-tohex) | -| [biModInv](#variable-bimodinv) | [minimallyEncode](#variable-minimallyencode) | [toUTF8](#variable-toutf8) | -| [biModMul](#variable-bimodmul) | [modInvN](#variable-modinvn) | [verify](#variable-verify) | -| [biModPow](#variable-bimodpow) | [modMulN](#variable-modmuln) | [zero2](#variable-zero2) | -| [biModSqr](#variable-bimodsqr) | [modN](#variable-modn) | | +| [BI_EIGHT](#variable-bi_eight) | [biModSqrt](#variable-bimodsqrt) | [modN](#variable-modn) | +| [BI_FOUR](#variable-bi_four) | [biModSub](#variable-bimodsub) | [multiply](#variable-multiply) | +| [BI_ONE](#variable-bi_one) | [checkBit](#variable-checkbit) | [rightShift](#variable-rightshift) | +| [BI_THREE](#variable-bi_three) | [encode](#variable-encode) | [ripemd160](#variable-ripemd160) | +| [BI_TWO](#variable-bi_two) | [exclusiveOR](#variable-exclusiveor) | [scalarMultiplyWNAF](#variable-scalarmultiplywnaf) | +| [BI_ZERO](#variable-bi_zero) | [fromBase58](#variable-frombase58) | [sha1](#variable-sha1) | +| [GX_BIGINT](#variable-gx_bigint) | [fromBase58Check](#variable-frombase58check) | [sha256](#variable-sha256) | +| [GY_BIGINT](#variable-gy_bigint) | [getBytes](#variable-getbytes) | [sha256hmac](#variable-sha256hmac) | +| [MASK_256](#variable-mask_256) | [getBytes64](#variable-getbytes64) | [sha512](#variable-sha512) | +| [N_BIGINT](#variable-n_bigint) | [hash160](#variable-hash160) | [sha512hmac](#variable-sha512hmac) | +| [P_BIGINT](#variable-p_bigint) | [hash256](#variable-hash256) | [sign](#variable-sign) | +| [P_PLUS1_DIV4](#variable-p_plus1_div4) | [incrementLeastSignificantThirtyTwoBits](#variable-incrementleastsignificantthirtytwobits) | [toArray](#variable-toarray) | +| [biMod](#variable-bimod) | [jpAdd](#variable-jpadd) | [toBase58](#variable-tobase58) | +| [biModAdd](#variable-bimodadd) | [jpDouble](#variable-jpdouble) | [toBase58Check](#variable-tobase58check) | +| [biModInv](#variable-bimodinv) | [jpNeg](#variable-jpneg) | [toHex](#variable-tohex) | +| [biModMul](#variable-bimodmul) | [minimallyEncode](#variable-minimallyencode) | [toUTF8](#variable-toutf8) | +| [biModPow](#variable-bimodpow) | [modInvN](#variable-modinvn) | [verify](#variable-verify) | +| [biModSqr](#variable-bimodsqr) | [modMulN](#variable-modmuln) | [zero2](#variable-zero2) | Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables) @@ -5491,20 +5492,20 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions]( ```ts biModPow = (base: bigint, exp: bigint): bigint => { - let result = BI_ONE; + let result = 1n; base = biMod(base); - let e = exp; - while (e > BI_ZERO) { - if ((e & BI_ONE) === BI_ONE) + while (exp > 0n) { + if ((exp & 1n) !== 0n) { result = biModMul(result, base); + } base = biModMul(base, base); - e >>= BI_ONE; + exp >>= 1n; } return result; } ``` -See also: [BI_ONE](./primitives.md#variable-bi_one), [BI_ZERO](./primitives.md#variable-bi_zero), [biMod](./primitives.md#variable-bimod), [biModMul](./primitives.md#variable-bimodmul) +See also: [biMod](./primitives.md#variable-bimod), [biModMul](./primitives.md#variable-bimodmul) Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables) @@ -5525,7 +5526,10 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions]( ```ts biModSqrt = (a: bigint): bigint | null => { const r = biModPow(a, P_PLUS1_DIV4); - return biModMul(r, r) === biMod(a) ? r : null; + if (biModMul(r, r) !== biMod(a)) { + return null; + } + return r; } ``` @@ -5579,11 +5583,11 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions]( ### Variable: exclusiveOR ```ts -exclusiveOR = function (block0: number[], block1: number[]): number[] { +exclusiveOR = function (block0: Bytes, block1: Bytes): Bytes { const len = block0.length; - const result = new Array(len); + const result = new Uint8Array(len); for (let i = 0; i < len; i++) { - result[i] = block0[i] ^ block1[i]; + result[i] = block0[i] ^ (block1[i] ?? 0); } return result; } @@ -5673,6 +5677,31 @@ getBytes = function (numericValue: number): number[] { Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables) +--- +### Variable: getBytes64 + +```ts +getBytes64 = function (numericValue: number): number[] { + if (numericValue < 0 || numericValue > Number.MAX_SAFE_INTEGER) { + throw new Error("getBytes64: value out of range"); + } + const hi = Math.floor(numericValue / 4294967296); + const lo = numericValue >>> 0; + return [ + (hi >>> 24) & 255, + (hi >>> 16) & 255, + (hi >>> 8) & 255, + hi & 255, + (lo >>> 24) & 255, + (lo >>> 16) & 255, + (lo >>> 8) & 255, + lo & 255 + ]; +} +``` + +Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables) + --- ### Variable: hash160 @@ -5705,15 +5734,11 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions]( ### Variable: incrementLeastSignificantThirtyTwoBits ```ts -incrementLeastSignificantThirtyTwoBits = function (block: number[]): number[] { - let i; +incrementLeastSignificantThirtyTwoBits = function (block: Bytes): Bytes { const result = block.slice(); - for (i = 15; i !== 11; i--) { - result[i] = result[i] + 1; - if (result[i] === 256) { - result[i] = 0; - } - else { + for (let i = 15; i !== 11; i--) { + result[i] = (result[i] + 1) & 255; + if (result[i] !== 0) { break; } } @@ -5885,7 +5910,7 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions]( ### Variable: multiply ```ts -multiply = function (block0: number[], block1: number[]): number[] { +multiply = function (block0: Bytes, block1: Bytes): Bytes { const v = block1.slice(); const z = createZeroBlock(16); for (let i = 0; i < 16; i++) { @@ -5914,11 +5939,10 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions]( ### Variable: rightShift ```ts -rightShift = function (block: number[]): number[] { - let i: number; +rightShift = function (block: Bytes): Bytes { let carry = 0; let oldCarry = 0; - for (i = 0; i < block.length; i++) { + for (let i = 0; i < block.length; i++) { oldCarry = carry; carry = block[i] & 1; block[i] = block[i] >> 1; diff --git a/package-lock.json b/package-lock.json index 13bea3ef..72a53fab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bsv/sdk", - "version": "1.9.27", + "version": "1.9.28", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bsv/sdk", - "version": "1.9.27", + "version": "1.9.28", "license": "SEE LICENSE IN LICENSE.txt", "devDependencies": { "@eslint/js": "^9.39.1", @@ -67,7 +67,6 @@ "version": "7.28.5", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -567,7 +566,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -591,7 +589,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -940,9 +937,7 @@ } }, "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "version": "3.14.1", "dev": true, "license": "MIT", "dependencies": { @@ -1412,6 +1407,7 @@ "version": "0.3.11", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -1883,7 +1879,6 @@ "integrity": "sha512-AqaOMA6MTNhqMYYwrhvPA+2uS662SkAi8Rb7B/IFOzh/Z5ooyczL4lUX+qyhAO3ymn50iwM4jikQCf9XfBiaQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@module-federation/runtime-tools": "0.21.4", "@rspack/binding": "1.6.5", @@ -2053,6 +2048,7 @@ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -2064,6 +2060,7 @@ "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint": "*", "@types/estree": "*" @@ -2378,7 +2375,6 @@ "integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.48.0", "@typescript-eslint/types": "8.48.0", @@ -2894,6 +2890,7 @@ "version": "1.14.1", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" @@ -2902,22 +2899,26 @@ "node_modules/@webassemblyjs/floating-point-hex-parser": { "version": "1.13.2", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.13.2", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.14.1", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.13.2", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", @@ -2927,12 +2928,14 @@ "node_modules/@webassemblyjs/helper-wasm-bytecode": { "version": "1.13.2", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.14.1", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -2944,6 +2947,7 @@ "version": "1.13.2", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } @@ -2952,6 +2956,7 @@ "version": "1.13.2", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@xtuc/long": "4.2.2" } @@ -2959,12 +2964,14 @@ "node_modules/@webassemblyjs/utf8": { "version": "1.13.2", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.14.1", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -2980,6 +2987,7 @@ "version": "1.14.1", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", @@ -2992,6 +3000,7 @@ "version": "1.14.1", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -3003,6 +3012,7 @@ "version": "1.14.1", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", @@ -3016,6 +3026,7 @@ "version": "1.14.1", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" @@ -3026,14 +3037,16 @@ "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", "dev": true, - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/accepts": { "version": "1.3.8", @@ -3055,7 +3068,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3069,6 +3081,7 @@ "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10.13.0" }, @@ -3697,7 +3710,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -3934,6 +3946,7 @@ "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6.0" } @@ -4061,7 +4074,8 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/compressible": { "version": "2.0.18", @@ -4704,7 +4718,8 @@ "node_modules/es-module-lexer": { "version": "1.7.0", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/es-object-atoms": { "version": "1.1.1", @@ -4948,7 +4963,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -4993,7 +5007,6 @@ "integrity": "sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "builtins": "^5.0.1", "eslint-plugin-es": "^4.1.0", @@ -5033,7 +5046,6 @@ "integrity": "sha512-57Zzfw8G6+Gq7axm2Pdo3gW/Rx3h9Yywgn61uE/3elTCOePEHVrn2i5CdfBwA1BLK0Q0WqctICIUSqXZW/VprQ==", "dev": true, "license": "ISC", - "peer": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -5048,7 +5060,6 @@ "version": "7.37.5", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", @@ -5255,6 +5266,7 @@ "version": "3.3.0", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.8.x" } @@ -5860,7 +5872,8 @@ "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "dev": true, - "license": "BSD-2-Clause" + "license": "BSD-2-Clause", + "peer": true }, "node_modules/glob/node_modules/brace-expansion": { "version": "2.0.2", @@ -7034,7 +7047,6 @@ "version": "30.2.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -7654,7 +7666,6 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -7831,6 +7842,7 @@ "version": "4.3.1", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6.11.5" }, @@ -9014,6 +9026,7 @@ "version": "2.1.0", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "safe-buffer": "^5.1.0" } @@ -9467,7 +9480,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -9593,6 +9605,7 @@ "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "dependencies": { "randombytes": "^2.1.0" } @@ -10486,6 +10499,7 @@ "version": "5.44.1", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -10505,6 +10519,7 @@ "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", @@ -10540,6 +10555,7 @@ "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -10555,6 +10571,7 @@ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -10569,6 +10586,7 @@ "version": "0.5.21", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -10679,7 +10697,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -10971,7 +10988,6 @@ "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.4.0", "@typescript-eslint/scope-manager": "5.62.0", @@ -11007,7 +11023,6 @@ "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -11193,7 +11208,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -11556,8 +11570,7 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tsutils": { "version": "3.21.0", @@ -11706,7 +11719,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11930,6 +11942,7 @@ "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -12182,6 +12195,7 @@ "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10.13.0" } @@ -12192,6 +12206,7 @@ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -12206,6 +12221,7 @@ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=4.0" } diff --git a/package.json b/package.json index 241e7f17..2a11df72 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bsv/sdk", - "version": "1.9.27", + "version": "1.9.28", "type": "module", "description": "BSV Blockchain Software Development Kit", "main": "dist/cjs/mod.js", diff --git a/src/primitives/AESGCM.ts b/src/primitives/AESGCM.ts index 18098d38..22eaf4da 100644 --- a/src/primitives/AESGCM.ts +++ b/src/primitives/AESGCM.ts @@ -202,33 +202,73 @@ export const getBytes = function (numericValue: number): number[] { ] } -const createZeroBlock = function (length: number): number[] { - return new Array(length).fill(0) +export const getBytes64 = function (numericValue: number): number[] { + if (numericValue < 0 || numericValue > Number.MAX_SAFE_INTEGER) { + throw new Error('getBytes64: value out of range') + } + + const hi = Math.floor(numericValue / 0x100000000) + const lo = numericValue >>> 0 + + return [ + (hi >>> 24) & 0xFF, + (hi >>> 16) & 0xFF, + (hi >>> 8) & 0xFF, + hi & 0xFF, + (lo >>> 24) & 0xFF, + (lo >>> 16) & 0xFF, + (lo >>> 8) & 0xFF, + lo & 0xFF + ] } -const R = [0xe1].concat(createZeroBlock(15)) +type Bytes = Uint8Array + +const createZeroBlock = function (length: number): Bytes { + // Uint8Array is already zero-filled + return new Uint8Array(length) +} + +// R = 0xe1 || 15 zero bytes +const R: Bytes = (() => { + const r = new Uint8Array(16) + r[0] = 0xe1 + return r +})() + +const concatBytes = (...arrays: Bytes[]): Bytes => { + let total = 0 + for (const a of arrays) total += a.length + + const out = new Uint8Array(total) + let offset = 0 + for (const a of arrays) { + out.set(a, offset) + offset += a.length + } + return out +} -export const exclusiveOR = function (block0: number[], block1: number[]): number[] { +export const exclusiveOR = function (block0: Bytes, block1: Bytes): Bytes { const len = block0.length - const result = new Array(len) + const result = new Uint8Array(len) for (let i = 0; i < len; i++) { - result[i] = block0[i] ^ block1[i] + result[i] = block0[i] ^ (block1[i] ?? 0) } return result } -const xorInto = function (target: number[], block: number[]): void { +const xorInto = function (target: Bytes, block: Bytes): void { for (let i = 0; i < target.length; i++) { - target[i] ^= block[i] + target[i] ^= block[i] ?? 0 } } -export const rightShift = function (block: number[]): number[] { - let i: number +export const rightShift = function (block: Bytes): Bytes { let carry = 0 let oldCarry = 0 - for (i = 0; i < block.length; i++) { + for (let i = 0; i < block.length; i++) { oldCarry = carry carry = block[i] & 0x01 block[i] = block[i] >> 1 @@ -241,7 +281,7 @@ export const rightShift = function (block: number[]): number[] { return block } -export const multiply = function (block0: number[], block1: number[]): number[] { +export const multiply = function (block0: Bytes, block1: Bytes): Bytes { const v = block1.slice() const z = createZeroBlock(16) @@ -264,16 +304,14 @@ export const multiply = function (block0: number[], block1: number[]): number[] } export const incrementLeastSignificantThirtyTwoBits = function ( - block: number[] -): number[] { - let i + block: Bytes +): Bytes { const result = block.slice() - for (i = 15; i !== 11; i--) { - result[i] = result[i] + 1 - if (result[i] === 256) { - result[i] = 0 - } else { + for (let i = 15; i > 11; i--) { + result[i] = (result[i] + 1) & 0xff // wrap explicitly + + if (result[i] !== 0) { break } } @@ -281,11 +319,12 @@ export const incrementLeastSignificantThirtyTwoBits = function ( return result } -export function ghash (input: number[], hashSubKey: number[]): number[] { +export function ghash (input: Bytes, hashSubKey: Bytes): Bytes { let result = createZeroBlock(16) + const block = new Uint8Array(16) for (let i = 0; i < input.length; i += 16) { - const block = result.slice() + block.set(result) for (let j = 0; j < 16; j++) { block[j] ^= input[i + j] ?? 0 } @@ -296,14 +335,14 @@ export function ghash (input: number[], hashSubKey: number[]): number[] { } function gctr ( - input: number[], - initialCounterBlock: number[], - key: number[] -): number[] { - if (input.length === 0) return [] - - const output = new Array(input.length) - let counterBlock = initialCounterBlock + input: Bytes, + initialCounterBlock: Bytes, + key: Bytes +): Bytes { + if (input.length === 0) return new Uint8Array(0) + + const output = new Uint8Array(input.length) + let counterBlock = initialCounterBlock.slice() let pos = 0 const n = Math.ceil(input.length / 16) @@ -323,6 +362,45 @@ function gctr ( return output } +function buildAuthInput (cipherText: Bytes): Bytes { + const aadLenBits = 0 + const ctLenBits = cipherText.length * 8 + + let padLen: number + if (cipherText.length === 0) { + padLen = 16 + } else if (cipherText.length % 16 === 0) { + padLen = 0 + } else { + padLen = 16 - (cipherText.length % 16) + } + + const total = + 16 + + cipherText.length + + padLen + + 16 + + const out = new Uint8Array(total) + let offset = 0 + + offset += 16 + + out.set(cipherText, offset) + offset += cipherText.length + + offset += padLen + + const aadLen = getBytes64(aadLenBits) + out.set(aadLen, offset) + offset += 8 + + const ctLen = getBytes64(ctLenBits) + out.set(ctLen, offset) + + return out +} + /** * SECURITY NOTE – NON-STANDARD AES-GCM PADDING * @@ -371,10 +449,10 @@ function gctr ( * undecryptable by newer versions of the library. */ export function AESGCM ( - plainText: number[], - initializationVector: number[], - key: number[] -): { result: number[], authenticationTag: number[] } { + plainText: Bytes, + initializationVector: Bytes, + key: Bytes +): { result: Bytes, authenticationTag: Bytes } { if (initializationVector.length === 0) { throw new Error('Initialization vector must not be empty') } @@ -383,52 +461,54 @@ export function AESGCM ( throw new Error('Key must not be empty') } - let preCounterBlock - let plainTag: number[] = [] - const hashSubKey = AES(createZeroBlock(16), key) - preCounterBlock = [...initializationVector] + const hashSubKey = new Uint8Array(AES(createZeroBlock(16), key)) + + let preCounterBlock: Bytes + if (initializationVector.length === 12) { - preCounterBlock = preCounterBlock.concat(createZeroBlock(3)).concat([0x01]) + preCounterBlock = concatBytes(initializationVector, createZeroBlock(3), new Uint8Array([0x01])) } else { - if (initializationVector.length % 16 !== 0) { - preCounterBlock = preCounterBlock.concat( - createZeroBlock(16 - (initializationVector.length % 16)) + let ivPadded = initializationVector + if (ivPadded.length % 16 !== 0) { + ivPadded = concatBytes( + ivPadded, + createZeroBlock(16 - (ivPadded.length % 16)) ) } - preCounterBlock = preCounterBlock.concat(createZeroBlock(8)) + const lenBlock = getBytes64(initializationVector.length * 8) + const s = concatBytes( + ivPadded, + createZeroBlock(8), + new Uint8Array(lenBlock) + ) - preCounterBlock = ghash(preCounterBlock.concat(createZeroBlock(4)) - .concat(getBytes(initializationVector.length * 8)), hashSubKey) + preCounterBlock = ghash(s, hashSubKey) } - const cipherText = gctr(plainText, incrementLeastSignificantThirtyTwoBits(preCounterBlock), key) + const cipherText = gctr( + plainText, + incrementLeastSignificantThirtyTwoBits(preCounterBlock), + key + ) - plainTag = plainTag.concat(createZeroBlock(16)) - plainTag = plainTag.concat(cipherText) + const authInput = buildAuthInput(cipherText) - if (cipherText.length === 0) { - plainTag = plainTag.concat(createZeroBlock(16)) - } else if (cipherText.length % 16 !== 0) { - plainTag = plainTag.concat(createZeroBlock(16 - (cipherText.length % 16))) - } - - plainTag = plainTag.concat(createZeroBlock(4)) - .concat(getBytes(0)) - .concat(createZeroBlock(4)).concat(getBytes(cipherText.length * 8)) + const s = ghash(authInput, hashSubKey) + const authenticationTag = gctr(s, preCounterBlock, key) return { result: cipherText, - authenticationTag: gctr(ghash(plainTag, hashSubKey), preCounterBlock, key) + authenticationTag } } export function AESGCMDecrypt ( - cipherText: number[], - initializationVector: number[], - authenticationTag: number[], - key: number[] -): number[] | null { + cipherText: Bytes, + initializationVector: Bytes, + authenticationTag: Bytes, + key: Bytes +): Bytes | null { if (cipherText.length === 0) { throw new Error('Cipher text must not be empty') } @@ -441,47 +521,57 @@ export function AESGCMDecrypt ( throw new Error('Key must not be empty') } - let preCounterBlock - let compareTag: number[] = [] - // Generate the hash subkey - const hashSubKey = AES(createZeroBlock(16), key) + const hashSubKey = new Uint8Array(AES(createZeroBlock(16), key)) + + let preCounterBlock: Bytes - preCounterBlock = [...initializationVector] if (initializationVector.length === 12) { - preCounterBlock = preCounterBlock.concat(createZeroBlock(3)).concat([0x01]) + preCounterBlock = concatBytes( + initializationVector, + createZeroBlock(3), + new Uint8Array([0x01]) + ) } else { - if (initializationVector.length % 16 !== 0) { - preCounterBlock = preCounterBlock.concat(createZeroBlock(16 - (initializationVector.length % 16))) + let ivPadded = initializationVector + if (ivPadded.length % 16 !== 0) { + ivPadded = concatBytes( + ivPadded, + createZeroBlock(16 - (ivPadded.length % 16)) + ) } - preCounterBlock = preCounterBlock.concat(createZeroBlock(8)) + const lenBlock = getBytes64(initializationVector.length * 8) + const s = concatBytes( + ivPadded, + createZeroBlock(8), + new Uint8Array(lenBlock) + ) - preCounterBlock = ghash(preCounterBlock.concat(createZeroBlock(4)).concat(getBytes(initializationVector.length * 8)), hashSubKey) + preCounterBlock = ghash(s, hashSubKey) } // Decrypt to obtain the plain text - const plainText = gctr(cipherText, incrementLeastSignificantThirtyTwoBits(preCounterBlock), key) + const plainText = gctr( + cipherText, + incrementLeastSignificantThirtyTwoBits(preCounterBlock), + key + ) - compareTag = compareTag.concat(createZeroBlock(16)) - compareTag = compareTag.concat(cipherText) + const authInput = buildAuthInput(cipherText) + const s = ghash(authInput, hashSubKey) + const calculatedTag = gctr(s, preCounterBlock, key) - if (cipherText.length === 0) { - compareTag = compareTag.concat(createZeroBlock(16)) - } else if (cipherText.length % 16 !== 0) { - compareTag = compareTag.concat(createZeroBlock(16 - (cipherText.length % 16))) + if (calculatedTag.length !== authenticationTag.length) { + return null } - compareTag = compareTag.concat(createZeroBlock(4)) - .concat(getBytes(0)) - .concat(createZeroBlock(4)) - .concat(getBytes(cipherText.length * 8)) - - // Generate the authentication tag - const calculatedTag = gctr(ghash(compareTag, hashSubKey), preCounterBlock, key) + let diff = 0 + for (let i = 0; i < calculatedTag.length; i++) { + diff |= calculatedTag[i] ^ authenticationTag[i] + } - // If the calculated tag does not match the provided tag, return null - the decryption failed. - if (calculatedTag.join() !== authenticationTag.join()) { + if (diff !== 0) { return null } diff --git a/src/primitives/SymmetricKey.ts b/src/primitives/SymmetricKey.ts index e5365748..364572f1 100644 --- a/src/primitives/SymmetricKey.ts +++ b/src/primitives/SymmetricKey.ts @@ -41,19 +41,27 @@ export default class SymmetricKey extends BigNumber { * const encryptedMessage = key.encrypt('plainText', 'utf8'); */ encrypt (msg: number[] | string, enc?: 'hex'): string | number[] { - const iv = Random(32) - msg = toArray(msg, enc) - const keyBytes = this.toArray('be', 32) - const { result, authenticationTag } = AESGCM(msg, iv, keyBytes) + const iv = new Uint8Array(Random(32)) + const msgBytes = new Uint8Array(toArray(msg, enc)) + const keyBytes = new Uint8Array(this.toArray('be', 32)) + + const { result, authenticationTag } = AESGCM( + msgBytes, + iv, + keyBytes + ) + const totalLength = iv.length + result.length + authenticationTag.length - const combined = new Array(totalLength) + const combined = new Uint8Array(totalLength) let offset = 0 - for (const chunk of [iv, result, authenticationTag]) { - for (let i = 0; i < chunk.length; i++) { - combined[offset++] = chunk[i] - } - } - return encode(combined, enc) + + combined.set(iv, offset) + offset += iv.length + combined.set(result, offset) + offset += result.length + combined.set(authenticationTag, offset) + + return encode(Array.from(combined), enc) } /** @@ -73,29 +81,30 @@ export default class SymmetricKey extends BigNumber { * @throws {Error} Will throw an error if the decryption fails, likely due to message tampering or incorrect decryption key. */ decrypt (msg: number[] | string, enc?: 'hex' | 'utf8'): string | number[] { - msg = toArray(msg, enc) + const msgBytes = new Uint8Array(toArray(msg, enc)) const ivLength = 32 const tagLength = 16 - if (msg.length < ivLength + tagLength) { + if (msgBytes.length < ivLength + tagLength) { throw new Error('Ciphertext too short') } - const iv = msg.slice(0, ivLength) - const tagStart = msg.length - tagLength - const ciphertext = msg.slice(ivLength, tagStart) - const messageTag = msg.slice(tagStart) + const iv = msgBytes.slice(0, ivLength) + const tagStart = msgBytes.length - tagLength + const ciphertext = msgBytes.slice(ivLength, tagStart) + const messageTag = msgBytes.slice(tagStart) + const keyBytes = new Uint8Array(this.toArray('be', 32)) const result = AESGCMDecrypt( ciphertext, iv, messageTag, - this.toArray('be', 32) + keyBytes ) if (result === null) { throw new Error('Decryption failed!') } - return encode(result, enc) + return encode(Array.from(result), enc) } } diff --git a/src/primitives/__tests/AESGCM.test.ts b/src/primitives/__tests/AESGCM.test.ts index f5645c43..0e880c86 100644 --- a/src/primitives/__tests/AESGCM.test.ts +++ b/src/primitives/__tests/AESGCM.test.ts @@ -68,258 +68,335 @@ describe('AES', () => { describe('ghash', () => { it('should ghash', () => { - expect(toArray('f38cbb1ad69223dcc3457ae5b6b0f885', 'hex')).toEqual( - ghash( - toArray( - '000000000000000000000000000000000388dace60b6a392f328c2b971b2fe780000000000000000000000' + - '0000000080', - 'hex' - ), - toArray('66e94bd4ef8a2c3b884cfa59ca342b2e', 'hex') + const input = new Uint8Array( + toArray( + '000000000000000000000000000000000388dace60b6a392f328c2b971b2fe780000000000000000000000' + + '0000000080', + 'hex' ) ) + const h = new Uint8Array( + toArray('66e94bd4ef8a2c3b884cfa59ca342b2e', 'hex') + ) + const out = ghash(input, h) + + expect(toArray('f38cbb1ad69223dcc3457ae5b6b0f885', 'hex')).toEqual( + Array.from(out) + ) }) }) describe('AESGCM', () => { it('should encrypt: Test Case 1', () => { - const output = AESGCM( - [], - toArray('000000000000000000000000', 'hex'), + const plainText = new Uint8Array(0) + const iv = new Uint8Array( + toArray('000000000000000000000000', 'hex') + ) + const key = new Uint8Array( toArray('00000000000000000000000000000000', 'hex') ) - expect([]).toEqual(output.result) + const output = AESGCM(plainText, iv, key) + + expect([]).toEqual(Array.from(output.result)) expect(toArray('58e2fccefa7e3061367f1d57a4e7455a', 'hex')).toEqual( - output.authenticationTag + Array.from(output.authenticationTag) ) }) it('should encrypt: Test Case 2', () => { - const output = AESGCM( - toArray('00000000000000000000000000000000', 'hex'), - toArray('000000000000000000000000', 'hex'), + const plainText = new Uint8Array( toArray('00000000000000000000000000000000', 'hex') ) + const iv = new Uint8Array( + toArray('000000000000000000000000', 'hex') + ) + const key = new Uint8Array( + toArray('00000000000000000000000000000000', 'hex') + ) + + const output = AESGCM(plainText, iv, key) expect(toArray('0388dace60b6a392f328c2b971b2fe78', 'hex')).toEqual( - output.result + Array.from(output.result) ) expect(toArray('ab6e47d42cec13bdf53a67b21257bddf', 'hex')).toEqual( - output.authenticationTag + Array.from(output.authenticationTag) ) }) it('should encrypt: Test Case 3', () => { - const output = AESGCM( + const plainText = new Uint8Array( toArray( 'd9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956' + '809532fcf0e2449a6b525b16aedf5aa0de657ba637b391aafd255', 'hex' - ), - toArray('cafebabefacedbaddecaf888', 'hex'), + ) + ) + const iv = new Uint8Array( + toArray('cafebabefacedbaddecaf888', 'hex') + ) + const key = new Uint8Array( toArray('feffe9928665731c6d6a8f9467308308', 'hex') ) + const output = AESGCM(plainText, iv, key) + expect( toArray( '42831ec2217774244b7221b784d0d49ce3aa212f2c02a4e035c17e2329aca12e21d514b25466931c7d8' + 'f6a5aac84aa051ba30b396a0aac973d58e091473f5985', 'hex' ) - ).toEqual(output.result) + ).toEqual(Array.from(output.result)) expect(toArray('4d5c2af327cd64a62cf35abd2ba6fab4', 'hex')).toEqual( - output.authenticationTag + Array.from(output.authenticationTag) ) }) it('should encrypt: Test Case 7', () => { - const output = AESGCM( - [], - toArray('000000000000000000000000', 'hex'), + const plainText = new Uint8Array(0) + const iv = new Uint8Array( + toArray('000000000000000000000000', 'hex') + ) + const key = new Uint8Array( toArray('000000000000000000000000000000000000000000000000', 'hex') ) - expect([]).toEqual(output.result) + const output = AESGCM(plainText, iv, key) + + expect([]).toEqual(Array.from(output.result)) expect(toArray('cd33b28ac773f74ba00ed1f312572435', 'hex')).toEqual( - output.authenticationTag + Array.from(output.authenticationTag) ) }) it('should encrypt: Test Case 8', () => { - const output = AESGCM( - toArray('00000000000000000000000000000000', 'hex'), - toArray('000000000000000000000000', 'hex'), + const plainText = new Uint8Array( + toArray('00000000000000000000000000000000', 'hex') + ) + const iv = new Uint8Array( + toArray('000000000000000000000000', 'hex') + ) + const key = new Uint8Array( toArray('000000000000000000000000000000000000000000000000', 'hex') ) + const output = AESGCM(plainText, iv, key) + expect(toArray('98e7247c07f0fe411c267e4384b0f600', 'hex')).toEqual( - output.result + Array.from(output.result) ) expect(toArray('2ff58d80033927ab8ef4d4587514f0fb', 'hex')).toEqual( - output.authenticationTag + Array.from(output.authenticationTag) ) }) it('should encrypt: Test Case 9', () => { - const output = AESGCM( + const plainText = new Uint8Array( toArray( 'd9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956' + '809532fcf0e2449a6b525b16aedf5aa0de657ba637b391aafd255', 'hex' - ), - toArray('cafebabefacedbaddecaf888', 'hex'), + ) + ) + const iv = new Uint8Array( + toArray('cafebabefacedbaddecaf888', 'hex') + ) + const key = new Uint8Array( toArray('feffe9928665731c6d6a8f9467308308feffe9928665731c', 'hex') ) + const output = AESGCM(plainText, iv, key) + expect( toArray( '3980ca0b3c00e841eb06fac4872a2757859e1ceaa6efd984628593b40ca1e19c7d773d00c144c525ac6' + '19d18c84a3f4718e2448b2fe324d9ccda2710acade256', 'hex' ) - ).toEqual(output.result) + ).toEqual(Array.from(output.result)) expect(toArray('9924a7c8587336bfb118024db8674a14', 'hex')).toEqual( - output.authenticationTag + Array.from(output.authenticationTag) ) }) it('should encrypt: Test Case 13', () => { - const output = AESGCM( - [], - toArray('000000000000000000000000', 'hex'), + const plainText = new Uint8Array(0) + const iv = new Uint8Array( + toArray('000000000000000000000000', 'hex') + ) + const key = new Uint8Array( toArray( '0000000000000000000000000000000000000000000000000000000000000000', 'hex' ) ) - expect([]).toEqual(output.result) + const output = AESGCM(plainText, iv, key) + + expect([]).toEqual(Array.from(output.result)) expect(toArray('530f8afbc74536b9a963b4f1c4cb738b', 'hex')).toEqual( - output.authenticationTag + Array.from(output.authenticationTag) ) }) it('should encrypt: Test Case 14', () => { - const output = AESGCM( - toArray('00000000000000000000000000000000', 'hex'), - toArray('000000000000000000000000', 'hex'), + const plainText = new Uint8Array( + toArray('00000000000000000000000000000000', 'hex') + ) + const iv = new Uint8Array( + toArray('000000000000000000000000', 'hex') + ) + const key = new Uint8Array( toArray( '0000000000000000000000000000000000000000000000000000000000000000', 'hex' ) ) + const output = AESGCM(plainText, iv, key) + expect(toArray('cea7403d4d606b6e074ec5d3baf39d18', 'hex')).toEqual( - output.result + Array.from(output.result) ) expect(toArray('d0d1c8a799996bf0265b98b5d48ab919', 'hex')).toEqual( - output.authenticationTag + Array.from(output.authenticationTag) ) }) it('should encrypt: Test Case 15', () => { - const output = AESGCM( + const plainText = new Uint8Array( toArray( 'd9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956' + '809532fcf0e2449a6b525b16aedf5aa0de657ba637b391aafd255', 'hex' - ), - toArray('cafebabefacedbaddecaf888', 'hex'), + ) + ) + const iv = new Uint8Array( + toArray('cafebabefacedbaddecaf888', 'hex') + ) + const key = new Uint8Array( toArray( 'feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308', 'hex' ) ) + const output = AESGCM(plainText, iv, key) + expect( toArray( '522dc1f099567d07f47f37a32a84427d643a8cdcbfe5c0c97598a2bd2555d1aa8cb08e48590dbb3da7b' + '08b1056828838c5f61e6393ba7a0abcc9f662898015ad', 'hex' ) - ).toEqual(output.result) + ).toEqual(Array.from(output.result)) expect(toArray('b094dac5d93471bdec1a502270e3cc6c', 'hex')).toEqual( - output.authenticationTag + Array.from(output.authenticationTag) ) }) }) describe('exclusiveOR', () => { it('should exclusiveOR', () => { + const out1 = exclusiveOR( + new Uint8Array([ + 0xf0, 0xf8, 0x7f, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + new Uint8Array([0x0f, 0x0f, 0x00, 0xf0]) + ) + expect([ 0xff, 0xf7, 0x7f, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 - ]).toEqual( - exclusiveOR( - [ - 0xf0, 0xf8, 0x7f, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00 - ], - [0x0f, 0x0f, 0x00, 0xf0] - ) - ) + ]).toEqual(Array.from(out1)) - expect([0xff, 0xf7, 0x7f, 0x0f]).toEqual( - exclusiveOR([0xf0, 0xf8, 0x7f, 0xff], [0x0f, 0x0f, 0x00, 0xf0]) + const out2 = exclusiveOR( + new Uint8Array([0xf0, 0xf8, 0x7f, 0xff]), + new Uint8Array([0x0f, 0x0f, 0x00, 0xf0]) ) + + expect([0xff, 0xf7, 0x7f, 0x0f]).toEqual(Array.from(out2)) }) }) describe('rightShift', () => { it('should rightShift', () => { + const input = new Uint8Array( + toArray('7b5b54657374566563746f725d53475d', 'hex') + ) + const out = rightShift(input) + expect(toArray('3dadaa32b9ba2b32b1ba37b92ea9a3ae', 'hex')).toEqual( - rightShift(toArray('7b5b54657374566563746f725d53475d', 'hex')) + Array.from(out) ) }) }) describe('multiply', () => { it('should multiply', () => { + const a = new Uint8Array( + toArray('952b2a56a5604ac0b32b6656a05b40b6', 'hex') + ) + const b = new Uint8Array( + toArray('dfa6bf4ded81db03ffcaff95f830f061', 'hex') + ) + const out = multiply(a, b) + expect(toArray('da53eb0ad2c55bb64fc4802cc3feda60', 'hex')).toEqual( - multiply( - toArray('952b2a56a5604ac0b32b6656a05b40b6', 'hex'), - toArray('dfa6bf4ded81db03ffcaff95f830f061', 'hex') - ) + Array.from(out) ) }) it('should commutatively multiply', () => { - expect( - multiply( - toArray('48692853686179295b477565726f6e5d', 'hex'), - toArray('7b5b54657374566563746f725d53475d', 'hex') - ) - ).toEqual( - multiply( - toArray('7b5b54657374566563746f725d53475d', 'hex'), - toArray('48692853686179295b477565726f6e5d', 'hex') - ) + const x = new Uint8Array( + toArray('48692853686179295b477565726f6e5d', 'hex') ) + const y = new Uint8Array( + toArray('7b5b54657374566563746f725d53475d', 'hex') + ) + + const out1 = multiply(x, y) + const out2 = multiply(y, x) + + expect(Array.from(out1)).toEqual(Array.from(out2)) }) }) describe('incrementLeastSignificantThirtyTwoBits', () => { it('should incrementLeastSignificantThirtyTwoBits', () => { + const in1 = new Uint8Array( + toArray('00000000000000000000000000000000', 'hex') + ) + const out1 = incrementLeastSignificantThirtyTwoBits(in1) expect(toArray('00000000000000000000000000000001', 'hex')).toEqual( - incrementLeastSignificantThirtyTwoBits( - toArray('00000000000000000000000000000000', 'hex') - ) + Array.from(out1) + ) + + const in2 = new Uint8Array( + toArray('000000000000000000000000000000ff', 'hex') ) + const out2 = incrementLeastSignificantThirtyTwoBits(in2) expect(toArray('00000000000000000000000000000100', 'hex')).toEqual( - incrementLeastSignificantThirtyTwoBits( - toArray('000000000000000000000000000000ff', 'hex') - ) + Array.from(out2) + ) + + const in3 = new Uint8Array( + toArray('00000000000000000000000000ffffff', 'hex') ) + const out3 = incrementLeastSignificantThirtyTwoBits(in3) expect(toArray('00000000000000000000000001000000', 'hex')).toEqual( - incrementLeastSignificantThirtyTwoBits( - toArray('00000000000000000000000000ffffff', 'hex') - ) + Array.from(out3) ) + + const in4 = new Uint8Array( + toArray('000000000000000000000000ffffffff', 'hex') + ) + const out4 = incrementLeastSignificantThirtyTwoBits(in4) expect(toArray('00000000000000000000000000000000', 'hex')).toEqual( - incrementLeastSignificantThirtyTwoBits( - toArray('000000000000000000000000ffffffff', 'hex') - ) + Array.from(out4) ) }) }) @@ -329,7 +406,9 @@ describe('checkBit', () => { let i let j let k = 0 - let block = toArray('7b5b54657374566563746f725d53475d', 'hex') + let block = new Uint8Array( + toArray('7b5b54657374566563746f725d53475d', 'hex') + ) as any const expected = [ 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, @@ -342,12 +421,12 @@ describe('checkBit', () => { for (i = 0; i < 16; i++) { for (j = 7; j !== -1; j--) { - expect(expected[k++]).toEqual(checkBit(block, i, j)) + expect(expected[k++]).toEqual(checkBit(Array.from(block), i, j)) } } for (i = 0; i < 128; i++) { - expect(expectedLSB[i]).toEqual(checkBit(block, 15, 0)) + expect(expectedLSB[i]).toEqual(checkBit(Array.from(block), 15, 0)) block = rightShift(block) } }) @@ -400,55 +479,122 @@ describe('getBytes', () => { }) describe('AESGCM IV validation', () => { - const key = new Array(16).fill(0x01) - const plaintext = [1, 2, 3, 4] + const key = new Uint8Array(new Array(16).fill(0x01)) + const plaintext = new Uint8Array([1, 2, 3, 4]) it('AESGCM throws when IV is empty', () => { expect(() => { - AESGCM(plaintext, [], key) + AESGCM(plaintext, new Uint8Array(), key) }).toThrow(new Error('Initialization vector must not be empty')) }) it('AESGCMDecrypt throws when IV is empty', () => { - const iv = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] - const { result: ciphertext, authenticationTag } = AESGCM(plaintext, iv, key) + const iv = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) + const { result: ciphertext, authenticationTag } = AESGCM( + plaintext, + iv, + key + ) // Now call decrypt but with an empty IV – this should be rejected expect(() => { - AESGCMDecrypt(ciphertext, [], authenticationTag, key) + AESGCMDecrypt(ciphertext, new Uint8Array(), authenticationTag, key) }).toThrow(new Error('Initialization vector must not be empty')) }) it('AESGCM throws when key is empty', () => { - const iv = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] + const iv = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) expect(() => { - AESGCM(plaintext, iv, []) + AESGCM(plaintext, iv, new Uint8Array()) }).toThrow(new Error('Key must not be empty')) }) it('AESGCMDecrypt throws when key is empty', () => { - const iv = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] - const { result: ciphertext, authenticationTag } = AESGCM(plaintext, iv, key) + const iv = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) + const { result: ciphertext, authenticationTag } = AESGCM( + plaintext, + iv, + key + ) expect(() => { - AESGCMDecrypt(ciphertext, iv, authenticationTag, []) + AESGCMDecrypt( + ciphertext, + iv, + authenticationTag, + new Uint8Array() + ) }).toThrow(new Error('Key must not be empty')) }) it('AESGCMDecrypt throws when cipher text is empty', () => { - const iv = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] + const iv = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) expect(() => { - AESGCMDecrypt([], iv, [], key) + AESGCMDecrypt(new Uint8Array(), iv, new Uint8Array(), key) }).toThrow(new Error('Cipher text must not be empty')) }) it('AESGCM still work with a valid IV', () => { - const iv = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] - const { result: ciphertext, authenticationTag } = AESGCM(plaintext, iv, key) - const decrypted = AESGCMDecrypt(ciphertext, iv, authenticationTag, key) + const iv = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) + const { result: ciphertext, authenticationTag } = AESGCM( + plaintext, + iv, + key + ) + const decrypted = AESGCMDecrypt( + ciphertext, + iv, + authenticationTag, + key + ) as Uint8Array + + expect(Array.from(decrypted)).toEqual(Array.from(plaintext)) + }) +}) - expect(decrypted).toEqual(plaintext) +function expectUint8ArrayEqual (a: Uint8Array, b: Uint8Array) { + expect(a.length).toBe(b.length) + + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) { + throw new Error(`mismatch at index ${i}: ${a[i]} !== ${b[i]}`) + } + } +} + +describe('AESGCM large input (non-mocked)', () => { + // NOTE: This test is intentionally skipped by default because it allocates + // ~500MB+ and will be very slow / memory-heavy. + // Un-skip locally when you want to manually verify behavior for lengths + // larger than 2^32 bits. + it.skip('handles ciphertext longer than 2^32 bits', () => { + const key = new Uint8Array(new Array(16).fill(0x01)) + const iv = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) + + // 2^32 bits = 2^29 bytes. Go just beyond that boundary. + const bigSizeBytes = (1 << 29) + 16 // 2^29 + 16 bytes (> 2^32 bits) + + // Use a typed array instead of a giant sparse JS array. + const plaintext = new Uint8Array(bigSizeBytes) // already zero-initialized + + const { result: ciphertext, authenticationTag } = AESGCM( + plaintext, + iv, + key + ) + + const decrypted = AESGCMDecrypt( + ciphertext, + iv, + authenticationTag, + key + ) as Uint8Array | null + + expect(decrypted).not.toBeNull() + const decryptedBytes = decrypted as Uint8Array + expect(decryptedBytes.length).toBe(bigSizeBytes) + expectUint8ArrayEqual(decryptedBytes, plaintext) }) }) diff --git a/src/primitives/hex.ts b/src/primitives/hex.ts index 655c7723..bc0b0fa1 100644 --- a/src/primitives/hex.ts +++ b/src/primitives/hex.ts @@ -5,15 +5,13 @@ const PURE_HEX_REGEX = /^[0-9a-fA-F]*$/ export function assertValidHex (msg: string): void { if (typeof msg !== 'string') { - console.error('assertValidHex FAIL (non-string):', msg) - throw new Error('Invalid hex string') + throw new TypeError('Invalid hex string') } // allow empty if (msg.length === 0) return if (!PURE_HEX_REGEX.test(msg)) { - console.error('assertValidHex FAIL (bad hex):', msg) throw new Error('Invalid hex string') } }