From 70f4e40ef54568fd0aad6d6866a3c1e107819196 Mon Sep 17 00:00:00 2001 From: Alina Sireneva Date: Mon, 6 Nov 2023 02:28:35 +0300 Subject: [PATCH] chore: embraced native bigints --- packages/core/package.json | 1 - packages/core/src/network/authorization.ts | 76 +++---- .../core/src/network/transports/obfuscated.ts | 5 +- packages/core/src/utils/bigint-utils.ts | 200 ++++++++++++++---- packages/core/src/utils/buffer-utils.ts | 17 ++ .../core/src/utils/crypto/factorization.ts | 67 +++--- .../core/src/utils/crypto/miller-rabin.ts | 40 ++-- packages/core/src/utils/crypto/password.ts | 22 +- packages/core/tests/bigint-utils.spec.ts | 59 ++++-- packages/core/tests/buffer-utils.spec.ts | 33 ++- packages/core/tests/miller-rabin.spec.ts | 7 +- packages/mtproxy/fake-tls.ts | 48 +++-- packages/mtproxy/package.json | 3 +- pnpm-lock.yaml | 6 - 14 files changed, 380 insertions(+), 204 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 078cde3c..88761f0d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -48,7 +48,6 @@ "@mtcute/tl-runtime": "workspace:^", "@mtcute/wasm": "workspace:^", "@types/events": "3.0.0", - "big-integer": "1.6.51", "events": "3.2.0", "long": "5.2.3" }, diff --git a/packages/core/src/network/authorization.ts b/packages/core/src/network/authorization.ts index 82f7a39b..6ef3d27e 100644 --- a/packages/core/src/network/authorization.ts +++ b/packages/core/src/network/authorization.ts @@ -1,4 +1,3 @@ -import bigInt from 'big-integer' import Long from 'long' import { mtp } from '@mtcute/tl' @@ -11,45 +10,46 @@ import { findKeyByFingerprints } from '../utils/crypto/keys.js' import { millerRabin } from '../utils/crypto/miller-rabin.js' import { generateKeyAndIvFromNonce } from '../utils/crypto/mtproto.js' import { xorBuffer, xorBufferInPlace } from '../utils/crypto/utils.js' -import { bigIntToBuffer, bufferToBigInt, ICryptoProvider, Logger } from '../utils/index.js' +import { bigIntModPow, bigIntToBuffer, bufferToBigInt, ICryptoProvider, Logger } from '../utils/index.js' import { mtpAssertTypeIs } from '../utils/type-assertions.js' import { SessionConnection } from './session-connection.js' // Heavily based on code from https://github.com/LonamiWebs/Telethon/blob/master/telethon/network/authenticator.py // see https://core.telegram.org/mtproto/security_guidelines -const DH_SAFETY_RANGE = bigInt[2].pow(2048 - 64) -const KNOWN_DH_PRIME = bigInt( - 'C71CAEB9C6B1C9048E6C522F70F13F73980D40238E3E21C14934D037563D930F48198A0AA7C14058229493D22530F4DBFA336F6E0AC925139543AED44CCE7C3720FD51F69458705AC68CD4FE6B6B13ABDC9746512969328454F18FAF8C595F642477FE96BB2A941D5BCD1D4AC8CC49880708FA9B378E3C4F3A9060BEE67CF9A4A4A695811051907E162753B56B0F6B410DBA74D8A84B2A14B3144E0EF1284754FD17ED950D5965B4B9DD46582DB1178D169C6BC465B0D6FF9CA3928FEF5B9AE4E418FC15E83EBEA0F87FA9FF5EED70050DED2849F47BF959D956850CE929851F0D8115F635B105EE2E4E15D04B2454BF6F4FADF034B10403119CD8E3B92FCC5B', - 16, -) -const TWO_POW_2047 = bigInt[2].pow(2047) -const TWO_POW_2048 = bigInt[2].pow(2048) +// const DH_SAFETY_RANGE = bigInt[2].pow(2048 - 64) +const DH_SAFETY_RANGE = 2n ** (2048n - 64n) +const KNOWN_DH_PRIME = + // eslint-disable-next-line max-len + 0xc71caeb9c6b1c9048e6c522f70f13f73980d40238e3e21c14934d037563d930f48198a0aa7c14058229493d22530f4dbfa336f6e0ac925139543aed44cce7c3720fd51f69458705ac68cd4fe6b6b13abdc9746512969328454f18faf8c595f642477fe96bb2a941d5bcd1d4ac8cc49880708fa9b378e3c4f3a9060bee67cf9a4a4a695811051907e162753b56b0f6b410dba74d8a84b2a14b3144e0ef1284754fd17ed950d5965b4b9dd46582db1178d169c6bc465b0d6ff9ca3928fef5b9ae4e418fc15e83ebea0f87fa9ff5eed70050ded2849f47bf959d956850ce929851f0d8115f635b105ee2e4e15d04b2454bf6f4fadf034b10403119cd8e3b92fcc5bn +const TWO_POW_2047 = 2n ** 2047n +const TWO_POW_2048 = 2n ** 2048n interface CheckedPrime { - prime: bigInt.BigInteger + prime: bigint generators: number[] } + const checkedPrimesCache: CheckedPrime[] = [] -function checkDhPrime(log: Logger, dhPrime: bigInt.BigInteger, g: number) { - if (KNOWN_DH_PRIME.eq(dhPrime)) { +function checkDhPrime(log: Logger, dhPrime: bigint, g: number) { + if (KNOWN_DH_PRIME === dhPrime) { log.debug('server is using known dh prime, skipping validation') return } - let checkedPrime = checkedPrimesCache.find((x) => x.prime.eq(dhPrime)) + let checkedPrime = checkedPrimesCache.find((x) => x.prime === dhPrime) if (!checkedPrime) { - if (dhPrime.lesserOrEquals(TWO_POW_2047) || dhPrime.greaterOrEquals(TWO_POW_2048)) { + if (dhPrime <= TWO_POW_2047 || dhPrime >= TWO_POW_2048) { throw new MtSecurityError('Step 3: dh_prime is not in the 2048-bit range') } if (!millerRabin(dhPrime)) { throw new MtSecurityError('Step 3: dh_prime is not prime') } - if (!millerRabin(dhPrime.minus(1).divide(2))) { + if (!millerRabin((dhPrime - 1n) / 2n)) { throw new MtSecurityError('Step 3: dh_prime is not a safe prime - (dh_prime-1)/2 is not prime') } @@ -74,37 +74,37 @@ function checkDhPrime(log: Logger, dhPrime: bigInt.BigInteger, g: number) { switch (g) { case 2: - if (dhPrime.mod(8).notEquals(7)) { + if (dhPrime % 8n !== 7n) { throw new MtSecurityError('Step 3: ivalid g - dh_prime mod 8 != 7') } break case 3: - if (dhPrime.mod(3).notEquals(2)) { + if (dhPrime % 3n !== 2n) { throw new MtSecurityError('Step 3: ivalid g - dh_prime mod 3 != 2') } break case 4: break case 5: { - const mod = dhPrime.mod(5) + const mod = dhPrime % 5n - if (mod.notEquals(1) && mod.notEquals(4)) { + if (mod !== 1n && mod !== 4n) { throw new MtSecurityError('Step 3: ivalid g - dh_prime mod 5 != 1 && dh_prime mod 5 != 4') } break } case 6: { - const mod = dhPrime.mod(24) + const mod = dhPrime % 24n - if (mod.notEquals(19) && mod.notEquals(23)) { + if (mod !== 19n && mod !== 23n) { throw new MtSecurityError('Step 3: ivalid g - dh_prime mod 24 != 19 && dh_prime mod 24 != 23') } break } case 7: { - const mod = dhPrime.mod(7) + const mod = dhPrime % 7n - if (mod.notEquals(3) && mod.notEquals(5) && mod.notEquals(6)) { + if (mod !== 3n && mod !== 5n && mod !== 6n) { throw new MtSecurityError( 'Step 3: ivalid g - dh_prime mod 7 != 3 && dh_prime mod 7 != 5 && dh_prime mod 7 != 6', ) @@ -123,8 +123,8 @@ function checkDhPrime(log: Logger, dhPrime: bigInt.BigInteger, g: number) { async function rsaPad(data: Uint8Array, crypto: ICryptoProvider, key: TlPublicKey): Promise { // since Summer 2021, they use "version of RSA with a variant of OAEP+ padding explained below" - const keyModulus = bigInt(key.modulus, 16) - const keyExponent = bigInt(key.exponent, 16) + const keyModulus = BigInt(`0x${key.modulus}`) + const keyExponent = BigInt(`0x${key.exponent}`) if (data.length > 144) { throw new MtArgumentError('Failed to pad: too big data') @@ -150,11 +150,11 @@ async function rsaPad(data: Uint8Array, crypto: ICryptoProvider, key: TlPublicKe const decryptedDataBigint = bufferToBigInt(decryptedData) - if (decryptedDataBigint.geq(keyModulus)) { + if (decryptedDataBigint >= keyModulus) { continue } - const encryptedBigint = decryptedDataBigint.modPow(keyExponent, keyModulus) + const encryptedBigint = bigIntModPow(decryptedDataBigint, keyExponent, keyModulus) return bigIntToBuffer(encryptedBigint, 256) } @@ -168,7 +168,11 @@ async function rsaEncrypt(data: Uint8Array, crypto: ICryptoProvider, key: TlPubl randomBytes(235 - data.length), ]) - const encryptedBigInt = bufferToBigInt(toEncrypt).modPow(bigInt(key.exponent, 16), bigInt(key.modulus, 16)) + const encryptedBigInt = bigIntModPow( + bufferToBigInt(toEncrypt), + BigInt(`0x${key.exponent}`), + BigInt(`0x${key.modulus}`), + ) return bigIntToBuffer(encryptedBigInt) } @@ -323,7 +327,7 @@ export async function doAuthorization( const dhPrime = bufferToBigInt(serverDhInner.dhPrime) const timeOffset = Math.floor(Date.now() / 1000) - serverDhInner.serverTime - const g = bigInt(serverDhInner.g) + const g = BigInt(serverDhInner.g) const gA = bufferToBigInt(serverDhInner.gA) checkDhPrime(log, dhPrime, serverDhInner.g) @@ -333,26 +337,26 @@ export async function doAuthorization( for (;;) { const b = bufferToBigInt(randomBytes(256)) - const gB = g.modPow(b, dhPrime) + const gB = bigIntModPow(g, b, dhPrime) - const authKey = bigIntToBuffer(gA.modPow(b, dhPrime)) + const authKey = bigIntToBuffer(bigIntModPow(gA, b, dhPrime)) const authKeyAuxHash = (await crypto.sha1(authKey)).subarray(0, 8) // validate DH params - if (g.lesserOrEquals(1) || g.greaterOrEquals(dhPrime.minus(bigInt.one))) { + if (g <= 1 || g >= dhPrime - 1n) { throw new MtSecurityError('g is not within (1, dh_prime - 1)') } - if (gA.lesserOrEquals(1) || gA.greaterOrEquals(dhPrime.minus(bigInt.one))) { + if (gA <= 1 || gA >= dhPrime - 1n) { throw new MtSecurityError('g_a is not within (1, dh_prime - 1)') } - if (gB.lesserOrEquals(1) || gB.greaterOrEquals(dhPrime.minus(bigInt.one))) { + if (gB <= 1 || gB >= dhPrime - 1n) { throw new MtSecurityError('g_b is not within (1, dh_prime - 1)') } - if (gA.lt(DH_SAFETY_RANGE) || gA.gt(dhPrime.minus(DH_SAFETY_RANGE))) { + if (gA <= DH_SAFETY_RANGE || gA >= dhPrime - DH_SAFETY_RANGE) { throw new MtSecurityError('g_a is not within (2^{2048-64}, dh_prime - 2^{2048-64})') } - if (gB.lt(DH_SAFETY_RANGE) || gB.gt(dhPrime.minus(DH_SAFETY_RANGE))) { + if (gB <= DH_SAFETY_RANGE || gB >= dhPrime - DH_SAFETY_RANGE) { throw new MtSecurityError('g_b is not within (2^{2048-64}, dh_prime - 2^{2048-64})') } diff --git a/packages/core/src/network/transports/obfuscated.ts b/packages/core/src/network/transports/obfuscated.ts index b62a2f7a..5af233b0 100644 --- a/packages/core/src/network/transports/obfuscated.ts +++ b/packages/core/src/network/transports/obfuscated.ts @@ -1,4 +1,4 @@ -import { concatBuffers, dataViewFromBuffer } from '../../utils/buffer-utils.js' +import { bufferToReversed, concatBuffers, dataViewFromBuffer } from '../../utils/buffer-utils.js' import { IAesCtr, randomBytes } from '../../utils/index.js' import { IPacketCodec } from './abstract.js' import { WrappedCodec } from './wrapped.js' @@ -64,8 +64,7 @@ export class ObfuscatedPacketCodec extends WrappedCodec implements IPacketCodec dv.setInt16(60, dcId, true) } - // randomBytes may return a Buffer in Node.js, whose .slice() doesn't copy - const randomRev = Uint8Array.prototype.slice.call(random, 8, 56).reverse() + const randomRev = bufferToReversed(random, 8, 56) let encryptKey = random.subarray(8, 40) const encryptIv = random.subarray(40, 56) diff --git a/packages/core/src/utils/bigint-utils.ts b/packages/core/src/utils/bigint-utils.ts index ddb6a00b..91c684c5 100644 --- a/packages/core/src/utils/bigint-utils.ts +++ b/packages/core/src/utils/bigint-utils.ts @@ -1,55 +1,91 @@ -import bigInt, { BigInteger } from 'big-integer' +import { bufferToReversed, randomBytes } from './buffer-utils.js' -import { randomBytes } from './buffer-utils.js' +/** + * Get the minimum number of bits required to represent a number + */ +export function bigIntBitLength(n: bigint) { + // not the fastest way, but at least not .toString(2) and not too complex + // taken from: https://stackoverflow.com/a/76616288/22656950 + + const i = (n.toString(16).length - 1) * 4 + + return i + 32 - Math.clz32(Number(n >> BigInt(i))) +} /** * Convert a big integer to a buffer * * @param value Value to convert - * @param length Length of the resulting buffer (by default it's computed automatically) + * @param length Length of the resulting buffer (by default it's the minimum required) * @param le Whether to use little-endian encoding */ -export function bigIntToBuffer(value: BigInteger, length = 0, le = false): Uint8Array { - const array = value.toArray(256).value +export function bigIntToBuffer(value: bigint, length = 0, le = false): Uint8Array { + const bits = bigIntBitLength(value) + const bytes = Math.ceil(bits / 8) - if (length !== 0 && array.length > length) { + if (length !== 0 && bytes > length) { throw new Error('Value out of bounds') } - if (length !== 0) { - // padding - while (array.length !== length) array.unshift(0) + if (length === 0) length = bytes + + const buf = new ArrayBuffer(length) + const u8 = new Uint8Array(buf) + + const unaligned = length % 8 + const dv = new DataView(buf, 0, length - unaligned) + + // it is faster to work with 64-bit words than with bytes directly + for (let i = 0; i < dv.byteLength; i += 8) { + dv.setBigUint64(i, value & 0xffffffffffffffffn, true) + value >>= 64n } - if (le) array.reverse() + if (unaligned > 0) { + for (let i = length - unaligned; i < length; i++) { + u8[i] = Number(value & 0xffn) + value >>= 8n + } + } - const buffer = new Uint8Array(length || array.length) - buffer.set(array, 0) + if (!le) u8.reverse() - return buffer + return u8 } /** * Convert a buffer to a big integer * * @param buffer Buffer to convert - * @param offset Offset to start reading from - * @param length Length to read * @param le Whether to use little-endian encoding */ -export function bufferToBigInt(buffer: Uint8Array, offset = 0, length = buffer.length, le = false): BigInteger { - const arr = [...buffer.subarray(offset, offset + length)] +export function bufferToBigInt(buffer: Uint8Array, le = false): bigint { + if (le) buffer = bufferToReversed(buffer) - if (le) arr.reverse() + const unaligned = buffer.length % 8 + const dv = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength - unaligned) - return bigInt.fromArray(arr as unknown as number[], 256) + let res = 0n + + // it is faster to work with 64-bit words than with bytes directly + for (let i = 0; i < dv.byteLength; i += 8) { + res = (res << 64n) | BigInt(dv.getBigUint64(i, false)) + } + + if (unaligned > 0) { + for (let i = buffer.length - unaligned; i < buffer.length; i++) { + res = (res << 8n) | BigInt(buffer[i]) + } + } + + return res } /** * Generate a random big integer of the given size (in bytes) * @param size Size in bytes */ -export function randomBigInt(size: number): BigInteger { +export function randomBigInt(size: number): bigint { return bufferToBigInt(randomBytes(size)) } @@ -57,14 +93,14 @@ export function randomBigInt(size: number): BigInteger { * Generate a random big integer of the given size (in bits) * @param bits */ -export function randomBigIntBits(bits: number): BigInteger { +export function randomBigIntBits(bits: number): bigint { let num = randomBigInt(Math.ceil(bits / 8)) - const bitLength = num.bitLength() + const bitLength = bigIntBitLength(num) - if (bitLength.gt(bits)) { - const toTrim = bigInt.randBetween(bitLength.minus(bits), 8) - num = num.shiftRight(toTrim) + if (bitLength > bits) { + const toTrim = bitLength - bits + num >>= BigInt(toTrim) } return num @@ -76,31 +112,119 @@ export function randomBigIntBits(bits: number): BigInteger { * @param max Maximum value (exclusive) * @param min Minimum value (inclusive) */ -export function randomBigIntInRange(max: BigInteger, min = bigInt.one): BigInteger { - const interval = max.minus(min) - if (interval.isNegative()) throw new Error('expected min < max') +export function randomBigIntInRange(max: bigint, min = 1n): bigint { + const interval = max - min + if (interval < 0n) throw new Error('expected min < max') - const byteSize = interval.bitLength().divide(8).toJSNumber() + const byteSize = bigIntBitLength(interval) / 8 let result = randomBigInt(byteSize) - while (result.gt(interval)) result = result.minus(interval) + while (result > interval) result -= interval - return min.plus(result) + return min + result } /** * Compute the multiplicity of 2 in the prime factorization of n * @param n */ -export function twoMultiplicity(n: BigInteger): BigInteger { - if (n === bigInt.zero) return bigInt.zero +export function twoMultiplicity(n: bigint): bigint { + if (n === 0n) return 0n - let m = bigInt.zero - let pow = bigInt.one + let m = 0n + let pow = 1n while (true) { - if (!n.and(pow).isZero()) return m - m = m.plus(bigInt.one) - pow = pow.shiftLeft(1) + if ((n & pow) !== 0n) return m + m += 1n + pow <<= 1n + } +} + +export function bigIntMin(a: bigint, b: bigint): bigint { + return a < b ? a : b +} + +export function bigIntAbs(a: bigint): bigint { + return a < 0n ? -a : a +} + +export function bigIntGcd(a: bigint, b: bigint): bigint { + // using euclidean algorithm is fast enough on smaller numbers + // https://en.wikipedia.org/wiki/Euclidean_algorithm#Implementations + + while (b !== 0n) { + const t = b + b = a % b + a = t + } + + return a +} + +export function bigIntModPow(base: bigint, exp: bigint, mod: bigint): bigint { + // using the binary method is good enough for our use case + // https://en.wikipedia.org/wiki/Modular_exponentiation#Right-to-left_binary_method + + base %= mod + + let result = 1n + + while (exp > 0n) { + if (exp % 2n === 1n) { + result = (result * base) % mod + } + + exp >>= 1n + base = base ** 2n % mod + } + + return result +} + +// below code is based on https://github.com/juanelas/bigint-mod-arith, MIT license + +function eGcd(a: bigint, b: bigint): [bigint, bigint, bigint] { + let x = 0n + let y = 1n + let u = 1n + let v = 0n + + while (a !== 0n) { + const q = b / a + const r: bigint = b % a + const m = x - u * q + const n = y - v * q + b = a + a = r + x = u + y = v + u = m + v = n + } + + return [b, x, y] +} + +function toZn(a: number | bigint, n: number | bigint): bigint { + if (typeof a === 'number') a = BigInt(a) + if (typeof n === 'number') n = BigInt(n) + + if (n <= 0n) { + throw new RangeError('n must be > 0') + } + + const aZn = a % n + + return aZn < 0n ? aZn + n : aZn +} + +export function bigIntModInv(a: bigint, n: bigint): bigint { + const [g, x] = eGcd(toZn(a, n), n) + + if (g !== 1n) { + throw new RangeError(`${a.toString()} does not have inverse modulo ${n.toString()}`) // modular inverse does not exist + } else { + return toZn(x, n) } } diff --git a/packages/core/src/utils/buffer-utils.ts b/packages/core/src/utils/buffer-utils.ts index a8082b4c..12b09989 100644 --- a/packages/core/src/utils/buffer-utils.ts +++ b/packages/core/src/utils/buffer-utils.ts @@ -59,6 +59,23 @@ export function concatBuffers(buffers: Uint8Array[]): Uint8Array { return ret } +/** + * Shortcut for creating a DataView from a Uint8Array + */ export function dataViewFromBuffer(buf: Uint8Array): DataView { return new DataView(buf.buffer, buf.byteOffset, buf.byteLength) } + +/** + * Reverse a buffer (or a part of it) into a new buffer + */ +export function bufferToReversed(buf: Uint8Array, start = 0, end = buf.length): Uint8Array { + const len = end - start + const ret = new Uint8Array(len) + + for (let i = 0; i < len; i++) { + ret[i] = buf[end - i - 1] + } + + return ret +} diff --git a/packages/core/src/utils/crypto/factorization.ts b/packages/core/src/utils/crypto/factorization.ts index 181aa916..b7919bc7 100644 --- a/packages/core/src/utils/crypto/factorization.ts +++ b/packages/core/src/utils/crypto/factorization.ts @@ -1,6 +1,11 @@ -import bigInt, { BigInteger } from 'big-integer' - -import { bigIntToBuffer, bufferToBigInt, randomBigIntInRange } from '../bigint-utils.js' +import { + bigIntAbs, + bigIntGcd, + bigIntMin, + bigIntToBuffer, + bufferToBigInt, + randomBigIntInRange, +} from '../bigint-utils.js' /** * Factorize `p*q` to `p` and `q` synchronously using Brent-Pollard rho algorithm @@ -10,12 +15,12 @@ export function factorizePQSync(pq: Uint8Array): [Uint8Array, Uint8Array] { const pq_ = bufferToBigInt(pq) const n = PollardRhoBrent(pq_) - const m = pq_.divide(n) + const m = pq_ / n let p let q - if (n.lt(m)) { + if (n < m) { p = n q = m } else { @@ -26,50 +31,46 @@ export function factorizePQSync(pq: Uint8Array): [Uint8Array, Uint8Array] { return [bigIntToBuffer(p), bigIntToBuffer(q)] } -function PollardRhoBrent(n: BigInteger): BigInteger { - if (n.isEven()) return bigInt[2] +function PollardRhoBrent(n: bigint): bigint { + if (n % 2n === 0n) return 2n - let y = randomBigIntInRange(n.minus(1)) - const c = randomBigIntInRange(n.minus(1)) - const m = randomBigIntInRange(n.minus(1)) - let g = bigInt.one - let r = bigInt.one - let q = bigInt.one + let y = randomBigIntInRange(n - 1n) + const c = randomBigIntInRange(n - 1n) + const m = randomBigIntInRange(n - 1n) + let g = 1n + let r = 1n + let q = 1n - let ys: BigInteger - let x: BigInteger + let ys: bigint + let x: bigint - while (g.eq(bigInt.one)) { + while (g === 1n) { x = y - for (let i = 0; r.geq(i); i++) y = y.multiply(y).mod(n).plus(c).mod(n) - // y = ((y * y) % n + c) % n + for (let i = 0; r >= i; i++) y = (((y * y) % n) + c) % n - let k = bigInt.zero + let k = 0n - while (k.lt(r) && g.eq(1)) { + while (k < r && g === 1n) { ys = y - for (let i = bigInt.zero; i.lt(bigInt.min(m, r.minus(k))); i = i.plus(bigInt.one)) { - y = y.multiply(y).mod(n).plus(c).mod(n) - q = q.multiply(x.minus(y).abs()).mod(n) - // y = (y * y % n + c) % n - // q = q * abs(x - y) % n + for (let i = 0n; i < bigIntMin(m, r - k); i++) { + y = (((y * y) % n) + c) % n + q = (q * bigIntAbs(x - y)) % n } - g = bigInt.gcd(q, n) - k = k.plus(m) + g = bigIntGcd(q, n) + k = k + m } - r = r.multiply(bigInt[2]) + r <<= 1n } - if (g.eq(n)) { + if (g === n) { do { - ys = ys!.multiply(ys!).mod(n).plus(c).mod(n) - // ys = ((ys * ys) % n + c) % n + ys = (((ys! * ys!) % n) + c) % n - g = bigInt.gcd(x!.minus(ys), n) - } while (g.leq(bigInt.one)) + g = bigIntGcd(x! - ys!, n) + } while (g <= 1n) } return g diff --git a/packages/core/src/utils/crypto/miller-rabin.ts b/packages/core/src/utils/crypto/miller-rabin.ts index 554adb16..acf14674 100644 --- a/packages/core/src/utils/crypto/miller-rabin.ts +++ b/packages/core/src/utils/crypto/miller-rabin.ts @@ -1,42 +1,42 @@ -import bigInt, { BigInteger } from 'big-integer' +import { bigIntBitLength, bigIntModPow, randomBigIntBits, twoMultiplicity } from '../bigint-utils.js' -import { randomBigIntBits, twoMultiplicity } from '../bigint-utils.js' - -export function millerRabin(n: BigInteger, rounds = 20): boolean { +export function millerRabin(n: bigint, rounds = 20): boolean { // small numbers: 0, 1 are not prime, 2, 3 are prime - if (n.lt(bigInt[4])) return n.gt(bigInt[1]) - if (n.isEven() || n.isNegative()) return false + if (n < 4n) return n > 1n + if (n % 2n === 0n || n < 0n) return false - const nBits = n.bitLength().toJSNumber() - const nSub = n.minus(1) + const nBits = bigIntBitLength(n) + const nSub = n - 1n const r = twoMultiplicity(nSub) - const d = nSub.shiftRight(r) + const d = nSub >> r for (let i = 0; i < rounds; i++) { let base do { base = randomBigIntBits(nBits) - } while (base.leq(bigInt.one) || base.geq(nSub)) + } while (base <= 1n || base >= nSub) - let x = base.modPow(d, n) - if (x.eq(bigInt.one) || x.eq(nSub)) continue + let x = bigIntModPow(base, d, n) + // if (x.eq(bigInt.one) || x.eq(nSub)) continue + if (x === 1n || x === nSub) continue - let i = bigInt.zero - let y: BigInteger + let i = 0n + let y: bigint - while (i.lt(r)) { - y = x.modPow(bigInt[2], n) + while (i < r) { + // y = x.modPow(bigInt[2], n) + y = bigIntModPow(x, 2n, n) - if (x.eq(bigInt.one)) return false - if (x.eq(nSub)) break - i = i.plus(bigInt.one) + if (x === 1n) return false + if (x === nSub) break + i += 1n x = y } - if (i.eq(r)) return false + if (i === r) return false } return true diff --git a/packages/core/src/utils/crypto/password.ts b/packages/core/src/utils/crypto/password.ts index b0a0b402..4dd82488 100644 --- a/packages/core/src/utils/crypto/password.ts +++ b/packages/core/src/utils/crypto/password.ts @@ -1,10 +1,8 @@ -import bigInt from 'big-integer' - import { tl } from '@mtcute/tl' import { utf8EncodeToBuffer } from '@mtcute/tl-runtime' import { MtSecurityError, MtUnsupportedError } from '../../types/errors.js' -import { bigIntToBuffer, bufferToBigInt } from '../bigint-utils.js' +import { bigIntModPow, bigIntToBuffer, bufferToBigInt } from '../bigint-utils.js' import { concatBuffers, randomBytes } from '../buffer-utils.js' import { ICryptoProvider } from './abstract.js' import { xorBuffer } from './utils.js' @@ -47,11 +45,11 @@ export async function computeNewPasswordHash( const _x = await computePasswordHash(crypto, utf8EncodeToBuffer(password), algo.salt1, algo.salt2) - const g = bigInt(algo.g) + const g = BigInt(algo.g) const p = bufferToBigInt(algo.p) const x = bufferToBigInt(_x) - return bigIntToBuffer(g.modPow(x, p), 256) + return bigIntToBuffer(bigIntModPow(g, x, p), 256) } /** @@ -86,13 +84,13 @@ export async function computeSrpParams( throw new MtSecurityError('SRP_ID is not present in the request') } - const g = bigInt(algo.g) + const g = BigInt(algo.g) const _g = bigIntToBuffer(g, 256) const p = bufferToBigInt(algo.p) const gB = bufferToBigInt(request.srpB) const a = bufferToBigInt(randomBytes(256)) - const gA = g.modPow(a, p) + const gA = bigIntModPow(g, a, p) const _gA = bigIntToBuffer(gA, 256) const H = (data: Uint8Array) => crypto.sha256(data) @@ -107,12 +105,12 @@ export async function computeSrpParams( const u = bufferToBigInt(_u) const x = bufferToBigInt(_x) - const v = g.modPow(x, p) - const kV = k.multiply(v).mod(p) + const v = bigIntModPow(g, x, p) + const kV = (k * v) % p - let t = gB.minus(kV).mod(p) - if (t.isNegative()) t = t.plus(p) - const sA = t.modPow(a.plus(u.multiply(x)), p) + let t = gB - kV + if (t < 0n) t += p + const sA = bigIntModPow(t, a + u * x, p) const _kA = await H(bigIntToBuffer(sA, 256)) const _M1 = await H( diff --git a/packages/core/tests/bigint-utils.spec.ts b/packages/core/tests/bigint-utils.spec.ts index 8e9f9c98..ac9aa46e 100644 --- a/packages/core/tests/bigint-utils.spec.ts +++ b/packages/core/tests/bigint-utils.spec.ts @@ -1,4 +1,3 @@ -import bigInt from 'big-integer' import { expect } from 'chai' import { describe, it } from 'mocha' @@ -6,38 +5,62 @@ import { hexDecodeToBuffer } from '@mtcute/tl-runtime' import { bigIntToBuffer, bufferToBigInt } from '../src/utils/index.js' -// since bigIntToBuffer is a tiny wrapper over writeBigInt, no need to test it individually describe('bigIntToBuffer', () => { it('should handle writing to BE', () => { - expect([...bigIntToBuffer(bigInt('10495708'), 0, false)]).eql([0xa0, 0x26, 0xdc]) - expect([...bigIntToBuffer(bigInt('10495708'), 4, false)]).eql([0x00, 0xa0, 0x26, 0xdc]) - expect([...bigIntToBuffer(bigInt('10495708'), 8, false)]).eql([0x00, 0x00, 0x00, 0x00, 0x00, 0xa0, 0x26, 0xdc]) - expect([...bigIntToBuffer(bigInt('3038102549'), 4, false)]).eql([0xb5, 0x15, 0xc4, 0x15]) - expect([...bigIntToBuffer(bigInt('9341376580368336208'), 8, false)]).eql([ + expect([...bigIntToBuffer(BigInt('10495708'), 0, false)]).eql([0xa0, 0x26, 0xdc]) + expect([...bigIntToBuffer(BigInt('10495708'), 4, false)]).eql([0x00, 0xa0, 0x26, 0xdc]) + expect([...bigIntToBuffer(BigInt('10495708'), 8, false)]).eql([0x00, 0x00, 0x00, 0x00, 0x00, 0xa0, 0x26, 0xdc]) + expect([...bigIntToBuffer(BigInt('3038102549'), 4, false)]).eql([0xb5, 0x15, 0xc4, 0x15]) + expect([...bigIntToBuffer(BigInt('9341376580368336208'), 8, false)]).eql([ ...hexDecodeToBuffer('81A33C81D2020550'), ]) }) + it('should handle writing to LE', () => { - expect([...bigIntToBuffer(bigInt('10495708'), 0, true)]).eql([0xdc, 0x26, 0xa0]) - expect([...bigIntToBuffer(bigInt('10495708'), 4, true)]).eql([0xdc, 0x26, 0xa0, 0x00]) - expect([...bigIntToBuffer(bigInt('10495708'), 8, true)]).eql([0xdc, 0x26, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00]) - expect([...bigIntToBuffer(bigInt('3038102549'), 4, true)]).eql([0x15, 0xc4, 0x15, 0xb5]) - expect([...bigIntToBuffer(bigInt('9341376580368336208'), 8, true)]).eql([ + expect([...bigIntToBuffer(BigInt('10495708'), 0, true)]).eql([0xdc, 0x26, 0xa0]) + expect([...bigIntToBuffer(BigInt('10495708'), 4, true)]).eql([0xdc, 0x26, 0xa0, 0x00]) + expect([...bigIntToBuffer(BigInt('10495708'), 8, true)]).eql([0xdc, 0x26, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00]) + expect([...bigIntToBuffer(BigInt('3038102549'), 4, true)]).eql([0x15, 0xc4, 0x15, 0xb5]) + expect([...bigIntToBuffer(BigInt('9341376580368336208'), 8, true)]).eql([ ...hexDecodeToBuffer('81A33C81D2020550').reverse(), ]) }) + + it('should handle large integers', () => { + const buf = hexDecodeToBuffer( + '1a981ce8bf86bf4a1bd79c2ef829914172f8d0e54cb7ad807552d56977e1c946872e2c7bd77052be30e7e9a7a35c4feff848a25759f5f2f5b0e96538', + ) + const num = BigInt( + '0x1a981ce8bf86bf4a1bd79c2ef829914172f8d0e54cb7ad807552d56977e1c946872e2c7bd77052be30e7e9a7a35c4feff848a25759f5f2f5b0e96538', + ) + + expect([...bigIntToBuffer(num, 0, false)]).eql([...buf]) + expect([...bigIntToBuffer(num, 0, true)]).eql([...buf.reverse()]) + }) }) describe('bufferToBigInt', () => { it('should handle reading BE', () => { - expect(bufferToBigInt(new Uint8Array([0xa0, 0x26, 0xdc]), 0, 3, false).toString()).eq('10495708') - expect(bufferToBigInt(new Uint8Array([0x00, 0xa0, 0x26, 0xdc]), 0, 4, false).toString()).eq('10495708') - expect(bufferToBigInt(new Uint8Array([0xb5, 0x15, 0xc4, 0x15]), 0, 4, false).toString()).eq('3038102549') + expect(bufferToBigInt(new Uint8Array([0xa0, 0x26, 0xdc]), false).toString()).eq('10495708') + expect(bufferToBigInt(new Uint8Array([0x00, 0xa0, 0x26, 0xdc]), false).toString()).eq('10495708') + expect(bufferToBigInt(new Uint8Array([0xb5, 0x15, 0xc4, 0x15]), false).toString()).eq('3038102549') }) it('should handle reading LE', () => { - expect(bufferToBigInt(new Uint8Array([0xdc, 0x26, 0xa0]), 0, 3, true).toString()).eq('10495708') - expect(bufferToBigInt(new Uint8Array([0xdc, 0x26, 0xa0, 0x00]), 0, 4, true).toString()).eq('10495708') - expect(bufferToBigInt(new Uint8Array([0x15, 0xc4, 0x15, 0xb5]), 0, 4, true).toString()).eq('3038102549') + expect(bufferToBigInt(new Uint8Array([0xdc, 0x26, 0xa0]), true).toString()).eq('10495708') + expect(bufferToBigInt(new Uint8Array([0xdc, 0x26, 0xa0, 0x00]), true).toString()).eq('10495708') + expect(bufferToBigInt(new Uint8Array([0x15, 0xc4, 0x15, 0xb5]), true).toString()).eq('3038102549') + }) + + it('should handle large integers', () => { + const buf = hexDecodeToBuffer( + '1a981ce8bf86bf4a1bd79c2ef829914172f8d0e54cb7ad807552d56977e1c946872e2c7bd77052be30e7e9a7a35c4feff848a25759f5f2f5b0e96538', + ) + const num = BigInt( + '0x1a981ce8bf86bf4a1bd79c2ef829914172f8d0e54cb7ad807552d56977e1c946872e2c7bd77052be30e7e9a7a35c4feff848a25759f5f2f5b0e96538', + ) + + expect(bufferToBigInt(buf, false).toString()).eq(num.toString()) + expect(bufferToBigInt(buf.reverse(), true).toString()).eq(num.toString()) }) }) diff --git a/packages/core/tests/buffer-utils.spec.ts b/packages/core/tests/buffer-utils.spec.ts index 067ca39a..45184782 100644 --- a/packages/core/tests/buffer-utils.spec.ts +++ b/packages/core/tests/buffer-utils.spec.ts @@ -3,11 +3,7 @@ import { describe, it } from 'mocha' import { hexEncode, utf8Decode, utf8EncodeToBuffer } from '@mtcute/tl-runtime' -import { - buffersEqual, - cloneBuffer, concatBuffers, - randomBytes, -} from '../src/utils/buffer-utils.js' +import { buffersEqual, bufferToReversed, cloneBuffer, concatBuffers, randomBytes } from '../src/utils/buffer-utils.js' import { xorBuffer, xorBufferInPlace } from '../src/utils/crypto/utils.js' describe('buffersEqual', () => { @@ -113,10 +109,7 @@ describe('cloneBuffer', () => { describe('concatBuffers', () => { it('should concat buffers', () => { - const buf = concatBuffers([ - new Uint8Array([1, 2, 3]), - new Uint8Array([4, 5, 6]), - ]) + const buf = concatBuffers([new Uint8Array([1, 2, 3]), new Uint8Array([4, 5, 6])]) expect([...buf]).eql([1, 2, 3, 4, 5, 6]) }) @@ -130,3 +123,25 @@ describe('concatBuffers', () => { expect(buf1[0]).not.eql(0xff) }) }) + +describe('bufferToReversed', () => { + it('should reverse the buffer', () => { + const buf = bufferToReversed(new Uint8Array([1, 2, 3, 4, 5, 6])) + + expect([...buf]).eql([6, 5, 4, 3, 2, 1]) + }) + + it('should reverse a part of the buffer', () => { + const buf = bufferToReversed(new Uint8Array([1, 2, 3, 4, 5, 6]), 1, 5) + + expect([...buf]).eql([5, 4, 3, 2]) + }) + + it('should create a new buffer', () => { + const buf1 = new Uint8Array([1, 2, 3]) + const buf2 = bufferToReversed(buf1) + + buf2[0] = 0xff + expect([...buf1]).eql([1, 2, 3]) + }) +}) diff --git a/packages/core/tests/miller-rabin.spec.ts b/packages/core/tests/miller-rabin.spec.ts index 5bb2c079..1fc63e8a 100644 --- a/packages/core/tests/miller-rabin.spec.ts +++ b/packages/core/tests/miller-rabin.spec.ts @@ -1,4 +1,3 @@ -import bigInt from 'big-integer' import { expect } from 'chai' import { describe, it } from 'mocha' @@ -7,8 +6,8 @@ import { millerRabin } from '../src/utils/crypto/miller-rabin.js' describe('miller-rabin test', function () { this.timeout(10000) // since miller-rabin factorization relies on RNG, it may take a while (or may not!) - const testMillerRabin = (n: bigInt.BigNumber, isPrime: boolean) => { - expect(millerRabin(bigInt(n as number))).eq(isPrime) + const testMillerRabin = (n: number | string | bigint, isPrime: boolean) => { + expect(millerRabin(BigInt(n))).eq(isPrime) } it('should correctly label small primes as probable primes', () => { @@ -134,6 +133,6 @@ describe('miller-rabin test', function () { // dh_prime used by telegram, as seen in https://core.telegram.org/mtproto/security_guidelines const telegramDhPrime = 'C7 1C AE B9 C6 B1 C9 04 8E 6C 52 2F 70 F1 3F 73 98 0D 40 23 8E 3E 21 C1 49 34 D0 37 56 3D 93 0F 48 19 8A 0A A7 C1 40 58 22 94 93 D2 25 30 F4 DB FA 33 6F 6E 0A C9 25 13 95 43 AE D4 4C CE 7C 37 20 FD 51 F6 94 58 70 5A C6 8C D4 FE 6B 6B 13 AB DC 97 46 51 29 69 32 84 54 F1 8F AF 8C 59 5F 64 24 77 FE 96 BB 2A 94 1D 5B CD 1D 4A C8 CC 49 88 07 08 FA 9B 37 8E 3C 4F 3A 90 60 BE E6 7C F9 A4 A4 A6 95 81 10 51 90 7E 16 27 53 B5 6B 0F 6B 41 0D BA 74 D8 A8 4B 2A 14 B3 14 4E 0E F1 28 47 54 FD 17 ED 95 0D 59 65 B4 B9 DD 46 58 2D B1 17 8D 16 9C 6B C4 65 B0 D6 FF 9C A3 92 8F EF 5B 9A E4 E4 18 FC 15 E8 3E BE A0 F8 7F A9 FF 5E ED 70 05 0D ED 28 49 F4 7B F9 59 D9 56 85 0C E9 29 85 1F 0D 81 15 F6 35 B1 05 EE 2E 4E 15 D0 4B 24 54 BF 6F 4F AD F0 34 B1 04 03 11 9C D8 E3 B9 2F CC 5B' - testMillerRabin(bigInt(telegramDhPrime.replace(/ /g, ''), 16), true) + testMillerRabin(BigInt('0x' + telegramDhPrime.replace(/ /g, '')), true) }) }) diff --git a/packages/mtproxy/fake-tls.ts b/packages/mtproxy/fake-tls.ts index 10e43a4d..763f79a6 100644 --- a/packages/mtproxy/fake-tls.ts +++ b/packages/mtproxy/fake-tls.ts @@ -1,50 +1,54 @@ /* eslint-disable no-restricted-globals */ -// todo fixme -import bigInt, { BigInteger } from 'big-integer' - import { IPacketCodec, WrappedCodec } from '@mtcute/core' -import { bigIntToBuffer, bufferToBigInt, ICryptoProvider, randomBytes } from '@mtcute/core/utils.js' +import { + bigIntModInv, + bigIntModPow, + bigIntToBuffer, + bufferToBigInt, + ICryptoProvider, + randomBytes, +} from '@mtcute/core/utils.js' const MAX_TLS_PACKET_LENGTH = 2878 const TLS_FIRST_PREFIX = Buffer.from('140303000101', 'hex') // ref: https://github.com/tdlib/td/blob/master/td/mtproto/TlsInit.cpp -const KEY_MOD = bigInt('7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed', 16) +const KEY_MOD = 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffedn // 2^255 - 19 -const QUAD_RES_MOD = bigInt('7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed', 16) +const QUAD_RES_MOD = 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffedn // (mod - 1) / 2 = 2^254 - 10 -const QUAD_RES_POW = bigInt('3ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6', 16) +const QUAD_RES_POW = 0x3ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6n -function _getY2(x: BigInteger, mod: BigInteger): BigInteger { +function _getY2(x: bigint, mod: bigint): bigint { // returns y = x^3 + x^2 * 486662 + x let y = x - y = y.add(486662).mod(mod) - y = y.multiply(x).mod(mod) - y = y.plus(1).mod(mod) - y = y.multiply(x).mod(mod) + y = (y + 486662n) % mod + y = (y * x) % mod + y = (y + 1n) % mod + y = (y * x) % mod return y } -function _getDoubleX(x: BigInteger, mod: BigInteger): BigInteger { +function _getDoubleX(x: bigint, mod: bigint): bigint { // returns x_2 = (x^2 - 1)^2/(4*y^2) let denominator = _getY2(x, mod) - denominator = denominator.multiply(4).mod(mod) + denominator = (denominator * 4n) % mod - let numerator = x.multiply(x).mod(mod) - numerator = numerator.minus(1).mod(mod) - numerator = numerator.multiply(numerator).mod(mod) + let numerator = (x * x) % mod + numerator = (numerator - 1n) % mod + numerator = (numerator * numerator) % mod - denominator = denominator.modInv(mod) - numerator = numerator.multiply(denominator).mod(mod) + denominator = bigIntModInv(denominator, mod) + numerator = (numerator * denominator) % mod return numerator } -function _isQuadraticResidue(a: BigInteger): boolean { - const r = a.modPow(QUAD_RES_POW, QUAD_RES_MOD) +function _isQuadraticResidue(a: bigint): boolean { + const r = bigIntModPow(a, QUAD_RES_POW, QUAD_RES_MOD) - return r.eq(1) + return r === 1n } interface TlsOperationHandler { diff --git a/packages/mtproxy/package.json b/packages/mtproxy/package.json index 510d2db8..146a8c22 100644 --- a/packages/mtproxy/package.json +++ b/packages/mtproxy/package.json @@ -20,7 +20,6 @@ } }, "dependencies": { - "@mtcute/core": "workspace:^", - "big-integer": "1.6.51" + "@mtcute/core": "workspace:^" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2bde8c29..18712586 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -132,9 +132,6 @@ importers: '@types/events': specifier: 3.0.0 version: 3.0.0 - big-integer: - specifier: 1.6.51 - version: 1.6.51 events: specifier: 3.2.0 version: 3.2.0 @@ -238,9 +235,6 @@ importers: '@mtcute/core': specifier: workspace:^ version: link:../core - big-integer: - specifier: 1.6.51 - version: 1.6.51 packages/node: dependencies: