189 lines
5.8 KiB
TypeScript
189 lines
5.8 KiB
TypeScript
import type bigInt from 'big-integer';
|
|
import BigInt from 'big-integer';
|
|
import AuthKey from '../../../lib/gramjs/crypto/AuthKey';
|
|
import Logger from '../../../lib/gramjs/extensions/Logger';
|
|
import Helpers from '../../../lib/gramjs/Helpers';
|
|
import MTProtoState from '../../../lib/gramjs/network/MTProtoState';
|
|
|
|
type DhConfig = {
|
|
p: number[];
|
|
g: number;
|
|
random: number[];
|
|
};
|
|
|
|
let currentPhoneCallState: PhoneCallState | undefined;
|
|
|
|
class PhoneCallState {
|
|
private state?: MTProtoState;
|
|
|
|
private seq = 0;
|
|
|
|
private gA?: bigInt.BigInteger;
|
|
|
|
private gB: any;
|
|
|
|
private p?: bigInt.BigInteger;
|
|
|
|
private random?: bigInt.BigInteger;
|
|
|
|
private waitForState: Promise<void>;
|
|
|
|
private resolveState?: VoidFunction;
|
|
|
|
constructor(
|
|
private isOutgoing: boolean,
|
|
) {
|
|
this.waitForState = new Promise<void>((resolve) => {
|
|
this.resolveState = resolve;
|
|
});
|
|
}
|
|
|
|
async requestCall({ p, g, random }: DhConfig) {
|
|
const pBN = Helpers.readBigIntFromBuffer(Buffer.from(p), false);
|
|
const randomBN = Helpers.readBigIntFromBuffer(Buffer.from(random), false);
|
|
|
|
const gA = Helpers.modExp(BigInt(g), randomBN, pBN);
|
|
|
|
this.gA = gA;
|
|
this.p = pBN;
|
|
this.random = randomBN;
|
|
|
|
const gAHash: Buffer = await Helpers.sha256(Helpers.getByteArray(gA));
|
|
return Array.from(gAHash);
|
|
}
|
|
|
|
acceptCall({ p, g, random }: DhConfig) {
|
|
const pLast = Helpers.readBigIntFromBuffer(p, false);
|
|
const randomLast = Helpers.readBigIntFromBuffer(random, false);
|
|
|
|
const gB = Helpers.modExp(BigInt(g), randomLast, pLast);
|
|
this.gB = gB;
|
|
this.p = pLast;
|
|
this.random = randomLast;
|
|
|
|
return Array.from(Helpers.getByteArray(gB));
|
|
}
|
|
|
|
async confirmCall(gAOrB: number[], emojiData: Uint16Array, emojiOffsets: number[]) {
|
|
if (this.isOutgoing) {
|
|
this.gB = Helpers.readBigIntFromBuffer(Buffer.from(gAOrB), false);
|
|
} else {
|
|
this.gA = Helpers.readBigIntFromBuffer(Buffer.from(gAOrB), false);
|
|
}
|
|
const authKey = Helpers.modExp(
|
|
!this.isOutgoing ? this.gA : this.gB,
|
|
this.random,
|
|
this.p,
|
|
);
|
|
const fingerprint: Buffer = await Helpers.sha1(Helpers.getByteArray(authKey));
|
|
const keyFingerprint = Helpers.readBigIntFromBuffer(fingerprint.slice(-8).reverse(), false);
|
|
|
|
const emojis = await generateEmojiFingerprint(
|
|
Helpers.getByteArray(authKey),
|
|
Helpers.getByteArray(this.gA),
|
|
emojiData,
|
|
emojiOffsets,
|
|
);
|
|
|
|
const key = new AuthKey();
|
|
await key.setKey(Helpers.getByteArray(authKey));
|
|
this.state = new MTProtoState(key, new Logger(), true, this.isOutgoing);
|
|
this.resolveState!();
|
|
|
|
return { gA: Array.from(Helpers.getByteArray(this.gA)), keyFingerprint: keyFingerprint.toString(), emojis };
|
|
}
|
|
|
|
async encode(data: string) {
|
|
if (!this.state) return undefined;
|
|
|
|
const seqArray = new Uint32Array(1);
|
|
seqArray[0] = this.seq++;
|
|
const encodedData = await this.state.encryptMessageData(
|
|
Buffer.concat([Helpers.convertToLittle(seqArray), Buffer.from(data)]),
|
|
);
|
|
return Array.from(encodedData);
|
|
}
|
|
|
|
async decode(data: number[]): Promise<any> {
|
|
if (!this.state) {
|
|
return this.waitForState.then(() => {
|
|
return this.decode(data);
|
|
});
|
|
}
|
|
|
|
const message = await this.state.decryptMessageData(Buffer.from(data));
|
|
|
|
return JSON.parse(message.toString());
|
|
}
|
|
}
|
|
|
|
// https://github.com/TelegramV/App/blob/ead52320975362139cabad18cf8346f98c349a22/src/js/MTProto/Calls/Internal.js#L72
|
|
function computeEmojiIndex(bytes: Uint8Array) {
|
|
return ((BigInt(bytes[0]).and(0x7F)).shiftLeft(56))
|
|
.or((BigInt(bytes[1]).shiftLeft(48)))
|
|
.or((BigInt(bytes[2]).shiftLeft(40)))
|
|
.or((BigInt(bytes[3]).shiftLeft(32)))
|
|
.or((BigInt(bytes[4]).shiftLeft(24)))
|
|
.or((BigInt(bytes[5]).shiftLeft(16)))
|
|
.or((BigInt(bytes[6]).shiftLeft(8)))
|
|
.or((BigInt(bytes[7])));
|
|
}
|
|
|
|
async function generateEmojiFingerprint(
|
|
authKey: Uint8Array, gA: Uint8Array, emojiData: Uint16Array, emojiOffsets: number[],
|
|
) {
|
|
const hash = await Helpers.sha256(Buffer.concat([new Uint8Array(authKey), new Uint8Array(gA)]));
|
|
const result = [];
|
|
const emojiCount = emojiOffsets.length - 1;
|
|
const kPartSize = 8;
|
|
for (let partOffset = 0; partOffset !== hash.byteLength; partOffset += kPartSize) {
|
|
const value = computeEmojiIndex(hash.subarray(partOffset, partOffset + kPartSize));
|
|
const index = value.modPow(1, emojiCount).toJSNumber();
|
|
const offset = emojiOffsets[index];
|
|
const size = emojiOffsets[index + 1] - offset;
|
|
result.push(String.fromCharCode(...emojiData.subarray(offset, offset + size)));
|
|
}
|
|
return result.join('');
|
|
}
|
|
|
|
export function createPhoneCallState(params: ConstructorParameters<typeof PhoneCallState>) {
|
|
currentPhoneCallState = new PhoneCallState(...params);
|
|
}
|
|
|
|
export function destroyPhoneCallState() {
|
|
currentPhoneCallState = undefined;
|
|
}
|
|
|
|
type FunctionPropertyOf<T> = {
|
|
[P in keyof T]: T[P] extends Function
|
|
? P
|
|
: never
|
|
}[keyof T];
|
|
|
|
type ParamsOf<T extends FunctionPropertyOf<PhoneCallState>> = Parameters<PhoneCallState[T]>;
|
|
type ReturnTypeOf<T extends FunctionPropertyOf<PhoneCallState>> = ReturnType<PhoneCallState[T]>;
|
|
|
|
export function encodePhoneCallData(params: ParamsOf<'encode'>): ReturnTypeOf<'encode'> {
|
|
return currentPhoneCallState!.encode(...params);
|
|
}
|
|
|
|
export async function decodePhoneCallData(params: ParamsOf<'decode'>) {
|
|
if (!currentPhoneCallState) {
|
|
return undefined;
|
|
}
|
|
const result = await currentPhoneCallState.decode(...params);
|
|
return result;
|
|
}
|
|
|
|
export function confirmPhoneCall(params: ParamsOf<'confirmCall'>): ReturnTypeOf<'confirmCall'> {
|
|
return currentPhoneCallState!.confirmCall(...params);
|
|
}
|
|
|
|
export function acceptPhoneCall(params: ParamsOf<'acceptCall'>): ReturnTypeOf<'acceptCall'> {
|
|
return currentPhoneCallState!.acceptCall(...params);
|
|
}
|
|
|
|
export function requestPhoneCall(params: ParamsOf<'requestCall'>): ReturnTypeOf<'requestCall'> {
|
|
return currentPhoneCallState!.requestCall(...params);
|
|
}
|