chore(core): moved random to crypto provider, added tests for functions relying on rng

This commit is contained in:
alina 🌸 2023-11-12 00:36:00 +03:00
parent ecbcf05589
commit 964f47497c
Signed by: teidesu
SSH key fingerprint: SHA256:uNeCpw6aTSU4aIObXLvHfLkDa82HWH9EiOj9AXOIRpI
30 changed files with 587 additions and 299 deletions

View file

@ -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",

View file

@ -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')
})
})
})

View file

@ -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
}

View file

@ -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))

View file

@ -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)

View file

@ -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')
})
})

View file

@ -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
}

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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])

View file

@ -1,5 +1,3 @@
export { _randomBytes as randomBytes } from './platform/random.js'
/**
* Check if two buffers are equal
*

View file

@ -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
}
}

View file

@ -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<ICryptoProvider> {
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()
})
})
}

View file

@ -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

View file

@ -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', () => {

View file

@ -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)

View file

@ -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 {

View file

@ -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')
})
})

View file

@ -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<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)
@ -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)

View 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')
})
})

View file

@ -23,9 +23,11 @@ export interface WasmCryptoProviderOptions {
wasmInput?: InitInput
}
export class WasmCryptoProvider extends BaseCryptoProvider implements Partial<ICryptoProvider> {
export abstract class WasmCryptoProvider extends BaseCryptoProvider implements Partial<ICryptoProvider> {
readonly wasmInput?: InitInput
abstract randomFill(buf: Uint8Array): void
constructor(params?: WasmCryptoProviderOptions) {
super()
this.wasmInput = params?.wasmInput

View file

@ -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 }))
})

View file

@ -8,16 +8,16 @@ const ALGO_TO_SUBTLE: Record<string, string> = {
}
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<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(
{
name: 'PBKDF2',
@ -44,7 +44,7 @@ export class WebCryptoProvider extends WasmCryptoProvider implements ICryptoProv
}
async hmacSha256(data: Uint8Array, key: Uint8Array): Promise<Uint8Array> {
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)
}
}

View file

@ -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 })
}

View file

@ -1,4 +0,0 @@
// eslint-disable-next-line no-restricted-imports
import { randomBytes } from 'crypto'
export const _randomBytes = randomBytes as (size: number) => Uint8Array

View file

@ -1,6 +0,0 @@
export function _randomBytes(size: number): Uint8Array {
const ret = new Uint8Array(size)
crypto.getRandomValues(ret)
return ret
}

View file

@ -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<Buffer> {
async finish(secret: Buffer): Promise<Buffer> {
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<Buffer> {
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)
}
/**

View file

@ -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

View file

@ -10,6 +10,6 @@ export default defineConfig({
include: [
'packages/**/*.test-d.ts',
],
}
},
},
})