Skip to content

Commit 3b665c7

Browse files
zubidenAjaxy
authored andcommitted
GramJS: Add PFS support (#7382)
1 parent 1dba428 commit 3b665c7

15 files changed

Lines changed: 437 additions & 96 deletions

File tree

‎src/api/types/misc.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,11 @@ export interface ApiWebSession {
150150
export interface ApiSessionData {
151151
mainDcId: number;
152152
keys: Record<number, string>;
153+
authKeyCreatedAt?: Record<number, number>;
154+
tempAuthKey?: {
155+
key: string;
156+
expiresAt: number;
157+
};
153158
isTest?: true;
154159
}
155160

‎src/lib/gramjs/client/TelegramClient.ts‎

Lines changed: 75 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ import { authFlow, checkAuthorization } from './auth';
4545
import { downloadFile } from './downloadFile';
4646
import { uploadFile } from './uploadFile';
4747

48-
import { HttpStreamError } from '../extensions/HttpStream';
48+
import { PermAuthKeyInvalidError } from '../errors/Common';
4949
import { generateRandomBigInt, sleep } from '../Helpers';
5050
import RequestState from '../network/RequestState';
5151
import Session from '../sessions/Abstract';
@@ -201,6 +201,9 @@ class TelegramClient {
201201
private _exportedSenderPromises: Record<number, Record<number, Promise<MTProtoSender> | undefined>> = {};
202202
private _exportedSenderRefCounter: Record<number, Record<number, number>> = {};
203203
private _waitingForAuthKey: Record<number, Promise<void> | undefined> = {};
204+
// Keyed by `dcId` for regular hosts and `-dcId` for media hosts, which only accept temporary keys created for them
205+
private _tempAuthKeys: Record<number, AuthKey | undefined> = {};
206+
private _tempAuthKeyPromises: Record<number, Promise<AuthKey> | undefined> = {};
204207
private _exportedSenderReleaseTimeouts: Record<number, Record<number, TimeoutId | undefined>> = {};
205208
private _loopStarted = false;
206209
private _isSwitchingDc = false;
@@ -310,6 +313,9 @@ class TelegramClient {
310313
autoReconnect: this._autoReconnect,
311314
connectTimeout: this._timeout,
312315
authKeyCallback: this._authKeyCallback.bind(this),
316+
getTempAuthKey: this._getTempAuthKey.bind(this),
317+
dropTempAuthKey: this._dropTempAuthKey.bind(this),
318+
initConnection: this._initWith,
313319
updateCallback: this._handleUpdate.bind(this),
314320
getShouldDebugExportedSenders: this.getShouldDebugExportedSenders.bind(this),
315321
isMainSender: true,
@@ -369,6 +375,13 @@ class TelegramClient {
369375
this.defaultDcId, DC.ipAddress, this._args.useWSS ? 443 : 80, this._args.isTestServerRequested,
370376
);
371377
}
378+
379+
const { dcId } = this.session;
380+
if (dcId in this._tempAuthKeys) {
381+
this.session.setTempAuthKey(this._tempAuthKeys[dcId]);
382+
} else {
383+
this._tempAuthKeys[dcId] = this.session.getTempAuthKey();
384+
}
372385
}
373386

374387
setPingCallback(callback: () => Promise<void>) {
@@ -535,7 +548,7 @@ class TelegramClient {
535548
// authKey's are associated with a server, which has now changed
536549
// so it's not valid anymore. Set to None to force recreating it.
537550
await this._sender.authKey.setKey(undefined);
538-
this.session.setAuthKey(undefined);
551+
this._resetAuthKey(newDc);
539552
this._isSwitchingDc = true;
540553
this.disconnect();
541554
this._sender = undefined;
@@ -546,12 +559,59 @@ class TelegramClient {
546559
this.session.setAuthKey(authKey, dcId);
547560
}
548561

562+
_resetAuthKey(dcId: number) {
563+
this.session.setAuthKey(undefined, dcId);
564+
this._tempAuthKeyPromises[dcId] = undefined;
565+
this._tempAuthKeyPromises[-dcId] = undefined;
566+
this._setTempAuthKey(dcId, undefined);
567+
this._setTempAuthKey(-dcId, undefined);
568+
}
569+
570+
_getTempAuthKey(clusterId: number, create: () => Promise<AuthKey>, isUsable: (authKey: AuthKey) => boolean) {
571+
const pendingAuthKey = this._tempAuthKeyPromises[clusterId];
572+
if (pendingAuthKey) return pendingAuthKey;
573+
574+
const authKey = this._tempAuthKeys[clusterId];
575+
if (authKey && isUsable(authKey)) return Promise.resolve(authKey);
576+
577+
let isReset = false;
578+
const creationPromise = create();
579+
const promise = creationPromise
580+
.then((newAuthKey) => {
581+
if (this._tempAuthKeyPromises[clusterId] !== promise) {
582+
isReset = true;
583+
throw new Error('Temporary auth key was reset');
584+
}
585+
586+
this._setTempAuthKey(clusterId, newAuthKey);
587+
return newAuthKey;
588+
})
589+
.finally(() => {
590+
if (this._tempAuthKeyPromises[clusterId] === promise) this._tempAuthKeyPromises[clusterId] = undefined;
591+
});
592+
this._tempAuthKeyPromises[clusterId] = promise;
593+
// The creator's bound key remains usable when a reset invalidates the shared promise
594+
return promise.catch((err: unknown) => {
595+
if (isReset) return creationPromise;
596+
throw err;
597+
});
598+
}
599+
600+
_dropTempAuthKey(clusterId: number, keyId: bigint) {
601+
if (this._tempAuthKeys[clusterId]?.keyId === keyId) this._setTempAuthKey(clusterId, undefined);
602+
}
603+
604+
_setTempAuthKey(clusterId: number, authKey?: AuthKey) {
605+
this._tempAuthKeys[clusterId] = authKey;
606+
if (clusterId === this.session.dcId) this.session.setTempAuthKey(authKey);
607+
}
608+
549609
// endregion
550610
// export region
551611

552612
async _cleanupExportedSender(dcId: number, index: number) {
553613
if (this.session.dcId !== dcId) {
554-
this.session.setAuthKey(undefined, dcId);
614+
this._resetAuthKey(dcId);
555615
}
556616
// eslint-disable-next-line no-console
557617
if (this._shouldDebugExportedSenders) console.log(`🧹 Cleanup idx=${index} dcId=${dcId}`);
@@ -573,11 +633,10 @@ class TelegramClient {
573633
await this._waitingForAuthKey[dcId];
574634

575635
const authKey = this.session.getAuthKey(dcId);
576-
const replacementKey = authKey?.getKey();
577636

578-
hasAuthKey = Boolean(replacementKey);
637+
hasAuthKey = Boolean(authKey?.getKey());
579638
if (hasAuthKey) {
580-
await sender.authKey.setKey(replacementKey);
639+
await sender.authKey.setKey(authKey);
581640
}
582641
} else {
583642
this._waitingForAuthKey[dcId] = new Promise((resolve) => {
@@ -597,13 +656,15 @@ class TelegramClient {
597656
isTestServer: this.session.isTestServer,
598657
// Premium DCs are not stable for obtaining auth keys, so need to we first connect to regular ones
599658
isPremium: hasAuthKey ? isPremium : false,
659+
isMediaDc: hasAuthKey,
600660
}), false, new this._fallbackConnection({
601661
ip: dc.ipAddress,
602662
port: dc.port,
603663
dcId,
604664
loggers: this._log,
605665
isTestServer: this.session.isTestServer,
606666
isPremium: hasAuthKey ? isPremium : false,
667+
isMediaDc: hasAuthKey,
607668
}));
608669

609670
if (this.session.dcId !== dcId && !sender._authenticated) {
@@ -639,14 +700,13 @@ class TelegramClient {
639700

640701
return sender;
641702
} catch (err: any) {
642-
if (err instanceof HttpStreamError && err.status === 404 && this.session.dcId !== dcId) {
703+
if (err instanceof PermAuthKeyInvalidError && this.session.dcId !== dcId) {
643704
const pendingAuthKey = this._waitingForAuthKey[dcId];
644705
if (pendingAuthKey && !firstConnectResolver) {
645706
await pendingAuthKey;
646707
const authKey = this.session.getAuthKey(dcId);
647-
const replacementKey = authKey?.getKey();
648-
if (replacementKey && authKey !== sender.authKey) {
649-
await sender.authKey.setKey(replacementKey);
708+
if (authKey?.getKey() && authKey !== sender.authKey) {
709+
await sender.authKey.setKey(authKey);
650710
}
651711
hasAuthKey = Boolean(sender.authKey.getKey());
652712
} else if (sender.authKey.getKey()) {
@@ -657,7 +717,7 @@ class TelegramClient {
657717
}
658718

659719
await sender.authKey.setKey(undefined);
660-
this.session.setAuthKey(undefined, dcId);
720+
this._resetAuthKey(dcId);
661721
sender._authenticated = false;
662722
hasAuthKey = false;
663723
}
@@ -683,7 +743,7 @@ class TelegramClient {
683743

684744
await sleep(1000);
685745
sender.disconnect();
686-
if (err instanceof HttpStreamError && err.status === 404 && this.session.dcId === dcId && hasAuthKey) {
746+
if (err instanceof PermAuthKeyInvalidError && this.session.dcId === dcId) {
687747
throw err;
688748
}
689749
}
@@ -814,6 +874,9 @@ class TelegramClient {
814874
autoReconnect: this._autoReconnect,
815875
connectTimeout: this._timeout,
816876
authKeyCallback: this._authKeyCallback.bind(this),
877+
getTempAuthKey: this._getTempAuthKey.bind(this),
878+
dropTempAuthKey: this._dropTempAuthKey.bind(this),
879+
initConnection: this._initWith,
817880
isMainSender: dcId === this.session.dcId,
818881
isExported: true,
819882
updateCallback: this._handleUpdate.bind(this),

‎src/lib/gramjs/crypto/AuthKey.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ export class AuthKey {
1818

1919
keyId?: bigint;
2020

21+
expiresAt?: number;
22+
23+
createdAt?: number;
24+
2125
constructor(value?: Uint8Array<ArrayBuffer>, hash?: Uint8Array<ArrayBuffer>) {
2226
if (!hash || !value) {
2327
return;
@@ -31,6 +35,8 @@ export class AuthKey {
3135
}
3236

3337
async setKey(value?: Uint8Array<ArrayBuffer> | AuthKey) {
38+
this.expiresAt = undefined;
39+
this.createdAt = undefined;
3440
if (!value) {
3541
this._key = undefined;
3642
this.auxHash = undefined;
@@ -43,6 +49,8 @@ export class AuthKey {
4349
this.auxHash = value.auxHash;
4450
this.keyId = value.keyId;
4551
this._hash = value._hash;
52+
this.expiresAt = value.expiresAt;
53+
this.createdAt = value.createdAt;
4654
return;
4755
}
4856
this._key = value;

‎src/lib/gramjs/errors/Common.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,12 @@ export class SecurityError extends Error {
8585
}
8686
}
8787

88+
export class PermAuthKeyInvalidError extends Error {
89+
constructor() {
90+
super('The permanent auth key is invalid');
91+
}
92+
}
93+
8894
/**
8995
* Occurs when there's a hash mismatch between the decrypted CDN file
9096
* and its expected hash.

‎src/lib/gramjs/extensions/MessagePacker.ts‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,15 @@ export default class MessagePacker {
105105
this.setReady?.(true);
106106
}
107107

108-
getBeacon(state: RequestState) {
108+
remove(state: RequestState) {
109+
const index = this._queue.indexOf(state);
110+
if (index === -1) return false;
111+
112+
this._queue.splice(index, 1);
113+
return true;
114+
}
115+
116+
getBeacon(state: RequestState, presetMsgId?: bigint) {
109117
const buffer = new BinaryWriter(new Uint8Array(0));
110118
const size = state.data.length + TLMessage.SIZE_OVERHEAD;
111119
if (size <= MessageContainer.MAXIMUM_SIZE) {
@@ -114,7 +122,7 @@ export default class MessagePacker {
114122
afterId = state.after.msgId;
115123
}
116124
const { msgId, seqNo } = this._state.writeDataAsMessage(
117-
buffer, state.data, state.isContentRelated, afterId,
125+
buffer, state.data, state.isContentRelated, afterId, presetMsgId,
118126
);
119127
state.msgId = msgId;
120128
state.seqNo = seqNo;

‎src/lib/gramjs/network/Authenticator.ts‎

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,17 @@ const MAX_PQ_BYTES = 8;
3737
const MAX_PQ = 0x7FFFFFFFFFFFFFFFn;
3838
const MAX_DH_GEN_ATTEMPTS = 5;
3939

40+
type TempAuthKeyParams = {
41+
expiresIn: number;
42+
isMedia?: boolean;
43+
};
44+
4045
export async function doAuthentication(
4146
sender: MTProtoPlainSender,
4247
log: any,
4348
dcId: number,
4449
isTestServer?: boolean,
50+
tempAuthKeyParams?: TempAuthKeyParams,
4551
) {
4652
// Step 1 sending: PQ Request, endianness doesn't matter since it's random
4753
let bytes = generateRandomBytes(16);
@@ -92,15 +98,19 @@ export async function doAuthentication(
9298

9399
bytes = generateRandomBytes(32);
94100
const newNonce = readBigIntFromBuffer(bytes, true, true);
95-
const pqInnerData = new Api.PQInnerDataDc({
101+
const pqInnerDataParams = {
96102
pq: getByteArray(pq), // unsigned
97103
p: pBuffer,
98104
q: qBuffer,
99105
nonce: resPQ.nonce,
100106
serverNonce: resPQ.serverNonce,
101107
newNonce,
102-
dc: buildAuthDcId(dcId, isTestServer),
103-
}).getBytes();
108+
dc: buildAuthDcId(dcId, isTestServer, tempAuthKeyParams?.isMedia),
109+
};
110+
const pqInnerData = (tempAuthKeyParams
111+
? new Api.PQInnerDataTempDc({ ...pqInnerDataParams, expiresIn: tempAuthKeyParams.expiresIn })
112+
: new Api.PQInnerDataDc(pqInnerDataParams)
113+
).getBytes();
104114
if (pqInnerData.length > 144) {
105115
throw new SecurityError('Step 1 invalid nonce from server');
106116
}
@@ -302,6 +312,7 @@ export async function doAuthentication(
302312
throw new SecurityError('Step 3 DH retry limit exceeded');
303313
}
304314

305-
function buildAuthDcId(dcId: number, isTestServer?: boolean) {
306-
return isTestServer ? dcId + 10000 : dcId;
315+
function buildAuthDcId(dcId: number, isTestServer?: boolean, isMedia?: boolean) {
316+
const authDcId = isTestServer ? dcId + 10000 : dcId;
317+
return isMedia ? -authDcId : authDcId;
307318
}

0 commit comments

Comments
 (0)