From 964f47497c40e063e5d3edaf36cbfdfd918489d4 Mon Sep 17 00:00:00 2001 From: Alina Sireneva Date: Sun, 12 Nov 2023 00:36:00 +0300 Subject: [PATCH] chore(core): moved random to crypto provider, added tests for functions relying on rng --- package.json | 2 + packages/core/src/network/auth-key.test.ts | 187 ++++++++++++++---- packages/core/src/network/auth-key.ts | 17 +- packages/core/src/network/authorization.ts | 25 +-- .../core/src/network/session-connection.ts | 5 +- .../network/transports/intermediate.test.ts | 30 +-- .../src/network/transports/intermediate.ts | 12 +- .../core/src/network/transports/obfuscated.ts | 4 +- packages/core/src/utils/bigint-utils.test.ts | 35 ++-- packages/core/src/utils/bigint-utils.ts | 17 +- packages/core/src/utils/buffer-utils.test.ts | 74 +------ packages/core/src/utils/buffer-utils.ts | 2 - packages/core/src/utils/crypto/abstract.ts | 14 +- .../src/utils/crypto/crypto.test-utils.ts | 92 ++++++++- .../core/src/utils/crypto/factorization.ts | 13 +- .../src/utils/crypto/miller-rabin.test.ts | 6 +- .../core/src/utils/crypto/miller-rabin.ts | 5 +- packages/core/src/utils/crypto/node.ts | 6 +- .../core/src/utils/crypto/password.test.ts | 127 +++++++----- packages/core/src/utils/crypto/password.ts | 9 +- packages/core/src/utils/crypto/utils.test.ts | 61 ++++++ packages/core/src/utils/crypto/wasm.ts | 4 +- packages/core/src/utils/crypto/web.test.ts | 12 +- packages/core/src/utils/crypto/web.ts | 24 ++- .../core/src/utils/platform/crypto.web.ts | 2 +- packages/core/src/utils/platform/random.ts | 4 - .../core/src/utils/platform/random.web.ts | 6 - packages/mtproxy/fake-tls.ts | 33 ++-- pnpm-lock.yaml | 56 +++++- vite.config.mts | 2 +- 30 files changed, 587 insertions(+), 299 deletions(-) create mode 100644 packages/core/src/utils/crypto/utils.test.ts delete mode 100644 packages/core/src/utils/platform/random.ts delete mode 100644 packages/core/src/utils/platform/random.web.ts diff --git a/package.json b/package.json index 1eaf4238..5e1ea2cb 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "postinstall": "node scripts/validate-deps-versions.mjs", "test": "vitest run && pnpm run -r test", "test:dev": "vitest watch", + "test:ui": "vitest --ui", "test:coverage": "vitest run --coverage", "lint": "eslint .", "lint:ci": "NODE_OPTIONS=\"--max_old_space_size=8192\" eslint --config .eslintrc.ci.js .", @@ -33,6 +34,7 @@ "@typescript-eslint/eslint-plugin": "6.4.0", "@typescript-eslint/parser": "6.4.0", "@vitest/coverage-v8": "^0.34.6", + "@vitest/ui": "^0.34.6", "dotenv-flow": "3.2.0", "dpdm": "^3.14.0", "eslint": "8.47.0", diff --git a/packages/core/src/network/auth-key.test.ts b/packages/core/src/network/auth-key.test.ts index 7916a392..813a0c5c 100644 --- a/packages/core/src/network/auth-key.test.ts +++ b/packages/core/src/network/auth-key.test.ts @@ -1,49 +1,166 @@ -/* eslint-disable no-restricted-globals */ -import { describe, expect, it } from 'vitest' +import Long from 'long' +import { describe, expect, it, vi } from 'vitest' -import { TlReaderMap } from '@mtcute/tl-runtime' +import { + hexDecode, + hexDecodeToBuffer, + hexEncode, + TlBinaryReader, + TlReaderMap, + utf8Decode, + utf8EncodeToBuffer, +} from '@mtcute/tl-runtime' -import { NodeCryptoProvider } from '../utils/crypto/node.js' +import { defaultTestCryptoProvider } from '../utils/crypto/crypto.test-utils.js' import { LogManager } from '../utils/index.js' import { AuthKey } from './auth-key.js' -const authKey = Buffer.alloc( - 2048 / 8, - Buffer.from('98cb29c6ffa89e79da695a54f572e6cb101e81c688b63a4bf73c3622dec230e0', 'hex'), -) +const authKey = new Uint8Array(256) + +for (let i = 0; i < 256; i += 32) { + hexDecode(authKey.subarray(i, i + 32), '98cb29c6ffa89e79da695a54f572e6cb101e81c688b63a4bf73c3622dec230e0') +} describe('AuthKey', () => { - const crypto = new NodeCryptoProvider() - const logger = new LogManager() - const readerMap: TlReaderMap = {} + async function create() { + const logger = new LogManager() + const readerMap: TlReaderMap = {} + const crypto = await defaultTestCryptoProvider() - it('should correctly calculate derivatives', () => { const key = new AuthKey(crypto, logger, readerMap) key.setup(authKey) - expect(key.key).to.eql(authKey) - expect(key.clientSalt).to.eql( - Buffer.from('f73c3622dec230e098cb29c6ffa89e79da695a54f572e6cb101e81c688b63a4b', 'hex'), - ) - expect(key.serverSalt).to.eql( - Buffer.from('98cb29c6ffa89e79da695a54f572e6cb101e81c688b63a4bf73c3622dec230e0', 'hex'), - ) - expect(key.id).to.eql(Buffer.from('40fa5bb7cb56a895', 'hex')) + return key + } + + const msgId = Long.fromBits(0xbeef1234, 0x1234beef, true) + const seqNo = 777 + const serverSalt = Long.fromBits(0xdeadbeef, 0xbeefdead) + const sessionId = Long.fromBits(0xfeedbeef, 0xbeeffeed) + + function writeMessage(body: Uint8Array) { + const buf = new Uint8Array(16 + body.length) + const dv = new DataView(buf.buffer) + + dv.setInt32(0, msgId.low, true) + dv.setInt32(4, msgId.high, true) + dv.setInt32(8, seqNo, true) + dv.setInt32(12, body.length, true) + buf.set(body, 16) + + return buf + } + + it('should calculate derivatives', async () => { + const key = await create() + + expect(hexEncode(key.key)).toEqual(hexEncode(authKey)) + expect(hexEncode(key.clientSalt)).toEqual('f73c3622dec230e098cb29c6ffa89e79da695a54f572e6cb101e81c688b63a4b') + expect(hexEncode(key.serverSalt)).toEqual('98cb29c6ffa89e79da695a54f572e6cb101e81c688b63a4bf73c3622dec230e0') + expect(hexEncode(key.id)).toEqual('40fa5bb7cb56a895') }) - // todo - need predictable random bytes - // it('should correctly encrypt a message', async () => { - // const crypto = new NodeCryptoProvider() - // const key = new AuthKey(crypto, logger, readerMap) - // await key.setup(authKey) - // - // const msg = await key.encryptMessage(message, serverSalt, sessionId) - // - // expect(msg).to.eql( - // Buffer.from( - // '...', - // 'hex', - // ), - // ) - // }) + it('should encrypt a message', async () => { + const message = writeMessage(utf8EncodeToBuffer('hello, world!!!!')) + + const key = await create() + const msg = key.encryptMessage(message, serverSalt, sessionId) + + expect(hexEncode(msg)).toEqual( + '40fa5bb7cb56a895f6f5a88914892aadf87c68031cc953ba29d68e118021f329' + + 'be386a620d49f3ad3a50c60dcef3733f214e8cefa3e403c11d193637d4971dc1' + + '5db7f74b26fd16cb0e8fee30bf7e3f68858fe82927e2cd06', + ) + }) + + describe('decrypt', () => { + async function decrypt(message: Uint8Array) { + const key = await create() + + return new Promise<[Long, number, TlBinaryReader]>((resolve, reject) => { + // in this method, errors are not thrown but rather logged + vi.spyOn(key.log, 'warn').mockImplementation((msg, ...fmt) => + reject(`${msg} : ${fmt.map((it) => JSON.stringify(it)).join(' ')}`), + ) + + key.decryptMessage(message, sessionId, (...args) => resolve(args)) + }) + } + + it('should decrypt a message', async () => { + const message = hexDecodeToBuffer( + '40fa5bb7cb56a8950c394b884f1529efc42fea22d972fea650a714ce6d2d1bdb' + + '3d98ff5929b8768c401771a69795f36a7e720dcafac2efbccd0ba368e8a7f48b' + + '07362cac1a32ffcabe188b51a36cc4d54e1d0633cf9eaf35', + ) + + const [decMsgId, decSeqNo, data] = await decrypt(message) + + expect(decMsgId).toEqual(msgId) + expect(decSeqNo).toEqual(seqNo) + expect(utf8Decode(data.raw(16))).toEqual('hello, world!!!!') + }) + + it('should decrypt a message with padding', async () => { + const message = hexDecodeToBuffer( + '40fa5bb7cb56a8950c394b884f1529efc42fea22d972fea650a714ce6d2d1bdb' + + '3d98ff5929b8768c401771a69795f36a7e720dcafac2efbccd0ba368e8a7f48b' + + '07362cac1a32ffcabe188b51a36cc4d54e1d0633cf9eaf35' + + 'deadbeef', // some padding (e.g. from padded transport), + ) + + const [decMsgId, decSeqNo, data] = await decrypt(message) + + expect(decMsgId).toEqual(msgId) + expect(decSeqNo).toEqual(seqNo) + expect(utf8Decode(data.raw(16))).toEqual('hello, world!!!!') + }) + + it('should ignore messages with invalid message key', async () => { + const message = hexDecodeToBuffer( + '40fa5bb7cb56a8950000000000000000000000000000000050a714ce6d2d1bdb' + + '3d98ff5929b8768c401771a69795f36a7e720dcafac2efbccd0ba368e8a7f48b' + + '07362cac1a32ffcabe188b51a36cc4d54e1d0633cf9eaf35', + ) + + await expect(() => decrypt(message)).rejects.toThrow('message with invalid messageKey') + }) + + it('should ignore messages with invalid session_id', async () => { + const message = hexDecodeToBuffer( + '40fa5bb7cb56a895a986a7e97f4e90aa2769b5e702c6e86f5e1e82c6ff0c6829' + + '2521a2ba9704fa37fb341d895cf32662c6cf47ba31cbf27c30d5c03f6c2930f4' + + '30fd8858b836b73fe32d4a95b8ebcdbc9ca8908f7964c40a', + ) + + await expect(() => decrypt(message)).rejects.toThrow('message with invalid sessionId') + }) + + it('should ignore messages with invalid length', async () => { + const messageTooLong = hexDecodeToBuffer( + '40fa5bb7cb56a8950d19412233dd5d24be697c73274e08fbe515cf65e0c5f70c' + + 'ad75fd2badc18c9f999f287351144eeb1cfcaa9bea33ef5058999ad96a498306' + + '08d2859425685a55b21fab413bfabc42ec5da283853b28c0', + ) + const messageUnaligned = hexDecodeToBuffer( + '40fa5bb7cb56a8957b4e4bec561eee4a5a1025bc8a35d3d0c79a3685d2b90ff0' + + '5f638e9c42c9fd9448b0ce8e7d49e7ea1ce458e47b825b5c7fd8ddf5b4fded46' + + '2a4bcc02f3ff2e89de6764d6d219f575e457fdcf8c163cdf', + ) + + await expect(() => decrypt(messageTooLong)).rejects.toThrow('message with invalid length: %d > %d') + await expect(() => decrypt(messageUnaligned)).rejects.toThrow( + 'message with invalid length: %d is not a multiple of 4', + ) + }) + + it('should ignore messages with invalid padding', async () => { + const message = hexDecodeToBuffer( + '40fa5bb7cb56a895133671d1c637a9836e2c64b4d1a0521d8a25a6416fd4dc9e' + + '79f9478fb837703cc9efa0a19d12143c2a26e57cb4bc64d7bc972dd8f19c53c590cc258162f44afc', + ) + + await expect(() => decrypt(message)).rejects.toThrow('message with invalid padding size') + }) + }) }) diff --git a/packages/core/src/network/auth-key.ts b/packages/core/src/network/auth-key.ts index 4a0162dd..6e858ffa 100644 --- a/packages/core/src/network/auth-key.ts +++ b/packages/core/src/network/auth-key.ts @@ -5,14 +5,7 @@ import { TlBinaryReader, TlReaderMap } from '@mtcute/tl-runtime' import { MtcuteError } from '../types/errors.js' import { createAesIgeForMessage } from '../utils/crypto/mtproto.js' -import { - buffersEqual, - concatBuffers, - dataViewFromBuffer, - ICryptoProvider, - Logger, - randomBytes, -} from '../utils/index.js' +import { buffersEqual, concatBuffers, dataViewFromBuffer, ICryptoProvider, Logger } from '../utils/index.js' export class AuthKey { ready = false @@ -58,7 +51,7 @@ export class AuthKey { dv.setInt32(8, sessionId.low, true) dv.setInt32(12, sessionId.high, true) buf.set(message, 16) - buf.set(randomBytes(padding), 16 + message.length) + this._crypto.randomFill(buf.subarray(16 + message.length, 16 + message.length + padding)) const messageKey = this._crypto.sha256(concatBuffers([this.clientSalt, buf])).subarray(8, 24) const ige = createAesIgeForMessage(this._crypto, this.key, messageKey, true) @@ -91,11 +84,7 @@ export class AuthKey { const expectedMessageKey = msgKeySource.subarray(8, 24) if (!buffersEqual(messageKey, expectedMessageKey)) { - this.log.warn( - '[%h] received message with invalid messageKey = %h (expected %h)', - messageKey, - expectedMessageKey, - ) + this.log.warn('received message with invalid messageKey = %h (expected %h)', messageKey, expectedMessageKey) return } diff --git a/packages/core/src/network/authorization.ts b/packages/core/src/network/authorization.ts index e33d5530..89af4f05 100644 --- a/packages/core/src/network/authorization.ts +++ b/packages/core/src/network/authorization.ts @@ -5,7 +5,7 @@ import { TlPublicKey } from '@mtcute/tl/binary/rsa-keys.js' import { TlBinaryReader, TlBinaryWriter, TlSerializationCounter } from '@mtcute/tl-runtime' import { MtArgumentError, MtSecurityError, MtTypeAssertionError } from '../types/index.js' -import { buffersEqual, concatBuffers, dataViewFromBuffer, randomBytes } from '../utils/buffer-utils.js' +import { buffersEqual, concatBuffers, dataViewFromBuffer } from '../utils/buffer-utils.js' import { findKeyByFingerprints } from '../utils/crypto/keys.js' import { millerRabin } from '../utils/crypto/miller-rabin.js' import { generateKeyAndIvFromNonce } from '../utils/crypto/mtproto.js' @@ -32,7 +32,7 @@ interface CheckedPrime { const checkedPrimesCache: CheckedPrime[] = [] -function checkDhPrime(log: Logger, dhPrime: bigint, g: number) { +function checkDhPrime(crypto: ICryptoProvider, log: Logger, dhPrime: bigint, g: number) { if (KNOWN_DH_PRIME === dhPrime) { log.debug('server is using known dh prime, skipping validation') @@ -46,10 +46,10 @@ function checkDhPrime(log: Logger, dhPrime: bigint, g: number) { throw new MtSecurityError('Step 3: dh_prime is not in the 2048-bit range') } - if (!millerRabin(dhPrime)) { + if (!millerRabin(crypto, dhPrime)) { throw new MtSecurityError('Step 3: dh_prime is not prime') } - if (!millerRabin((dhPrime - 1n) / 2n)) { + if (!millerRabin(crypto, (dhPrime - 1n) / 2n)) { throw new MtSecurityError('Step 3: dh_prime is not a safe prime - (dh_prime-1)/2 is not prime') } @@ -130,12 +130,15 @@ function rsaPad(data: Uint8Array, crypto: ICryptoProvider, key: TlPublicKey): Ui throw new MtArgumentError('Failed to pad: too big data') } - data = concatBuffers([data, randomBytes(192 - data.length)]) + const dataPadded = new Uint8Array(192) + dataPadded.set(data, 0) + crypto.randomFill(dataPadded.subarray(data.length)) + data = dataPadded for (;;) { const aesIv = new Uint8Array(32) - const aesKey = randomBytes(32) + const aesKey = crypto.randomBytes(32) const dataWithHash = concatBuffers([data, crypto.sha256(concatBuffers([aesKey, data]))]) // we only need to reverse the data @@ -165,7 +168,7 @@ function rsaEncrypt(data: Uint8Array, crypto: ICryptoProvider, key: TlPublicKey) crypto.sha1(data), data, // sha1 is always 20 bytes, so we're left with 255 - 20 - x padding - randomBytes(235 - data.length), + crypto.randomBytes(235 - data.length), ]) const encryptedBigInt = bigIntModPow( @@ -222,7 +225,7 @@ export async function doAuthorization( if (expiresIn) log.prefix = '[PFS] ' - const nonce = randomBytes(16) + const nonce = crypto.randomBytes(16) // Step 1: PQ request log.debug('starting PQ handshake (temp = %b), nonce = %h', expiresIn, nonce) @@ -251,7 +254,7 @@ export async function doAuthorization( const [p, q] = await crypto.factorizePQ(resPq.pq) log.debug('factorized PQ: PQ = %h, P = %h, Q = %h', resPq.pq, p, q) - const newNonce = randomBytes(32) + const newNonce = crypto.randomBytes(32) let dcId = connection.params.dc.id if (connection.params.testMode) dcId += 10000 @@ -330,13 +333,13 @@ export async function doAuthorization( const g = BigInt(serverDhInner.g) const gA = bufferToBigInt(serverDhInner.gA) - checkDhPrime(log, dhPrime, serverDhInner.g) + checkDhPrime(crypto, log, dhPrime, serverDhInner.g) let retryId = Long.ZERO const serverSalt = xorBuffer(newNonce.subarray(0, 8), resPq.serverNonce.subarray(0, 8)) for (;;) { - const b = bufferToBigInt(randomBytes(256)) + const b = bufferToBigInt(crypto.randomBytes(256)) const gB = bigIntModPow(g, b, dhPrime) const authKey = bigIntToBuffer(bigIntModPow(gA, b, dhPrime)) diff --git a/packages/core/src/network/session-connection.ts b/packages/core/src/network/session-connection.ts index 053e352e..9508640b 100644 --- a/packages/core/src/network/session-connection.ts +++ b/packages/core/src/network/session-connection.ts @@ -14,7 +14,6 @@ import { EarlyTimer, ICryptoProvider, longFromBuffer, - randomBytes, randomLong, removeFromLongArray, } from '../utils/index.js' @@ -348,14 +347,14 @@ export class SessionConnection extends PersistentConnection { const writer = TlBinaryWriter.alloc(this.params.writerMap, 80) // = 40 (inner length) + 32 (mtproto header) + 8 (pad 72 so mod 16 = 0) - writer.raw(randomBytes(16)) + writer.raw(this._crypto.randomBytes(16)) writer.long(msgId) writer.int(0) // seq_no writer.int(40) // msg_len writer.object(inner) const msgWithoutPadding = writer.result() - writer.raw(randomBytes(8)) + writer.raw(this._crypto.randomBytes(8)) const msgWithPadding = writer.result() const hash = this._crypto.sha1(msgWithoutPadding) diff --git a/packages/core/src/network/transports/intermediate.test.ts b/packages/core/src/network/transports/intermediate.test.ts index fb9c7a33..ab427204 100644 --- a/packages/core/src/network/transports/intermediate.test.ts +++ b/packages/core/src/network/transports/intermediate.test.ts @@ -3,7 +3,8 @@ import { describe, expect, it } from 'vitest' import { hexDecodeToBuffer, hexEncode } from '@mtcute/tl-runtime' import { IntermediatePacketCodec, PaddedIntermediatePacketCodec, TransportError } from '../../index.js' -import { concatBuffers, dataViewFromBuffer } from '../../utils/index.js' +import { defaultTestCryptoProvider, useFakeMathRandom } from '../../utils/crypto/crypto.test-utils.js' +import { concatBuffers } from '../../utils/index.js' describe('IntermediatePacketCodec', () => { it('should return correct tag', () => { @@ -89,23 +90,22 @@ describe('IntermediatePacketCodec', () => { }) describe('PaddedIntermediatePacketCodec', () => { - it('should return correct tag', () => { - expect(hexEncode(new PaddedIntermediatePacketCodec().tag())).eq('dddddddd') + useFakeMathRandom() + + const create = async () => { + const codec = new PaddedIntermediatePacketCodec() + codec.setup!(await defaultTestCryptoProvider()) + + return codec + } + + it('should return correct tag', async () => { + expect(hexEncode((await create()).tag())).eq('dddddddd') }) - it('should correctly frame packets', () => { - // todo: once we have predictable random, test this properly - + it('should correctly frame packets', async () => { const data = hexDecodeToBuffer('6cfeffff') - const encoded = new PaddedIntermediatePacketCodec().encode(data) - const dv = dataViewFromBuffer(encoded) - const packetSize = dv.getUint32(0, true) - const paddingSize = packetSize - data.length - - // padding size, 0-15 - expect(paddingSize).toBeGreaterThanOrEqual(0) - expect(paddingSize).toBeLessThanOrEqual(15) - expect([...encoded.slice(4, 4 + packetSize - paddingSize)]).toEqual([...data]) + expect(hexEncode((await create()).encode(data))).toEqual('0a0000006cfeffff29afd26df40f') }) }) diff --git a/packages/core/src/network/transports/intermediate.ts b/packages/core/src/network/transports/intermediate.ts index dca4876c..66f93a54 100644 --- a/packages/core/src/network/transports/intermediate.ts +++ b/packages/core/src/network/transports/intermediate.ts @@ -1,4 +1,4 @@ -import { dataViewFromBuffer, randomBytes } from '../../utils/index.js' +import { dataViewFromBuffer, getRandomInt, ICryptoProvider } from '../../utils/index.js' import { IPacketCodec, TransportError } from './abstract.js' import { StreamedCodec } from './streamed.js' @@ -58,16 +58,20 @@ export class PaddedIntermediatePacketCodec extends IntermediatePacketCodec { return PADDED_TAG } + private _crypto!: ICryptoProvider + setup?(crypto: ICryptoProvider) { + this._crypto = crypto + } + encode(packet: Uint8Array): Uint8Array { // padding size, 0-15 - const padSize = Math.floor(Math.random() * 16) - const padding = randomBytes(padSize) + const padSize = getRandomInt(16) const ret = new Uint8Array(packet.length + 4 + padSize) const dv = dataViewFromBuffer(ret) dv.setUint32(0, packet.length + padSize, true) ret.set(packet, 4) - ret.set(padding, 4 + packet.length) + this._crypto.randomFill(ret.subarray(4 + packet.length)) return ret } diff --git a/packages/core/src/network/transports/obfuscated.ts b/packages/core/src/network/transports/obfuscated.ts index a4fbf2ef..ffd5cb9f 100644 --- a/packages/core/src/network/transports/obfuscated.ts +++ b/packages/core/src/network/transports/obfuscated.ts @@ -1,5 +1,5 @@ import { bufferToReversed, concatBuffers, dataViewFromBuffer } from '../../utils/buffer-utils.js' -import { IAesCtr, randomBytes } from '../../utils/index.js' +import { IAesCtr } from '../../utils/index.js' import { IPacketCodec } from './abstract.js' import { WrappedCodec } from './wrapped.js' @@ -26,7 +26,7 @@ export class ObfuscatedPacketCodec extends WrappedCodec implements IPacketCodec let dv: DataView for (;;) { - random = randomBytes(64) + random = this._crypto.randomBytes(64) if (random[0] === 0xef) continue dv = dataViewFromBuffer(random) diff --git a/packages/core/src/utils/bigint-utils.test.ts b/packages/core/src/utils/bigint-utils.test.ts index 44a24a94..c559ff2d 100644 --- a/packages/core/src/utils/bigint-utils.test.ts +++ b/packages/core/src/utils/bigint-utils.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest' import { hexDecodeToBuffer } from '@mtcute/tl-runtime' +import { defaultTestCryptoProvider } from './crypto/crypto.test-utils.js' import { bigIntBitLength, bigIntGcd, @@ -85,50 +86,56 @@ describe('bufferToBigInt', () => { }) }) -describe('randomBigInt', () => { +describe('randomBigInt', async () => { + const c = await defaultTestCryptoProvider() + it('should return a random bigint', () => { - const a = randomBigInt(32) - const b = randomBigInt(32) + const a = randomBigInt(c, 32) + const b = randomBigInt(c, 32) expect(a).not.toEqual(b) }) it('should return a random bigint up to specified byte length', () => { - const a = randomBigInt(32) - const b = randomBigInt(64) + const a = randomBigInt(c, 32) + const b = randomBigInt(c, 64) expect(bigIntBitLength(a)).toBeLessThanOrEqual(32 * 8) expect(bigIntBitLength(b)).toBeLessThanOrEqual(64 * 8) }) }) -describe('randomBigIntBits', () => { +describe('randomBigIntBits', async () => { + const c = await defaultTestCryptoProvider() + it('should return a random bigint', () => { - const a = randomBigIntBits(32) - const b = randomBigIntBits(32) + const a = randomBigIntBits(c, 32) + const b = randomBigIntBits(c, 32) expect(a).not.toEqual(b) }) it('should return a random bigint up to specified bit length', () => { - const a = randomBigIntBits(32) - const b = randomBigIntBits(64) + const a = randomBigIntBits(c, 32) + const b = randomBigIntBits(c, 64) expect(bigIntBitLength(a)).toBeLessThanOrEqual(32) expect(bigIntBitLength(b)).toBeLessThanOrEqual(64) }) }) -describe('randomBigIntInRange', () => { +describe('randomBigIntInRange', async () => { + const c = await defaultTestCryptoProvider() + it('should return a random bigint', () => { - const a = randomBigIntInRange(10000n) - const b = randomBigIntInRange(10000n) + const a = randomBigIntInRange(c, 10000n) + const b = randomBigIntInRange(c, 10000n) expect(a).not.toEqual(b) }) it('should return a bigint within a given range', () => { - const a = randomBigIntInRange(200n, 100n) + const a = randomBigIntInRange(c, 200n, 100n) expect(a).toBeGreaterThanOrEqual(100n) expect(a).toBeLessThan(200n) diff --git a/packages/core/src/utils/bigint-utils.ts b/packages/core/src/utils/bigint-utils.ts index 91c684c5..b8bec315 100644 --- a/packages/core/src/utils/bigint-utils.ts +++ b/packages/core/src/utils/bigint-utils.ts @@ -1,4 +1,5 @@ -import { bufferToReversed, randomBytes } from './buffer-utils.js' +import { bufferToReversed } from './buffer-utils.js' +import { ICryptoProvider } from './crypto/abstract.js' /** * Get the minimum number of bits required to represent a number @@ -82,19 +83,19 @@ export function bufferToBigInt(buffer: Uint8Array, le = false): bigint { } /** - * Generate a random big integer of the given size (in bytes) + * Generate a cryptographically safe random big integer of the given size (in bytes) * @param size Size in bytes */ -export function randomBigInt(size: number): bigint { - return bufferToBigInt(randomBytes(size)) +export function randomBigInt(crypto: ICryptoProvider, size: number): bigint { + return bufferToBigInt(crypto.randomBytes(size)) } /** * Generate a random big integer of the given size (in bits) * @param bits */ -export function randomBigIntBits(bits: number): bigint { - let num = randomBigInt(Math.ceil(bits / 8)) +export function randomBigIntBits(crypto: ICryptoProvider, bits: number): bigint { + let num = randomBigInt(crypto, Math.ceil(bits / 8)) const bitLength = bigIntBitLength(num) @@ -112,13 +113,13 @@ export function randomBigIntBits(bits: number): bigint { * @param max Maximum value (exclusive) * @param min Minimum value (inclusive) */ -export function randomBigIntInRange(max: bigint, min = 1n): bigint { +export function randomBigIntInRange(crypto: ICryptoProvider, max: bigint, min = 1n): bigint { const interval = max - min if (interval < 0n) throw new Error('expected min < max') const byteSize = bigIntBitLength(interval) / 8 - let result = randomBigInt(byteSize) + let result = randomBigInt(crypto, byteSize) while (result > interval) result -= interval return min + result diff --git a/packages/core/src/utils/buffer-utils.test.ts b/packages/core/src/utils/buffer-utils.test.ts index 6cd025f0..8bf7203c 100644 --- a/packages/core/src/utils/buffer-utils.test.ts +++ b/packages/core/src/utils/buffer-utils.test.ts @@ -1,9 +1,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest' -import { hexEncode, utf8Decode, utf8EncodeToBuffer } from '@mtcute/tl-runtime' - -import { buffersEqual, bufferToReversed, cloneBuffer, concatBuffers, randomBytes } from './buffer-utils.js' -import { xorBuffer, xorBufferInPlace } from './crypto/utils.js' +import { buffersEqual, bufferToReversed, cloneBuffer, concatBuffers } from './buffer-utils.js' describe('buffersEqual', () => { it('should return true for equal buffers', () => { @@ -17,75 +14,6 @@ describe('buffersEqual', () => { }) }) -describe('xorBuffer', () => { - it('should xor buffers without modifying original', () => { - const data = utf8EncodeToBuffer('hello') - const key = utf8EncodeToBuffer('xor') - - const xored = xorBuffer(data, key) - expect(data.toString()).eq('hello') - expect(key.toString()).eq('xor') - expect(hexEncode(xored)).eq('100a1e6c6f') - }) - - it('should be deterministic', () => { - const data = utf8EncodeToBuffer('hello') - const key = utf8EncodeToBuffer('xor') - - const xored1 = xorBuffer(data, key) - expect(hexEncode(xored1)).eq('100a1e6c6f') - - const xored2 = xorBuffer(data, key) - expect(hexEncode(xored2)).eq('100a1e6c6f') - }) - - it('second call should decode content', () => { - const data = utf8EncodeToBuffer('hello') - const key = utf8EncodeToBuffer('xor') - - const xored1 = xorBuffer(data, key) - expect(hexEncode(xored1)).eq('100a1e6c6f') - - const xored2 = xorBuffer(xored1, key) - expect(utf8Decode(xored2)).eq('hello') - }) -}) - -describe('xorBufferInPlace', () => { - it('should xor buffers by modifying original', () => { - const data = utf8EncodeToBuffer('hello') - const key = utf8EncodeToBuffer('xor') - - xorBufferInPlace(data, key) - expect(hexEncode(data)).eq('100a1e6c6f') - expect(key.toString()).eq('xor') - }) - - it('second call should decode content', () => { - const data = utf8EncodeToBuffer('hello') - const key = utf8EncodeToBuffer('xor') - - xorBufferInPlace(data, key) - expect(hexEncode(data)).eq('100a1e6c6f') - - xorBufferInPlace(data, key) - expect(data.toString()).eq('hello') - }) -}) - -describe('randomBytes', () => { - it('should return exactly N bytes', () => { - expect(randomBytes(0).length).eq(0) - expect(randomBytes(5).length).eq(5) - expect(randomBytes(10).length).eq(10) - expect(randomBytes(256).length).eq(256) - }) - - it('should not be deterministic', () => { - expect([...randomBytes(8)]).not.eql([...randomBytes(8)]) - }) -}) - describe('cloneBuffer', () => { it('should clone buffer', () => { const orig = new Uint8Array([1, 2, 3]) diff --git a/packages/core/src/utils/buffer-utils.ts b/packages/core/src/utils/buffer-utils.ts index 12b09989..47cd8a4f 100644 --- a/packages/core/src/utils/buffer-utils.ts +++ b/packages/core/src/utils/buffer-utils.ts @@ -1,5 +1,3 @@ -export { _randomBytes as randomBytes } from './platform/random.js' - /** * Check if two buffers are equal * diff --git a/packages/core/src/utils/crypto/abstract.ts b/packages/core/src/utils/crypto/abstract.ts index f4048a42..19af66f8 100644 --- a/packages/core/src/utils/crypto/abstract.ts +++ b/packages/core/src/utils/crypto/abstract.ts @@ -36,11 +36,23 @@ export interface ICryptoProvider { gzip(data: Uint8Array, maxSize: number): Uint8Array | null gunzip(data: Uint8Array): Uint8Array + + randomFill(buf: Uint8Array): void + randomBytes(size: number): Uint8Array } export abstract class BaseCryptoProvider { + abstract randomFill(buf: Uint8Array): void + factorizePQ(pq: Uint8Array) { - return factorizePQSync(pq) + return factorizePQSync(this as unknown as ICryptoProvider, pq) + } + + randomBytes(size: number) { + const buf = new Uint8Array(size) + this.randomFill(buf) + + return buf } } diff --git a/packages/core/src/utils/crypto/crypto.test-utils.ts b/packages/core/src/utils/crypto/crypto.test-utils.ts index e9614f11..0b76cca5 100644 --- a/packages/core/src/utils/crypto/crypto.test-utils.ts +++ b/packages/core/src/utils/crypto/crypto.test-utils.ts @@ -1,11 +1,69 @@ -import { beforeAll, expect, it } from 'vitest' +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' import { gzipSync, inflateSync } from 'zlib' import { hexDecodeToBuffer, hexEncode, utf8EncodeToBuffer } from '@mtcute/tl-runtime' +import { dataViewFromBuffer } from '../buffer-utils.js' import { ICryptoProvider } from './abstract.js' -import { factorizePQSync } from './factorization.js' +import { defaultCryptoProviderFactory } from './index.js' +// some random 1024 bytes of entropy +const DEFAULT_ENTROPY = ` +29afd26df40fb8ed10b6b4ad6d56ef5df9453f88e6ee6adb6e0544ba635dc6a8a990c9b8b980c343936b33fa7f97bae025102532233abb269c489920ef99021b +259ce3a2c964c5c8972b4a84ff96f3375a94b535a9468f2896e2080ac7a32ed58e910474a4b02415e07671cbb5bdd58a5dd26fd137c4c98b8c346571fae6ead3 +9dfd612bd6b480b6723433f5218e9d6e271591153fb3ffefc089f7e848d3f4633459fff66b33cf939e5655813149fa34be8625f9bc4814d1ee6cf40e4d0de229 +1aa22e68c8ad8cc698103734f9aaf79f2bdc052a787a7a9b3629d1ed38750f88cb0481c0ba30a9c611672f9a4d1dc02637abb4e98913ee810a3b152d3d75f25d +7efdc263c08833569968b1771ebbe843d187e2c917d9ad8e8865e44b69f7b74d72ab86a4ef1891dce196ee11a7c9d7d8074fc0450e745bd3a827d77bb0820b90 +3055dc15f0abd897ea740a99606b64d28968d770b5e43492ddbf07a7c75104d3e522be9b72050c0fdae8412cdf49014be21105b87a06cb7202dd580387adc007 +6280d98b015a1a413819d817f007939d1490467a1ef85a345584c7e594bb729c12a1233f806e515e7088360219dfa109264310ba84777b93eb1ad3c40727a25a +a5d9cdd6748c6ab2ca0bd4daa2ba8225bce2b066a163bcacf05609fc84055bb86a4742c28addd7d7ab8d87b64cfde0b3f4b3bc8e05f3d0a1a2fadb294860e099 +a10b3721b0d5b28918b8fb49a18a82a0fde6680a64ed915637805e35ffe8b2c1d4177ec10d10eaaf24425e0351b6a89e794944e1aa82eb5c0210a37da66cccac +895398cf915a8aa141f611521fc258514a99c02721113942c66f2c9a8f9601ff0044a953d17a47b07ad1b5f8725cc020a1a5239be65db0a43d42c206903740f0 +27c3f749ecfff2e646570118cd54db2fec392b44d8eb8377309f3e4d164dbc0530914b117b9d278b06db8359d97442d4dcbcaff93cd9a08a6b06a5ba8725d0d7 +06b313a5d792be254d33e087b7a4fafcdf819941b9bec4c6057d4c050bd01eb243efd4e6b707281b127820a2b734c6d8f6b2131bf0b5b215c7a798ff3fe90ceb +da91539fcc7b03d2b8b1381bd6023fff20278344ad944d364ba684842db3901c346335f0d455eda414f99c1e794a86aa3a90bcc6e085eecb0b4bf61198d16ed3 +89cfa495f977a37a51502b2f60649f2efd7d89c757b6366776ba4c0612017bf1fbfc682dd62e9960d39cbea854d2dcc708b1db5d268192954d13ee72c0bb1bd8 +558a3cf3b02b1cd795b40f7a57780391bb8724883d3f7764846c3823e165b3f8c025f59d896905f9a955478586ce57f820d958a01aa59a4cace7ecdf125df334 +fa3de8e50aac96c1275591a1221c32a60a1513370a33a228e00894341b10cf44a6ae6ac250d17a364e956ab1a17b068df3fb2d5b5a672d8a409eeb8b6ca1ade6 +`.replace(/\s/g, '') + +export function withFakeRandom(provider: ICryptoProvider, source = DEFAULT_ENTROPY): ICryptoProvider { + const sourceBytes = hexDecodeToBuffer(source) + let offset = 0 + + function getRandomValues(buf: Uint8Array) { + buf.set(sourceBytes.subarray(offset, offset + buf.length)) + offset += buf.length + } + + provider.randomFill = getRandomValues + + return provider +} + +export function useFakeMathRandom(source = DEFAULT_ENTROPY): void { + beforeEach(() => { + const sourceBytes = hexDecodeToBuffer(source) + let offset = 0 + + vi.spyOn(globalThis.Math, 'random').mockImplementation(() => { + const ret = dataViewFromBuffer(sourceBytes).getUint32(offset, true) / 0xffffffff + offset += 4 + + return ret + }) + }) + afterEach(() => { + vi.spyOn(globalThis.Math, 'random').mockRestore() + }) +} + +export async function defaultTestCryptoProvider(source = DEFAULT_ENTROPY): Promise { + const prov = withFakeRandom(defaultCryptoProviderFactory(), source) + await prov.initialize?.() + + return prov +} export function testCryptoProvider(c: ICryptoProvider): void { beforeAll(() => c.initialize?.()) @@ -98,17 +156,17 @@ export function testCryptoProvider(c: ICryptoProvider): void { it( 'should decompose PQ to prime factors P and Q', - () => { - const testFactorization = (pq: string, p: string, q: string) => { - const [p1, q1] = factorizePQSync(hexDecodeToBuffer(pq)) + async () => { + const testFactorization = async (pq: string, p: string, q: string) => { + const [p1, q1] = await c.factorizePQ(hexDecodeToBuffer(pq)) expect(hexEncode(p1)).eq(p.toLowerCase()) expect(hexEncode(q1)).eq(q.toLowerCase()) } // from samples at https://core.telegram.org/mtproto/samples-auth_key - testFactorization('17ED48941A08F981', '494C553B', '53911073') + await testFactorization('17ED48941A08F981', '494C553B', '53911073') // random example - testFactorization('14fcab4dfc861f45', '494c5c99', '494c778d') + await testFactorization('14fcab4dfc861f45', '494c5c99', '494c778d') }, // since PQ factorization relies on RNG, it may take a while (or may not!) { timeout: 10000 }, @@ -137,4 +195,24 @@ export function testCryptoProvider(c: ICryptoProvider): void { // eslint-disable-next-line no-restricted-globals expect(Buffer.from(decompressed)).toEqual(Buffer.from(data)) }) + + describe('randomBytes', () => { + it('should return exactly N bytes', () => { + expect(c.randomBytes(0).length).eq(0) + expect(c.randomBytes(5).length).eq(5) + expect(c.randomBytes(10).length).eq(10) + expect(c.randomBytes(256).length).eq(256) + }) + + it('should not be deterministic', () => { + expect([...c.randomBytes(8)]).not.eql([...c.randomBytes(8)]) + }) + + it('should use randomFill', () => { + const spy = vi.spyOn(c, 'randomFill') + c.randomBytes(8) + + expect(spy).toHaveBeenCalled() + }) + }) } diff --git a/packages/core/src/utils/crypto/factorization.ts b/packages/core/src/utils/crypto/factorization.ts index b7919bc7..bbae08ff 100644 --- a/packages/core/src/utils/crypto/factorization.ts +++ b/packages/core/src/utils/crypto/factorization.ts @@ -6,15 +6,16 @@ import { bufferToBigInt, randomBigIntInRange, } from '../bigint-utils.js' +import { ICryptoProvider } from './abstract.js' /** * Factorize `p*q` to `p` and `q` synchronously using Brent-Pollard rho algorithm * @param pq */ -export function factorizePQSync(pq: Uint8Array): [Uint8Array, Uint8Array] { +export function factorizePQSync(crypto: ICryptoProvider, pq: Uint8Array): [Uint8Array, Uint8Array] { const pq_ = bufferToBigInt(pq) - const n = PollardRhoBrent(pq_) + const n = PollardRhoBrent(crypto, pq_) const m = pq_ / n let p @@ -31,12 +32,12 @@ export function factorizePQSync(pq: Uint8Array): [Uint8Array, Uint8Array] { return [bigIntToBuffer(p), bigIntToBuffer(q)] } -function PollardRhoBrent(n: bigint): bigint { +function PollardRhoBrent(crypto: ICryptoProvider, n: bigint): bigint { if (n % 2n === 0n) return 2n - let y = randomBigIntInRange(n - 1n) - const c = randomBigIntInRange(n - 1n) - const m = randomBigIntInRange(n - 1n) + let y = randomBigIntInRange(crypto, n - 1n) + const c = randomBigIntInRange(crypto, n - 1n) + const m = randomBigIntInRange(crypto, n - 1n) let g = 1n let r = 1n let q = 1n diff --git a/packages/core/src/utils/crypto/miller-rabin.test.ts b/packages/core/src/utils/crypto/miller-rabin.test.ts index 8fe73af5..adecd832 100644 --- a/packages/core/src/utils/crypto/miller-rabin.test.ts +++ b/packages/core/src/utils/crypto/miller-rabin.test.ts @@ -1,12 +1,16 @@ import { describe, expect, it } from 'vitest' +import { defaultCryptoProviderFactory } from './index.js' import { millerRabin } from './miller-rabin.js' describe( 'miller-rabin test', function () { + // miller-rabin factorization relies on RNG, so we should use a real random number generator + const c = defaultCryptoProviderFactory() + const testMillerRabin = (n: number | string | bigint, isPrime: boolean) => { - expect(millerRabin(BigInt(n))).eq(isPrime) + expect(millerRabin(c, BigInt(n))).eq(isPrime) } it('should correctly label small primes as probable primes', () => { diff --git a/packages/core/src/utils/crypto/miller-rabin.ts b/packages/core/src/utils/crypto/miller-rabin.ts index acf14674..ad0c24eb 100644 --- a/packages/core/src/utils/crypto/miller-rabin.ts +++ b/packages/core/src/utils/crypto/miller-rabin.ts @@ -1,6 +1,7 @@ import { bigIntBitLength, bigIntModPow, randomBigIntBits, twoMultiplicity } from '../bigint-utils.js' +import { ICryptoProvider } from './abstract.js' -export function millerRabin(n: bigint, rounds = 20): boolean { +export function millerRabin(crypto: ICryptoProvider, n: bigint, rounds = 20): boolean { // small numbers: 0, 1 are not prime, 2, 3 are prime if (n < 4n) return n > 1n if (n % 2n === 0n || n < 0n) return false @@ -15,7 +16,7 @@ export function millerRabin(n: bigint, rounds = 20): boolean { let base do { - base = randomBigIntBits(nBits) + base = randomBigIntBits(crypto, nBits) } while (base <= 1n || base >= nSub) let x = bigIntModPow(base, d, n) diff --git a/packages/core/src/utils/crypto/node.ts b/packages/core/src/utils/crypto/node.ts index 30cc156a..f71e00a2 100644 --- a/packages/core/src/utils/crypto/node.ts +++ b/packages/core/src/utils/crypto/node.ts @@ -1,5 +1,5 @@ // eslint-disable-next-line no-restricted-imports -import { createCipheriv, createHash, createHmac, pbkdf2 } from 'crypto' +import { createCipheriv, createHash, createHmac, pbkdf2, randomFillSync } from 'crypto' import { deflateSync, gunzipSync } from 'zlib' import { ige256Decrypt, ige256Encrypt, initAsync, InitInput } from '@mtcute/wasm' @@ -65,6 +65,10 @@ export abstract class BaseNodeCryptoProvider extends BaseCryptoProvider { gunzip(data: Uint8Array): Uint8Array { return gunzipSync(data) } + + randomFill(buf: Uint8Array) { + randomFillSync(buf) + } } export class NodeCryptoProvider extends BaseNodeCryptoProvider implements ICryptoProvider { diff --git a/packages/core/src/utils/crypto/password.test.ts b/packages/core/src/utils/crypto/password.test.ts index 020b96c6..2ea4dede 100644 --- a/packages/core/src/utils/crypto/password.test.ts +++ b/packages/core/src/utils/crypto/password.test.ts @@ -1,9 +1,11 @@ +import Long from 'long' import { describe, expect, it } from 'vitest' import { tl } from '@mtcute/tl' import { hexDecodeToBuffer, hexEncode, utf8EncodeToBuffer } from '@mtcute/tl-runtime' -import { computePasswordHash, defaultCryptoProviderFactory } from './index.js' +import { defaultTestCryptoProvider } from './crypto.test-utils.js' +import { computeNewPasswordHash, computePasswordHash, computeSrpParams } from './index.js' // a real-world request from an account with "qwe123" password const fakeAlgo: tl.RawPasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow = { @@ -22,55 +24,86 @@ const fakeAlgo: tl.RawPasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA25 '0d8115f635b105ee2e4e15d04b2454bf6f4fadf034b10403119cd8e3b92fcc5b', ), } -// const fakeRequest: tl.account.RawPassword = { -// _: 'account.password', -// hasRecovery: false, -// hasSecureValues: false, -// hasPassword: true, -// currentAlgo: fakeAlgo, -// srpB: hexDecodeToBuffer( -// '1476a7b5991d7f028bbee33b3455cad3f2cd0eb3737409fcce92fa7d4cd5c733' + -// 'ec6d2cb3454e587d4c17eda2fd7ef9a57327215f38292cc8bd5dc77d3e1d31cd' + -// 'dae2652f8347c4b0093f7c78242f70e6cc13137ee7acc257a49855a63113db8f' + -// '163992b9101551f3b6f7eb5d196cee3647c359553b1bcbe82ba8933c0fb1ac35' + -// '0243c535b8e634613e1f626ba8a6d141ef957c859e71a117b557c0298bfbb107' + -// 'c91f71f5b4275fded58289aa1e87c612f44b7aa0b5e0de7def4458f58db80019' + -// 'd2e7b181eb66dc270374af2d160dd0c53edd677b2701694d71ea8718c49df6a9' + -// 'dbe2cbae051ffc1986336cd26f11a8ab426dfe0813d7b3f4eedf4e34182ccc3a', -// ), -// srpId: Long.fromBits(-2046015018, 875006452), -// newAlgo: { -// _: 'passwordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow', -// salt1: hexDecodeToBuffer('9b3accc457c0d528'), -// salt2: hexDecodeToBuffer('6c619bb0786dc4ed1bf211d23f6e4065'), -// g: 3, -// p: hexDecodeToBuffer( -// 'c71caeb9c6b1c9048e6c522f70f13f73980d40238e3e21c14934d037563d930f' + -// '48198a0aa7c14058229493d22530f4dbfa336f6e0ac925139543aed44cce7c37' + -// '20fd51f69458705ac68cd4fe6b6b13abdc9746512969328454f18faf8c595f64' + -// '2477fe96bb2a941d5bcd1d4ac8cc49880708fa9b378e3c4f3a9060bee67cf9a4' + -// 'a4a695811051907e162753b56b0f6b410dba74d8a84b2a14b3144e0ef1284754' + -// 'fd17ed950d5965b4b9dd46582db1178d169c6bc465b0d6ff9ca3928fef5b9ae4' + -// 'e418fc15e83ebea0f87fa9ff5eed70050ded2849f47bf959d956850ce929851f' + -// '0d8115f635b105ee2e4e15d04b2454bf6f4fadf034b10403119cd8e3b92fcc5b', -// ), -// }, -// newSecureAlgo: { -// _: 'securePasswordKdfAlgoPBKDF2HMACSHA512iter100000', -// salt: hexDecodeToBuffer('fdd59abc0bffb24d'), -// }, -// secureRandom: new Uint8Array(), // unused -// } -const password = utf8EncodeToBuffer('qwe123') - -describe('computePasswordHash', () => { - const crypto = defaultCryptoProviderFactory() +const fakeRequest: tl.account.RawPassword = { + _: 'account.password', + hasRecovery: false, + hasSecureValues: false, + hasPassword: true, + currentAlgo: fakeAlgo, + srpB: hexDecodeToBuffer( + '1476a7b5991d7f028bbee33b3455cad3f2cd0eb3737409fcce92fa7d4cd5c733' + + 'ec6d2cb3454e587d4c17eda2fd7ef9a57327215f38292cc8bd5dc77d3e1d31cd' + + 'dae2652f8347c4b0093f7c78242f70e6cc13137ee7acc257a49855a63113db8f' + + '163992b9101551f3b6f7eb5d196cee3647c359553b1bcbe82ba8933c0fb1ac35' + + '0243c535b8e634613e1f626ba8a6d141ef957c859e71a117b557c0298bfbb107' + + 'c91f71f5b4275fded58289aa1e87c612f44b7aa0b5e0de7def4458f58db80019' + + 'd2e7b181eb66dc270374af2d160dd0c53edd677b2701694d71ea8718c49df6a9' + + 'dbe2cbae051ffc1986336cd26f11a8ab426dfe0813d7b3f4eedf4e34182ccc3a', + ), + srpId: Long.fromBits(-2046015018, 875006452), + newAlgo: { + _: 'passwordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow', + salt1: hexDecodeToBuffer('9b3accc457c0d528'), + salt2: hexDecodeToBuffer('6c619bb0786dc4ed1bf211d23f6e4065'), + g: 3, + p: hexDecodeToBuffer( + 'c71caeb9c6b1c9048e6c522f70f13f73980d40238e3e21c14934d037563d930f' + + '48198a0aa7c14058229493d22530f4dbfa336f6e0ac925139543aed44cce7c37' + + '20fd51f69458705ac68cd4fe6b6b13abdc9746512969328454f18faf8c595f64' + + '2477fe96bb2a941d5bcd1d4ac8cc49880708fa9b378e3c4f3a9060bee67cf9a4' + + 'a4a695811051907e162753b56b0f6b410dba74d8a84b2a14b3144e0ef1284754' + + 'fd17ed950d5965b4b9dd46582db1178d169c6bc465b0d6ff9ca3928fef5b9ae4' + + 'e418fc15e83ebea0f87fa9ff5eed70050ded2849f47bf959d956850ce929851f' + + '0d8115f635b105ee2e4e15d04b2454bf6f4fadf034b10403119cd8e3b92fcc5b', + ), + }, + newSecureAlgo: { + _: 'securePasswordKdfAlgoPBKDF2HMACSHA512iter100000', + salt: hexDecodeToBuffer('fdd59abc0bffb24d'), + }, + secureRandom: new Uint8Array(), // unused +} +const password = 'qwe123' +describe('SRP', () => { it('should correctly compute password hash as defined by MTProto', async () => { - const actual = await computePasswordHash(crypto, password, fakeAlgo.salt1, fakeAlgo.salt2) + const crypto = await defaultTestCryptoProvider() + const hash = await computePasswordHash(crypto, utf8EncodeToBuffer(password), fakeAlgo.salt1, fakeAlgo.salt2) - expect(hexEncode(actual)).toEqual('750f1fe282965e63ce17b98427b35549fb864465211840f6a7c1f2fb657cc33b') + expect(hexEncode(hash)).toEqual('750f1fe282965e63ce17b98427b35549fb864465211840f6a7c1f2fb657cc33b') }) - // todo: computeNewPasswordHash and computeSrpParams both require predictable random + it('should correctly compute new password hash as defined by MTProto', async () => { + const crypto = await defaultTestCryptoProvider() + const hash = await computeNewPasswordHash(crypto, fakeAlgo, '123qwe') + + expect(hexEncode(hash)).toEqual( + '2540539ceeffd4543cd845bf319b8392e6b17bf7cf26bafcf6282ce9ae795368' + + '4ff49469c2863b17e6d65ddb16ae6f60bc07cc254c00e5ba389292f6cea0b3aa' + + 'c459d1d08984d65319df8c5d124042169bbe2ab8c0c93bc7178827f2ea84e7c3' + + 'a4f2660099fb6a4c38984c914283d3015278369521a4b81ecf927669b8c89746' + + 'ef49ec7b019af7f3addc746362f298d96409bef4677b9c3d8e5b5afe7a44c0bc' + + '130ebc7a79b5d5980966d88d3d9eba511b101b0703abd86df7410cd120edad12' + + '2a7a3ccad92d906dbf6f43bba13555bafb626b45551275f3626a4ae26a14908d' + + '38d640680e501f52bd08a0e3ff9d9185eebdae890c167459449b2c205b3ecde4', + ) + }) + + it('should correctly compute srp parameters as defined by MTProto', async () => { + const crypto = await defaultTestCryptoProvider() + const params = await computeSrpParams(crypto, fakeRequest, password) + + expect(params.srpId).toEqual(fakeRequest.srpId) + expect(hexEncode(params.A)).toEqual( + '363976f55edb57cc5cc0c4aaca9b7539eff98a43a93fa84be34860d18ac3a80f' + + 'ffd57c4617896ff667677d0552a079eb189d25d147ec96edd4495c946a18652d' + + '31d78eede40a8b29da340c19b32ccac78f8482406e392102c03d850d1db87223' + + '2c144bfacadb58856971aafb70ca3aac4efa7f73977ddc50dfc0a2c76c0ac950' + + '728d58b8480fa89c701703855148fadd885aaf1ca313ae3a3b2942de58a9a6fb' + + '9e3e65c7ac7a1b7f4e6aa4742b957f81927bd8cc761b76f90229dec34d6f15d3' + + '4fa454aa69d9219d9c5fa3625f5c6f1ac03892a70aa17269c76cd9bf2949a961' + + 'fad2a71e5fa961824b32db037130c7e9aad4c1e9f02ebc5b832622f98b59597e', + ) + expect(hexEncode(params.M1)).toEqual('25a91b21c634ad670a144165a9829192d152e131a716f676abc48cd817f508c6') + }) }) diff --git a/packages/core/src/utils/crypto/password.ts b/packages/core/src/utils/crypto/password.ts index 800660ec..6f37d729 100644 --- a/packages/core/src/utils/crypto/password.ts +++ b/packages/core/src/utils/crypto/password.ts @@ -3,7 +3,7 @@ import { utf8EncodeToBuffer } from '@mtcute/tl-runtime' import { MtSecurityError, MtUnsupportedError } from '../../types/errors.js' import { bigIntModPow, bigIntToBuffer, bufferToBigInt } from '../bigint-utils.js' -import { concatBuffers, randomBytes } from '../buffer-utils.js' +import { concatBuffers } from '../buffer-utils.js' import { ICryptoProvider } from './abstract.js' import { xorBuffer } from './utils.js' @@ -41,7 +41,10 @@ export async function computeNewPasswordHash( algo: tl.RawPasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow, password: string, ): Promise { - (algo as tl.Mutable).salt1 = concatBuffers([algo.salt1, randomBytes(32)]) + const salt1 = new Uint8Array(algo.salt1.length + 32) + salt1.set(algo.salt1) + crypto.randomFill(salt1.subarray(algo.salt1.length)) + ;(algo as tl.Mutable).salt1 = salt1 const _x = await computePasswordHash(crypto, utf8EncodeToBuffer(password), algo.salt1, algo.salt2) @@ -89,7 +92,7 @@ export async function computeSrpParams( const p = bufferToBigInt(algo.p) const gB = bufferToBigInt(request.srpB) - const a = bufferToBigInt(randomBytes(256)) + const a = bufferToBigInt(crypto.randomBytes(256)) const gA = bigIntModPow(g, a, p) const _gA = bigIntToBuffer(gA, 256) diff --git a/packages/core/src/utils/crypto/utils.test.ts b/packages/core/src/utils/crypto/utils.test.ts new file mode 100644 index 00000000..cd4faed4 --- /dev/null +++ b/packages/core/src/utils/crypto/utils.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it } from 'vitest' + +import { hexEncode, utf8Decode, utf8EncodeToBuffer } from '@mtcute/tl-runtime' + +import { xorBuffer, xorBufferInPlace } from './utils.js' + +describe('xorBuffer', () => { + it('should xor buffers without modifying original', () => { + const data = utf8EncodeToBuffer('hello') + const key = utf8EncodeToBuffer('xor') + + const xored = xorBuffer(data, key) + expect(data.toString()).eq('hello') + expect(key.toString()).eq('xor') + expect(hexEncode(xored)).eq('100a1e6c6f') + }) + + it('should be deterministic', () => { + const data = utf8EncodeToBuffer('hello') + const key = utf8EncodeToBuffer('xor') + + const xored1 = xorBuffer(data, key) + expect(hexEncode(xored1)).eq('100a1e6c6f') + + const xored2 = xorBuffer(data, key) + expect(hexEncode(xored2)).eq('100a1e6c6f') + }) + + it('second call should decode content', () => { + const data = utf8EncodeToBuffer('hello') + const key = utf8EncodeToBuffer('xor') + + const xored1 = xorBuffer(data, key) + expect(hexEncode(xored1)).eq('100a1e6c6f') + + const xored2 = xorBuffer(xored1, key) + expect(utf8Decode(xored2)).eq('hello') + }) +}) + +describe('xorBufferInPlace', () => { + it('should xor buffers by modifying original', () => { + const data = utf8EncodeToBuffer('hello') + const key = utf8EncodeToBuffer('xor') + + xorBufferInPlace(data, key) + expect(hexEncode(data)).eq('100a1e6c6f') + expect(key.toString()).eq('xor') + }) + + it('second call should decode content', () => { + const data = utf8EncodeToBuffer('hello') + const key = utf8EncodeToBuffer('xor') + + xorBufferInPlace(data, key) + expect(hexEncode(data)).eq('100a1e6c6f') + + xorBufferInPlace(data, key) + expect(data.toString()).eq('hello') + }) +}) diff --git a/packages/core/src/utils/crypto/wasm.ts b/packages/core/src/utils/crypto/wasm.ts index 00da9601..9c2b2ed4 100644 --- a/packages/core/src/utils/crypto/wasm.ts +++ b/packages/core/src/utils/crypto/wasm.ts @@ -23,9 +23,11 @@ export interface WasmCryptoProviderOptions { wasmInput?: InitInput } -export class WasmCryptoProvider extends BaseCryptoProvider implements Partial { +export abstract class WasmCryptoProvider extends BaseCryptoProvider implements Partial { readonly wasmInput?: InitInput + abstract randomFill(buf: Uint8Array): void + constructor(params?: WasmCryptoProviderOptions) { super() this.wasmInput = params?.wasmInput diff --git a/packages/core/src/utils/crypto/web.test.ts b/packages/core/src/utils/crypto/web.test.ts index 438a51b2..327e3924 100644 --- a/packages/core/src/utils/crypto/web.test.ts +++ b/packages/core/src/utils/crypto/web.test.ts @@ -4,17 +4,17 @@ import { testCryptoProvider } from './crypto.test-utils.js' import { WebCryptoProvider } from './web.js' describe('WebCryptoProvider', async () => { - let subtle = globalThis.crypto?.subtle + let crypto = globalThis.crypto - if (!subtle && typeof process !== 'undefined') { - subtle = await import('crypto').then((m) => m.subtle) + if (!crypto && typeof process !== 'undefined') { + crypto = await import('crypto').then((m) => m.webcrypto as Crypto) } - if (!subtle) { - console.warn('Skipping WebCryptoProvider tests (no crypto.subtle)') + if (!crypto) { + console.warn('Skipping WebCryptoProvider tests (no webcrypto)') return } - testCryptoProvider(new WebCryptoProvider({ subtle })) + testCryptoProvider(new WebCryptoProvider({ crypto })) }) diff --git a/packages/core/src/utils/crypto/web.ts b/packages/core/src/utils/crypto/web.ts index 8938cec6..e3c1964f 100644 --- a/packages/core/src/utils/crypto/web.ts +++ b/packages/core/src/utils/crypto/web.ts @@ -8,16 +8,16 @@ const ALGO_TO_SUBTLE: Record = { } export class WebCryptoProvider extends WasmCryptoProvider implements ICryptoProvider { - readonly subtle: SubtleCrypto + readonly crypto: Crypto - constructor(params?: WasmCryptoProviderOptions & { subtle?: SubtleCrypto }) { + constructor(params?: WasmCryptoProviderOptions & { crypto?: Crypto }) { super(params) - const subtle = params?.subtle ?? globalThis.crypto?.subtle + const crypto = params?.crypto ?? globalThis.crypto - if (!subtle) { - throw new Error('SubtleCrypto is not available') + if (!crypto || !crypto.subtle) { + throw new Error('WebCrypto is not available') } - this.subtle = subtle + this.crypto = crypto } async pbkdf2( @@ -27,9 +27,9 @@ export class WebCryptoProvider extends WasmCryptoProvider implements ICryptoProv keylen?: number | undefined, algo?: string | undefined, ): Promise { - const keyMaterial = await this.subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits']) + const keyMaterial = await this.crypto.subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits']) - return this.subtle + return this.crypto.subtle .deriveBits( { name: 'PBKDF2', @@ -44,7 +44,7 @@ export class WebCryptoProvider extends WasmCryptoProvider implements ICryptoProv } async hmacSha256(data: Uint8Array, key: Uint8Array): Promise { - const keyMaterial = await this.subtle.importKey( + const keyMaterial = await this.crypto.subtle.importKey( 'raw', key, { name: 'HMAC', hash: { name: 'SHA-256' } }, @@ -52,8 +52,12 @@ export class WebCryptoProvider extends WasmCryptoProvider implements ICryptoProv ['sign'], ) - const res = await this.subtle.sign({ name: 'HMAC' }, keyMaterial, data) + const res = await this.crypto.subtle.sign({ name: 'HMAC' }, keyMaterial, data) return new Uint8Array(res) } + + randomFill(buf: Uint8Array): void { + this.crypto.getRandomValues(buf) + } } diff --git a/packages/core/src/utils/platform/crypto.web.ts b/packages/core/src/utils/platform/crypto.web.ts index 2f27c213..c00dcc42 100644 --- a/packages/core/src/utils/platform/crypto.web.ts +++ b/packages/core/src/utils/platform/crypto.web.ts @@ -7,5 +7,5 @@ export const _defaultCryptoProviderFactory = () => { throw new MtUnsupportedError('WebCrypto API is not available') } - return new WebCryptoProvider({ subtle: crypto.subtle }) + return new WebCryptoProvider({ crypto }) } diff --git a/packages/core/src/utils/platform/random.ts b/packages/core/src/utils/platform/random.ts deleted file mode 100644 index 4f989290..00000000 --- a/packages/core/src/utils/platform/random.ts +++ /dev/null @@ -1,4 +0,0 @@ -// eslint-disable-next-line no-restricted-imports -import { randomBytes } from 'crypto' - -export const _randomBytes = randomBytes as (size: number) => Uint8Array diff --git a/packages/core/src/utils/platform/random.web.ts b/packages/core/src/utils/platform/random.web.ts deleted file mode 100644 index 6e54d974..00000000 --- a/packages/core/src/utils/platform/random.web.ts +++ /dev/null @@ -1,6 +0,0 @@ -export function _randomBytes(size: number): Uint8Array { - const ret = new Uint8Array(size) - crypto.getRandomValues(ret) - - return ret -} diff --git a/packages/mtproxy/fake-tls.ts b/packages/mtproxy/fake-tls.ts index 763f79a6..94fc403b 100644 --- a/packages/mtproxy/fake-tls.ts +++ b/packages/mtproxy/fake-tls.ts @@ -1,13 +1,6 @@ /* eslint-disable no-restricted-globals */ import { IPacketCodec, WrappedCodec } from '@mtcute/core' -import { - bigIntModInv, - bigIntModPow, - bigIntToBuffer, - bufferToBigInt, - ICryptoProvider, - randomBytes, -} from '@mtcute/core/utils.js' +import { bigIntModInv, bigIntModPow, bigIntToBuffer, bufferToBigInt, ICryptoProvider } from '@mtcute/core/utils.js' const MAX_TLS_PACKET_LENGTH = 2878 const TLS_FIRST_PREFIX = Buffer.from('140303000101', 'hex') @@ -151,8 +144,8 @@ function executeTlsOperations(h: TlsOperationHandler): void { // } // } -function initGrease(size: number): Buffer { - const buf = randomBytes(size) +function initGrease(crypto: ICryptoProvider, size: number): Buffer { + const buf = crypto.randomBytes(size) for (let i = 0; i < size; i++) { buf[i] = (buf[i] & 0xf0) + 0x0a @@ -172,10 +165,14 @@ class TlsHelloWriter implements TlsOperationHandler { pos = 0 private _domain: Buffer - private _grease = initGrease(7) + private _grease = initGrease(this.crypto, 7) private _scopes: number[] = [] - constructor(size: number, domain: Buffer) { + constructor( + readonly crypto: ICryptoProvider, + size: number, + domain: Buffer, + ) { this._domain = domain this.buf = Buffer.allocUnsafe(size) } @@ -186,7 +183,7 @@ class TlsHelloWriter implements TlsOperationHandler { } random(size: number) { - this.string(Buffer.from(randomBytes(size))) + this.string(Buffer.from(this.crypto.randomBytes(size))) } zero(size: number) { @@ -204,7 +201,7 @@ class TlsHelloWriter implements TlsOperationHandler { key() { for (;;) { - const key = randomBytes(32) + const key = this.crypto.randomBytes(32) key[31] &= 127 let x = bufferToBigInt(key) @@ -241,7 +238,7 @@ class TlsHelloWriter implements TlsOperationHandler { this.buf.writeUInt16BE(size, begin) } - async finish(secret: Buffer, crypto: ICryptoProvider): Promise { + async finish(secret: Buffer): Promise { const padSize = 515 - this.pos const unixTime = ~~(Date.now() / 1000) @@ -249,7 +246,7 @@ class TlsHelloWriter implements TlsOperationHandler { this.zero(padSize) this.endScope() - const hash = Buffer.from(await crypto.hmacSha256(this.buf, secret)) + const hash = Buffer.from(await this.crypto.hmacSha256(this.buf, secret)) const old = hash.readInt32LE(28) hash.writeInt32LE(old ^ unixTime, 28) @@ -264,10 +261,10 @@ class TlsHelloWriter implements TlsOperationHandler { export async function generateFakeTlsHeader(domain: string, secret: Buffer, crypto: ICryptoProvider): Promise { const domainBuf = Buffer.from(domain) - const writer = new TlsHelloWriter(517, domainBuf) + const writer = new TlsHelloWriter(crypto, 517, domainBuf) executeTlsOperations(writer) - return writer.finish(secret, crypto) + return writer.finish(secret) } /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c4402755..d4fc9764 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,6 +33,9 @@ importers: '@vitest/coverage-v8': specifier: ^0.34.6 version: 0.34.6(vitest@0.34.6) + '@vitest/ui': + specifier: ^0.34.6 + version: 0.34.6(vitest@0.34.6) dotenv-flow: specifier: 3.2.0 version: 3.2.0 @@ -95,7 +98,7 @@ importers: version: 4.5.0(@types/node@18.16.0) vitest: specifier: ^0.34.6 - version: 0.34.6 + version: 0.34.6(@vitest/ui@0.34.6) packages/client: dependencies: @@ -928,6 +931,10 @@ packages: requiresBuild: true optional: true + /@polka/url@1.0.0-next.23: + resolution: {integrity: sha512-C16M+IYz0rgRhWZdCmK+h58JMv8vijAA61gmz2rspCSwKwzBebpdcsiUmwrtJRdphuY30i6BSLEOP8ppbNLyLg==} + dev: true + /@sinclair/typebox@0.27.8: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} dev: true @@ -1177,7 +1184,7 @@ packages: std-env: 3.4.3 test-exclude: 6.0.0 v8-to-istanbul: 9.1.3 - vitest: 0.34.6 + vitest: 0.34.6(@vitest/ui@0.34.6) transitivePeerDependencies: - supports-color dev: true @@ -1212,6 +1219,21 @@ packages: tinyspy: 2.2.0 dev: true + /@vitest/ui@0.34.6(vitest@0.34.6): + resolution: {integrity: sha512-/fxnCwGC0Txmr3tF3BwAbo3v6U2SkBTGR9UB8zo0Ztlx0BTOXHucE0gDHY7SjwEktCOHatiGmli9kZD6gYSoWQ==} + peerDependencies: + vitest: '>=0.30.1 <1' + dependencies: + '@vitest/utils': 0.34.6 + fast-glob: 3.3.1 + fflate: 0.8.1 + flatted: 3.2.9 + pathe: 1.1.1 + picocolors: 1.0.0 + sirv: 2.0.3 + vitest: 0.34.6(@vitest/ui@0.34.6) + dev: true + /@vitest/utils@0.34.6: resolution: {integrity: sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==} dependencies: @@ -2567,6 +2589,10 @@ packages: reusify: 1.0.4 dev: true + /fflate@0.8.1: + resolution: {integrity: sha512-/exOvEuc+/iaUm105QIiOt4LpBdMTWsXxqR0HDF35vx3fmaKzw7354gTilCh5rkzEt8WYyG//ku3h3nRmd7CHQ==} + dev: true + /figures@5.0.0: resolution: {integrity: sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg==} engines: {node: '>=14'} @@ -2621,6 +2647,10 @@ packages: resolution: {integrity: sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==} dev: true + /flatted@3.2.9: + resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} + dev: true + /for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} dependencies: @@ -3820,6 +3850,11 @@ packages: ufo: 1.3.1 dev: true + /mrmime@1.0.1: + resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==} + engines: {node: '>=10'} + dev: true + /ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} @@ -4603,6 +4638,15 @@ packages: simple-concat: 1.0.1 dev: false + /sirv@2.0.3: + resolution: {integrity: sha512-O9jm9BsID1P+0HOi81VpXPoDxYP374pkOLzACAoyUQ/3OUVndNpsz6wMnY2z+yOxzbllCKZrM+9QrWsv4THnyA==} + engines: {node: '>= 10'} + dependencies: + '@polka/url': 1.0.0-next.23 + mrmime: 1.0.1 + totalist: 3.0.1 + dev: true + /slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -4947,6 +4991,11 @@ packages: is-number: 7.0.0 dev: true + /totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + dev: true + /trim-newlines@3.0.1: resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} engines: {node: '>=8'} @@ -5219,7 +5268,7 @@ packages: fsevents: 2.3.3 dev: true - /vitest@0.34.6: + /vitest@0.34.6(@vitest/ui@0.34.6): resolution: {integrity: sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==} engines: {node: '>=v14.18.0'} hasBin: true @@ -5257,6 +5306,7 @@ packages: '@vitest/runner': 0.34.6 '@vitest/snapshot': 0.34.6 '@vitest/spy': 0.34.6 + '@vitest/ui': 0.34.6(vitest@0.34.6) '@vitest/utils': 0.34.6 acorn: 8.10.0 acorn-walk: 8.2.0 diff --git a/vite.config.mts b/vite.config.mts index 4fddc942..d533ada6 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -10,6 +10,6 @@ export default defineConfig({ include: [ 'packages/**/*.test-d.ts', ], - } + }, }, })