feat(convert): tdata conversion

This commit is contained in:
alina 🌸 2024-11-28 17:55:47 +03:00
parent a711ebaa1d
commit 0d97b10ff5
Signed by: teidesu
SSH key fingerprint: SHA256:uNeCpw6aTSU4aIObXLvHfLkDa82HWH9EiOj9AXOIRpI
21 changed files with 6390 additions and 3 deletions

View file

@ -10,10 +10,12 @@
"exports": "./src/index.ts",
"dependencies": {
"@mtcute/core": "workspace:^",
"@fuman/utils": "https://pkg.pr.new/teidesu/fuman/@fuman/utils@6017eb4",
"@fuman/net": "https://pkg.pr.new/teidesu/fuman/@fuman/net@6017eb4"
"@fuman/utils": "https://pkg.pr.new/teidesu/fuman/@fuman/utils@b0c74cb",
"@fuman/net": "https://pkg.pr.new/teidesu/fuman/@fuman/net@b0c74cb",
"@fuman/io": "https://pkg.pr.new/teidesu/fuman/@fuman/io@b0c74cb"
},
"devDependencies": {
"@mtcute/test": "workspace:^"
"@mtcute/test": "workspace:^",
"@mtcute/node": "workspace:^"
}
}

View file

@ -3,3 +3,4 @@ export * from './gramjs/index.js'
export * from './mtkruto/index.js'
export * from './pyrogram/index.js'
export * from './telethon/index.js'
export * from './tdesktop/index.js'

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,73 @@
import { type StringSessionData, readStringSession } from '@mtcute/core/utils.js'
import type { MaybeArray } from '@fuman/utils'
import { Long } from '@mtcute/core'
import { DC_MAPPING_PROD } from '../dcs.js'
import type { TdataOptions } from './tdata.js'
import { Tdata } from './tdata.js'
import type { InputTdKeyData } from './types.js'
export async function convertFromTdata(tdata: Tdata | TdataOptions, accountIdx = 0): Promise<StringSessionData> {
if (!(tdata instanceof Tdata)) {
tdata = await Tdata.open(tdata)
}
const auth = await tdata.readMtpAuthorization(accountIdx)
const authKey = auth.authKeys.find(it => it.dcId === auth.mainDcId)
if (!authKey) throw new Error('Failed to find auth key')
return {
version: 3,
primaryDcs: DC_MAPPING_PROD[auth.mainDcId],
authKey: authKey.key,
self: {
userId: auth.userId.toNumber(),
isBot: false,
isPremium: false,
usernames: [],
},
}
}
export async function convertToTdata(
sessions: MaybeArray<StringSessionData | string>,
tdata: Tdata | TdataOptions,
): Promise<void> {
if (!Array.isArray(sessions)) {
sessions = [sessions]
}
if (!(tdata instanceof Tdata)) {
const keyData: InputTdKeyData = {
count: sessions.length,
order: Array.from({ length: sessions.length }, (_, i) => i),
active: 0,
}
tdata = await Tdata.create({
keyData,
...tdata,
})
}
for (let i = 0; i < sessions.length; i++) {
let session = sessions[i]
if (typeof session === 'string') {
session = readStringSession(session)
}
await tdata.writeMtpAuthorization({
userId: Long.fromNumber(session.self?.userId ?? 0),
mainDcId: session.primaryDcs.main.id,
authKeys: [
{
dcId: session.primaryDcs.main.id,
key: session.authKey,
},
],
authKeysToDestroy: [],
}, i)
await tdata.writeEmptyMapFile(i)
}
}

View file

