Skip to content

Commit

Permalink
runfix: Improve renewal timer computation (#16935)
Browse files Browse the repository at this point in the history
  • Loading branch information
atomrc authored Feb 28, 2024
1 parent dcfd5d0 commit 5e9856c
Show file tree
Hide file tree
Showing 7 changed files with 111 additions and 61 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"@lexical/react": "0.12.5",
"@wireapp/avs": "9.6.12",
"@wireapp/commons": "5.2.5",
"@wireapp/core": "45.0.4",
"@wireapp/core": "45.0.5",
"@wireapp/react-ui-kit": "9.16.0",
"@wireapp/store-engine-dexie": "2.1.8",
"@wireapp/webapp-events": "0.20.1",
Expand Down
6 changes: 3 additions & 3 deletions src/script/E2EIdentity/E2EIdentityEnrollment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*/

import {waitFor} from '@testing-library/react';
import {TaskScheduler} from '@wireapp/core/lib/util';
import {LowPrecisionTaskScheduler} from '@wireapp/core/lib/util/LowPrecisionTaskScheduler';
import {container} from 'tsyringe';

import {PrimaryModal} from 'Components/Modals/PrimaryModal';
Expand Down Expand Up @@ -163,12 +163,12 @@ describe('E2EIHandler', () => {
jest.spyOn(coreMock.service!.e2eIdentity!, 'isEnrollmentInProgress').mockResolvedValue(false);
jest.spyOn(coreMock.service!.e2eIdentity!, 'isFreshMLSSelfClient').mockResolvedValue(false);

const taskMock = jest.spyOn(TaskScheduler, 'addTask');
const taskMock = jest.spyOn(LowPrecisionTaskScheduler, 'addTask');

const instance = await E2EIHandler.getInstance().initialize(params);

await instance.startTimers();

expect(taskMock).toHaveBeenCalledWith(expect.objectContaining({key: 'enrollmentTimer', persist: true}));
expect(taskMock).toHaveBeenCalledWith(expect.objectContaining({key: 'enrollmentTimer'}));
});
});
40 changes: 22 additions & 18 deletions src/script/E2EIdentity/E2EIdentityEnrollment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@
*
*/

import {LowPrecisionTaskScheduler} from '@wireapp/core/lib/util/LowPrecisionTaskScheduler';
import {amplify} from 'amplify';
import {container} from 'tsyringe';

import {TypedEventEmitter} from '@wireapp/commons';
import {util} from '@wireapp/core';
import {WebAppEvents} from '@wireapp/webapp-events';

import {PrimaryModal, removeCurrentModal} from 'Components/Modals/PrimaryModal';
Expand All @@ -38,7 +38,6 @@ import {getModalOptions, ModalType} from './Modals';
import {OIDCService} from './OIDCService';
import {OIDCServiceStore} from './OIDCService/OIDCServiceStorage';

const {TaskScheduler} = util;
interface E2EIHandlerParams {
discoveryUrl: string;
gracePeriodInSeconds: number;
Expand Down Expand Up @@ -147,21 +146,28 @@ export class E2EIHandler extends TypedEventEmitter<Events> {

const timerKey = 'enrollmentTimer';
const identity = await getActiveWireIdentity();
const {firingDate, isSnoozable} = getEnrollmentTimer(identity, e2eActivatedAt, this.config.gracePeriodInMs);
const {firingDate: computedFiringDate, isSnoozable} = getEnrollmentTimer(
identity,
e2eActivatedAt,
this.config.gracePeriodInMs,
);

const task = async () => {
EnrollmentStore.clear.timer();
await this.processEnrollmentUponExpiry(isSnoozable);
};

const task = async () => this.processEnrollmentUponExpiry(isSnoozable);
const firingDate = EnrollmentStore.get.timer() || computedFiringDate;
EnrollmentStore.store.timer(firingDate);

if (TaskScheduler.hasActiveTask(timerKey)) {
TaskScheduler.continueTask({
key: timerKey,
task,
});
if (firingDate <= Date.now()) {
void task();
} else {
TaskScheduler.addTask({
LowPrecisionTaskScheduler.addTask({
key: timerKey,
task,
firingDate: firingDate,
persist: true,
intervalDelay: TIME_IN_MILLIS.SECOND * 10,
});
}
return firingDate - Date.now();
Expand Down Expand Up @@ -250,6 +256,10 @@ export class E2EIHandler extends TypedEventEmitter<Events> {
await this.cleanUp(false);
this.emit('deviceStatusUpdated', {status: 'valid'});

if (isCertificateRenewal) {
await this.startTimers();
}

await this.showSuccessMessage(isCertificateRenewal);
} catch (error) {
this.logger.error('E2EI enrollment failed', error);
Expand Down Expand Up @@ -278,13 +288,7 @@ export class E2EIHandler extends TypedEventEmitter<Events> {
extraParams: {
isRenewal: isCertificateRenewal,
},
primaryActionFn: async () => {
if (isCertificateRenewal) {
// restart the timers for device certificate renewal
await this.startTimers();
}
resolve();
},
primaryActionFn: resolve,
secondaryActionFn: () => {
amplify.publish(WebAppEvents.PREFERENCES.MANAGE_DEVICES);
resolve();
Expand Down
4 changes: 4 additions & 0 deletions src/script/E2EIdentity/Enrollment.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,19 @@
*/

const e2eActivatedAtKey = 'e2eActivatedAt';
const e2eTimer = 'e2eTimer';

export const EnrollmentStore = {
store: {
e2eiActivatedAt: (time: number) => localStorage.setItem(e2eActivatedAtKey, String(time)),
timer: (time: number) => localStorage.setItem(e2eTimer, String(time)),
},
get: {
e2eiActivatedAt: () => Number(localStorage.getItem(e2eActivatedAtKey)),
timer: () => Number(localStorage.getItem(e2eTimer)),
},
clear: {
deviceCreatedAt: () => localStorage.removeItem(e2eActivatedAtKey),
timer: () => localStorage.removeItem(e2eTimer),
},
};
42 changes: 30 additions & 12 deletions src/script/E2EIdentity/EnrollmentTimer/EnrollmentTimer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,17 @@
*
*/

import {TimeInMillis} from '@wireapp/commons/lib/util/TimeUtil';

import {getEnrollmentTimer, messageRetentionTime} from './EnrollmentTimer';

import {MLSStatuses} from '../E2EIdentityVerification';

describe('e2ei delays', () => {
const gracePeriod = 3600;
const gracePeriod = 7 * TimeInMillis.DAY;
beforeEach(() => {
jest.useFakeTimers();
jest.setSystemTime(0);
jest.setSystemTime(1709050878009);
});

it('should return an immediate delay if the identity is expired', () => {
Expand All @@ -34,32 +36,48 @@ describe('e2ei delays', () => {
expect(delay).toEqual({firingDate: Date.now(), isSnoozable: false});
});

it('should return a snoozable timer if device is new and still in the grace period', () => {
const {firingDate, isSnoozable} = getEnrollmentTimer(undefined, Date.now(), gracePeriod);
it.each([
[TimeInMillis.DAY * 2, TimeInMillis.DAY * 30, TimeInMillis.DAY],
[TimeInMillis.DAY, TimeInMillis.DAY * 30, TimeInMillis.HOUR * 4],
[TimeInMillis.HOUR, TimeInMillis.DAY * 30, TimeInMillis.MINUTE * 15],
[TimeInMillis.HOUR * 3, TimeInMillis.DAY * 30, TimeInMillis.HOUR],
[TimeInMillis.MINUTE * 10, TimeInMillis.DAY * 30, TimeInMillis.MINUTE * 5],
[TimeInMillis.MINUTE * 30, TimeInMillis.DAY * 30, TimeInMillis.MINUTE * 15],
])('should return a snoozable timer if device is still valid', (validityPeriod, grace, expectedTimer) => {
const {firingDate, isSnoozable} = getEnrollmentTimer(
{certificate: ' ', notAfter: (Date.now() + validityPeriod) / 1000} as any,
Date.now(),
grace,
);

expect(isSnoozable).toBeTruthy();
expect(firingDate).toBeLessThanOrEqual(gracePeriod);
expect(firingDate).toBe(Date.now() + expectedTimer);
});

it('should return a snoozable timer if device is certified and still in the grace period', () => {
it('should return a snoozable timer in the long future if device is certified before the grace period', () => {
const deadline = Date.now() + messageRetentionTime + gracePeriod + 1000;
const gracePeriodStartingPoint = deadline - gracePeriod;

const {firingDate, isSnoozable} = getEnrollmentTimer(
{certificate: ' ', notAfter: Date.now() + messageRetentionTime + gracePeriod + 1000} as any,
{certificate: ' ', notAfter: deadline / 1000} as any,
Date.now(),
gracePeriod,
);

expect(isSnoozable).toBeTruthy();
expect(firingDate).toBeLessThanOrEqual(gracePeriod);
expect(firingDate).toBe(gracePeriodStartingPoint);
});

it('should return a non snoozable timer if device is certified about to expired', () => {
it('should return a non snoozable timer if device is out of the grace period', () => {
const deadline = Date.now() + gracePeriod + 1000;
const gracePeriodStartingPoint = deadline - gracePeriod;
const {firingDate, isSnoozable} = getEnrollmentTimer(
{certificate: ' ', notAfter: Date.now() + gracePeriod + 1000} as any,
{certificate: ' ', notAfter: deadline / 1000} as any,
Date.now(),
gracePeriod,
);

expect(isSnoozable).toBeFalsy();
expect(firingDate).toBeLessThanOrEqual(gracePeriod);
expect(isSnoozable).toBeTruthy();
expect(firingDate).toBe(gracePeriodStartingPoint);
});
});
68 changes: 46 additions & 22 deletions src/script/E2EIdentity/EnrollmentTimer/EnrollmentTimer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,31 +32,58 @@ export const ONE_DAY = TimeInMillis.DAY;
// message retention time on backend (hardcoded to 28 days)
export const messageRetentionTime = 28 * TimeInMillis.DAY;

type GracePeriod = {
/** start date of the grace period (unix timestamp) */
start: number;
/** end date of the grace period (unix timestamp) */
end: number;
};
/**
* Will return a suitable snooze time based on the grace period
* @param expiryDate - the full grace period length in milliseconds
* @param deadline - the full grace period length in milliseconds
*/
function getNextTick(expiryDate: number, gracePeriodDuration: number, isFirstEnrollment: boolean): number {
const leftoverTimer = expiryDate - Date.now();
function getNextTick({end, start}: GracePeriod): number {
if (Date.now() >= end) {
// If the grace period is over, we should force the user to enroll
return 0;
}

// First a first enrollment we only consider the grace period. For enrolled devices we also consider the backend message retention time
const extraDelay = isFirstEnrollment ? 0 : randomInt(TimeInMillis.DAY) + messageRetentionTime;
if (Date.now() < start) {
// If we are not in the grace period yet, we start the timer when the grace period starts
return start - Date.now();
}
const validityPeriod = end - Date.now();

const gracePeriod = Math.max(0, Math.min(gracePeriodDuration, leftoverTimer - extraDelay));
if (gracePeriod <= 0) {
return 0;
if (validityPeriod <= FIFTEEN_MINUTES) {
return Math.min(FIVE_MINUTES, validityPeriod);
} else if (validityPeriod <= ONE_HOUR) {
return Math.min(FIFTEEN_MINUTES, validityPeriod);
} else if (validityPeriod <= FOUR_HOURS) {
return Math.min(ONE_HOUR, validityPeriod);
} else if (validityPeriod <= ONE_DAY) {
return Math.min(FOUR_HOURS, validityPeriod);
}
return Math.min(ONE_DAY, validityPeriod);
}

if (gracePeriod <= FIFTEEN_MINUTES) {
return Math.min(FIVE_MINUTES, gracePeriod);
} else if (gracePeriod <= ONE_HOUR) {
return Math.min(FIFTEEN_MINUTES, gracePeriod);
} else if (gracePeriod <= FOUR_HOURS) {
return Math.min(ONE_HOUR, gracePeriod);
} else if (gracePeriod <= ONE_DAY) {
return Math.min(FOUR_HOURS, gracePeriod);
function getGracePeriod(
identity: WireIdentity | undefined,
e2eActivatedAt: number,
teamGracePeriodDuration: number,
): GracePeriod {
const isFirstEnrollment = !identity?.certificate;
if (isFirstEnrollment) {
// For a new device, the deadline is the e2ei activate date + the grace period
return {end: e2eActivatedAt + teamGracePeriodDuration, start: Date.now()};
}
return Math.min(ONE_DAY, gracePeriod);

// To be sure the device does not expire, we want to keep a safe delay
const safeDelay = randomInt(TimeInMillis.DAY) + messageRetentionTime;

const end = Number(identity.notAfter) * TimeInMillis.SECOND;
const start = Math.max(end - safeDelay, end - teamGracePeriodDuration);

return {end, start};
}

export function getEnrollmentTimer(
Expand All @@ -68,12 +95,9 @@ export function getEnrollmentTimer(
return {isSnoozable: false, firingDate: Date.now()};
}

const isFirstEnrollment = !identity?.certificate;
const expiryDate = isFirstEnrollment
? e2eiActivatedAt + teamGracePeriodDuration
: Number(identity.notAfter) * TimeInMillis.SECOND;
const deadline = getGracePeriod(identity, e2eiActivatedAt, teamGracePeriodDuration);
const nextTick = getNextTick(deadline);

const nextTick = getNextTick(expiryDate, teamGracePeriodDuration, isFirstEnrollment);
// When logging in to a old device that doesn't have an identity yet, we trigger an enrollment timer
return {isSnoozable: nextTick > 0, firingDate: Date.now() + nextTick};
}
10 changes: 5 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4754,9 +4754,9 @@ __metadata:
languageName: node
linkType: hard

"@wireapp/core@npm:45.0.4":
version: 45.0.4
resolution: "@wireapp/core@npm:45.0.4"
"@wireapp/core@npm:45.0.5":
version: 45.0.5
resolution: "@wireapp/core@npm:45.0.5"
dependencies:
"@wireapp/api-client": ^26.10.12
"@wireapp/commons": ^5.2.5
Expand All @@ -4775,7 +4775,7 @@ __metadata:
long: ^5.2.0
uuidjs: 4.2.13
zod: 3.22.4
checksum: e9b3af41c2092c589f20eda71a79336489008ebcddeb42076c298a2b694bd9318ea83d8141e998b3d5ea5729535f95c01b305b8a7750b8e90e9f63bac5f6e708
checksum: 6efa9c7351b6ac6aeea6288437e3ddb41e1928f9876d4ae3d853c7c4787f2864fce5052d484e49f07e181fe85937e4fa61ea6df5a9ad67781e2340cc80e3f1d7
languageName: node
linkType: hard

Expand Down Expand Up @@ -17444,7 +17444,7 @@ __metadata:
"@wireapp/avs": 9.6.12
"@wireapp/commons": 5.2.5
"@wireapp/copy-config": 2.1.14
"@wireapp/core": 45.0.4
"@wireapp/core": 45.0.5
"@wireapp/eslint-config": 3.0.5
"@wireapp/prettier-config": 0.6.3
"@wireapp/react-ui-kit": 9.16.0
Expand Down

0 comments on commit 5e9856c

Please sign in to comment.