diff --git a/README.md b/README.md index a30ac1e..0847a6e 100644 --- a/README.md +++ b/README.md @@ -40,8 +40,8 @@ npm install @noble/curves ``` ```ts -// Short Weierstrass curve -import shortw from '@noble/curves/shortw'; +import shortw from '@noble/curves/shortw'; // Short Weierstrass curve +import twistede from '@noble/curves/twistede'; // Twisted Edwards curve import { sha256 } from '@noble/hashes/sha256'; import { hmac } from '@noble/hashes/hmac'; import { concatBytes, randomBytes } from '@noble/hashes/utils'; diff --git a/package.json b/package.json index 79552aa..f80844c 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,11 @@ "import": "./lib/esm/shortw.js", "default": "./lib/shortw.js" }, + "./twisted": { + "types": "./lib/twisted.d.ts", + "import": "./lib/esm/twisted.js", + "default": "./lib/twisted.js" + }, "./utils": { "types": "./lib/utils.d.ts", "import": "./lib/esm/utils.js", @@ -72,4 +77,4 @@ "url": "https://paulmillr.com/funding/" } ] -} +} \ No newline at end of file diff --git a/src/group.ts b/src/group.ts new file mode 100644 index 0000000..e4a3cd5 --- /dev/null +++ b/src/group.ts @@ -0,0 +1,124 @@ +/*! @noble/curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ +// Default group related functions +const _0n = BigInt(0); +const _1n = BigInt(1); + +export interface Group> { + double(): T; + add(other: T): T; + negate(): T; +} + +export type GroupConstructor = { + BASE: T; + ZERO: T; +}; +// Not big, but pretty complex and it is easy to break stuff. To avoid too much copy paste +export function wNAF>(c: GroupConstructor, bits: number) { + const constTimeNegate = (condition: boolean, item: T): T => { + const neg = item.negate(); + return condition ? neg : item; + }; + const opts = (W: number) => { + if (256 % W) throw new Error('Invalid precomputation window, must be power of 2'); + const windows = Math.ceil(bits / W) + 1; // +1, because + const windowSize = 2 ** (W - 1); // -1 because we skip zero + return { windows, windowSize }; + }; + return { + constTimeNegate, + // non-const time multiplication ladder + unsafeLadder(elm: T, n: bigint) { + let p = c.ZERO; + let d: T = elm; + while (n > _0n) { + if (n & _1n) p = p.add(d); + d = d.double(); + n >>= _1n; + } + return p; + }, + + /** + * Creates a wNAF precomputation window. Used for caching. + * Default window size is set by `utils.precompute()` and is equal to 8. + * Which means we are caching 65536 points: 256 points for every bit from 0 to 256. + * @returns 65K precomputed points, depending on W + */ + precomputeWindow(elm: T, W: number): Group[] { + const { windows, windowSize } = opts(W); + const points: T[] = []; + let p: T = elm; + let base = p; + for (let window = 0; window < windows; window++) { + base = p; + points.push(base); + // =1, because we skip zero + for (let i = 1; i < windowSize; i++) { + base = base.add(p); + points.push(base); + } + p = base.double(); + } + return points; + }, + + /** + * Implements w-ary non-adjacent form for calculating ec multiplication. + * @param n + * @param affinePoint optional 2d point to save cached precompute windows on it. + * @returns real and fake (for const-time) points + */ + wNAF(W: number, precomputes: T[], n: bigint): { p: T; f: T } { + const { windows, windowSize } = opts(W); + + let p = c.ZERO; + let f = c.BASE; + + const mask = BigInt(2 ** W - 1); // Create mask with W ones: 0b1111 for W=4 etc. + const maxNumber = 2 ** W; + const shiftBy = BigInt(W); + + for (let window = 0; window < windows; window++) { + const offset = window * windowSize; + // Extract W bits. + let wbits = Number(n & mask); + + // Shift number by W bits. + n >>= shiftBy; + + // If the bits are bigger than max size, we'll split those. + // +224 => 256 - 32 + if (wbits > windowSize) { + wbits -= maxNumber; + n += _1n; + } + + // This code was first written with assumption that 'f' and 'p' will never be infinity point: + // since each addition is multiplied by 2 ** W, it cannot cancel each other. However, + // there is negate now: it is possible that negated element from low value + // would be the same as high element, which will create carry into next window. + // It's not obvious how this can fail, but still worth investigating later. + + // Check if we're onto Zero point. + // Add random point inside current window to f. + const offset1 = offset; + const offset2 = offset + Math.abs(wbits) - 1; // -1 because we skip zero + const cond1 = window % 2 !== 0; + const cond2 = wbits < 0; + if (wbits === 0) { + // The most important part for const-time getPublicKey + f = f.add(constTimeNegate(cond1, precomputes[offset1])); + } else { + p = p.add(constTimeNegate(cond2, precomputes[offset2])); + } + } + // JIT-compiler should not eliminate f here, since it will later be used in normalizeZ() + // Even if the variable is still unused, there are some checks which will + // throw an exception, so compiler needs to prove they won't happen, which is hard. + // At this point there is a way to F be infinity-point even if p is not, + // which makes it less const-time: around 1 bigint multiply. + return { p, f }; + }, + }; +} diff --git a/src/modular.ts b/src/modular.ts index 1b7eb34..1ce038b 100644 --- a/src/modular.ts +++ b/src/modular.ts @@ -1,4 +1,6 @@ /*! @noble/curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ + +// Utilities for modular arithmetics const _0n = BigInt(0); const _1n = BigInt(1); const _2n = BigInt(2); @@ -144,3 +146,14 @@ export function sqrt(number: bigint, modulo: bigint): bigint { } return r; } + +// Little-endian check for first LE bit (last BE bit); +export const isNegativeLE = (num: bigint, modulo: bigint) => (mod(num, modulo) & _1n) === _1n; + +// An idea on modular arithmetic for bls12-381: +// const FIELD = {add, pow, sqrt, mul}; +// Functions will take field elements, no need for an additional class +// Could be faster. 1 bigint field will just do operations and mod later: +// instead of 'r = mod(r * b, P)' we will write r = mul(r, b); +// Could be insecure without shape check, so it needs to be done. +// Functions could be inlined by JIT. diff --git a/src/shortw.ts b/src/shortw.ts index 9dada99..aad6ca1 100644 --- a/src/shortw.ts +++ b/src/shortw.ts @@ -1,8 +1,14 @@ /*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ -// Implementation of Short weierstrass curve. The formula is: y² = x³ + ax + b +// Implementation of Short Weierstrass curve. The formula is: y² = x³ + ax + b // TODO: sync vs async naming // TODO: default randomBytes +// Differences from noble/secp256k1: +// 1. Different double() formula (but same addition) +// 2. Different sqrt() function +// 3. truncateHash() truncateOnly mode +// 4. DRBG supports outputLen bigger than outputLen of hmac + import * as mod from './modular.js'; import { bytesToHex, @@ -12,7 +18,9 @@ import { hexToBytes, hexToNumber, numberToHexUnpadded, + nLength, } from './utils.js'; +import { wNAF } from './group.js'; export type CHash = { (message: Uint8Array | string): Uint8Array; @@ -91,10 +99,8 @@ function validateOpts(curve: CurveType) { throw new Error('Expected endomorphism with beta: bigint and splitScalar: function'); } } - const nBitLength = curve.n.toString(2).length; // Bit size of CURVE.n - const nByteLength = Math.ceil(nBitLength / 8); // Byte size of CURVE.n // Set defaults - return Object.freeze({ lowS: true, nBitLength, nByteLength, ...curve } as const); + return Object.freeze({ lowS: true, ...nLength(curve.n, curve.nBitLength), ...curve } as const); } // TODO: convert bits to bytes aligned to 32 bits? (224 for example) @@ -287,8 +293,8 @@ class HmacDrbg { if (typeof qByteLen !== 'number' || qByteLen < 2) throw new Error('qByteLen must be a number'); if (typeof hmacFn !== 'function') throw new Error('hmacFn must be a function'); // Step B, Step C: set hashLen to 8*ceil(hlen/8) - this.v = new Uint8Array(this.hashLen).fill(1); - this.k = new Uint8Array(this.hashLen).fill(0); + this.v = new Uint8Array(hashLen).fill(1); + this.k = new Uint8Array(hashLen).fill(0); this.counter = 0; } private hmacSync(...values: Uint8Array[]) { @@ -327,8 +333,11 @@ class HmacDrbg { // Use only input from curveOpts! export function weierstrass(curveDef: CurveType): CurveFn { const CURVE = validateOpts(curveDef) as ReturnType; + const CURVE_ORDER = CURVE.n; // Lengths - const fieldLen = CURVE.nByteLength!; // 32 (length of one field element) + // All curves has same field / group length as for now, but it can be different for other curves + const groupLen = CURVE.nByteLength; + const fieldLen = CURVE.nByteLength; // 32 (length of one field element) if (fieldLen > 2048) throw new Error('Field lengths over 2048 are not supported'); const compressedLen = fieldLen + 1; // 33 @@ -378,11 +387,11 @@ export function weierstrass(curveDef: CurveType): CurveFn { } else if (typeof key === 'number' && Number.isSafeInteger(key) && key > 0) { num = BigInt(key); } else if (typeof key === 'string') { - key = key.padStart(2 * fieldLen, '0'); // Eth-like hexes - if (key.length !== 2 * fieldLen) throw new Error(`Expected ${fieldLen} bytes of private key`); + key = key.padStart(2 * groupLen, '0'); // Eth-like hexes + if (key.length !== 2 * groupLen) throw new Error(`Expected ${groupLen} bytes of private key`); num = hexToNumber(key); } else if (key instanceof Uint8Array) { - if (key.length !== fieldLen) throw new Error(`Expected ${fieldLen} bytes of private key`); + if (key.length !== groupLen) throw new Error(`Expected ${groupLen} bytes of private key`); num = bytesToNumber(key); } else { throw new TypeError('Expected valid private key'); @@ -405,12 +414,12 @@ export function weierstrass(curveDef: CurveType): CurveFn { } function isBiggerThanHalfOrder(number: bigint) { - const HALF = CURVE.n >> _1n; + const HALF = CURVE_ORDER >> _1n; return number > HALF; } function normalizeS(s: bigint) { - return isBiggerThanHalfOrder(s) ? mod.mod(-s, CURVE.n) : s; + return isBiggerThanHalfOrder(s) ? mod.mod(-s, CURVE_ORDER) : s; } function normalizeScalar(num: number | bigint): bigint { @@ -499,6 +508,23 @@ export function weierstrass(curveDef: CurveType): CurveFn { double(): JacobianPoint { const { x: X1, y: Y1, z: Z1 } = this; const { a } = CURVE; + + // // Faster algorithm: when a=0 + // // From: https://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian-0.html#doubling-dbl-2009-l + // // Cost: 2M + 5S + 6add + 3*2 + 1*3 + 1*8. + if (a === _0n) { + const A = modP(X1 * X1); + const B = modP(Y1 * Y1); + const C = modP(B * B); + const x1b = X1 + B; + const D = modP(_2n * (modP(x1b * x1b) - A - C)); + const E = modP(_3n * A); + const F = modP(E * E); + const X3 = modP(F - _2n * D); + const Y3 = modP(E * (D - X3) - _8n * C); + const Z3 = modP(_2n * Y1 * Z1); + return new JacobianPoint(X3, Y3, Z3); + } const XX = modP(X1 * X1); const YY = modP(Y1 * Y1); const YYYY = modP(YY * YY); @@ -520,8 +546,6 @@ export function weierstrass(curveDef: CurveType): CurveFn { // Note: 2007 Bernstein-Lange (11M + 5S + 9add + 4*2) is actually 10% slower. add(other: JacobianPoint): JacobianPoint { if (!(other instanceof JacobianPoint)) throw new TypeError('JacobianPoint expected'); - // TODO: remove - if (this.equals(JacobianPoint.ZERO)) return other; const { x: X1, y: Y1, z: Z1 } = this; const { x: X2, y: Y2, z: Z2 } = other; if (X2 === _0n || Y2 === _0n) return this; @@ -562,16 +586,7 @@ export function weierstrass(curveDef: CurveType): CurveFn { let n = normalizeScalar(scalar); if (n === _1n) return this; - if (!CURVE.endo) { - let p = P0; - let d: JacobianPoint = this; - while (n > _0n) { - if (n & _1n) p = p.add(d); - d = d.double(); - n >>= _1n; - } - return p; - } + if (!CURVE.endo) return wnaf.unsafeLadder(this, n); // Apply endomorphism let { k1neg, k1, k2neg, k2 } = CURVE.endo.splitScalar(n); @@ -591,106 +606,22 @@ export function weierstrass(curveDef: CurveType): CurveFn { return k1p.add(k2p); } - /** - * Creates a wNAF precomputation window. Used for caching. - * Default window size is set by `utils.precompute()` and is equal to 8. - * Which means we are caching 65536 points: 256 points for every bit from 0 to 256. - * @returns 65K precomputed points, depending on W - */ - private precomputeWindow(W: number): JacobianPoint[] { - const windows = CURVE.endo - ? Math.ceil(CURVE.nBitLength / 2) / W + 1 - : CURVE.nBitLength / W + 1; - const points: JacobianPoint[] = []; - let p: JacobianPoint = this; - let base = p; - for (let window = 0; window < windows; window++) { - base = p; - points.push(base); - for (let i = 1; i < 2 ** (W - 1); i++) { - base = base.add(p); - points.push(base); - } - p = base.double(); - } - return points; - } - /** * Implements w-ary non-adjacent form for calculating ec multiplication. - * @param n - * @param affinePoint optional 2d point to save cached precompute windows on it. - * @returns real and fake (for const-time) points */ private wNAF(n: bigint, affinePoint?: Point): { p: JacobianPoint; f: JacobianPoint } { if (!affinePoint && this.equals(JacobianPoint.BASE)) affinePoint = Point.BASE; const W = (affinePoint && affinePoint._WINDOW_SIZE) || 1; - if (256 % W) { - throw new Error('Point#wNAF: Invalid precomputation window, must be power of 2'); - } - // Calculate precomputes on a first run, reuse them after let precomputes = affinePoint && pointPrecomputes.get(affinePoint); if (!precomputes) { - precomputes = this.precomputeWindow(W); + precomputes = wnaf.precomputeWindow(this, W) as JacobianPoint[]; if (affinePoint && W !== 1) { precomputes = JacobianPoint.normalizeZ(precomputes); pointPrecomputes.set(affinePoint, precomputes); } } - - // Initialize real and fake points for const-time - let p = JacobianPoint.ZERO; - // Should be G (base) point, since otherwise f can be infinity point in the end - let f = JacobianPoint.BASE; - - const nBits = CURVE.endo ? CURVE.nBitLength / 2 : CURVE.nBitLength; - const windows = 1 + Math.ceil(nBits / W); // W=8 17 - const windowSize = 2 ** (W - 1); // W=8 128 - const mask = BigInt(2 ** W - 1); // Create mask with W ones: 0b11111111 for W=8 - const maxNumber = 2 ** W; // W=8 256 - const shiftBy = BigInt(W); // W=8 8 - - for (let window = 0; window < windows; window++) { - const offset = window * windowSize; - // Extract W bits. - let wbits = Number(n & mask); - - // Shift number by W bits. - n >>= shiftBy; - - // If the bits are bigger than max size, we'll split those. - // +224 => 256 - 32 - if (wbits > windowSize) { - wbits -= maxNumber; - n += _1n; - } - - // This code was first written with assumption that 'f' and 'p' will never be infinity point: - // since each addition is multiplied by 2 ** W, it cannot cancel each other. However, - // there is negate now: it is possible that negated element from low value - // would be the same as high element, which will create carry into next window. - // It's not obvious how this can fail, but still worth investigating later. - - // Check if we're onto Zero point. - // Add random point inside current window to f. - const offset1 = offset; - const offset2 = offset + Math.abs(wbits) - 1; - const cond1 = window % 2 !== 0; - const cond2 = wbits < 0; - if (wbits === 0) { - // The most important part for const-time getPublicKey - f = f.add(constTimeNegate(cond1, precomputes[offset1])); - } else { - p = p.add(constTimeNegate(cond2, precomputes[offset2])); - } - } - // JIT-compiler should not eliminate f here, since it will later be used in normalizeZ() - // Even if the variable is still unused, there are some checks which will - // throw an exception, so compiler needs to prove they won't happen, which is hard. - // At this point there is a way to F be infinity-point even if p is not, - // which makes it less const-time: around 1 bigint multiply. - return { p, f }; + return wnaf.wNAF(W, precomputes, n); } /** @@ -712,8 +643,8 @@ export function weierstrass(curveDef: CurveType): CurveFn { const { k1neg, k1, k2neg, k2 } = CURVE.endo.splitScalar(n); let { p: k1p, f: f1p } = this.wNAF(k1, affinePoint); let { p: k2p, f: f2p } = this.wNAF(k2, affinePoint); - k1p = constTimeNegate(k1neg, k1p); - k2p = constTimeNegate(k2neg, k2p); + k1p = wnaf.constTimeNegate(k1neg, k1p); + k2p = wnaf.constTimeNegate(k2neg, k2p); k2p = new JacobianPoint(modP(k2p.x * CURVE.endo.beta), k2p.y, k2p.z); point = k1p.add(k2p); fake = f1p.add(f2p); @@ -747,11 +678,8 @@ export function weierstrass(curveDef: CurveType): CurveFn { return new Point(ax, ay); } } - // Const-time utility for wNAF - function constTimeNegate(condition: boolean, item: JacobianPoint) { - const neg = item.negate(); - return condition ? neg : item; - } + const wnaf = wNAF(JacobianPoint, CURVE.endo ? CURVE.nBitLength / 2 : CURVE.nBitLength); + // Stores precomputed values for points. const pointPrecomputes = new WeakMap(); @@ -787,8 +715,7 @@ export function weierstrass(curveDef: CurveType): CurveFn { } /** - * Supports compressed ECDSA (33-byte) points - * @param bytes 33 bytes + * Supports compressed ECDSA points * @returns Point instance */ private static fromCompressedHex(bytes: Uint8Array) { @@ -816,7 +743,7 @@ export function weierstrass(curveDef: CurveType): CurveFn { /** * Converts hash string or Uint8Array to Point. - * @param hex 33/65-byte (ECDSA) hex + * @param hex short/long ECDSA hex */ static fromHex(hex: Hex): Point { const bytes = ensureBytes(hex); @@ -986,7 +913,7 @@ export function weierstrass(curveDef: CurveType): CurveFn { normalizeS() { return this.hasHighS() - ? new Signature(this.r, mod.mod(-this.s, CURVE.n), this.recovery) + ? new Signature(this.r, mod.mod(-this.s, CURVE_ORDER), this.recovery) : this; } @@ -1041,7 +968,7 @@ export function weierstrass(curveDef: CurveType): CurveFn { if (hash.length < minLen || hash.length > 1024) { throw new Error(`Expected ${minLen}-1024 bytes of private key as per FIPS 186`); } - const num = mod.mod(bytesToNumber(hash), CURVE.n - _1n) + _1n; + const num = mod.mod(bytesToNumber(hash), CURVE_ORDER - _1n) + _1n; return numToField(num); }, @@ -1067,8 +994,8 @@ export function weierstrass(curveDef: CurveType): CurveFn { /** * Computes public key for a private key. - * @param privateKey 32-byte private key - * @param isCompressed whether to return compact (33-byte), or full (65-byte) key + * @param privateKey private key + * @param isCompressed whether to return compact, or full key * @returns Public key, full by default; short when isCompressed=true */ function getPublicKey(privateKey: PrivKey, isCompressed = false): Uint8Array { @@ -1112,7 +1039,7 @@ export function weierstrass(curveDef: CurveType): CurveFn { } function bits2octets(bytes: Uint8Array): Uint8Array { const z1 = bits2int(bytes); - const z2 = mod.mod(z1, CURVE.n); + const z2 = mod.mod(z1, CURVE_ORDER); return int2octets(z2 < _0n ? z1 : z2); } function int2octets(num: bigint): Uint8Array { @@ -1253,7 +1180,6 @@ export function weierstrass(curveDef: CurveType): CurveFn { getSharedSecret, sign, verify, - Point, JacobianPoint, Signature, diff --git a/src/twistede.ts b/src/twistede.ts new file mode 100644 index 0000000..5ee7f09 --- /dev/null +++ b/src/twistede.ts @@ -0,0 +1,698 @@ +/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ +// Implementation of Twisted Edwards curve. The formula is: ax² + y² = 1 + dx²y² + +// Differences with @noble/ed25519: +// 1. EDDSA & ECDH have different field element lengths (for ed448/x448 only) +// https://www.rfc-editor.org/rfc/rfc8032.html says bitLength is 456 (57 bytes) +// https://www.rfc-editor.org/rfc/rfc7748 says bitLength is 448 (56 bytes) +// 2. Different addition formula (doubling is same) +// 3. uvRatio differs between curves (half-expected, not only pow fn changes) +// 4. Point decompression code is different too (unexpected), now using generalized formula +// 5. Domain function was no-op for ed25519, but adds some data even with empty context for ed448 + +import * as mod from './modular.js'; +import { bytesToHex, concatBytes, ensureBytes, numberToBytesLE, nLength } from './utils.js'; +import { wNAF } from './group.js'; + +// Be friendly to bad ECMAScript parsers by not using bigint literals like 123n +const _0n = BigInt(0); +const _1n = BigInt(1); +const _2n = BigInt(2); +const _8n = BigInt(8); + +export type CHash = { + (message: Uint8Array | string): Uint8Array; + blockLen: number; + outputLen: number; + create(): any; +}; + +export type CurveType = { + // Params: a, d + a: bigint; + d: bigint; + // Field over which we'll do calculations. Verify with: + P: bigint; + // Subgroup order: how many points ed25519 has + n: bigint; // in rfc8032 called l + // Cofactor + h: bigint; + nBitLength?: number; + nByteLength?: number; + // Base point (x, y) aka generator point + Gx: bigint; + Gy: bigint; + // Other constants + a24: bigint; + // ECDH bits (can be different from N bits) + scalarBits: number; + // Hashes + hash: CHash; // Because we need outputLen for DRBG + randomBytes: (bytesLength?: number) => Uint8Array; + adjustScalarBytes: (bytes: Uint8Array) => Uint8Array; + domain: (data: Uint8Array, ctx: Uint8Array, hflag: boolean) => Uint8Array; + uvRatio: (u: bigint, v: bigint) => { isValid: boolean; value: bigint }; +}; + +// We accept hex strings besides Uint8Array for simplicity +type Hex = Uint8Array | string; +// Very few implementations accept numbers, we do it to ease learning curve +type PrivKey = Hex | bigint | number; + +// Should be separate from overrides, since overrides can use information about curve (for example nBits) +function validateOpts(curve: CurveType) { + if (typeof curve.hash !== 'function' || !Number.isSafeInteger(curve.hash.outputLen)) + throw new Error('Invalid hash function'); + for (const i of ['a', 'd', 'P', 'n', 'h', 'Gx', 'Gy', 'a24'] as const) { + if (typeof curve[i] !== 'bigint') + throw new Error(`Invalid curve param ${i}=${curve[i]} (${typeof curve[i]})`); + } + for (const i of ['scalarBits'] as const) { + if (typeof curve[i] !== 'number') + throw new Error(`Invalid curve param ${i}=${curve[i]} (${typeof curve[i]})`); + } + for (const i of ['nBitLength', 'nByteLength'] as const) { + if (curve[i] === undefined) continue; // Optional + if (!Number.isSafeInteger(curve[i])) + throw new Error(`Invalid curve param ${i}=${curve[i]} (${typeof curve[i]})`); + } + for (const fn of ['randomBytes', 'adjustScalarBytes', 'domain', 'uvRatio'] as const) { + if (typeof curve[fn] !== 'function') throw new Error(`Invalid ${fn} function`); + } + // Set defaults + return Object.freeze({ ...nLength(curve.n, curve.nBitLength), ...curve } as const); +} + +// Instance +export interface SignatureType { + readonly r: PointType; + readonly s: bigint; + assertValidity(): SignatureType; + toRawBytes(): Uint8Array; + toHex(): string; +} +// Static methods +export type SignatureConstructor = { + new (r: PointType, s: bigint): SignatureType; + fromHex(hex: Hex): SignatureType; +}; + +// Instance +export interface ExtendedPointType { + readonly x: bigint; + readonly y: bigint; + readonly z: bigint; + readonly t: bigint; + equals(other: ExtendedPointType): boolean; + negate(): ExtendedPointType; + double(): ExtendedPointType; + add(other: ExtendedPointType): ExtendedPointType; + subtract(other: ExtendedPointType): ExtendedPointType; + multiply(scalar: number | bigint, affinePoint?: PointType): ExtendedPointType; + multiplyUnsafe(scalar: number | bigint): ExtendedPointType; + isSmallOrder(): boolean; + isTorsionFree(): boolean; + toAffine(invZ?: bigint): PointType; +} +// Static methods +export type ExtendedPointConstructor = { + new (x: bigint, y: bigint, z: bigint, t: bigint): ExtendedPointType; + BASE: ExtendedPointType; + ZERO: ExtendedPointType; + fromAffine(p: PointType): ExtendedPointType; + toAffineBatch(points: ExtendedPointType[]): PointType[]; + normalizeZ(points: ExtendedPointType[]): ExtendedPointType[]; +}; + +// Instance +export interface PointType { + readonly x: bigint; + readonly y: bigint; + _setWindowSize(windowSize: number): void; + toRawBytes(isCompressed?: boolean): Uint8Array; + toHex(isCompressed?: boolean): string; + // toX25519(): Uint8Array; + isTorsionFree(): boolean; + equals(other: PointType): boolean; + negate(): PointType; + add(other: PointType): PointType; + subtract(other: PointType): PointType; + multiply(scalar: number | bigint): PointType; +} +// Static methods +export type PointConstructor = { + BASE: PointType; + ZERO: PointType; + new (x: bigint, y: bigint): PointType; + fromHex(hex: Hex): PointType; + fromPrivateKey(privateKey: PrivKey): PointType; +}; + +export type PubKey = Hex | PointType; +export type SigType = Hex | SignatureType; + +export type CurveFn = { + CURVE: ReturnType; + getPublicKey: (privateKey: PrivKey, isCompressed?: boolean) => Uint8Array; + sign: (message: Hex, privateKey: Hex) => Uint8Array; + verify: (sig: SigType, message: Hex, publicKey: PubKey) => boolean; + Point: PointConstructor; + ExtendedPoint: ExtendedPointConstructor; + Signature: SignatureConstructor; + utils: { + mod: (a: bigint, b?: bigint) => bigint; + invert: (number: bigint, modulo?: bigint) => bigint; + randomPrivateKey: () => Uint8Array; + getExtendedPublicKey: (key: PrivKey) => { + head: Uint8Array; + prefix: Uint8Array; + scalar: bigint; + point: PointType; + pointBytes: Uint8Array; + }; + }; +}; + +// NOTE: it is not generic twisted curve for now, but ed25519/ed448 generic implementation +export function twistedEdwards(curveDef: CurveType): CurveFn { + const CURVE = validateOpts(curveDef) as ReturnType; + const CURVE_ORDER = CURVE.n; + const fieldLen = CURVE.nByteLength; // 32 (length of one field element) + if (fieldLen > 2048) throw new Error('Field lengths over 2048 are not supported'); + const groupLen = CURVE.nByteLength; + // (2n ** 256n).toString(16); + const maxGroupElement = _2n ** BigInt(groupLen * 8); // previous POW_2_256 + + // Function overrides + const { adjustScalarBytes, randomBytes, uvRatio } = CURVE; + + function modP(a: bigint) { + return mod.mod(a, CURVE.P); + } + + /** + * Extended Point works in extended coordinates: (x, y, z, t) ∋ (x=x/z, y=y/z, t=xy). + * Default Point works in affine coordinates: (x, y) + * https://en.wikipedia.org/wiki/Twisted_Edwards_curve#Extended_coordinates + */ + class ExtendedPoint implements ExtendedPointType { + constructor(readonly x: bigint, readonly y: bigint, readonly z: bigint, readonly t: bigint) {} + + static BASE = new ExtendedPoint(CURVE.Gx, CURVE.Gy, _1n, modP(CURVE.Gx * CURVE.Gy)); + static ZERO = new ExtendedPoint(_0n, _1n, _1n, _0n); + static fromAffine(p: Point): ExtendedPoint { + if (!(p instanceof Point)) { + throw new TypeError('ExtendedPoint#fromAffine: expected Point'); + } + if (p.equals(Point.ZERO)) return ExtendedPoint.ZERO; + return new ExtendedPoint(p.x, p.y, _1n, modP(p.x * p.y)); + } + // Takes a bunch of Jacobian Points but executes only one + // invert on all of them. invert is very slow operation, + // so this improves performance massively. + static toAffineBatch(points: ExtendedPoint[]): Point[] { + const toInv = mod.invertBatch(points.map((p) => p.z), CURVE.P); + return points.map((p, i) => p.toAffine(toInv[i])); + } + + static normalizeZ(points: ExtendedPoint[]): ExtendedPoint[] { + return this.toAffineBatch(points).map(this.fromAffine); + } + + // Compare one point to another. + equals(other: ExtendedPoint): boolean { + assertExtPoint(other); + const { x: X1, y: Y1, z: Z1 } = this; + const { x: X2, y: Y2, z: Z2 } = other; + const X1Z2 = modP(X1 * Z2); + const X2Z1 = modP(X2 * Z1); + const Y1Z2 = modP(Y1 * Z2); + const Y2Z1 = modP(Y2 * Z1); + return X1Z2 === X2Z1 && Y1Z2 === Y2Z1; + } + + // Inverses point to one corresponding to (x, -y) in Affine coordinates. + negate(): ExtendedPoint { + return new ExtendedPoint(modP(-this.x), this.y, this.z, modP(-this.t)); + } + + // Fast algo for doubling Extended Point. + // https://hyperelliptic.org/EFD/g1p/auto-twisted-extended.html#doubling-dbl-2008-hwcd + // Cost: 4M + 4S + 1*a + 6add + 1*2. + double(): ExtendedPoint { + const { a } = CURVE; + const { x: X1, y: Y1, z: Z1 } = this; + const A = modP(X1 * X1); // A = X12 + const B = modP(Y1 * Y1); // B = Y12 + const C = modP(_2n * modP(Z1 * Z1)); // C = 2*Z12 + const D = modP(a * A); // D = a*A + const x1y1 = X1 + Y1; + const E = modP(modP(x1y1 * x1y1) - A - B); // E = (X1+Y1)2-A-B + const G = D + B; // G = D+B + const F = G - C; // F = G-C + const H = D - B; // H = D-B + const X3 = modP(E * F); // X3 = E*F + const Y3 = modP(G * H); // Y3 = G*H + const T3 = modP(E * H); // T3 = E*H + const Z3 = modP(F * G); // Z3 = F*G + return new ExtendedPoint(X3, Y3, Z3, T3); + } + + // Fast algo for adding 2 Extended Points. + // https://hyperelliptic.org/EFD/g1p/auto-twisted-extended.html#addition-add-2008-hwcd + // Cost: 9M + 1*a + 1*d + 7add. + add(other: ExtendedPoint) { + assertExtPoint(other); + const { a, d } = CURVE; + const { x: X1, y: Y1, z: Z1, t: T1 } = this; + const { x: X2, y: Y2, z: Z2, t: T2 } = other; + const A = modP(X1 * X2); // A = X1*X2 + const B = modP(Y1 * Y2); // B = Y1*Y2 + const C = modP(T1 * d * T2); // C = T1*d*T2 + const D = modP(Z1 * Z2); // D = Z1*Z2 + const E = modP((X1 + Y1) * (X2 + Y2) - A - B); // E = (X1+Y1)*(X2+Y2)-A-B + // TODO: do we need to check for same point here? Looks like working without it + const F = D - C; // F = D-C + const G = D + C; // G = D+C + const H = modP(B - a * A); // H = B-a*A + const X3 = modP(E * F); // X3 = E*F + const Y3 = modP(G * H); // Y3 = G*H + const T3 = modP(E * H); // T3 = E*H + const Z3 = modP(F * G); // Z3 = F*G + + return new ExtendedPoint(X3, Y3, Z3, T3); + } + + subtract(other: ExtendedPoint): ExtendedPoint { + return this.add(other.negate()); + } + + private wNAF(n: bigint, affinePoint?: Point): ExtendedPoint { + if (!affinePoint && this.equals(ExtendedPoint.BASE)) affinePoint = Point.BASE; + const W = (affinePoint && affinePoint._WINDOW_SIZE) || 1; + let precomputes = affinePoint && pointPrecomputes.get(affinePoint); + if (!precomputes) { + precomputes = wnaf.precomputeWindow(this, W) as ExtendedPoint[]; + if (affinePoint && W !== 1) { + precomputes = ExtendedPoint.normalizeZ(precomputes); + pointPrecomputes.set(affinePoint, precomputes); + } + } + const { p, f } = wnaf.wNAF(W, precomputes, n); + return ExtendedPoint.normalizeZ([p, f])[0]; + } + + // Constant time multiplication. + // Uses wNAF method. Windowed method may be 10% faster, + // but takes 2x longer to generate and consumes 2x memory. + multiply(scalar: number | bigint, affinePoint?: Point): ExtendedPoint { + return this.wNAF(normalizeScalar(scalar, CURVE_ORDER), affinePoint); + } + + // Non-constant-time multiplication. Uses double-and-add algorithm. + // It's faster, but should only be used when you don't care about + // an exposed private key e.g. sig verification. + // Allows scalar bigger than curve order, but less than 2^256 + multiplyUnsafe(scalar: number | bigint): ExtendedPoint { + let n = normalizeScalar(scalar, CURVE_ORDER, false); + const G = ExtendedPoint.BASE; + const P0 = ExtendedPoint.ZERO; + if (n === _0n) return P0; + if (this.equals(P0) || n === _1n) return this; + if (this.equals(G)) return this.wNAF(n); + return wnaf.unsafeLadder(this, n); + } + + // Multiplies point by cofactor and checks if the result is 0. + isSmallOrder(): boolean { + return this.multiplyUnsafe(CURVE.h).equals(ExtendedPoint.ZERO); + } + + // Multiplies point by a very big scalar n and checks if the result is 0. + isTorsionFree(): boolean { + return this.multiplyUnsafe(CURVE_ORDER).equals(ExtendedPoint.ZERO); + } + + // Converts Extended point to default (x, y) coordinates. + // Can accept precomputed Z^-1 - for example, from invertBatch. + toAffine(invZ?: bigint): Point { + const { x, y, z } = this; + const is0 = this.equals(ExtendedPoint.ZERO); + if (invZ == null) invZ = is0 ? _8n : mod.invert(z, CURVE.P); // 8 was chosen arbitrarily + const ax = modP(x * invZ); + const ay = modP(y * invZ); + const zz = modP(z * invZ); + if (is0) return Point.ZERO; + if (zz !== _1n) throw new Error('invZ was invalid'); + return new Point(ax, ay); + } + } + const wnaf = wNAF(ExtendedPoint, groupLen * 8); + + function assertExtPoint(other: unknown) { + if (!(other instanceof ExtendedPoint)) throw new TypeError('ExtendedPoint expected'); + } + // Stores precomputed values for points. + const pointPrecomputes = new WeakMap(); + + /** + * Default Point works in affine coordinates: (x, y) + */ + class Point implements PointType { + // Base point aka generator + // public_key = Point.BASE * private_key + static BASE: Point = new Point(CURVE.Gx, CURVE.Gy); + // Identity point aka point at infinity + // point = point + zero_point + static ZERO: Point = new Point(_0n, _1n); + // We calculate precomputes for elliptic curve point multiplication + // using windowed method. This specifies window size and + // stores precomputed values. Usually only base point would be precomputed. + _WINDOW_SIZE?: number; + + constructor(readonly x: bigint, readonly y: bigint) {} + + // "Private method", don't use it directly. + _setWindowSize(windowSize: number) { + this._WINDOW_SIZE = windowSize; + pointPrecomputes.delete(this); + } + + // Converts hash string or Uint8Array to Point. + // Uses algo from RFC8032 5.1.3. + static fromHex(hex: Hex, strict = true) { + const { d, P, a } = CURVE; + hex = ensureBytes(hex, fieldLen); + // 1. First, interpret the string as an integer in little-endian + // representation. Bit 255 of this number is the least significant + // bit of the x-coordinate and denote this value x_0. The + // y-coordinate is recovered simply by clearing this bit. If the + // resulting value is >= p, decoding fails. + const normed = hex.slice(); + const lastByte = hex[fieldLen - 1]; + normed[fieldLen - 1] = lastByte & ~0x80; + const y = bytesToNumberLE(normed); + + if (strict && y >= P) throw new Error('Expected 0 < hex < P'); + if (!strict && y >= maxGroupElement) throw new Error('Expected 0 < hex < 2**256'); + + // 2. To recover the x-coordinate, the curve equation implies + // Ed25519: x² = (y² - 1) / (d y² + 1) (mod p). + // Ed448: x² = (y² - 1) / (d y² - 1) (mod p). + // For generic case: + // a*x²+y²=1+d*x²*y² + // -> y²-1 = d*x²*y²-a*x² + // -> y²-1 = x² (d*y²-a) + // -> x² = (y²-1) / (d*y²-a) + + // The denominator is always non-zero mod p. Let u = y² - 1 and v = d y² + 1. + const y2 = modP(y * y); + const u = modP(y2 - _1n); + const v = modP(d * y2 - a); + let { isValid, value: x } = uvRatio(u, v); + if (!isValid) throw new Error('Point.fromHex: invalid y coordinate'); + // 4. Finally, use the x_0 bit to select the right square root. If + // x = 0, and x_0 = 1, decoding fails. Otherwise, if x_0 != x mod + // 2, set x <-- p - x. Return the decoded point (x,y). + const isXOdd = (x & _1n) === _1n; + const isLastByteOdd = (lastByte & 0x80) !== 0; + if (isLastByteOdd !== isXOdd) { + x = modP(-x); + } + return new Point(x, y); + } + + static fromPrivateKey(privateKey: PrivKey) { + return getExtendedPublicKey(privateKey).point; + } + + // There can always be only two x values (x, -x) for any y + // When compressing point, it's enough to only store its y coordinate + // and use the last byte to encode sign of x. + toRawBytes(): Uint8Array { + const bytes = numberToBytesLE(this.y, fieldLen); + bytes[fieldLen - 1] |= this.x & _1n ? 0x80 : 0; + return bytes; + } + + // Same as toRawBytes, but returns string. + toHex(): string { + return bytesToHex(this.toRawBytes()); + } + + isTorsionFree(): boolean { + return ExtendedPoint.fromAffine(this).isTorsionFree(); + } + + equals(other: Point): boolean { + return this.x === other.x && this.y === other.y; + } + + negate() { + return new Point(modP(-this.x), this.y); + } + + add(other: Point) { + return ExtendedPoint.fromAffine(this).add(ExtendedPoint.fromAffine(other)).toAffine(); + } + + subtract(other: Point) { + return this.add(other.negate()); + } + + /** + * Constant time multiplication. + * @param scalar Big-Endian number + * @returns new point + */ + multiply(scalar: number | bigint): Point { + return ExtendedPoint.fromAffine(this).multiply(scalar, this).toAffine(); + } + } + + /** + * EDDSA signature. + */ + class Signature implements SignatureType { + constructor(readonly r: Point, readonly s: bigint) { + this.assertValidity(); + } + + static fromHex(hex: Hex) { + const bytes = ensureBytes(hex, 2 * fieldLen); + const r = Point.fromHex(bytes.slice(0, fieldLen), false); + const s = bytesToNumberLE(bytes.slice(fieldLen, 2 * fieldLen)); + return new Signature(r, s); + } + + assertValidity() { + const { r, s } = this; + if (!(r instanceof Point)) throw new Error('Expected Point instance'); + // 0 <= s < l + normalizeScalar(s, CURVE_ORDER, false); + return this; + } + + toRawBytes() { + return concatBytes(this.r.toRawBytes(), numberToBytesLE(this.s, fieldLen)); + } + + toHex() { + return bytesToHex(this.toRawBytes()); + } + } + + // Little Endian + function bytesToNumberLE(uint8a: Uint8Array): bigint { + if (!(uint8a instanceof Uint8Array)) throw new Error('Expected Uint8Array'); + return BigInt('0x' + bytesToHex(Uint8Array.from(uint8a).reverse())); + } + + // ------------------------- + // Little-endian SHA512 with modulo n + function modlLE(hash: Uint8Array): bigint { + return mod.mod(bytesToNumberLE(hash), CURVE_ORDER); + } + + /** + * Checks for num to be in range: + * For strict == true: `0 < num < max`. + * For strict == false: `0 <= num < max`. + * Converts non-float safe numbers to bigints. + */ + function normalizeScalar(num: number | bigint, max: bigint, strict = true): bigint { + if (!max) throw new TypeError('Specify max value'); + if (typeof num === 'number' && Number.isSafeInteger(num)) num = BigInt(num); + if (typeof num === 'bigint' && num < max) { + if (strict) { + if (_0n < num) return num; + } else { + if (_0n <= num) return num; + } + } + throw new TypeError('Expected valid scalar: 0 < scalar < max'); + } + + function checkPrivateKey(key: PrivKey) { + // Normalize bigint / number / string to Uint8Array + key = + typeof key === 'bigint' || typeof key === 'number' + ? numberToBytesLE(normalizeScalar(key, maxGroupElement), groupLen) + : ensureBytes(key); + if (key.length !== groupLen) throw new Error(`Expected ${groupLen} bytes, got ${key.length}`); + return key; + } + + // Takes 64 bytes + function getKeyFromHash(hashed: Uint8Array) { + // First 32 bytes of 64b uniformingly random input are taken, + // clears 3 bits of it to produce a random field element. + const head = adjustScalarBytes(hashed.slice(0, groupLen)); + // Second 32 bytes is called key prefix (5.1.6) + const prefix = hashed.slice(groupLen, 2 * groupLen); + // The actual private scalar + const scalar = modlLE(head); + // Point on Edwards curve aka public key + const point = Point.BASE.multiply(scalar); + const pointBytes = point.toRawBytes(); + return { head, prefix, scalar, point, pointBytes }; + } + + /** Convenience method that creates public key and other stuff. RFC8032 5.1.5 */ + function getExtendedPublicKey(key: PrivKey) { + return getKeyFromHash(CURVE.hash(checkPrivateKey(key))); + } + + /** + * Calculates ed25519 public key. RFC8032 5.1.5 + * 1. private key is hashed with sha512, then first 32 bytes are taken from the hash + * 2. 3 least significant bits of the first byte are cleared + */ + function getPublicKey(privateKey: PrivKey): Uint8Array { + return getExtendedPublicKey(privateKey).pointBytes; + } + + /** Signs message with privateKey. RFC8032 5.1.6 */ + function sign(message: Hex, privateKey: Hex): Uint8Array { + message = ensureBytes(message); + const { prefix, scalar, pointBytes } = getExtendedPublicKey(privateKey); + const rDomain = CURVE.domain(concatBytes(prefix, message), new Uint8Array(), false); + const r = modlLE(CURVE.hash(rDomain)); // r = hash(prefix + msg) + const R = Point.BASE.multiply(r); // R = rG + const kDomain = CURVE.domain( + concatBytes(R.toRawBytes(), pointBytes, message), + new Uint8Array(), + false + ); + const k = modlLE(CURVE.hash(kDomain)); // k = hash(R+P+msg) + const s = mod.mod(r + k * scalar, CURVE_ORDER); // s = r + kp + return new Signature(R, s).toRawBytes(); + } + + // Helper functions because we have async and sync methods. + function prepareVerification(sig: SigType, message: Hex, publicKey: PubKey) { + message = ensureBytes(message); + // When hex is passed, we check public key fully. + // When Point instance is passed, we assume it has already been checked, for performance. + // If user passes Point/Sig instance, we assume it has been already verified. + // We don't check its equations for performance. We do check for valid bounds for s though + // We always check for: a) s bounds. b) hex validity + if (publicKey instanceof Point) { + // ignore + } else if (publicKey instanceof Uint8Array || typeof publicKey === 'string') { + publicKey = Point.fromHex(publicKey, false); + } else { + throw new Error(`Invalid publicKey: ${publicKey}`); + } + + if (sig instanceof Signature) sig.assertValidity(); + else if (sig instanceof Uint8Array || typeof sig === 'string') sig = Signature.fromHex(sig); + else throw new Error(`Wrong signature: ${sig}`); + + const { r, s } = sig; + const SB = ExtendedPoint.BASE.multiplyUnsafe(s); + return { r, s, SB, pub: publicKey, msg: message }; + } + + function finishVerification(publicKey: Point, r: Point, SB: ExtendedPoint, hashed: Uint8Array) { + const k = modlLE(hashed); + const kA = ExtendedPoint.fromAffine(publicKey).multiplyUnsafe(k); + const RkA = ExtendedPoint.fromAffine(r).add(kA); + // [8][S]B = [8]R + [8][k]A' + return RkA.subtract(SB).multiplyUnsafe(CURVE.h).equals(ExtendedPoint.ZERO); + } + + /** + * Verifies EdDSA signature against message and public key. + * An extended group equation is checked. + * RFC8032 5.1.7 + * Compliant with ZIP215: + * 0 <= sig.R/publicKey < 2**256 (can be >= curve.P) + * 0 <= sig.s < l + * Not compliant with RFC8032: it's not possible to comply to both ZIP & RFC at the same time. + */ + function verify(sig: SigType, message: Hex, publicKey: PubKey): boolean { + const { r, SB, msg, pub } = prepareVerification(sig, message, publicKey); + const domain = CURVE.domain( + concatBytes(r.toRawBytes(), pub.toRawBytes(), msg), + new Uint8Array([]), + false + ); + const hashed = CURVE.hash(domain); + return finishVerification(pub, r, SB, hashed); + } + + // Enable precomputes. Slows down first publicKey computation by 20ms. + Point.BASE._setWindowSize(8); + + const utils = { + getExtendedPublicKey, + mod: modP, + invert: (a: bigint, m = CURVE.P) => mod.invert(a, m), + + /** + * Can take 40 or more bytes of uniform input e.g. from CSPRNG or KDF + * and convert them into private scalar, with the modulo bias being neglible. + * As per FIPS 186 B.4.1. + * Not needed for ed25519 private keys. Needed if you use scalars directly (rare). + * @param hash hash output from sha512, or a similar function + * @returns valid private scalar + */ + hashToPrivateScalar: (hash: Hex): bigint => { + hash = ensureBytes(hash); + if (hash.length < 40 || hash.length > 1024) + throw new Error('Expected 40-1024 bytes of private key as per FIPS 186'); + return mod.mod(bytesToNumberLE(hash), CURVE_ORDER - _1n) + _1n; + }, + + /** + * ed25519 private keys are uniform 32-bit strings. We do not need to check for + * modulo bias like we do in noble-secp256k1 randomPrivateKey() + */ + randomPrivateKey: (): Uint8Array => randomBytes(fieldLen), + + /** + * We're doing scalar multiplication (used in getPublicKey etc) with precomputed BASE_POINT + * values. This slows down first getPublicKey() by milliseconds (see Speed section), + * but allows to speed-up subsequent getPublicKey() calls up to 20x. + * @param windowSize 2, 4, 8, 16 + */ + precompute(windowSize = 8, point = Point.BASE): Point { + const cached = point.equals(Point.BASE) ? point : new Point(point.x, point.y); + cached._setWindowSize(windowSize); + cached.multiply(_2n); + return cached; + }, + }; + + return { + CURVE, + ExtendedPoint, + Point, + Signature, + getPublicKey, + utils, + sign, + verify, + }; +} diff --git a/src/utils.ts b/src/utils.ts index 4b4bb1f..3a36c25 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -48,10 +48,17 @@ export function bytesToNumber(bytes: Uint8Array): bigint { return hexToNumber(bytesToHex(bytes)); } -export function ensureBytes(hex: string | Uint8Array): Uint8Array { +export const numberToBytesBE = (n: bigint, len: number) => + hexToBytes(n.toString(16).padStart(len * 2, '0')); +export const numberToBytesLE = (n: bigint, len: number) => numberToBytesBE(n, len).reverse(); + +export function ensureBytes(hex: string | Uint8Array, expectedLength?: number): Uint8Array { // Uint8Array.from() instead of hash.slice() because node.js Buffer // is instance of Uint8Array, and its slice() creates **mutable** copy - return hex instanceof Uint8Array ? Uint8Array.from(hex) : hexToBytes(hex); + const bytes = hex instanceof Uint8Array ? Uint8Array.from(hex) : hexToBytes(hex); + if (typeof expectedLength === 'number' && bytes.length !== expectedLength) + throw new Error(`Expected ${expectedLength} bytes`); + return bytes; } // Copies several Uint8Arrays into one. @@ -67,3 +74,11 @@ export function concatBytes(...arrays: Uint8Array[]): Uint8Array { } return result; } + +// CURVE.n lengths +export function nLength(n: bigint, nBitLength?: number) { + // Bit size, byte size of CURVE.n + const _nBitLength = nBitLength !== undefined ? nBitLength : n.toString(2).length; + const nByteLength = Math.ceil(_nBitLength / 8); + return { nBitLength: _nBitLength, nByteLength }; +}