@ -0,0 +1,55 @@
## How it works
### Data name
TDesktop allows for multiple "data names", which is basically
a prefix for data storage. Default one is `data`, but you can choose
any using `-key` CLI parameter.
### Local key
TDesktop uses something called "local key" to encrypt most of the files.
The local key itself is stored in `key_datas`, where `data` is the default
data name. That file can be passcode-protected, in which case you will
need a correct passcode to decrypt it.
### Encryption
Without going too deep into details, encryption used is the same
as the one used in MTProto v1 for message encryption (see
[Telegram docs](https://core.telegram.org/mtproto/description_v1#defining-aes-key-and-initialization-vector)
for details).
There, instead of `auth_key` a local key is used, and instead of
`msg_key` lower 16 bytes of sha1 of the contents are used.
Before encrypting (and computing sha1), content size is prepended, and
the result is padded.
### File naming
To name different files, TDesktop uses 8 lower bytes of md5 hash,
with nibbles switched (i.e. `0xFE => 0xEF`).
So, for example:
```typescript
const filename = 'data'
const md5 = crypto.md5(filename) // 8d777f385d3dfec8815d20f7496026dc
const md5Lower = md5.slice(0, 8) // 8d777f385d3dfec8
const result = swap8(md5Lower).toUpperHex() // D877F783D5D3EF8C
```
`D877F783D5D3EF8C` is the folder that is most likely present in your
`tdata` folder, which is derived simply from `data` and is used
for your first account data.
### Multi accounts
For second, third, etc. accounts, TDesktop appends `#2`, `#3` etc.
to the base data name respectively.
### MTProto auth keys
Auth keys are stored in a file named same as account data folder but with
`s` appended. So, for the first account that would be `D877F783D5D3EF8Cs`.

View file

@ -0,0 +1,6 @@
import * as qt from './qt-bundle.js'
export { qt }
export * from './convert.js'
export * from './tdata.js'
export * from './types.js'

View file

@ -0,0 +1,4 @@
import * as read from './qt-reader.js'
import * as write from './qt-writer.js'
export { read, write }

View file

@ -0,0 +1,35 @@
import { Long } from '@mtcute/core'
import { type ISyncReadable, read } from '@fuman/io'
import { u8 } from '@fuman/utils'
export function readQByteArray(readable: ISyncReadable): Uint8Array {
const length = read.uint32be(readable)
if (length === 0 || length === 0xFFFFFFFF) {
return u8.empty
}
return read.exactly(readable, length)
}
export function readLong(readable: ISyncReadable): Long {
const high = read.int32be(readable)
const low = read.int32be(readable)
return new Long(low, high)
}
export function readCharArray(readable: ISyncReadable): Uint8Array {
const buf = readQByteArray(readable)
if (buf.length > 0) {
// drop the last byte, which is always 0
return buf.subarray(0, buf.length - 1)
}
return buf
}
const u16Decoder = new TextDecoder('utf-16be')
export function readQString(readable: ISyncReadable): string {
const bytes = readQByteArray(readable)
return u16Decoder.decode(bytes)
}

View file

@ -0,0 +1,31 @@
import type { ISyncWritable } from '@fuman/io'
import { write } from '@fuman/io'
import { u8 } from '@fuman/utils'
import type { Long } from '@mtcute/core'
export function writeQByteArray(into: ISyncWritable, buf: Uint8Array): void {
write.uint32be(into, buf.length)
write.bytes(into, buf)
}
export function writeLong(into: ISyncWritable, long: Long): void {
write.int32be(into, long.high)
write.int32be(into, long.low)
}
export function writeCharArray(into: ISyncWritable, buf: Uint8Array): void {
const bytes = u8.alloc(buf.length + 1)
bytes.set(buf)
bytes[buf.length] = 0
writeQByteArray(into, bytes)
}
export function writeQString(into: ISyncWritable, str: string): void {
const length = str.length * 2
write.uint32be(into, length)
for (let i = 0; i < length; i++) {
write.uint16be(into, str.charCodeAt(i))
}
}

View file

@ -0,0 +1,106 @@
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { Long } from '@mtcute/core'
import type { INodeFsLike } from '../utils/fs.js'
import { getDefaultCryptoProvider } from '../utils/crypto.js'
import { Tdata } from './tdata.js'
class FakeFs implements INodeFsLike {
readonly files = new Map<string, Uint8Array>()
async readFile(path: string): Promise<Uint8Array> {
return this.files.get(path)!
}
async writeFile(path: string, data: Uint8Array): Promise<void> {
this.files.set(path, data)
}
async stat(path: string): Promise<{ size: number, lastModified: number }> {
return {
size: this.files.get(path)!.length,
lastModified: 0,
}
}
mkdir(): Promise<void> {
return Promise.resolve()
}
}
describe('tdata', () => {
it('should read simple tdata', async () => {
const tdata = await Tdata.open({
path: fileURLToPath(new URL('./__fixtures__/simple', import.meta.url)),
})
const auth = await tdata.readMtpAuthorization()
expect({ auth, key: tdata.keyData }).toMatchSnapshot()
})
it('should read passcode-protected tdata', async () => {
const tdata = await Tdata.open({
path: fileURLToPath(new URL('./__fixtures__/passcode', import.meta.url)),
passcode: '123123',
})
const auth = await tdata.readMtpAuthorization()
expect({ auth, key: tdata.keyData }).toMatchSnapshot()
})
it('should throw on invalid passcode', async () => {
await expect(Tdata.open({
path: fileURLToPath(new URL('./__fixtures__/passcode', import.meta.url)),
passcode: '123',
})).rejects.toThrow('Failed to decrypt, invalid password?')
})
it('should read multi-account tdata', async () => {
const tdata = await Tdata.open({
path: fileURLToPath(new URL('./__fixtures__/multiacc', import.meta.url)),
})
const auth0 = await tdata.readMtpAuthorization(0)
const auth1 = await tdata.readMtpAuthorization(1)
expect({ auth0, auth1, key: tdata.keyData }).toMatchSnapshot()
})
it('should write simple tdata', async () => {
const fs = new FakeFs()
const crypto = await getDefaultCryptoProvider()
crypto.randomBytes = size => new Uint8Array(size)
const tdata = await Tdata.create({
path: '/',
fs,
crypto,
keyData: {
count: 1,
order: [0],
active: 0,
},
})
const key = new Uint8Array(256)
key.fill(1)
await tdata.writeMtpAuthorization({
userId: Long.fromNumber(12345678),
mainDcId: 2,
authKeys: [
{
dcId: 2,
key,
},
],
authKeysToDestroy: [],
})
expect(fs.files).toMatchSnapshot()
})
})

View file

@ -0,0 +1,494 @@
import { dirname, join } from 'node:path/posix'
import { Bytes, read, write } from '@fuman/io'
import type { UnsafeMutable } from '@fuman/utils'
import { typed, u8, utf8 } from '@fuman/utils'
import { createAesIgeForMessageOld } from '@mtcute/core/utils.js'
import { Long, MtUnsupportedError } from '@mtcute/core'
import type { INodeFsLike } from '../utils/fs.js'
import { type IExtendedCryptoProvider, getDefaultCryptoProvider } from '../utils/crypto.js'
import { readLong, readQByteArray } from './qt-reader.js'
import type { InputTdKeyData, TdAuthKey, TdKeyData, TdMtpAuthorization } from './types.js'
import { writeLong, writeQByteArray } from './qt-writer.js'
const TDF_MAGIC = /* #__PURE__ */ utf8.encoder.encode('TDF$')
const TDF_VERSION = 5008003
const MTP_AUTHORIZATION_BLOCK = 0x4B // see https://github.com/telegramdesktop/tdesktop/blob/dev/Telegram/SourceFiles/storage/details/storage_settings_scheme.h
const HEX_ALPHABET = '0123456789ABCDEF'
function toFilePart(key: Uint8Array): string {
let str = ''
// we need to swap nibbles for whatever reason
for (let i = 0; i < 8; i++) {
const b = key[i]
const low = b & 0x0F
const high = b >> 4
str += HEX_ALPHABET[low] + HEX_ALPHABET[high]
}
return str
}
export interface TdataOptions {
/** Full path to the tdata directory */
path: string
/**
* File system to use for reading/writing.
*
* @default `import('node:fs/promises')`
*/
fs?: INodeFsLike
/**
* Crypto functions to use for encryption/decryption.
*
* @default `node:crypto`-based implementation
*/
crypto?: IExtendedCryptoProvider
/**
* Whether to ignore TDF version mismatch.
* If set to `true`, the version will be ignored and the file will be read as is,
* however the probability of errors is higher.
*/
ignoreVersion?: boolean
/**
* Whether the host machine has LE processor (default true, try changing in case of errors)
*/
le?: boolean
/**
* Value of -key cli parameter.
* Defaults to `data`
*/
dataKey?: string
/**
* Local passcode
*/
passcode?: string
}
export class Tdata {
private constructor(
readonly options: TdataOptions,
readonly fs: INodeFsLike,
readonly crypto: IExtendedCryptoProvider,
) {}
readonly keyData!: TdKeyData
static async open(options: TdataOptions): Promise<Tdata> {
const fs: INodeFsLike = options.fs ?? (await import('node:fs/promises') as unknown as INodeFsLike)
const crypto: IExtendedCryptoProvider = options.crypto ?? (await getDefaultCryptoProvider())
await crypto.initialize?.()
const tdata = new Tdata(options, fs, crypto)
;(tdata as UnsafeMutable<Tdata>).keyData = await tdata.readKeyData()
return tdata
}
static async create(options: TdataOptions & { keyData: InputTdKeyData }): Promise<Tdata> {
const fs: INodeFsLike = options.fs ?? (await import('node:fs/promises') as unknown as INodeFsLike)
const crypto: IExtendedCryptoProvider = options.crypto ?? (await getDefaultCryptoProvider())
await crypto.initialize?.()
const tdata = new Tdata(options, fs, crypto)
const keyData: TdKeyData = {
...options.keyData,
localKey: options.keyData.localKey ?? crypto.randomBytes(256),
version: options.keyData.version ?? TDF_VERSION,
}
;(tdata as UnsafeMutable<Tdata>).keyData = keyData
await tdata.writeKeyData(keyData)
return tdata
}
#readInt32(buf: Uint8Array): number {
return (this.options.le ?? true) ? read.int32le(buf) : read.int32be(buf)
}
#writeInt32(buf: Uint8Array, val: number): Uint8Array {
if (this.options.le ?? true) {
write.int32le(buf, val)
} else {
write.int32be(buf, val)
}
return buf
}
getDataName(idx: number): string {
let res = this.options.dataKey ?? 'data'
if (idx > 0) {
res += `#${idx + 1}`
}
return res
}
async readFile(filename: string): Promise<[number, Uint8Array]> {
const order: string[] = []
const modern = `${filename}s`
if (await this.fs.stat(join(this.options.path, modern))) {
order.push(modern)
} else {
const try0 = `${filename}0`
const try1 = `${filename}1`
const try0s = await this.fs.stat(join(this.options.path, try0))
const try1s = await this.fs.stat(join(this.options.path, try1))
if (try0s) {
order.push(try0)
if (try1s) {
order.push(try1)
if (try0s.lastModified < try1s.lastModified) {
order.reverse()
}
}
} else if (try1s) {
order.push(try1)
}
}
let lastError = 'file not found'
for (const file of order) {
const data = await this.fs.readFile(join(this.options.path, file))
const magic = data.subarray(0, 4)
if (!typed.equal(magic, TDF_MAGIC)) {
lastError = 'invalid magic'
continue
}
const versionBytes = data.subarray(4, 8)
const version = this.#readInt32(versionBytes)
if (version > TDF_VERSION && !this.options.ignoreVersion) {
lastError = `Unsupported version: ${version}`
continue
}
const dataSize = data.length - 24
const bytes = data.subarray(8, dataSize + 8)
const md5 = await this.crypto.createHash('md5')
await md5.update(bytes)
await md5.update(this.#writeInt32(u8.alloc(4), dataSize))
await md5.update(versionBytes)
await md5.update(magic)
const hash = await md5.digest()
if (!typed.equal(hash, data.subarray(dataSize + 8))) {
lastError = 'md5 mismatch'
continue
}
return [version, bytes]
}
throw new Error(`failed to read ${filename}, last error: ${lastError}`)
}
async writeFile(
filename: string,
data: Uint8Array,
mkdir = false,
): Promise<void> {
filename = join(this.options.path, `${filename}s`)
const version = this.#writeInt32(u8.alloc(4), TDF_VERSION)
const dataSize = this.#writeInt32(u8.alloc(4), data.length)
const md5 = await this.crypto.createHash('md5')
await md5.update(data)
await md5.update(dataSize)
await md5.update(version)
await md5.update(TDF_MAGIC)
if (mkdir) {
await this.fs.mkdir(dirname(filename), { recursive: true })
}
await this.fs.writeFile(
filename,
u8.concat([TDF_MAGIC, version, data, await md5.digest()]),
)
}
async createLocalKey(
salt: Uint8Array,
passcode: string = this.options.passcode ?? '',
): Promise<Uint8Array> {
const hasher = await this.crypto.createHash('sha512')
hasher.update(salt)
hasher.update(utf8.encoder.encode(passcode))
hasher.update(salt)
const hash = await hasher.digest()
return this.crypto.pbkdf2(
hash,
salt,
passcode === '' ? 1 : 100000,
256,
'sha512',
)
}
async decryptLocal(encrypted: Uint8Array, key: Uint8Array): Promise<Uint8Array> {
const encryptedKey = encrypted.subarray(0, 16)
const encryptedData = encrypted.subarray(16)
const ige = createAesIgeForMessageOld(
this.crypto,
key,
encryptedKey,
false,
)
const decrypted = ige.decrypt(encryptedData)
if (
!typed.equal(
this.crypto.sha1(decrypted).subarray(0, 16),
encryptedKey,
)
) {
throw new Error('Failed to decrypt, invalid password?')
}
const fullLen = encryptedData.length
const dataLen = this.#readInt32(decrypted)
if (
dataLen > decrypted.length
|| dataLen <= fullLen - 16
|| dataLen < 4
) {
throw new Error('Failed to decrypt, invalid data length')
}
return decrypted.subarray(4, dataLen)
}
async encryptLocal(data: Uint8Array, key: Uint8Array): Promise<Uint8Array> {
const dataSize = data.length + 4
const padding: Uint8Array
= dataSize & 0x0F
? this.crypto.randomBytes(0x10 - (dataSize & 0x0F))
: u8.empty
const toEncrypt = u8.alloc(dataSize + padding.length)
this.#writeInt32(toEncrypt, dataSize)
toEncrypt.set(data, 4)
toEncrypt.set(padding, dataSize)
const encryptedKey = this.crypto.sha1(toEncrypt).subarray(0, 16)
const ige = createAesIgeForMessageOld(
this.crypto,
key,
encryptedKey,
false,
)
const encryptedData = ige.encrypt(toEncrypt)
return u8.concat2(encryptedKey, encryptedData)
}
async readKeyData(): Promise<TdKeyData> {
const [version, data] = await this.readFile(`key_${this.options.dataKey ?? 'data'}`)
const bytes = Bytes.from(data)
const salt = readQByteArray(bytes)
const keyEncrypted = readQByteArray(bytes)
const infoEncrypted = readQByteArray(bytes)
const passcodeKey = await this.createLocalKey(salt)
const keyInnerData = await this.decryptLocal(keyEncrypted, passcodeKey)
const infoDecrypted = await this.decryptLocal(
infoEncrypted,
keyInnerData,
)
const info = Bytes.from(infoDecrypted)
const localKey = keyInnerData
const count = read.int32be(info)
const order = [...Array(count)].map(() => read.int32be(info))
const active = read.int32be(info)
return {
version,
localKey,
count,
order,
active,
}
}
async writeKeyData(keyData: TdKeyData): Promise<void> {
const info = Bytes.alloc()
write.int32be(info, keyData.count)
keyData.order.forEach(i => write.int32be(info, i))
write.int32be(info, keyData.active)
const infoDecrypted = info.result()
const infoEncrypted = await this.encryptLocal(infoDecrypted, keyData.localKey)
const salt = this.crypto.randomBytes(32)
const passcodeKey = await this.createLocalKey(salt)
const keyEncrypted = await this.encryptLocal(keyData.localKey, passcodeKey)
const data = Bytes.alloc()
writeQByteArray(data, salt)
writeQByteArray(data, keyEncrypted)
writeQByteArray(data, infoEncrypted)
await this.writeFile(`key_${this.options.dataKey ?? 'data'}`, data.result(), true)
}
async computeDataNameKey(accountIdx: number): Promise<Uint8Array> {
const md5 = await this.crypto.createHash('md5')
await md5.update(utf8.encoder.encode(this.getDataName(accountIdx)))
const r = await md5.digest()
return r.subarray(0, 8)
}
async computeDataNameKeyHex(accountIdx: number): Promise<string> {
return toFilePart(await this.computeDataNameKey(accountIdx))
}
async readEncryptedFile(filename: string): Promise<[number, Uint8Array]> {
const [version, data] = await this.readFile(filename)
const encrypted = readQByteArray(Bytes.from(data))
const decrypted = await this.decryptLocal(encrypted, this.keyData.localKey)
return [version, decrypted]
}
async writeEncryptedFile(
filename: string,
data: Uint8Array,
mkdir = false,
): Promise<void> {
const encryptedInner = await this.encryptLocal(data, this.keyData.localKey)
const writer = Bytes.alloc(data.length + 4)
writeQByteArray(writer, encryptedInner)
await this.writeFile(filename, writer.result(), mkdir)
}
async readMtpAuthorization(accountIdx: number = 0): Promise<TdMtpAuthorization> {
const [, mtpData] = await this.readEncryptedFile(
await this.computeDataNameKeyHex(accountIdx),
)
// nb: this is pretty much a hack that relies on the fact that
// most of the time the mtp auth data is in the first setting
// since the settings are not length-prefixed, we can't skip unknown settings,
// as we need to know their type.
// and this is very much tied to the actual tdesktop version, and would be a nightmare to maintain
let bytes = Bytes.from(mtpData)
const header = read.int32be(bytes)
if (header !== MTP_AUTHORIZATION_BLOCK) {
throw new MtUnsupportedError(`expected first setting to be mtp auth data, got 0x${header.toString(16)}`)
}
const mtpAuthBlock = readQByteArray(bytes)
bytes = Bytes.from(mtpAuthBlock)
const legacyUserId = read.int32be(bytes)
const legacyMainDcId = read.int32be(bytes)
let userId, mainDcId
if (legacyMainDcId === -1 && legacyMainDcId === -1) {
userId = readLong(bytes)
mainDcId = read.int32be(bytes)
} else {
userId = Long.fromInt(legacyUserId)
mainDcId = legacyMainDcId
}
function readKeys(target: TdAuthKey[]) {
const count = read.uint32be(bytes)
for (let i = 0; i < count; i++) {
const dcId = read.int32be(bytes)
const key = read.exactly(bytes, 256)
target.push({ dcId, key })
}
}
const authKeys: TdAuthKey[] = []
const authKeysToDestroy: TdAuthKey[] = []
readKeys(authKeys)
readKeys(authKeysToDestroy)
return {
userId,
mainDcId,
authKeys,
authKeysToDestroy,
}
}
async writeMtpAuthorization(auth: TdMtpAuthorization, accountIdx = 0): Promise<void> {
const bytes = Bytes.alloc()
// legacy user id & dc id
write.int32be(bytes, -1)
write.int32be(bytes, -1)
writeLong(bytes, auth.userId)
write.int32be(bytes, auth.mainDcId)
function writeKeys(keys: TdAuthKey[]) {
write.uint32be(bytes, keys.length)
keys.forEach((k) => {
write.int32be(bytes, k.dcId)
write.bytes(bytes, k.key)
})
}
writeKeys(auth.authKeys)
writeKeys(auth.authKeysToDestroy)
const file = Bytes.alloc()
write.int32be(file, MTP_AUTHORIZATION_BLOCK)
writeQByteArray(file, bytes.result())
await this.writeEncryptedFile(
await this.computeDataNameKeyHex(accountIdx),
file.result(),
)
}
async writeEmptyMapFile(accountIdx: number): Promise<void> {
// without this file tdesktop will not "see" the account
// however just creating an empty file seems to be enough to make it happy
const writer = Bytes.alloc()
writeQByteArray(writer, u8.empty) // legacySalt
writeQByteArray(writer, u8.empty) // legacyKeyEncrypted
writeQByteArray(writer, await this.encryptLocal(u8.empty, this.keyData.localKey))
await this.writeFile(
join(await this.computeDataNameKeyHex(accountIdx), 'map'),
writer.result(),
true,
)
}
}

View file

@ -0,0 +1,26 @@
import type { Long } from '@mtcute/core'
export interface TdAuthKey {
dcId: number
key: Uint8Array
}
export interface TdMtpAuthorization {
userId: Long
mainDcId: number
authKeys: TdAuthKey[]
authKeysToDestroy: TdAuthKey[]
}
export interface InputTdKeyData {
localKey?: Uint8Array
version?: number
count: number
order: number[]
active: number
}
export interface TdKeyData extends InputTdKeyData {
version: number
localKey: Uint8Array
}

View file

@ -0,0 +1,29 @@
import type { MaybePromise } from '@fuman/utils'
import type { ICryptoProvider } from '@mtcute/core/utils.js'
export interface IExtendedCryptoProvider extends ICryptoProvider {
createHash(algorithm: 'md5' | 'sha512'): MaybePromise<{
update(data: Uint8Array): MaybePromise<void>
digest(): MaybePromise<Uint8Array>
}>
}
export async function getDefaultCryptoProvider(): Promise<IExtendedCryptoProvider> {
const crypto = /* @vite-ignore */ await import('node:crypto')
const { NodeCryptoProvider } = /* @vite-ignore */ await import('@mtcute/node/utils.js')
return new (class extends NodeCryptoProvider implements IExtendedCryptoProvider {
createHash(algorithm: 'md5' | 'sha512') {
const hasher = crypto.createHash(algorithm)
return {
update(data: Uint8Array) {
hasher.update(data)
},
digest() {
return hasher.digest() as unknown as Uint8Array
},
}
}
})()
}

View file

@ -0,0 +1,6 @@
export interface INodeFsLike {
readFile(path: string): Promise<Uint8Array>
writeFile(path: string, data: Uint8Array): Promise<void>
mkdir(path: string, options?: { recursive?: boolean }): Promise<void>
stat(path: string): Promise<{ size: number, lastModified: number }>
}