Telegram Web K with changes to work inside I2P https://web.telegram.i2p/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

540 lines
19 KiB

4 years ago
import { TLSerialization, TLDeserialization } from "../tl_utils";
import dcConfigurator from "./dcConfigurator";
import { dT, bytesToHex, bytesCmp, bytesFromHex, bytesXor } from "../bin_utils";
import rsaKeysManager from "./rsaKeysManager";
import { tsNow } from "../utils";
import timeManager from "./timeManager";
// @ts-ignore
import { BigInteger } from "jsbn";
import CryptoWorker from "../crypto/cryptoworker";
import {MTProto} from './mtproto';
/* const chromeMatches = navigator.userAgent.match(/Chrome\/(\d+(\.\d+)?)/);
const chromeVersion = (chromeMatches && parseFloat(chromeMatches[1])) || false;
const xhrSendBuffer = !('ArrayBufferView' in window) && (chromeVersion > 0 && chromeVersion < 30); */
/* let fNewNonce: any = bytesFromHex('8761970c24cb2329b5b2459752c502f3057cb7e8dbab200e526e8767fdc73b3c').reverse();
let fNonce: any = bytesFromHex('b597720d11faa5914ef485c529cde414').reverse();
let fResult: any = new Uint8Array(bytesFromHex('000000000000000001b473a0661b285e480000006324160514e4cd29c585f44e91a5fa110d7297b5c0c4134c84893db5715ecd56af5ed618082182053cc5de91cd00000015c4b51c02000000a5b7f709355fc30b216be86c022bb4c3'));
fNewNonce = false;
fNonce = false;
fResult = false; */
type AuthOptions = {
dcID: number,
nonce: Uint8Array,
serverNonce?: Uint8Array,
pq?: any,
fingerprints?: string[],
publicKey?: {
modulus: string,
exponent: string,
fingerprint: string
},
// good
p?: number[],
q?: number[],
newNonce?: Uint8Array,
retry?: number,
b?: number[],
g?: number,
gA?: number,
dhPrime?: number,
tmpAesKey?: Uint8Array,
tmpAesIv?: Uint8Array,
authKeyID?: Uint8Array,
authKey?: number[],
serverSalt?: number[],
localTime?: number,
serverTime?: any
};
export class Authorizer {
private cached: {
[dcID: number]: Promise<AuthOptions>
} = {};
constructor() {
}
public mtpSendPlainRequest(dcID: number, requestArray: Uint8Array) {
var requestLength = requestArray.byteLength;
//requestArray = new /* Int32Array */Uint8Array(requestBuffer);
var header = new TLSerialization();
header.storeLongP(0, 0, 'auth_key_id'); // Auth key
header.storeLong(timeManager.generateID(), 'msg_id'); // Msg_id
header.storeInt(requestLength, 'request_length');
let headerArray = header.getBytes(true) as Uint8Array;
let resultArray = new Uint8Array(headerArray.byteLength + requestLength);
resultArray.set(headerArray);
resultArray.set(requestArray, headerArray.length);
/* var headerBuffer = header.getBuffer(),
headerArray = new Int32Array(headerBuffer);
var headerLength = headerBuffer.byteLength;
var resultBuffer = new ArrayBuffer(headerLength + requestLength),
resultArray = new Int32Array(resultBuffer);
resultArray.set(headerArray);
resultArray.set(requestArray, headerArray.length);
let requestData = xhrSendBuffer ? resultBuffer : resultArray; */
let transport = dcConfigurator.chooseServer(dcID);
let baseError = {
code: 406,
type: 'NETWORK_BAD_RESPONSE',
transport: transport
};
console.log(dT(), 'mtpSendPlainRequest: creating requestPromise');
return transport.send(resultArray).then(result => {
console.log(dT(), 'mtpSendPlainRequest: in good sector', result);
if(!result || !result.byteLength) {
return Promise.reject(baseError);
}
try {
/* result = fResult ? fResult : result;
fResult = new Uint8Array(0); */
let deserializer = new TLDeserialization(result, {mtproto: true});
let auth_key_id = deserializer.fetchLong('auth_key_id');
if(auth_key_id != 0) console.error('auth_key_id != 0', auth_key_id);
let msg_id = deserializer.fetchLong('msg_id');
if(msg_id == 0) console.error('msg_id == 0', msg_id);
let msg_len = deserializer.fetchInt('msg_len');
if(!msg_len) console.error('no msg_len', msg_len);
return deserializer;
} catch(e) {
console.error('mtpSendPlainRequest: deserialization went bad', e);
let error = Object.assign(baseError, {originalError: e});
throw error;
}
}, error => {
if(!error.message && !error.type) {
error = Object.assign(baseError, {
originalError: error
});
}
return Promise.reject(error);
});
}
public async mtpSendReqPQ(auth: AuthOptions) {
var request = new TLSerialization({mtproto: true});
request.storeMethod('req_pq_multi', {nonce: auth.nonce});
// need
rsaKeysManager.prepare().then(() => {});
console.log(dT(), 'Send req_pq', auth.nonce.hex);
try {
var deserializer = await this.mtpSendPlainRequest(auth.dcID, request.getBytes(true));
} catch(error) {
console.error(dT(), 'req_pq error', error.message);
throw error;
}
var response = deserializer.fetchObject('ResPQ');
if(response._ != 'resPQ') {
throw new Error('[MT] resPQ response invalid: ' + response._);
}
if(!bytesCmp(auth.nonce, response.nonce)) {
console.log(auth.nonce, response.nonce);
throw new Error('[MT] resPQ nonce mismatch');
}
//auth.serverNonce = response.server_nonce;
auth.serverNonce = new Uint8Array(response.server_nonce); // need
auth.pq = response.pq;
auth.fingerprints = response.server_public_key_fingerprints;
console.log(dT(), 'Got ResPQ', bytesToHex(auth.serverNonce), bytesToHex(auth.pq), auth.fingerprints);
let publicKey = await rsaKeysManager.select(auth.fingerprints);
if(!publicKey) {
throw new Error('[MT] No public key found');
}
auth.publicKey = publicKey;
console.log(dT(), 'PQ factorization start', auth.pq);
try {
var pAndQ = await CryptoWorker.factorize(auth.pq);
} catch(error) {
console.error('worker error factorize', error);
throw error;
}
auth.p = pAndQ[0];
auth.q = pAndQ[1];
console.log(dT(), 'PQ factorization done', pAndQ);
/* let p = new Uint32Array(new Uint8Array(auth.p).buffer)[0];
let q = new Uint32Array(new Uint8Array(auth.q).buffer)[0];
console.log(dT(), 'PQ factorization done', pAndQ, p.toString(16), q.toString(16)); */
return this.mtpSendReqDhParams(auth);
}
public async mtpSendReqDhParams(auth: AuthOptions) {
auth.newNonce = new Uint8Array(32).randomize();
/* auth.newNonce = new Array(32); // need array, not uint8array!
MTProto.secureRandom.nextBytes(auth.newNonce); */
//console.log("TCL: Authorizer -> mtpSendReqDhParams -> auth.newNonce", auth.newNonce)
// remove
// auth.newNonce = fNewNonce ? fNewNonce : auth.newNonce;
// console.log("TCL: Authorizer -> mtpSendReqDhParams -> auth.newNonce", auth.newNonce);
let p_q_inner_data = {
_: 'p_q_inner_data',
pq: auth.pq,
p: auth.p,
q: auth.q,
nonce: auth.nonce,
server_nonce: auth.serverNonce,
new_nonce: auth.newNonce
};
let data = new TLSerialization({mtproto: true});
data.storeObject(p_q_inner_data, 'P_Q_inner_data', 'DECRYPTED_DATA');
/* console.log('p_q_inner_data', p_q_inner_data,
bytesToHex(bytesFromArrayBuffer(data.getBuffer())),
sha1BytesSync(data.getBuffer()),
bytesFromArrayBuffer(await CryptoWorker.sha1Hash(data.getBuffer()))); */
let uint8Data = data.getBytes(true);
let sha1Hashed = await CryptoWorker.sha1Hash(uint8Data);
//var dataWithHash = sha1BytesSync(data.getBuffer()).concat(data.getBytes() as number[]);
let dataWithHash = sha1Hashed.concat(uint8Data);
//dataWithHash = addPadding(dataWithHash, 255);
//dataWithHash = dataWithHash.concat(bytesFromHex('96228ea7790e71caaabc2ab67f4412e9aa224c664d232cc08617a32ce1796aa052da4a737083211689858f461e4473fd6394afd3aa0c8014840dc13f47beaf4fc3b9229aea9cfa83f9f6e676e50ee7676542fb75606879ee7e65cf3a2295b4ba0934ceec1011560c62395a6e9593bfb117cd0da75ba56723672d100ac17ec4d805aa59f7852e3a25a79ee4'));
//console.log('sha1Hashed', bytesToHex(sha1Hashed), 'dataWithHash', bytesToHex(dataWithHash), dataWithHash.length);
let rsaEncrypted = await CryptoWorker.rsaEncrypt(auth.publicKey, dataWithHash);
//let rsaEncrypted = await CryptoWorker.rsaEncrypt(auth.publicKey, dataWithHash);
//console.log('rsaEncrypted', rsaEncrypted, new Uint8Array(rsaEncrypted).hex);
let req_DH_params = {
nonce: auth.nonce,
server_nonce: auth.serverNonce,
p: auth.p,
q: auth.q,
public_key_fingerprint: auth.publicKey.fingerprint,
encrypted_data: rsaEncrypted
};
var request = new TLSerialization({mtproto: true});
request.storeMethod('req_DH_params', req_DH_params);
let requestBytes = request.getBytes(true);
console.log(dT(), 'Send req_DH_params', req_DH_params/* , requestBytes.hex */);
try {
var deserializer = await this.mtpSendPlainRequest(auth.dcID, requestBytes);
} catch(error) {
console.log(dT(), 'Send req_DH_params FAIL!', error);
throw error;
}
var response = deserializer.fetchObject('Server_DH_Params', 'RESPONSE');
console.log(dT(), 'Sent req_DH_params, response:', response);
if(response._ != 'server_DH_params_fail' && response._ != 'server_DH_params_ok') {
throw new Error('[MT] Server_DH_Params response invalid: ' + response._);
}
if(!bytesCmp(auth.nonce, response.nonce)) {
throw new Error('[MT] Server_DH_Params nonce mismatch');
}
if(!bytesCmp(auth.serverNonce, response.server_nonce)) {
throw new Error('[MT] Server_DH_Params server_nonce mismatch');
}
if(response._ == 'server_DH_params_fail') {
//var newNonceHash = sha1BytesSync(auth.newNonce).slice(-16);
var newNonceHash = (await CryptoWorker.sha1Hash(auth.newNonce)).slice(-16);
if(!bytesCmp(newNonceHash, response.new_nonce_hash)) {
throw new Error('[MT] server_DH_params_fail new_nonce_hash mismatch');
}
throw new Error('[MT] server_DH_params_fail');
}
// fill auth object
try {
await this.mtpDecryptServerDhDataAnswer(auth, response.encrypted_answer);
} catch(e) {
console.error(dT(), 'mtpDecryptServerDhDataAnswer FAILED!', e);
throw e;
}
//console.log(dT(), 'mtpSendReqDhParams: executing mtpSendSetClientDhParams...');
return this.mtpSendSetClientDhParams(auth as any); // костыль
}
public async mtpDecryptServerDhDataAnswer(auth: AuthOptions, encryptedAnswer: any) {
auth.localTime = tsNow();
// can't concat Array with Uint8Array!
//auth.tmpAesKey = sha1BytesSync(auth.newNonce.concat(auth.serverNonce)).concat(sha1BytesSync(auth.serverNonce.concat(auth.newNonce)).slice(0, 12));
//auth.tmpAesIv = sha1BytesSync(auth.serverNonce.concat(auth.newNonce)).slice(12).concat(sha1BytesSync([].concat(auth.newNonce, auth.newNonce)), auth.newNonce.slice(0, 4));
auth.tmpAesKey = (await CryptoWorker.sha1Hash(auth.newNonce.concat(auth.serverNonce)))
.concat((await CryptoWorker.sha1Hash(auth.serverNonce.concat(auth.newNonce))).slice(0, 12));
auth.tmpAesIv = (await CryptoWorker.sha1Hash(auth.serverNonce.concat(auth.newNonce))).slice(12)
.concat(await CryptoWorker.sha1Hash(auth.newNonce.concat(auth.newNonce)), auth.newNonce.slice(0, 4));
/* console.log(auth.serverNonce.concat(auth.newNonce));
console.log(auth.newNonce.concat(auth.serverNonce));
console.log(auth.newNonce.concat(auth.newNonce)); */
//var answerWithHash = aesDecryptSync(encryptedAnswer, auth.tmpAesKey, auth.tmpAesIv);
var answerWithHash = new Uint8Array(await CryptoWorker.aesDecrypt(encryptedAnswer, auth.tmpAesKey, auth.tmpAesIv));
var hash = answerWithHash.slice(0, 20);
var answerWithPadding = answerWithHash.slice(20);
// console.log('hash', hash);
var deserializer = new TLDeserialization(answerWithPadding, {mtproto: true});
var response = deserializer.fetchObject('Server_DH_inner_data');
if(response._ != 'server_DH_inner_data') {
throw new Error('[MT] server_DH_inner_data response invalid: ' + response);
}
if(!bytesCmp(auth.nonce, response.nonce)) {
throw new Error('[MT] server_DH_inner_data nonce mismatch');
}
if(!bytesCmp(auth.serverNonce, response.server_nonce)) {
throw new Error('[MT] server_DH_inner_data serverNonce mismatch');
}
console.log(dT(), 'Done decrypting answer');
auth.g = response.g;
auth.dhPrime = response.dh_prime;
auth.gA = response.g_a;
auth.serverTime = response.server_time;
auth.retry = 0;
this.mtpVerifyDhParams(auth.g, auth.dhPrime, auth.gA);
var offset = deserializer.getOffset();
//if(!bytesCmp(hash, sha1BytesSync(answerWithPadding.slice(0, offset)))) {
if(!bytesCmp(hash, await CryptoWorker.sha1Hash(answerWithPadding.slice(0, offset)))) {
throw new Error('[MT] server_DH_inner_data SHA1-hash mismatch');
}
timeManager.applyServerTime(auth.serverTime, auth.localTime);
}
public mtpVerifyDhParams(g: number, dhPrime: any, gA: any) {
console.log(dT(), 'Verifying DH params');
var dhPrimeHex = bytesToHex(dhPrime);
if(g != 3 || dhPrimeHex !== 'c71caeb9c6b1c9048e6c522f70f13f73980d40238e3e21c14934d037563d930f48198a0aa7c14058229493d22530f4dbfa336f6e0ac925139543aed44cce7c3720fd51f69458705ac68cd4fe6b6b13abdc9746512969328454f18faf8c595f642477fe96bb2a941d5bcd1d4ac8cc49880708fa9b378e3c4f3a9060bee67cf9a4a4a695811051907e162753b56b0f6b410dba74d8a84b2a14b3144e0ef1284754fd17ed950d5965b4b9dd46582db1178d169c6bc465b0d6ff9ca3928fef5b9ae4e418fc15e83ebea0f87fa9ff5eed70050ded2849f47bf959d956850ce929851f0d8115f635b105ee2e4e15d04b2454bf6f4fadf034b10403119cd8e3b92fcc5b') {
// The verified value is from https://core.telegram.org/mtproto/security_guidelines
throw new Error('[MT] DH params are not verified: unknown dhPrime');
}
console.log(dT(), 'dhPrime cmp OK');
var gABigInt = new BigInteger(bytesToHex(gA), 16);
var dhPrimeBigInt = new BigInteger(dhPrimeHex, 16);
if(gABigInt.compareTo(BigInteger.ONE) <= 0) {
throw new Error('[MT] DH params are not verified: gA <= 1');
}
if(gABigInt.compareTo(dhPrimeBigInt.subtract(BigInteger.ONE)) >= 0) {
throw new Error('[MT] DH params are not verified: gA >= dhPrime - 1');
}
console.log(dT(), '1 < gA < dhPrime-1 OK');
var two = new BigInteger(/* null */'');
two.fromInt(2);
var twoPow = two.pow(2048 - 64);
if(gABigInt.compareTo(twoPow) < 0) {
throw new Error('[MT] DH params are not verified: gA < 2^{2048-64}');
}
if(gABigInt.compareTo(dhPrimeBigInt.subtract(twoPow)) >= 0) {
throw new Error('[MT] DH params are not verified: gA > dhPrime - 2^{2048-64}');
}
console.log(dT(), '2^{2048-64} < gA < dhPrime-2^{2048-64} OK');
return true;
}
public async mtpSendSetClientDhParams(auth: AuthOptions): Promise<AuthOptions> {
var gBytes = bytesFromHex(auth.g.toString(16));
auth.b = new Array(256);
MTProto.secureRandom.nextBytes(auth.b);
try {
var gB = await CryptoWorker.modPow(gBytes, auth.b, auth.dhPrime);
} catch(error) {
throw error;
}
var data = new TLSerialization({mtproto: true});
data.storeObject({
_: 'client_DH_inner_data',
nonce: auth.nonce,
server_nonce: auth.serverNonce,
retry_id: [0, auth.retry++],
g_b: gB
}, 'Client_DH_Inner_Data');
//var dataWithHash = sha1BytesSync(data.getBuffer()).concat(data.getBytes());
var dataWithHash = (await CryptoWorker.sha1Hash(data.getBuffer())).concat(data.getBytes());
//var encryptedData = aesEncryptSync(dataWithHash, auth.tmpAesKey, auth.tmpAesIv);
var encryptedData = await CryptoWorker.aesEncrypt(dataWithHash, auth.tmpAesKey, auth.tmpAesIv);
var request = new TLSerialization({mtproto: true});
request.storeMethod('set_client_DH_params', {
nonce: auth.nonce,
server_nonce: auth.serverNonce,
encrypted_data: encryptedData
});
console.log(dT(), 'Send set_client_DH_params');
try {
var deserializer = await this.mtpSendPlainRequest(auth.dcID, request.getBytes(true));
} catch(err) {
throw err;
}
let response = deserializer.fetchObject('Set_client_DH_params_answer');
if(response._ != 'dh_gen_ok' && response._ != 'dh_gen_retry' && response._ != 'dh_gen_fail') {
throw new Error('[MT] Set_client_DH_params_answer response invalid: ' + response._);
}
if(!bytesCmp(auth.nonce, response.nonce)) {
throw new Error('[MT] Set_client_DH_params_answer nonce mismatch');
}
if(!bytesCmp(auth.serverNonce, response.server_nonce)) {
throw new Error('[MT] Set_client_DH_params_answer server_nonce mismatch');
}
try {
var authKey = await CryptoWorker.modPow(auth.gA, auth.b, auth.dhPrime);
} catch(err) {
throw authKey;
}
//var authKeyHash = sha1BytesSync(authKey),
let authKeyHash = await CryptoWorker.sha1Hash(authKey),
authKeyAux = authKeyHash.slice(0, 8),
authKeyID = authKeyHash.slice(-8);
console.log(dT(), 'Got Set_client_DH_params_answer', response._, authKey);
switch(response._) {
case 'dh_gen_ok':
var newNonceHash1 = (await CryptoWorker.sha1Hash(auth.newNonce.concat([1], authKeyAux))).slice(-16);
//var newNonceHash1 = sha1BytesSync(auth.newNonce.concat([1], authKeyAux)).slice(-16);
if(!bytesCmp(newNonceHash1, response.new_nonce_hash1)) {
throw new Error('[MT] Set_client_DH_params_answer new_nonce_hash1 mismatch');
}
var serverSalt = bytesXor(auth.newNonce.slice(0, 8), auth.serverNonce.slice(0, 8));
console.log('Auth successfull!', authKeyID, authKey, serverSalt);
auth.authKeyID = authKeyID;
auth.authKey = authKey;
auth.serverSalt = serverSalt;
return auth;
break;
case 'dh_gen_retry':
//var newNonceHash2 = sha1BytesSync(auth.newNonce.concat([2], authKeyAux)).slice(-16);
var newNonceHash2 = (await CryptoWorker.sha1Hash(auth.newNonce.concat([2], authKeyAux))).slice(-16);
if(!bytesCmp(newNonceHash2, response.new_nonce_hash2)) {
throw new Error('[MT] Set_client_DH_params_answer new_nonce_hash2 mismatch');
}
return this.mtpSendSetClientDhParams(auth);
case 'dh_gen_fail':
//var newNonceHash3 = sha1BytesSync(auth.newNonce.concat([3], authKeyAux)).slice(-16);
var newNonceHash3 = (await CryptoWorker.sha1Hash(auth.newNonce.concat([3], authKeyAux))).slice(-16);
if(!bytesCmp(newNonceHash3, response.new_nonce_hash3)) {
throw new Error('[MT] Set_client_DH_params_answer new_nonce_hash3 mismatch');
}
throw new Error('[MT] Set_client_DH_params_answer fail');
}
}
// mtpAuth
public async auth(dcID: number): Promise<AuthOptions> {
if(dcID in this.cached) {
return this.cached[dcID];
}
let nonce = /* fNonce ? fNonce : */new Uint8Array(16).randomize();
/* var nonce = new Array(16);
MTProto.secureRandom.nextBytes(nonce); */
if(!dcConfigurator.chooseServer(dcID)) {
return Promise.reject(new Error('[MT] No server found for dc ' + dcID));
}
try {
let promise = this.mtpSendReqPQ({dcID, nonce});
this.cached[dcID] = promise;
return await promise;
} catch(err) {
delete this.cached[dcID];
throw err;
}
}
}
export default new Authorizer();