Skip to content

Commit 9c082be

Browse files
authored
Throttle E2EE Decryption Errors to Prevent Memory Leak (#1720)
1 parent 8858c8b commit 9c082be

File tree

1 file changed

+79
-7
lines changed

1 file changed

+79
-7
lines changed

src/e2ee/worker/FrameCryptor.ts

Lines changed: 79 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,19 @@ export class FrameCryptor extends BaseFrameCryptor {
7070

7171
private isTransformActive: boolean = false;
7272

73+
/**
74+
* Throttling mechanism for decryption errors to prevent memory leaks
75+
*/
76+
private lastErrorTimestamp: Map<string, number> = new Map();
77+
78+
private errorCounts: Map<string, number> = new Map();
79+
80+
private readonly ERROR_THROTTLE_MS = 1000; // Emit error at most once per second
81+
82+
private readonly MAX_ERRORS_PER_MINUTE = 5; // Maximum errors to emit per minute per key
83+
84+
private readonly ERROR_WINDOW_MS = 60000; // 1 minute window
85+
7386
constructor(opts: {
7487
keys: ParticipantKeyHandler;
7588
participantIdentity: string;
@@ -119,6 +132,8 @@ export class FrameCryptor extends BaseFrameCryptor {
119132
unsetParticipant() {
120133
workerLogger.debug('unsetting participant', this.logContext);
121134
this.participantIdentity = undefined;
135+
this.lastErrorTimestamp = new Map();
136+
this.errorCounts = new Map();
122137
}
123138

124139
isEnabled() {
@@ -210,6 +225,66 @@ export class FrameCryptor extends BaseFrameCryptor {
210225
this.sifTrailer = trailer;
211226
}
212227

228+
/**
229+
* Checks if we should emit an error based on throttling rules to prevent memory leaks
230+
* @param errorKey - unique key identifying the error context
231+
* @returns true if the error should be emitted, false otherwise
232+
*/
233+
private shouldEmitError(errorKey: string): boolean {
234+
const now = Date.now();
235+
const lastErrorTime = this.lastErrorTimestamp.get(errorKey) ?? 0;
236+
const errorCount = this.errorCounts.get(errorKey) ?? 0;
237+
238+
// Reset count if we're in a new time window
239+
if (now - lastErrorTime > this.ERROR_WINDOW_MS) {
240+
this.errorCounts.set(errorKey, 0);
241+
this.lastErrorTimestamp.set(errorKey, now);
242+
return true;
243+
}
244+
245+
// Check if we've exceeded the throttle time
246+
if (now - lastErrorTime < this.ERROR_THROTTLE_MS) {
247+
return false;
248+
}
249+
250+
// Check if we've exceeded the max errors per window
251+
if (errorCount >= this.MAX_ERRORS_PER_MINUTE) {
252+
// Only log a warning once when hitting the limit
253+
if (errorCount === this.MAX_ERRORS_PER_MINUTE) {
254+
workerLogger.warn(`Suppressing further decryption errors for ${this.participantIdentity}`, {
255+
...this.logContext,
256+
errorKey,
257+
});
258+
this.errorCounts.set(errorKey, errorCount + 1);
259+
}
260+
return false;
261+
}
262+
263+
// Update tracking
264+
this.lastErrorTimestamp.set(errorKey, now);
265+
this.errorCounts.set(errorKey, errorCount + 1);
266+
return true;
267+
}
268+
269+
/**
270+
* Emits a throttled error to prevent memory leaks from repeated decryption failures
271+
* @param error - the CryptorError to emit
272+
*/
273+
private emitThrottledError(error: CryptorError) {
274+
const errorKey = `${this.participantIdentity}-${error.reason}-decrypt`;
275+
276+
if (this.shouldEmitError(errorKey)) {
277+
const errorCount = this.errorCounts.get(errorKey) ?? 0;
278+
if (errorCount > 1) {
279+
workerLogger.debug(`Decryption error (${errorCount} occurrences in window)`, {
280+
...this.logContext,
281+
reason: CryptorErrorReason[error.reason],
282+
});
283+
}
284+
this.emit(CryptorEvent.Error, error);
285+
}
286+
}
287+
213288
/**
214289
* Function that will be injected in a stream and will encrypt the given encoded frames.
215290
*
@@ -245,8 +320,7 @@ export class FrameCryptor extends BaseFrameCryptor {
245320
}
246321
const keySet = this.keys.getKeySet();
247322
if (!keySet) {
248-
this.emit(
249-
CryptorEvent.Error,
323+
this.emitThrottledError(
250324
new CryptorError(
251325
`key set not found for ${
252326
this.participantIdentity
@@ -318,8 +392,7 @@ export class FrameCryptor extends BaseFrameCryptor {
318392
}
319393
} else {
320394
workerLogger.debug('failed to encrypt, emitting error', this.logContext);
321-
this.emit(
322-
CryptorEvent.Error,
395+
this.emitThrottledError(
323396
new CryptorError(
324397
`encryption key missing for encoding`,
325398
CryptorErrorReason.MissingKey,
@@ -379,7 +452,7 @@ export class FrameCryptor extends BaseFrameCryptor {
379452
if (error instanceof CryptorError && error.reason === CryptorErrorReason.InvalidKey) {
380453
// emit an error if the key handler thinks we have a valid key
381454
if (this.keys.hasValidKey) {
382-
this.emit(CryptorEvent.Error, error);
455+
this.emitThrottledError(error);
383456
this.keys.decryptionFailure(keyIndex);
384457
}
385458
} else {
@@ -389,8 +462,7 @@ export class FrameCryptor extends BaseFrameCryptor {
389462
} else {
390463
// emit an error if the key index is out of bounds but the key handler thinks we still have a valid key
391464
workerLogger.warn(`skipping decryption due to missing key at index ${keyIndex}`);
392-
this.emit(
393-
CryptorEvent.Error,
465+
this.emitThrottledError(
394466
new CryptorError(
395467
`missing key at index ${keyIndex} for participant ${this.participantIdentity}`,
396468
CryptorErrorReason.MissingKey,

0 commit comments

Comments
 (0)