chore(core): moved random to crypto provider, added tests for functions relying on rng
This commit is contained in:
parent
ecbcf05589
commit
964f47497c
30 changed files with 587 additions and 299 deletions
|
@ -10,6 +10,7 @@
|
||||||
"postinstall": "node scripts/validate-deps-versions.mjs",
|
"postinstall": "node scripts/validate-deps-versions.mjs",
|
||||||
"test": "vitest run && pnpm run -r test",
|
"test": "vitest run && pnpm run -r test",
|
||||||
"test:dev": "vitest watch",
|
"test:dev": "vitest watch",
|
||||||
|
"test:ui": "vitest --ui",
|
||||||
"test:coverage": "vitest run --coverage",
|
"test:coverage": "vitest run --coverage",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"lint:ci": "NODE_OPTIONS=\"--max_old_space_size=8192\" eslint --config .eslintrc.ci.js .",
|
"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/eslint-plugin": "6.4.0",
|
||||||
"@typescript-eslint/parser": "6.4.0",
|
"@typescript-eslint/parser": "6.4.0",
|
||||||
"@vitest/coverage-v8": "^0.34.6",
|
"@vitest/coverage-v8": "^0.34.6",
|
||||||
|
"@vitest/ui": "^0.34.6",
|
||||||
"dotenv-flow": "3.2.0",
|
"dotenv-flow": "3.2.0",
|
||||||
"dpdm": "^3.14.0",
|
"dpdm": "^3.14.0",
|
||||||
"eslint": "8.47.0",
|
"eslint": "8.47.0",
|
||||||
|
|
|
@ -1,49 +1,166 @@
|
||||||
/* eslint-disable no-restricted-globals */
|
import Long from 'long'
|
||||||
import { describe, expect, it } from 'vitest'
|
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 { LogManager } from '../utils/index.js'
|
||||||
import { AuthKey } from './auth-key.js'
|
import { AuthKey } from './auth-key.js'
|
||||||
|
|
||||||
const authKey = Buffer.alloc(
|
const authKey = new Uint8Array(256)
|
||||||
2048 / 8,
|
|
||||||
Buffer.from('98cb29c6ffa89e79da695a54f572e6cb101e81c688b63a4bf73c3622dec230e0', 'hex'),
|
for (let i = 0; i < 256; i += 32) {
|
||||||
)
|
hexDecode(authKey.subarray(i, i + 32), '98cb29c6ffa89e79da695a54f572e6cb101e81c688b63a4bf73c3622dec230e0')
|
||||||
|
}
|
||||||
|
|
||||||
describe('AuthKey', () => {
|
describe('AuthKey', () => {
|
||||||
const crypto = new NodeCryptoProvider()
|
async function create() {
|
||||||
const logger = new LogManager()
|
const logger = new LogManager()
|
||||||
const readerMap: TlReaderMap = {}
|
const readerMap: TlReaderMap = {}
|
||||||
|
const crypto = await defaultTestCryptoProvider()
|
||||||
|
|
||||||
it('should correctly calculate derivatives', () => {
|
|
||||||
const key = new AuthKey(crypto, logger, readerMap)
|
const key = new AuthKey(crypto, logger, readerMap)
|
||||||
key.setup(authKey)
|
key.setup(authKey)
|
||||||
|
|
||||||
expect(key.key).to.eql(authKey)
|
return key
|
||||||
expect(key.clientSalt).to.eql(
|
}
|
||||||
Buffer.from('f73c3622dec230e098cb29c6ffa89e79da695a54f572e6cb101e81c688b63a4b', 'hex'),
|
|
||||||
)
|
const msgId = Long.fromBits(0xbeef1234, 0x1234beef, true)
|
||||||
expect(key.serverSalt).to.eql(
|
const seqNo = 777
|
||||||
Buffer.from('98cb29c6ffa89e79da695a54f572e6cb101e81c688b63a4bf73c3622dec230e0', 'hex'),
|
const serverSalt = Long.fromBits(0xdeadbeef, 0xbeefdead)
|
||||||
)
|
const sessionId = Long.fromBits(0xfeedbeef, 0xbeeffeed)
|
||||||
expect(key.id).to.eql(Buffer.from('40fa5bb7cb56a895', 'hex'))
|
|
||||||
|
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 encrypt a message', async () => {
|
||||||
// it('should correctly encrypt a message', async () => {
|
const message = writeMessage(utf8EncodeToBuffer('hello, world!!!!'))
|
||||||
// const crypto = new NodeCryptoProvider()
|
|
||||||
// const key = new AuthKey(crypto, logger, readerMap)
|
const key = await create()
|
||||||
// await key.setup(authKey)
|
const msg = key.encryptMessage(message, serverSalt, sessionId)
|
||||||
//
|
|
||||||
// const msg = await key.encryptMessage(message, serverSalt, sessionId)
|
expect(hexEncode(msg)).toEqual(
|
||||||
//
|
'40fa5bb7cb56a895f6f5a88914892aadf87c68031cc953ba29d68e118021f329' +
|
||||||
// expect(msg).to.eql(
|
'be386a620d49f3ad3a50c60dcef3733f214e8cefa3e403c11d193637d4971dc1' +
|
||||||
// Buffer.from(
|
'5db7f74b26fd16cb0e8fee30bf7e3f68858fe82927e2cd06',
|
||||||
// '...',
|
)
|
||||||
// 'hex',
|
})
|
||||||
// ),
|
|
||||||
// )
|
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')
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -5,14 +5,7 @@ import { TlBinaryReader, TlReaderMap } from '@mtcute/tl-runtime'
|
||||||
|
|
||||||
import { MtcuteError } from '../types/errors.js'
|
import { MtcuteError } from '../types/errors.js'
|
||||||
import { createAesIgeForMessage } from '../utils/crypto/mtproto.js'
|
import { createAesIgeForMessage } from '../utils/crypto/mtproto.js'
|
||||||
import {
|
import { buffersEqual, concatBuffers, dataViewFromBuffer, ICryptoProvider, Logger } from '../utils/index.js'
|
||||||
buffersEqual,
|
|
||||||
concatBuffers,
|
|
||||||
dataViewFromBuffer,
|
|
||||||
ICryptoProvider,
|
|
||||||
Logger,
|
|
||||||
randomBytes,
|
|
||||||
} from '../utils/index.js'
|
|
||||||
|
|
||||||
export class AuthKey {
|
export class AuthKey {
|
||||||
ready = false
|
ready = false
|
||||||
|
@ -58,7 +51,7 @@ export class AuthKey {
|
||||||
dv.setInt32(8, sessionId.low, true)
|
dv.setInt32(8, sessionId.low, true)
|
||||||
dv.setInt32(12, sessionId.high, true)
|
dv.setInt32(12, sessionId.high, true)
|
||||||
buf.set(message, 16)
|
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 messageKey = this._crypto.sha256(concatBuffers([this.clientSalt, buf])).subarray(8, 24)
|
||||||
const ige = createAesIgeForMessage(this._crypto, this.key, messageKey, true)
|
const ige = createAesIgeForMessage(this._crypto, this.key, messageKey, true)
|
||||||
|
@ -91,11 +84,7 @@ export class AuthKey {
|
||||||
const expectedMessageKey = msgKeySource.subarray(8, 24)
|
const expectedMessageKey = msgKeySource.subarray(8, 24)
|
||||||
|
|
||||||
if (!buffersEqual(messageKey, expectedMessageKey)) {
|
if (!buffersEqual(messageKey, expectedMessageKey)) {
|
||||||
this.log.warn(
|
this.log.warn('received message with invalid messageKey = %h (expected %h)', messageKey, expectedMessageKey)
|
||||||
'[%h] received message with invalid messageKey = %h (expected %h)',
|
|
||||||
messageKey,
|
|
||||||
expectedMessageKey,
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { TlPublicKey } from '@mtcute/tl/binary/rsa-keys.js'
|
||||||
import { TlBinaryReader, TlBinaryWriter, TlSerializationCounter } from '@mtcute/tl-runtime'
|
import { TlBinaryReader, TlBinaryWriter, TlSerializationCounter } from '@mtcute/tl-runtime'
|
||||||
|
|
||||||
import { MtArgumentError, MtSecurityError, MtTypeAssertionError } from '../types/index.js'
|
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 { findKeyByFingerprints } from '../utils/crypto/keys.js'
|
||||||
import { millerRabin } from '../utils/crypto/miller-rabin.js'
|
import { millerRabin } from '../utils/crypto/miller-rabin.js'
|
||||||
import { generateKeyAndIvFromNonce } from '../utils/crypto/mtproto.js'
|
import { generateKeyAndIvFromNonce } from '../utils/crypto/mtproto.js'
|
||||||
|
@ -32,7 +32,7 @@ interface CheckedPrime {
|
||||||
|
|
||||||
const checkedPrimesCache: 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) {
|
if (KNOWN_DH_PRIME === dhPrime) {
|
||||||
log.debug('server is using known dh prime, skipping validation')
|
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')
|
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')
|
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')
|
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')
|
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 (;;) {
|
for (;;) {
|
||||||
const aesIv = new Uint8Array(32)
|
const aesIv = new Uint8Array(32)
|
||||||
|
|
||||||
const aesKey = randomBytes(32)
|
const aesKey = crypto.randomBytes(32)
|
||||||
|
|
||||||
const dataWithHash = concatBuffers([data, crypto.sha256(concatBuffers([aesKey, data]))])
|
const dataWithHash = concatBuffers([data, crypto.sha256(concatBuffers([aesKey, data]))])
|
||||||
// we only need to reverse the data
|
// we only need to reverse the data
|
||||||
|
@ -165,7 +168,7 @@ function rsaEncrypt(data: Uint8Array, crypto: ICryptoProvider, key: TlPublicKey)
|
||||||
crypto.sha1(data),
|
crypto.sha1(data),
|
||||||
data,
|
data,
|
||||||
// sha1 is always 20 bytes, so we're left with 255 - 20 - x padding
|
// 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(
|
const encryptedBigInt = bigIntModPow(
|
||||||
|
@ -222,7 +225,7 @@ export async function doAuthorization(
|
||||||
|
|
||||||
if (expiresIn) log.prefix = '[PFS] '
|
if (expiresIn) log.prefix = '[PFS] '
|
||||||
|
|
||||||
const nonce = randomBytes(16)
|
const nonce = crypto.randomBytes(16)
|
||||||
// Step 1: PQ request
|
// Step 1: PQ request
|
||||||
log.debug('starting PQ handshake (temp = %b), nonce = %h', expiresIn, nonce)
|
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)
|
const [p, q] = await crypto.factorizePQ(resPq.pq)
|
||||||
log.debug('factorized PQ: PQ = %h, P = %h, Q = %h', resPq.pq, p, q)
|
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
|
let dcId = connection.params.dc.id
|
||||||
if (connection.params.testMode) dcId += 10000
|
if (connection.params.testMode) dcId += 10000
|
||||||
|
@ -330,13 +333,13 @@ export async function doAuthorization(
|
||||||
const g = BigInt(serverDhInner.g)
|
const g = BigInt(serverDhInner.g)
|
||||||
const gA = bufferToBigInt(serverDhInner.gA)
|
const gA = bufferToBigInt(serverDhInner.gA)
|
||||||
|
|
||||||
checkDhPrime(log, dhPrime, serverDhInner.g)
|
checkDhPrime(crypto, log, dhPrime, serverDhInner.g)
|
||||||
|
|
||||||
let retryId = Long.ZERO
|
let retryId = Long.ZERO
|
||||||
const serverSalt = xorBuffer(newNonce.subarray(0, 8), resPq.serverNonce.subarray(0, 8))
|
const serverSalt = xorBuffer(newNonce.subarray(0, 8), resPq.serverNonce.subarray(0, 8))
|
||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
const b = bufferToBigInt(randomBytes(256))
|
const b = bufferToBigInt(crypto.randomBytes(256))
|
||||||
const gB = bigIntModPow(g, b, dhPrime)
|
const gB = bigIntModPow(g, b, dhPrime)
|
||||||
|
|
||||||
const authKey = bigIntToBuffer(bigIntModPow(gA, b, dhPrime))
|
const authKey = bigIntToBuffer(bigIntModPow(gA, b, dhPrime))
|
||||||
|
|
|
@ -14,7 +14,6 @@ import {
|
||||||
EarlyTimer,
|
EarlyTimer,
|
||||||
ICryptoProvider,
|
ICryptoProvider,
|
||||||
longFromBuffer,
|
longFromBuffer,
|
||||||
randomBytes,
|
|
||||||
randomLong,
|
randomLong,
|
||||||
removeFromLongArray,
|
removeFromLongArray,
|
||||||
} from '../utils/index.js'
|
} from '../utils/index.js'
|
||||||
|
@ -348,14 +347,14 @@ export class SessionConnection extends PersistentConnection {
|
||||||
const writer = TlBinaryWriter.alloc(this.params.writerMap, 80)
|
const writer = TlBinaryWriter.alloc(this.params.writerMap, 80)
|
||||||
// = 40 (inner length) + 32 (mtproto header) + 8 (pad 72 so mod 16 = 0)
|
// = 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.long(msgId)
|
||||||
writer.int(0) // seq_no
|
writer.int(0) // seq_no
|
||||||
writer.int(40) // msg_len
|
writer.int(40) // msg_len
|
||||||
writer.object(inner)
|
writer.object(inner)
|
||||||
|
|
||||||
const msgWithoutPadding = writer.result()
|
const msgWithoutPadding = writer.result()
|
||||||
writer.raw(randomBytes(8))
|
writer.raw(this._crypto.randomBytes(8))
|
||||||
const msgWithPadding = writer.result()
|
const msgWithPadding = writer.result()
|
||||||
|
|
||||||
const hash = this._crypto.sha1(msgWithoutPadding)
|
const hash = this._crypto.sha1(msgWithoutPadding)
|
||||||
|
|
|
@ -3,7 +3,8 @@ import { describe, expect, it } from 'vitest'
|
||||||
import { hexDecodeToBuffer, hexEncode } from '@mtcute/tl-runtime'
|
import { hexDecodeToBuffer, hexEncode } from '@mtcute/tl-runtime'
|
||||||
|
|
||||||
import { IntermediatePacketCodec, PaddedIntermediatePacketCodec, TransportError } from '../../index.js'
|
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', () => {
|
describe('IntermediatePacketCodec', () => {
|
||||||
it('should return correct tag', () => {
|
it('should return correct tag', () => {
|
||||||
|
@ -89,23 +90,22 @@ describe('IntermediatePacketCodec', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('PaddedIntermediatePacketCodec', () => {
|
describe('PaddedIntermediatePacketCodec', () => {
|
||||||
it('should return correct tag', () => {
|
useFakeMathRandom()
|
||||||
expect(hexEncode(new PaddedIntermediatePacketCodec().tag())).eq('dddddddd')
|
|
||||||
|
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', () => {
|
it('should correctly frame packets', async () => {
|
||||||
// todo: once we have predictable random, test this properly
|
|
||||||
|
|
||||||
const data = hexDecodeToBuffer('6cfeffff')
|
const data = hexDecodeToBuffer('6cfeffff')
|
||||||
const encoded = new PaddedIntermediatePacketCodec().encode(data)
|
|
||||||
const dv = dataViewFromBuffer(encoded)
|
|
||||||
|
|
||||||
const packetSize = dv.getUint32(0, true)
|
expect(hexEncode((await create()).encode(data))).toEqual('0a0000006cfeffff29afd26df40f')
|
||||||
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])
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -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 { IPacketCodec, TransportError } from './abstract.js'
|
||||||
import { StreamedCodec } from './streamed.js'
|
import { StreamedCodec } from './streamed.js'
|
||||||
|
|
||||||
|
@ -58,16 +58,20 @@ export class PaddedIntermediatePacketCodec extends IntermediatePacketCodec {
|
||||||
return PADDED_TAG
|
return PADDED_TAG
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _crypto!: ICryptoProvider
|
||||||
|
setup?(crypto: ICryptoProvider) {
|
||||||
|
this._crypto = crypto
|
||||||
|
}
|
||||||
|
|
||||||
encode(packet: Uint8Array): Uint8Array {
|
encode(packet: Uint8Array): Uint8Array {
|
||||||
// padding size, 0-15
|
// padding size, 0-15
|
||||||
const padSize = Math.floor(Math.random() * 16)
|
const padSize = getRandomInt(16)
|
||||||
const padding = randomBytes(padSize)
|
|
||||||
|
|
||||||
const ret = new Uint8Array(packet.length + 4 + padSize)
|
const ret = new Uint8Array(packet.length + 4 + padSize)
|
||||||
const dv = dataViewFromBuffer(ret)
|
const dv = dataViewFromBuffer(ret)
|
||||||
dv.setUint32(0, packet.length + padSize, true)
|
dv.setUint32(0, packet.length + padSize, true)
|
||||||
ret.set(packet, 4)
|
ret.set(packet, 4)
|
||||||
ret.set(padding, 4 + packet.length)
|
this._crypto.randomFill(ret.subarray(4 + packet.length))
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { bufferToReversed, concatBuffers, dataViewFromBuffer } from '../../utils/buffer-utils.js'
|
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 { IPacketCodec } from './abstract.js'
|
||||||
import { WrappedCodec } from './wrapped.js'
|
import { WrappedCodec } from './wrapped.js'
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ export class ObfuscatedPacketCodec extends WrappedCodec implements IPacketCodec
|
||||||
let dv: DataView
|
let dv: DataView
|
||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
random = randomBytes(64)
|
random = this._crypto.randomBytes(64)
|
||||||
if (random[0] === 0xef) continue
|
if (random[0] === 0xef) continue
|
||||||
|
|
||||||
dv = dataViewFromBuffer(random)
|
dv = dataViewFromBuffer(random)
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
import { hexDecodeToBuffer } from '@mtcute/tl-runtime'
|
import { hexDecodeToBuffer } from '@mtcute/tl-runtime'
|
||||||
|
|
||||||
|
import { defaultTestCryptoProvider } from './crypto/crypto.test-utils.js'
|
||||||
import {
|
import {
|
||||||
bigIntBitLength,
|
bigIntBitLength,
|
||||||
bigIntGcd,
|
bigIntGcd,
|
||||||
|
@ -85,50 +86,56 @@ describe('bufferToBigInt', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('randomBigInt', () => {
|
describe('randomBigInt', async () => {
|
||||||
|
const c = await defaultTestCryptoProvider()
|
||||||
|
|
||||||
it('should return a random bigint', () => {
|
it('should return a random bigint', () => {
|
||||||
const a = randomBigInt(32)
|
const a = randomBigInt(c, 32)
|
||||||
const b = randomBigInt(32)
|
const b = randomBigInt(c, 32)
|
||||||
|
|
||||||
expect(a).not.toEqual(b)
|
expect(a).not.toEqual(b)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return a random bigint up to specified byte length', () => {
|
it('should return a random bigint up to specified byte length', () => {
|
||||||
const a = randomBigInt(32)
|
const a = randomBigInt(c, 32)
|
||||||
const b = randomBigInt(64)
|
const b = randomBigInt(c, 64)
|
||||||
|
|
||||||
expect(bigIntBitLength(a)).toBeLessThanOrEqual(32 * 8)
|
expect(bigIntBitLength(a)).toBeLessThanOrEqual(32 * 8)
|
||||||
expect(bigIntBitLength(b)).toBeLessThanOrEqual(64 * 8)
|
expect(bigIntBitLength(b)).toBeLessThanOrEqual(64 * 8)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('randomBigIntBits', () => {
|
describe('randomBigIntBits', async () => {
|
||||||
|
const c = await defaultTestCryptoProvider()
|
||||||
|
|
||||||
it('should return a random bigint', () => {
|
it('should return a random bigint', () => {
|
||||||
const a = randomBigIntBits(32)
|
const a = randomBigIntBits(c, 32)
|
||||||
const b = randomBigIntBits(32)
|
const b = randomBigIntBits(c, 32)
|
||||||
|
|
||||||
expect(a).not.toEqual(b)
|
expect(a).not.toEqual(b)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return a random bigint up to specified bit length', () => {
|
it('should return a random bigint up to specified bit length', () => {
|
||||||
const a = randomBigIntBits(32)
|
const a = randomBigIntBits(c, 32)
|
||||||
const b = randomBigIntBits(64)
|
const b = randomBigIntBits(c, 64)
|
||||||
|
|
||||||
expect(bigIntBitLength(a)).toBeLessThanOrEqual(32)
|
expect(bigIntBitLength(a)).toBeLessThanOrEqual(32)
|
||||||
expect(bigIntBitLength(b)).toBeLessThanOrEqual(64)
|
expect(bigIntBitLength(b)).toBeLessThanOrEqual(64)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('randomBigIntInRange', () => {
|
describe('randomBigIntInRange', async () => {
|
||||||
|
const c = await defaultTestCryptoProvider()
|
||||||
|
|
||||||
it('should return a random bigint', () => {
|
it('should return a random bigint', () => {
|
||||||
const a = randomBigIntInRange(10000n)
|
const a = randomBigIntInRange(c, 10000n)
|
||||||
const b = randomBigIntInRange(10000n)
|
const b = randomBigIntInRange(c, 10000n)
|
||||||
|
|
||||||
expect(a).not.toEqual(b)
|
expect(a).not.toEqual(b)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return a bigint within a given range', () => {
|
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).toBeGreaterThanOrEqual(100n)
|
||||||
expect(a).toBeLessThan(200n)
|
expect(a).toBeLessThan(200n)
|
||||||
|
|
|
@ -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
|
* 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
|
* @param size Size in bytes
|
||||||
*/
|
*/
|
||||||
export function randomBigInt(size: number): bigint {
|
export function randomBigInt(crypto: ICryptoProvider, size: number): bigint {
|
||||||
return bufferToBigInt(randomBytes(size))
|
return bufferToBigInt(crypto.randomBytes(size))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a random big integer of the given size (in bits)
|
* Generate a random big integer of the given size (in bits)
|
||||||
* @param bits
|
* @param bits
|
||||||
*/
|
*/
|
||||||
export function randomBigIntBits(bits: number): bigint {
|
export function randomBigIntBits(crypto: ICryptoProvider, bits: number): bigint {
|
||||||
let num = randomBigInt(Math.ceil(bits / 8))
|
let num = randomBigInt(crypto, Math.ceil(bits / 8))
|
||||||
|
|
||||||
const bitLength = bigIntBitLength(num)
|
const bitLength = bigIntBitLength(num)
|
||||||
|
|
||||||
|
@ -112,13 +113,13 @@ export function randomBigIntBits(bits: number): bigint {
|
||||||
* @param max Maximum value (exclusive)
|
* @param max Maximum value (exclusive)
|
||||||
* @param min Minimum value (inclusive)
|
* @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
|
const interval = max - min
|
||||||
if (interval < 0n) throw new Error('expected min < max')
|
if (interval < 0n) throw new Error('expected min < max')
|
||||||
|
|
||||||
const byteSize = bigIntBitLength(interval) / 8
|
const byteSize = bigIntBitLength(interval) / 8
|
||||||
|
|
||||||
let result = randomBigInt(byteSize)
|
let result = randomBigInt(crypto, byteSize)
|
||||||
while (result > interval) result -= interval
|
while (result > interval) result -= interval
|
||||||
|
|
||||||
return min + result
|
return min + result
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
import { hexEncode, utf8Decode, utf8EncodeToBuffer } from '@mtcute/tl-runtime'
|
import { buffersEqual, bufferToReversed, cloneBuffer, concatBuffers } from './buffer-utils.js'
|
||||||
|
|
||||||
import { buffersEqual, bufferToReversed, cloneBuffer, concatBuffers, randomBytes } from './buffer-utils.js'
|
|
||||||
import { xorBuffer, xorBufferInPlace } from './crypto/utils.js'
|
|
||||||
|
|
||||||
describe('buffersEqual', () => {
|
describe('buffersEqual', () => {
|
||||||
it('should return true for equal buffers', () => {
|
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', () => {
|
describe('cloneBuffer', () => {
|
||||||
it('should clone buffer', () => {
|
it('should clone buffer', () => {
|
||||||
const orig = new Uint8Array([1, 2, 3])
|
const orig = new Uint8Array([1, 2, 3])
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
export { _randomBytes as randomBytes } from './platform/random.js'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if two buffers are equal
|
* Check if two buffers are equal
|
||||||
*
|
*
|
||||||
|
|
|
@ -36,11 +36,23 @@ export interface ICryptoProvider {
|
||||||
|
|
||||||
gzip(data: Uint8Array, maxSize: number): Uint8Array | null
|
gzip(data: Uint8Array, maxSize: number): Uint8Array | null
|
||||||
gunzip(data: Uint8Array): Uint8Array
|
gunzip(data: Uint8Array): Uint8Array
|
||||||
|
|
||||||
|
randomFill(buf: Uint8Array): void
|
||||||
|
randomBytes(size: number): Uint8Array
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class BaseCryptoProvider {
|
export abstract class BaseCryptoProvider {
|
||||||
|
abstract randomFill(buf: Uint8Array): void
|
||||||
|
|
||||||
factorizePQ(pq: Uint8Array) {
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 { gzipSync, inflateSync } from 'zlib'
|
||||||
|
|
||||||
import { hexDecodeToBuffer, hexEncode, utf8EncodeToBuffer } from '@mtcute/tl-runtime'
|
import { hexDecodeToBuffer, hexEncode, utf8EncodeToBuffer } from '@mtcute/tl-runtime'
|
||||||
|
|
||||||
|
import { dataViewFromBuffer } from '../buffer-utils.js'
|
||||||
import { ICryptoProvider } from './abstract.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<ICryptoProvider> {
|
||||||
|
const prov = withFakeRandom(defaultCryptoProviderFactory(), source)
|
||||||
|
await prov.initialize?.()
|
||||||
|
|
||||||
|
return prov
|
||||||
|
}
|
||||||
export function testCryptoProvider(c: ICryptoProvider): void {
|
export function testCryptoProvider(c: ICryptoProvider): void {
|
||||||
beforeAll(() => c.initialize?.())
|
beforeAll(() => c.initialize?.())
|
||||||
|
|
||||||
|
@ -98,17 +156,17 @@ export function testCryptoProvider(c: ICryptoProvider): void {
|
||||||
|
|
||||||
it(
|
it(
|
||||||
'should decompose PQ to prime factors P and Q',
|
'should decompose PQ to prime factors P and Q',
|
||||||
() => {
|
async () => {
|
||||||
const testFactorization = (pq: string, p: string, q: string) => {
|
const testFactorization = async (pq: string, p: string, q: string) => {
|
||||||
const [p1, q1] = factorizePQSync(hexDecodeToBuffer(pq))
|
const [p1, q1] = await c.factorizePQ(hexDecodeToBuffer(pq))
|
||||||
expect(hexEncode(p1)).eq(p.toLowerCase())
|
expect(hexEncode(p1)).eq(p.toLowerCase())
|
||||||
expect(hexEncode(q1)).eq(q.toLowerCase())
|
expect(hexEncode(q1)).eq(q.toLowerCase())
|
||||||
}
|
}
|
||||||
|
|
||||||
// from samples at https://core.telegram.org/mtproto/samples-auth_key
|
// from samples at https://core.telegram.org/mtproto/samples-auth_key
|
||||||
testFactorization('17ED48941A08F981', '494C553B', '53911073')
|
await testFactorization('17ED48941A08F981', '494C553B', '53911073')
|
||||||
// random example
|
// 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!)
|
// since PQ factorization relies on RNG, it may take a while (or may not!)
|
||||||
{ timeout: 10000 },
|
{ timeout: 10000 },
|
||||||
|
@ -137,4 +195,24 @@ export function testCryptoProvider(c: ICryptoProvider): void {
|
||||||
// eslint-disable-next-line no-restricted-globals
|
// eslint-disable-next-line no-restricted-globals
|
||||||
expect(Buffer.from(decompressed)).toEqual(Buffer.from(data))
|
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()
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,15 +6,16 @@ import {
|
||||||
bufferToBigInt,
|
bufferToBigInt,
|
||||||
randomBigIntInRange,
|
randomBigIntInRange,
|
||||||
} from '../bigint-utils.js'
|
} from '../bigint-utils.js'
|
||||||
|
import { ICryptoProvider } from './abstract.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factorize `p*q` to `p` and `q` synchronously using Brent-Pollard rho algorithm
|
* Factorize `p*q` to `p` and `q` synchronously using Brent-Pollard rho algorithm
|
||||||
* @param pq
|
* @param pq
|
||||||
*/
|
*/
|
||||||
export function factorizePQSync(pq: Uint8Array): [Uint8Array, Uint8Array] {
|
export function factorizePQSync(crypto: ICryptoProvider, pq: Uint8Array): [Uint8Array, Uint8Array] {
|
||||||
const pq_ = bufferToBigInt(pq)
|
const pq_ = bufferToBigInt(pq)
|
||||||
|
|
||||||
const n = PollardRhoBrent(pq_)
|
const n = PollardRhoBrent(crypto, pq_)
|
||||||
const m = pq_ / n
|
const m = pq_ / n
|
||||||
|
|
||||||
let p
|
let p
|
||||||
|
@ -31,12 +32,12 @@ export function factorizePQSync(pq: Uint8Array): [Uint8Array, Uint8Array] {
|
||||||
return [bigIntToBuffer(p), bigIntToBuffer(q)]
|
return [bigIntToBuffer(p), bigIntToBuffer(q)]
|
||||||
}
|
}
|
||||||
|
|
||||||
function PollardRhoBrent(n: bigint): bigint {
|
function PollardRhoBrent(crypto: ICryptoProvider, n: bigint): bigint {
|
||||||
if (n % 2n === 0n) return 2n
|
if (n % 2n === 0n) return 2n
|
||||||
|
|
||||||
let y = randomBigIntInRange(n - 1n)
|
let y = randomBigIntInRange(crypto, n - 1n)
|
||||||
const c = randomBigIntInRange(n - 1n)
|
const c = randomBigIntInRange(crypto, n - 1n)
|
||||||
const m = randomBigIntInRange(n - 1n)
|
const m = randomBigIntInRange(crypto, n - 1n)
|
||||||
let g = 1n
|
let g = 1n
|
||||||
let r = 1n
|
let r = 1n
|
||||||
let q = 1n
|
let q = 1n
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
import { defaultCryptoProviderFactory } from './index.js'
|
||||||
import { millerRabin } from './miller-rabin.js'
|
import { millerRabin } from './miller-rabin.js'
|
||||||
|
|
||||||
describe(
|
describe(
|
||||||
'miller-rabin test',
|
'miller-rabin test',
|
||||||
function () {
|
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) => {
|
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', () => {
|
it('should correctly label small primes as probable primes', () => {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { bigIntBitLength, bigIntModPow, randomBigIntBits, twoMultiplicity } from '../bigint-utils.js'
|
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
|
// small numbers: 0, 1 are not prime, 2, 3 are prime
|
||||||
if (n < 4n) return n > 1n
|
if (n < 4n) return n > 1n
|
||||||
if (n % 2n === 0n || n < 0n) return false
|
if (n % 2n === 0n || n < 0n) return false
|
||||||
|
@ -15,7 +16,7 @@ export function millerRabin(n: bigint, rounds = 20): boolean {
|
||||||
let base
|
let base
|
||||||
|
|
||||||
do {
|
do {
|
||||||
base = randomBigIntBits(nBits)
|
base = randomBigIntBits(crypto, nBits)
|
||||||
} while (base <= 1n || base >= nSub)
|
} while (base <= 1n || base >= nSub)
|
||||||
|
|
||||||
let x = bigIntModPow(base, d, n)
|
let x = bigIntModPow(base, d, n)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// eslint-disable-next-line no-restricted-imports
|
// 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 { deflateSync, gunzipSync } from 'zlib'
|
||||||
|
|
||||||
import { ige256Decrypt, ige256Encrypt, initAsync, InitInput } from '@mtcute/wasm'
|
import { ige256Decrypt, ige256Encrypt, initAsync, InitInput } from '@mtcute/wasm'
|
||||||
|
@ -65,6 +65,10 @@ export abstract class BaseNodeCryptoProvider extends BaseCryptoProvider {
|
||||||
gunzip(data: Uint8Array): Uint8Array {
|
gunzip(data: Uint8Array): Uint8Array {
|
||||||
return gunzipSync(data)
|
return gunzipSync(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
randomFill(buf: Uint8Array) {
|
||||||
|
randomFillSync(buf)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NodeCryptoProvider extends BaseNodeCryptoProvider implements ICryptoProvider {
|
export class NodeCryptoProvider extends BaseNodeCryptoProvider implements ICryptoProvider {
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
|
import Long from 'long'
|
||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
import { tl } from '@mtcute/tl'
|
import { tl } from '@mtcute/tl'
|
||||||
import { hexDecodeToBuffer, hexEncode, utf8EncodeToBuffer } from '@mtcute/tl-runtime'
|
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
|
// a real-world request from an account with "qwe123" password
|
||||||
const fakeAlgo: tl.RawPasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow = {
|
const fakeAlgo: tl.RawPasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow = {
|
||||||
|
@ -22,55 +24,86 @@ const fakeAlgo: tl.RawPasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA25
|
||||||
'0d8115f635b105ee2e4e15d04b2454bf6f4fadf034b10403119cd8e3b92fcc5b',
|
'0d8115f635b105ee2e4e15d04b2454bf6f4fadf034b10403119cd8e3b92fcc5b',
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
// const fakeRequest: tl.account.RawPassword = {
|
const fakeRequest: tl.account.RawPassword = {
|
||||||
// _: 'account.password',
|
_: 'account.password',
|
||||||
// hasRecovery: false,
|
hasRecovery: false,
|
||||||
// hasSecureValues: false,
|
hasSecureValues: false,
|
||||||
// hasPassword: true,
|
hasPassword: true,
|
||||||
// currentAlgo: fakeAlgo,
|
currentAlgo: fakeAlgo,
|
||||||
// srpB: hexDecodeToBuffer(
|
srpB: hexDecodeToBuffer(
|
||||||
// '1476a7b5991d7f028bbee33b3455cad3f2cd0eb3737409fcce92fa7d4cd5c733' +
|
'1476a7b5991d7f028bbee33b3455cad3f2cd0eb3737409fcce92fa7d4cd5c733' +
|
||||||
// 'ec6d2cb3454e587d4c17eda2fd7ef9a57327215f38292cc8bd5dc77d3e1d31cd' +
|
'ec6d2cb3454e587d4c17eda2fd7ef9a57327215f38292cc8bd5dc77d3e1d31cd' +
|
||||||
// 'dae2652f8347c4b0093f7c78242f70e6cc13137ee7acc257a49855a63113db8f' +
|
'dae2652f8347c4b0093f7c78242f70e6cc13137ee7acc257a49855a63113db8f' +
|
||||||
// '163992b9101551f3b6f7eb5d196cee3647c359553b1bcbe82ba8933c0fb1ac35' +
|
'163992b9101551f3b6f7eb5d196cee3647c359553b1bcbe82ba8933c0fb1ac35' +
|
||||||
// '0243c535b8e634613e1f626ba8a6d141ef957c859e71a117b557c0298bfbb107' +
|
'0243c535b8e634613e1f626ba8a6d141ef957c859e71a117b557c0298bfbb107' +
|
||||||
// 'c91f71f5b4275fded58289aa1e87c612f44b7aa0b5e0de7def4458f58db80019' +
|
'c91f71f5b4275fded58289aa1e87c612f44b7aa0b5e0de7def4458f58db80019' +
|
||||||
// 'd2e7b181eb66dc270374af2d160dd0c53edd677b2701694d71ea8718c49df6a9' +
|
'd2e7b181eb66dc270374af2d160dd0c53edd677b2701694d71ea8718c49df6a9' +
|
||||||
// 'dbe2cbae051ffc1986336cd26f11a8ab426dfe0813d7b3f4eedf4e34182ccc3a',
|
'dbe2cbae051ffc1986336cd26f11a8ab426dfe0813d7b3f4eedf4e34182ccc3a',
|
||||||
// ),
|
),
|
||||||
// srpId: Long.fromBits(-2046015018, 875006452),
|
srpId: Long.fromBits(-2046015018, 875006452),
|
||||||
// newAlgo: {
|
newAlgo: {
|
||||||
// _: 'passwordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow',
|
_: 'passwordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow',
|
||||||
// salt1: hexDecodeToBuffer('9b3accc457c0d528'),
|
salt1: hexDecodeToBuffer('9b3accc457c0d528'),
|
||||||
// salt2: hexDecodeToBuffer('6c619bb0786dc4ed1bf211d23f6e4065'),
|
salt2: hexDecodeToBuffer('6c619bb0786dc4ed1bf211d23f6e4065'),
|
||||||
// g: 3,
|
g: 3,
|
||||||
// p: hexDecodeToBuffer(
|
p: hexDecodeToBuffer(
|
||||||
// 'c71caeb9c6b1c9048e6c522f70f13f73980d40238e3e21c14934d037563d930f' +
|
'c71caeb9c6b1c9048e6c522f70f13f73980d40238e3e21c14934d037563d930f' +
|
||||||
// '48198a0aa7c14058229493d22530f4dbfa336f6e0ac925139543aed44cce7c37' +
|
'48198a0aa7c14058229493d22530f4dbfa336f6e0ac925139543aed44cce7c37' +
|
||||||
// '20fd51f69458705ac68cd4fe6b6b13abdc9746512969328454f18faf8c595f64' +
|
'20fd51f69458705ac68cd4fe6b6b13abdc9746512969328454f18faf8c595f64' +
|
||||||
// '2477fe96bb2a941d5bcd1d4ac8cc49880708fa9b378e3c4f3a9060bee67cf9a4' +
|
'2477fe96bb2a941d5bcd1d4ac8cc49880708fa9b378e3c4f3a9060bee67cf9a4' +
|
||||||
// 'a4a695811051907e162753b56b0f6b410dba74d8a84b2a14b3144e0ef1284754' +
|
'a4a695811051907e162753b56b0f6b410dba74d8a84b2a14b3144e0ef1284754' +
|
||||||
// 'fd17ed950d5965b4b9dd46582db1178d169c6bc465b0d6ff9ca3928fef5b9ae4' +
|
'fd17ed950d5965b4b9dd46582db1178d169c6bc465b0d6ff9ca3928fef5b9ae4' +
|
||||||
// 'e418fc15e83ebea0f87fa9ff5eed70050ded2849f47bf959d956850ce929851f' +
|
'e418fc15e83ebea0f87fa9ff5eed70050ded2849f47bf959d956850ce929851f' +
|
||||||
// '0d8115f635b105ee2e4e15d04b2454bf6f4fadf034b10403119cd8e3b92fcc5b',
|
'0d8115f635b105ee2e4e15d04b2454bf6f4fadf034b10403119cd8e3b92fcc5b',
|
||||||
// ),
|
),
|
||||||
// },
|
},
|
||||||
// newSecureAlgo: {
|
newSecureAlgo: {
|
||||||
// _: 'securePasswordKdfAlgoPBKDF2HMACSHA512iter100000',
|
_: 'securePasswordKdfAlgoPBKDF2HMACSHA512iter100000',
|
||||||
// salt: hexDecodeToBuffer('fdd59abc0bffb24d'),
|
salt: hexDecodeToBuffer('fdd59abc0bffb24d'),
|
||||||
// },
|
},
|
||||||
// secureRandom: new Uint8Array(), // unused
|
secureRandom: new Uint8Array(), // unused
|
||||||
// }
|
}
|
||||||
const password = utf8EncodeToBuffer('qwe123')
|
const password = 'qwe123'
|
||||||
|
|
||||||
describe('computePasswordHash', () => {
|
|
||||||
const crypto = defaultCryptoProviderFactory()
|
|
||||||
|
|
||||||
|
describe('SRP', () => {
|
||||||
it('should correctly compute password hash as defined by MTProto', async () => {
|
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')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { utf8EncodeToBuffer } from '@mtcute/tl-runtime'
|
||||||
|
|
||||||
import { MtSecurityError, MtUnsupportedError } from '../../types/errors.js'
|
import { MtSecurityError, MtUnsupportedError } from '../../types/errors.js'
|
||||||
import { bigIntModPow, bigIntToBuffer, bufferToBigInt } from '../bigint-utils.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 { ICryptoProvider } from './abstract.js'
|
||||||
import { xorBuffer } from './utils.js'
|
import { xorBuffer } from './utils.js'
|
||||||
|
|
||||||
|
@ -41,7 +41,10 @@ export async function computeNewPasswordHash(
|
||||||
algo: tl.RawPasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow,
|
algo: tl.RawPasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow,
|
||||||
password: string,
|
password: string,
|
||||||
): Promise<Uint8Array> {
|
): Promise<Uint8Array> {
|
||||||
(algo as tl.Mutable<typeof algo>).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<typeof algo>).salt1 = salt1
|
||||||
|
|
||||||
const _x = await computePasswordHash(crypto, utf8EncodeToBuffer(password), algo.salt1, algo.salt2)
|
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 p = bufferToBigInt(algo.p)
|
||||||
const gB = bufferToBigInt(request.srpB)
|
const gB = bufferToBigInt(request.srpB)
|
||||||
|
|
||||||
const a = bufferToBigInt(randomBytes(256))
|
const a = bufferToBigInt(crypto.randomBytes(256))
|
||||||
const gA = bigIntModPow(g, a, p)
|
const gA = bigIntModPow(g, a, p)
|
||||||
const _gA = bigIntToBuffer(gA, 256)
|
const _gA = bigIntToBuffer(gA, 256)
|
||||||
|
|
||||||
|
|
61
packages/core/src/utils/crypto/utils.test.ts
Normal file
61
packages/core/src/utils/crypto/utils.test.ts
Normal file
|
@ -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')
|
||||||
|
})
|
||||||
|
})
|
|
@ -23,9 +23,11 @@ export interface WasmCryptoProviderOptions {
|
||||||
wasmInput?: InitInput
|
wasmInput?: InitInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WasmCryptoProvider extends BaseCryptoProvider implements Partial<ICryptoProvider> {
|
export abstract class WasmCryptoProvider extends BaseCryptoProvider implements Partial<ICryptoProvider> {
|
||||||
readonly wasmInput?: InitInput
|
readonly wasmInput?: InitInput
|
||||||
|
|
||||||
|
abstract randomFill(buf: Uint8Array): void
|
||||||
|
|
||||||
constructor(params?: WasmCryptoProviderOptions) {
|
constructor(params?: WasmCryptoProviderOptions) {
|
||||||
super()
|
super()
|
||||||
this.wasmInput = params?.wasmInput
|
this.wasmInput = params?.wasmInput
|
||||||
|
|
|
@ -4,17 +4,17 @@ import { testCryptoProvider } from './crypto.test-utils.js'
|
||||||
import { WebCryptoProvider } from './web.js'
|
import { WebCryptoProvider } from './web.js'
|
||||||
|
|
||||||
describe('WebCryptoProvider', async () => {
|
describe('WebCryptoProvider', async () => {
|
||||||
let subtle = globalThis.crypto?.subtle
|
let crypto = globalThis.crypto
|
||||||
|
|
||||||
if (!subtle && typeof process !== 'undefined') {
|
if (!crypto && typeof process !== 'undefined') {
|
||||||
subtle = await import('crypto').then((m) => m.subtle)
|
crypto = await import('crypto').then((m) => m.webcrypto as Crypto)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!subtle) {
|
if (!crypto) {
|
||||||
console.warn('Skipping WebCryptoProvider tests (no crypto.subtle)')
|
console.warn('Skipping WebCryptoProvider tests (no webcrypto)')
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
testCryptoProvider(new WebCryptoProvider({ subtle }))
|
testCryptoProvider(new WebCryptoProvider({ crypto }))
|
||||||
})
|
})
|
||||||
|
|
|
@ -8,16 +8,16 @@ const ALGO_TO_SUBTLE: Record<string, string> = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WebCryptoProvider extends WasmCryptoProvider implements ICryptoProvider {
|
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)
|
super(params)
|
||||||
const subtle = params?.subtle ?? globalThis.crypto?.subtle
|
const crypto = params?.crypto ?? globalThis.crypto
|
||||||
|
|
||||||
if (!subtle) {
|
if (!crypto || !crypto.subtle) {
|
||||||
throw new Error('SubtleCrypto is not available')
|
throw new Error('WebCrypto is not available')
|
||||||
}
|
}
|
||||||
this.subtle = subtle
|
this.crypto = crypto
|
||||||
}
|
}
|
||||||
|
|
||||||
async pbkdf2(
|
async pbkdf2(
|
||||||
|
@ -27,9 +27,9 @@ export class WebCryptoProvider extends WasmCryptoProvider implements ICryptoProv
|
||||||
keylen?: number | undefined,
|
keylen?: number | undefined,
|
||||||
algo?: string | undefined,
|
algo?: string | undefined,
|
||||||
): Promise<Uint8Array> {
|
): Promise<Uint8Array> {
|
||||||
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(
|
.deriveBits(
|
||||||
{
|
{
|
||||||
name: 'PBKDF2',
|
name: 'PBKDF2',
|
||||||
|
@ -44,7 +44,7 @@ export class WebCryptoProvider extends WasmCryptoProvider implements ICryptoProv
|
||||||
}
|
}
|
||||||
|
|
||||||
async hmacSha256(data: Uint8Array, key: Uint8Array): Promise<Uint8Array> {
|
async hmacSha256(data: Uint8Array, key: Uint8Array): Promise<Uint8Array> {
|
||||||
const keyMaterial = await this.subtle.importKey(
|
const keyMaterial = await this.crypto.subtle.importKey(
|
||||||
'raw',
|
'raw',
|
||||||
key,
|
key,
|
||||||
{ name: 'HMAC', hash: { name: 'SHA-256' } },
|
{ name: 'HMAC', hash: { name: 'SHA-256' } },
|
||||||
|
@ -52,8 +52,12 @@ export class WebCryptoProvider extends WasmCryptoProvider implements ICryptoProv
|
||||||
['sign'],
|
['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)
|
return new Uint8Array(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
randomFill(buf: Uint8Array): void {
|
||||||
|
this.crypto.getRandomValues(buf)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,5 +7,5 @@ export const _defaultCryptoProviderFactory = () => {
|
||||||
throw new MtUnsupportedError('WebCrypto API is not available')
|
throw new MtUnsupportedError('WebCrypto API is not available')
|
||||||
}
|
}
|
||||||
|
|
||||||
return new WebCryptoProvider({ subtle: crypto.subtle })
|
return new WebCryptoProvider({ crypto })
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
// eslint-disable-next-line no-restricted-imports
|
|
||||||
import { randomBytes } from 'crypto'
|
|
||||||
|
|
||||||
export const _randomBytes = randomBytes as (size: number) => Uint8Array
|
|
|
@ -1,6 +0,0 @@
|
||||||
export function _randomBytes(size: number): Uint8Array {
|
|
||||||
const ret = new Uint8Array(size)
|
|
||||||
crypto.getRandomValues(ret)
|
|
||||||
|
|
||||||
return ret
|
|
||||||
}
|
|
|
@ -1,13 +1,6 @@
|
||||||
/* eslint-disable no-restricted-globals */
|
/* eslint-disable no-restricted-globals */
|
||||||
import { IPacketCodec, WrappedCodec } from '@mtcute/core'
|
import { IPacketCodec, WrappedCodec } from '@mtcute/core'
|
||||||
import {
|
import { bigIntModInv, bigIntModPow, bigIntToBuffer, bufferToBigInt, ICryptoProvider } from '@mtcute/core/utils.js'
|
||||||
bigIntModInv,
|
|
||||||
bigIntModPow,
|
|
||||||
bigIntToBuffer,
|
|
||||||
bufferToBigInt,
|
|
||||||
ICryptoProvider,
|
|
||||||
randomBytes,
|
|
||||||
} from '@mtcute/core/utils.js'
|
|
||||||
|
|
||||||
const MAX_TLS_PACKET_LENGTH = 2878
|
const MAX_TLS_PACKET_LENGTH = 2878
|
||||||
const TLS_FIRST_PREFIX = Buffer.from('140303000101', 'hex')
|
const TLS_FIRST_PREFIX = Buffer.from('140303000101', 'hex')
|
||||||
|
@ -151,8 +144,8 @@ function executeTlsOperations(h: TlsOperationHandler): void {
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
function initGrease(size: number): Buffer {
|
function initGrease(crypto: ICryptoProvider, size: number): Buffer {
|
||||||
const buf = randomBytes(size)
|
const buf = crypto.randomBytes(size)
|
||||||
|
|
||||||
for (let i = 0; i < size; i++) {
|
for (let i = 0; i < size; i++) {
|
||||||
buf[i] = (buf[i] & 0xf0) + 0x0a
|
buf[i] = (buf[i] & 0xf0) + 0x0a
|
||||||
|
@ -172,10 +165,14 @@ class TlsHelloWriter implements TlsOperationHandler {
|
||||||
pos = 0
|
pos = 0
|
||||||
|
|
||||||
private _domain: Buffer
|
private _domain: Buffer
|
||||||
private _grease = initGrease(7)
|
private _grease = initGrease(this.crypto, 7)
|
||||||
private _scopes: number[] = []
|
private _scopes: number[] = []
|
||||||
|
|
||||||
constructor(size: number, domain: Buffer) {
|
constructor(
|
||||||
|
readonly crypto: ICryptoProvider,
|
||||||
|
size: number,
|
||||||
|
domain: Buffer,
|
||||||
|
) {
|
||||||
this._domain = domain
|
this._domain = domain
|
||||||
this.buf = Buffer.allocUnsafe(size)
|
this.buf = Buffer.allocUnsafe(size)
|
||||||
}
|
}
|
||||||
|
@ -186,7 +183,7 @@ class TlsHelloWriter implements TlsOperationHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
random(size: number) {
|
random(size: number) {
|
||||||
this.string(Buffer.from(randomBytes(size)))
|
this.string(Buffer.from(this.crypto.randomBytes(size)))
|
||||||
}
|
}
|
||||||
|
|
||||||
zero(size: number) {
|
zero(size: number) {
|
||||||
|
@ -204,7 +201,7 @@ class TlsHelloWriter implements TlsOperationHandler {
|
||||||
|
|
||||||
key() {
|
key() {
|
||||||
for (;;) {
|
for (;;) {
|
||||||
const key = randomBytes(32)
|
const key = this.crypto.randomBytes(32)
|
||||||
key[31] &= 127
|
key[31] &= 127
|
||||||
|
|
||||||
let x = bufferToBigInt(key)
|
let x = bufferToBigInt(key)
|
||||||
|
@ -241,7 +238,7 @@ class TlsHelloWriter implements TlsOperationHandler {
|
||||||
this.buf.writeUInt16BE(size, begin)
|
this.buf.writeUInt16BE(size, begin)
|
||||||
}
|
}
|
||||||
|
|
||||||
async finish(secret: Buffer, crypto: ICryptoProvider): Promise<Buffer> {
|
async finish(secret: Buffer): Promise<Buffer> {
|
||||||
const padSize = 515 - this.pos
|
const padSize = 515 - this.pos
|
||||||
const unixTime = ~~(Date.now() / 1000)
|
const unixTime = ~~(Date.now() / 1000)
|
||||||
|
|
||||||
|
@ -249,7 +246,7 @@ class TlsHelloWriter implements TlsOperationHandler {
|
||||||
this.zero(padSize)
|
this.zero(padSize)
|
||||||
this.endScope()
|
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)
|
const old = hash.readInt32LE(28)
|
||||||
hash.writeInt32LE(old ^ unixTime, 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<Buffer> {
|
export async function generateFakeTlsHeader(domain: string, secret: Buffer, crypto: ICryptoProvider): Promise<Buffer> {
|
||||||
const domainBuf = Buffer.from(domain)
|
const domainBuf = Buffer.from(domain)
|
||||||
|
|
||||||
const writer = new TlsHelloWriter(517, domainBuf)
|
const writer = new TlsHelloWriter(crypto, 517, domainBuf)
|
||||||
executeTlsOperations(writer)
|
executeTlsOperations(writer)
|
||||||
|
|
||||||
return writer.finish(secret, crypto)
|
return writer.finish(secret)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -33,6 +33,9 @@ importers:
|
||||||
'@vitest/coverage-v8':
|
'@vitest/coverage-v8':
|
||||||
specifier: ^0.34.6
|
specifier: ^0.34.6
|
||||||
version: 0.34.6(vitest@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:
|
dotenv-flow:
|
||||||
specifier: 3.2.0
|
specifier: 3.2.0
|
||||||
version: 3.2.0
|
version: 3.2.0
|
||||||
|
@ -95,7 +98,7 @@ importers:
|
||||||
version: 4.5.0(@types/node@18.16.0)
|
version: 4.5.0(@types/node@18.16.0)
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^0.34.6
|
specifier: ^0.34.6
|
||||||
version: 0.34.6
|
version: 0.34.6(@vitest/ui@0.34.6)
|
||||||
|
|
||||||
packages/client:
|
packages/client:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -928,6 +931,10 @@ packages:
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/@polka/url@1.0.0-next.23:
|
||||||
|
resolution: {integrity: sha512-C16M+IYz0rgRhWZdCmK+h58JMv8vijAA61gmz2rspCSwKwzBebpdcsiUmwrtJRdphuY30i6BSLEOP8ppbNLyLg==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@sinclair/typebox@0.27.8:
|
/@sinclair/typebox@0.27.8:
|
||||||
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
|
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -1177,7 +1184,7 @@ packages:
|
||||||
std-env: 3.4.3
|
std-env: 3.4.3
|
||||||
test-exclude: 6.0.0
|
test-exclude: 6.0.0
|
||||||
v8-to-istanbul: 9.1.3
|
v8-to-istanbul: 9.1.3
|
||||||
vitest: 0.34.6
|
vitest: 0.34.6(@vitest/ui@0.34.6)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -1212,6 +1219,21 @@ packages:
|
||||||
tinyspy: 2.2.0
|
tinyspy: 2.2.0
|
||||||
dev: true
|
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:
|
/@vitest/utils@0.34.6:
|
||||||
resolution: {integrity: sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==}
|
resolution: {integrity: sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -2567,6 +2589,10 @@ packages:
|
||||||
reusify: 1.0.4
|
reusify: 1.0.4
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/fflate@0.8.1:
|
||||||
|
resolution: {integrity: sha512-/exOvEuc+/iaUm105QIiOt4LpBdMTWsXxqR0HDF35vx3fmaKzw7354gTilCh5rkzEt8WYyG//ku3h3nRmd7CHQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/figures@5.0.0:
|
/figures@5.0.0:
|
||||||
resolution: {integrity: sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg==}
|
resolution: {integrity: sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
@ -2621,6 +2647,10 @@ packages:
|
||||||
resolution: {integrity: sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==}
|
resolution: {integrity: sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/flatted@3.2.9:
|
||||||
|
resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/for-each@0.3.3:
|
/for-each@0.3.3:
|
||||||
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
|
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -3820,6 +3850,11 @@ packages:
|
||||||
ufo: 1.3.1
|
ufo: 1.3.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/mrmime@1.0.1:
|
||||||
|
resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/ms@2.1.2:
|
/ms@2.1.2:
|
||||||
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
|
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
|
||||||
|
|
||||||
|
@ -4603,6 +4638,15 @@ packages:
|
||||||
simple-concat: 1.0.1
|
simple-concat: 1.0.1
|
||||||
dev: false
|
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:
|
/slash@3.0.0:
|
||||||
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
@ -4947,6 +4991,11 @@ packages:
|
||||||
is-number: 7.0.0
|
is-number: 7.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/totalist@3.0.1:
|
||||||
|
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/trim-newlines@3.0.1:
|
/trim-newlines@3.0.1:
|
||||||
resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==}
|
resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
@ -5219,7 +5268,7 @@ packages:
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/vitest@0.34.6:
|
/vitest@0.34.6(@vitest/ui@0.34.6):
|
||||||
resolution: {integrity: sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==}
|
resolution: {integrity: sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==}
|
||||||
engines: {node: '>=v14.18.0'}
|
engines: {node: '>=v14.18.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
@ -5257,6 +5306,7 @@ packages:
|
||||||
'@vitest/runner': 0.34.6
|
'@vitest/runner': 0.34.6
|
||||||
'@vitest/snapshot': 0.34.6
|
'@vitest/snapshot': 0.34.6
|
||||||
'@vitest/spy': 0.34.6
|
'@vitest/spy': 0.34.6
|
||||||
|
'@vitest/ui': 0.34.6(vitest@0.34.6)
|
||||||
'@vitest/utils': 0.34.6
|
'@vitest/utils': 0.34.6
|
||||||
acorn: 8.10.0
|
acorn: 8.10.0
|
||||||
acorn-walk: 8.2.0
|
acorn-walk: 8.2.0
|
||||||
|
|
|
@ -10,6 +10,6 @@ export default defineConfig({
|
||||||
include: [
|
include: [
|
||||||
'packages/**/*.test-d.ts',
|
'packages/**/*.test-d.ts',
|
||||||
],
|
],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue