signal-desktop/ts/services/groupCredentialFetcher.ts

351 lines
10 KiB
TypeScript
Raw Normal View History

2020-10-30 20:34:04 +00:00
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
2023-11-13 22:33:43 +00:00
import { first, last, sortBy } from 'lodash';
2024-02-22 21:19:50 +00:00
import {
AuthCredentialWithPniResponse,
CallLinkAuthCredentialResponse,
GenericServerPublicParams,
} from '@signalapp/libsignal-client/zkgroup';
2020-09-09 02:25:05 +00:00
import { getClientZkAuthOperations } from '../util/zkgroup';
2020-09-09 02:25:05 +00:00
import type { GroupCredentialType } from '../textsecure/WebAPI';
2022-07-08 20:46:25 +00:00
import { strictAssert } from '../util/assert';
import * as durations from '../util/durations';
2021-06-09 22:28:54 +00:00
import { BackOff } from '../util/BackOff';
import { sleep } from '../util/sleep';
2022-07-08 20:46:25 +00:00
import { toDayMillis } from '../util/timestamp';
import { toTaggedPni } from '../types/ServiceId';
import { toPniObject, toAciObject } from '../util/ServiceId';
import * as log from '../logging/log';
2020-09-09 02:25:05 +00:00
export const GROUP_CREDENTIALS_KEY = 'groupCredentials';
2022-07-28 16:35:29 +00:00
type CredentialsDataType = ReadonlyArray<GroupCredentialType>;
2020-09-09 02:25:05 +00:00
type RequestDatesType = {
2022-07-08 20:46:25 +00:00
startDayInMs: number;
endDayInMs: number;
2020-09-09 02:25:05 +00:00
};
2024-02-22 21:19:50 +00:00
export type NextCredentialsType = {
2020-09-09 02:25:05 +00:00
today: GroupCredentialType;
tomorrow: GroupCredentialType;
};
let started = false;
2024-02-22 21:19:50 +00:00
function getCheckedGroupCredentials(reason: string): CredentialsDataType {
2022-07-08 20:46:25 +00:00
const result = window.storage.get('groupCredentials');
strictAssert(
result !== undefined,
`getCheckedCredentials: no credentials found, ${reason}`
);
return result;
}
2024-02-22 21:19:50 +00:00
function getCheckedCallLinkAuthCredentials(
reason: string
): CredentialsDataType {
const result = window.storage.get('callLinkAuthCredentials');
strictAssert(
result !== undefined,
`getCheckedCallLinkAuthCredentials: no credentials found, ${reason}`
);
return result;
}
2020-09-09 02:25:05 +00:00
export async function initializeGroupCredentialFetcher(): Promise<void> {
if (started) {
return;
}
log.info('initializeGroupCredentialFetcher: starting...');
2020-09-09 02:25:05 +00:00
started = true;
// Because we fetch eight days of credentials at a time, we really only need to run
// this about once a week. But there's no problem running it more often; it will do
// nothing if no new credentials are needed, and will only request needed credentials.
await runWithRetry(maybeFetchNewCredentials, {
scheduleAnother: 4 * durations.HOUR,
});
2020-09-09 02:25:05 +00:00
}
2021-06-09 22:28:54 +00:00
const BACKOFF_TIMEOUTS = [
durations.SECOND,
5 * durations.SECOND,
30 * durations.SECOND,
2 * durations.MINUTE,
5 * durations.MINUTE,
2021-06-09 22:28:54 +00:00
];
2020-09-09 02:25:05 +00:00
export async function runWithRetry(
fn: () => Promise<void>,
options: { scheduleAnother?: number } = {}
): Promise<void> {
2021-06-09 22:28:54 +00:00
const backOff = new BackOff(BACKOFF_TIMEOUTS);
2020-09-09 02:25:05 +00:00
// eslint-disable-next-line no-constant-condition
while (true) {
try {
// eslint-disable-next-line no-await-in-loop
await fn();
return;
} catch (error) {
2021-06-09 22:28:54 +00:00
const wait = backOff.getAndIncrement();
log.info(
2020-09-09 02:25:05 +00:00
`runWithRetry: ${fn.name} failed. Waiting ${wait}ms for retry. Error: ${error.stack}`
);
// eslint-disable-next-line no-await-in-loop
await sleep(wait);
}
}
// It's important to schedule our next run here instead of the level above; otherwise we
// could end up with multiple endlessly-retrying runs.
// eslint-disable-next-line no-unreachable -- Why is this here, its unreachable
2020-09-09 02:25:05 +00:00
const duration = options.scheduleAnother;
if (duration) {
log.info(
2020-09-09 02:25:05 +00:00
`runWithRetry: scheduling another run with a setTimeout duration of ${duration}ms`
);
setTimeout(async () => runWithRetry(fn, options), duration);
}
}
2024-02-22 21:19:50 +00:00
function getCredentialsForToday(
credentials: CredentialsDataType
2020-09-09 02:25:05 +00:00
): NextCredentialsType {
2022-07-08 20:46:25 +00:00
const today = toDayMillis(Date.now());
2024-02-22 21:19:50 +00:00
const todayIndex = credentials.findIndex(
2022-07-08 20:46:25 +00:00
(item: GroupCredentialType) => item.redemptionTime === today
2020-09-09 02:25:05 +00:00
);
if (todayIndex < 0) {
throw new Error(
2023-11-13 22:33:43 +00:00
'getCredentialsForToday: Cannot find credentials for today. ' +
2024-02-22 21:19:50 +00:00
`First: ${first(credentials)?.redemptionTime}, ` +
`last: ${last(credentials)?.redemptionTime}`
2020-09-09 02:25:05 +00:00
);
}
return {
2024-02-22 21:19:50 +00:00
today: credentials[todayIndex],
tomorrow: credentials[todayIndex + 1],
2020-09-09 02:25:05 +00:00
};
}
2024-02-22 21:19:50 +00:00
// In cases where we are at a day boundary, we might need to use tomorrow in a retry
export function getCheckedGroupCredentialsForToday(
reason: string
): NextCredentialsType {
return getCredentialsForToday(getCheckedGroupCredentials(reason));
}
export function getCheckedCallLinkAuthCredentialsForToday(
reason: string
): NextCredentialsType {
return getCredentialsForToday(getCheckedCallLinkAuthCredentials(reason));
}
2020-09-09 02:25:05 +00:00
export async function maybeFetchNewCredentials(): Promise<void> {
2022-07-08 20:46:25 +00:00
const logId = 'maybeFetchNewCredentials';
2024-02-22 21:19:50 +00:00
const maybeAci = window.textsecure.storage.user.getAci();
if (!maybeAci) {
2022-07-08 20:46:25 +00:00
log.info(`${logId}: no ACI, returning early`);
2020-09-09 02:25:05 +00:00
return;
}
2024-02-22 21:19:50 +00:00
const aci = maybeAci;
2022-07-08 20:46:25 +00:00
2024-02-22 21:19:50 +00:00
const prevGroupCredentials: CredentialsDataType =
2023-11-13 22:33:43 +00:00
window.storage.get('groupCredentials') ?? [];
2024-02-22 21:19:50 +00:00
const prevCallLinkAuthCredentials: CredentialsDataType =
window.storage.get('callLinkAuthCredentials') ?? [];
const requestDates = getDatesForRequest(prevGroupCredentials);
const requestDatesCallLinks = getDatesForRequest(prevCallLinkAuthCredentials);
2020-09-09 02:25:05 +00:00
2022-07-08 20:46:25 +00:00
const { server } = window.textsecure;
if (!server) {
log.error(`${logId}: unable to get server`);
2020-09-09 02:25:05 +00:00
return;
}
2024-02-22 21:19:50 +00:00
let startDayInMs: number;
let endDayInMs: number;
if (requestDates) {
startDayInMs = requestDates.startDayInMs;
endDayInMs = requestDates.endDayInMs;
if (requestDatesCallLinks) {
startDayInMs = Math.min(startDayInMs, requestDatesCallLinks.startDayInMs);
endDayInMs = Math.max(endDayInMs, requestDatesCallLinks.endDayInMs);
}
} else if (requestDatesCallLinks) {
startDayInMs = requestDatesCallLinks.startDayInMs;
endDayInMs = requestDatesCallLinks.endDayInMs;
} else {
log.info(`${logId}: no new credentials needed`);
return;
}
log.info(
2022-07-08 20:46:25 +00:00
`${logId}: fetching credentials for ${startDayInMs} through ${endDayInMs}`
2020-09-09 02:25:05 +00:00
);
const serverPublicParamsBase64 = window.getServerPublicParams();
const clientZKAuthOperations = getClientZkAuthOperations(
serverPublicParamsBase64
);
2023-08-16 20:54:39 +00:00
// Received credentials depend on us knowing up-to-date PNI. Use the latest
// value from the server and log error on mismatch.
2024-02-22 21:19:50 +00:00
const {
pni: untaggedPni,
credentials: rawCredentials,
callLinkAuthCredentials,
} = await server.getGroupCredentials({ startDayInMs, endDayInMs });
2023-08-16 20:54:39 +00:00
strictAssert(
untaggedPni,
'Server must give pni along with group credentials'
2022-07-28 16:35:29 +00:00
);
2023-08-16 20:54:39 +00:00
const pni = toTaggedPni(untaggedPni);
2022-07-28 16:35:29 +00:00
const localPni = window.storage.user.getPni();
if (pni !== localPni) {
2022-07-28 16:35:29 +00:00
log.error(`${logId}: local PNI ${localPni}, does not match remote ${pni}`);
}
2024-02-22 21:19:50 +00:00
function formatCredential(item: GroupCredentialType): GroupCredentialType {
const authCredential =
clientZKAuthOperations.receiveAuthCredentialWithPniAsServiceId(
toAciObject(aci),
toPniObject(pni),
item.redemptionTime,
new AuthCredentialWithPniResponse(
Buffer.from(item.credential, 'base64')
)
);
const credential = authCredential.serialize().toString('base64');
return {
redemptionTime: item.redemptionTime * durations.SECOND,
credential,
};
}
const newGroupCredentials =
sortCredentials(rawCredentials).map(formatCredential);
const genericServerPublicParamsBase64 = window.getGenericServerPublicParams();
const genericServerPublicParams = new GenericServerPublicParams(
Buffer.from(genericServerPublicParamsBase64, 'base64')
);
function formatCallingCredential(
item: GroupCredentialType
): GroupCredentialType {
const response = new CallLinkAuthCredentialResponse(
Buffer.from(item.credential, 'base64')
);
const authCredential = response.receive(
toAciObject(aci),
item.redemptionTime,
genericServerPublicParams
);
const credential = authCredential.serialize().toString('base64');
return {
redemptionTime: item.redemptionTime * durations.SECOND,
credential,
};
}
const newCallLinkAuthCredentialsRaw = sortCredentials(
callLinkAuthCredentials
);
const newCallLinkAuthCredentials = newCallLinkAuthCredentialsRaw.map(
formatCallingCredential
2022-07-28 16:35:29 +00:00
);
2020-09-09 02:25:05 +00:00
2022-07-08 20:46:25 +00:00
const today = toDayMillis(Date.now());
2024-02-22 21:19:50 +00:00
const prevGroupCredentialsCleaned =
prevGroupCredentials?.filter(
(item: GroupCredentialType) => item.redemptionTime >= today
) ?? [];
const prevCallLinkAuthCredentialsCleaned =
prevCallLinkAuthCredentials?.filter(
(item: GroupCredentialType) => item.redemptionTime >= today
) ?? [];
const finalGroupCredentials = [
...prevGroupCredentialsCleaned,
...newGroupCredentials,
];
const finalCallLinkAuthCredentials = [
...prevCallLinkAuthCredentialsCleaned,
...newCallLinkAuthCredentials,
];
2020-09-09 02:25:05 +00:00
2023-11-13 22:33:43 +00:00
log.info(
2024-02-22 21:19:50 +00:00
`${logId}: saving ${
finalGroupCredentials.length
} new group credentials, cleaning up ${
prevGroupCredentials.length - prevGroupCredentialsCleaned.length
} old group credentials, haveToday=${haveToday(finalGroupCredentials)}`
);
log.info(
`${logId}: saving ${
finalCallLinkAuthCredentials.length
} new call link auth credentials, cleaning up ${
prevCallLinkAuthCredentials.length -
prevCallLinkAuthCredentialsCleaned.length
} old call link auth credentials, haveToday=${haveToday(
finalCallLinkAuthCredentials
)}`
2023-11-13 22:33:43 +00:00
);
2024-02-22 21:19:50 +00:00
await window.storage.put('groupCredentials', finalGroupCredentials);
await window.storage.put(
'callLinkAuthCredentials',
finalCallLinkAuthCredentials
);
2022-07-08 20:46:25 +00:00
log.info(`${logId}: Save complete.`);
2020-09-09 02:25:05 +00:00
}
2023-11-13 22:33:43 +00:00
function haveToday(
data: CredentialsDataType,
today = toDayMillis(Date.now())
): boolean {
return data?.some(({ redemptionTime }) => redemptionTime === today);
}
2020-09-09 02:25:05 +00:00
export function getDatesForRequest(
2023-11-13 22:33:43 +00:00
data: CredentialsDataType
2020-09-09 02:25:05 +00:00
): RequestDatesType | undefined {
2022-07-08 20:46:25 +00:00
const today = toDayMillis(Date.now());
const sixDaysOut = today + 6 * durations.DAY;
2020-09-09 02:25:05 +00:00
const lastCredential = last(data);
2023-11-13 22:33:43 +00:00
if (
!haveToday(data, today) ||
!lastCredential ||
lastCredential.redemptionTime < today
) {
2020-09-09 02:25:05 +00:00
return {
2022-07-08 20:46:25 +00:00
startDayInMs: today,
endDayInMs: sixDaysOut,
2020-09-09 02:25:05 +00:00
};
}
if (lastCredential.redemptionTime >= sixDaysOut) {
2020-09-09 02:25:05 +00:00
return undefined;
}
return {
2022-07-08 20:46:25 +00:00
startDayInMs: lastCredential.redemptionTime + durations.DAY,
endDayInMs: sixDaysOut,
2020-09-09 02:25:05 +00:00
};
}
export function sortCredentials(
data: CredentialsDataType
): CredentialsDataType {
return sortBy(data, (item: GroupCredentialType) => item.redemptionTime);
}