Support for new GroupV2 groups
This commit is contained in:
parent
1ce0959fa1
commit
7a02cc815d
53 changed files with 7326 additions and 839 deletions
212
ts/services/groupCredentialFetcher.ts
Normal file
212
ts/services/groupCredentialFetcher.ts
Normal file
|
@ -0,0 +1,212 @@
|
|||
import { last, sortBy } from 'lodash';
|
||||
import { AuthCredentialResponse } from 'zkgroup';
|
||||
|
||||
import {
|
||||
base64ToCompatArray,
|
||||
compatArrayToBase64,
|
||||
getClientZkAuthOperations,
|
||||
} from '../util/zkgroup';
|
||||
|
||||
import { GroupCredentialType } from '../textsecure/WebAPI';
|
||||
|
||||
export const GROUP_CREDENTIALS_KEY = 'groupCredentials';
|
||||
|
||||
type CredentialsDataType = Array<GroupCredentialType>;
|
||||
type RequestDatesType = {
|
||||
startDay: number;
|
||||
endDay: number;
|
||||
};
|
||||
type NextCredentialsType = {
|
||||
today: GroupCredentialType;
|
||||
tomorrow: GroupCredentialType;
|
||||
};
|
||||
|
||||
const SECOND = 1000;
|
||||
const MINUTE = 60 * SECOND;
|
||||
const HOUR = 60 * MINUTE;
|
||||
const DAY = 24 * HOUR;
|
||||
|
||||
function getTodayInEpoch() {
|
||||
return Math.floor(Date.now() / DAY);
|
||||
}
|
||||
|
||||
async function sleep(ms: number) {
|
||||
// tslint:disable-next-line no-string-based-set-timeout
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
let started = false;
|
||||
|
||||
export async function initializeGroupCredentialFetcher(): Promise<void> {
|
||||
if (started) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.log.info('initializeGroupCredentialFetcher: starting...');
|
||||
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 * HOUR });
|
||||
}
|
||||
|
||||
type BackoffType = {
|
||||
[key: number]: number | undefined;
|
||||
max: number;
|
||||
};
|
||||
const BACKOFF: BackoffType = {
|
||||
0: SECOND,
|
||||
1: 5 * SECOND,
|
||||
2: 30 * SECOND,
|
||||
3: 2 * MINUTE,
|
||||
max: 5 * MINUTE,
|
||||
};
|
||||
|
||||
export async function runWithRetry(
|
||||
fn: () => Promise<void>,
|
||||
options: { scheduleAnother?: number } = {}
|
||||
): Promise<void> {
|
||||
let count = 0;
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
try {
|
||||
count += 1;
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await fn();
|
||||
return;
|
||||
} catch (error) {
|
||||
const wait = BACKOFF[count] || BACKOFF.max;
|
||||
window.log.info(
|
||||
`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.
|
||||
const duration = options.scheduleAnother;
|
||||
if (duration) {
|
||||
window.log.info(
|
||||
`runWithRetry: scheduling another run with a setTimeout duration of ${duration}ms`
|
||||
);
|
||||
setTimeout(async () => runWithRetry(fn, options), duration);
|
||||
}
|
||||
}
|
||||
|
||||
// In cases where we are at a day boundary, we might need to use tomorrow in a retry
|
||||
export function getCredentialsForToday(
|
||||
data: CredentialsDataType | undefined
|
||||
): NextCredentialsType {
|
||||
if (!data) {
|
||||
throw new Error('getCredentialsForToday: No credentials fetched!');
|
||||
}
|
||||
|
||||
const todayInEpoch = getTodayInEpoch();
|
||||
const todayIndex = data.findIndex(
|
||||
(item: GroupCredentialType) => item.redemptionTime === todayInEpoch
|
||||
);
|
||||
if (todayIndex < 0) {
|
||||
throw new Error(
|
||||
'getCredentialsForToday: Cannot find credentials for today'
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
today: data[todayIndex],
|
||||
tomorrow: data[todayIndex + 1],
|
||||
};
|
||||
}
|
||||
|
||||
export async function maybeFetchNewCredentials(): Promise<void> {
|
||||
const uuid = window.textsecure.storage.user.getUuid();
|
||||
if (!uuid) {
|
||||
window.log.info('maybeFetchCredentials: no UUID, returning early');
|
||||
return;
|
||||
}
|
||||
const previous: CredentialsDataType | undefined = window.storage.get(
|
||||
GROUP_CREDENTIALS_KEY
|
||||
);
|
||||
const requestDates = getDatesForRequest(previous);
|
||||
if (!requestDates) {
|
||||
window.log.info('maybeFetchCredentials: no new credentials needed');
|
||||
return;
|
||||
}
|
||||
|
||||
const accountManager = window.getAccountManager();
|
||||
if (!accountManager) {
|
||||
window.log.info('maybeFetchCredentials: unable to get AccountManager');
|
||||
return;
|
||||
}
|
||||
|
||||
const { startDay, endDay } = requestDates;
|
||||
window.log.info(
|
||||
`maybeFetchCredentials: fetching credentials for ${startDay} through ${endDay}`
|
||||
);
|
||||
|
||||
const serverPublicParamsBase64 = window.getServerPublicParams();
|
||||
const clientZKAuthOperations = getClientZkAuthOperations(
|
||||
serverPublicParamsBase64
|
||||
);
|
||||
const newCredentials = sortCredentials(
|
||||
await accountManager.getGroupCredentials(startDay, endDay)
|
||||
).map((item: GroupCredentialType) => {
|
||||
const authCredential = clientZKAuthOperations.receiveAuthCredential(
|
||||
uuid,
|
||||
item.redemptionTime,
|
||||
new AuthCredentialResponse(base64ToCompatArray(item.credential))
|
||||
);
|
||||
const credential = compatArrayToBase64(authCredential.serialize());
|
||||
|
||||
return {
|
||||
redemptionTime: item.redemptionTime,
|
||||
credential,
|
||||
};
|
||||
});
|
||||
|
||||
const todayInEpoch = getTodayInEpoch();
|
||||
const previousCleaned = previous
|
||||
? previous.filter(
|
||||
(item: GroupCredentialType) => item.redemptionTime >= todayInEpoch
|
||||
)
|
||||
: [];
|
||||
const finalCredentials = [...previousCleaned, ...newCredentials];
|
||||
|
||||
window.log.info('maybeFetchCredentials: Saving new credentials...');
|
||||
// Note: we don't wait for this to finish
|
||||
window.storage.put(GROUP_CREDENTIALS_KEY, finalCredentials);
|
||||
window.log.info('maybeFetchCredentials: Save complete.');
|
||||
}
|
||||
|
||||
export function getDatesForRequest(
|
||||
data?: CredentialsDataType
|
||||
): RequestDatesType | undefined {
|
||||
const todayInEpoch = getTodayInEpoch();
|
||||
const oneWeekOut = todayInEpoch + 7;
|
||||
|
||||
const lastCredential = last(data);
|
||||
if (!lastCredential || lastCredential.redemptionTime < todayInEpoch) {
|
||||
return {
|
||||
startDay: todayInEpoch,
|
||||
endDay: oneWeekOut,
|
||||
};
|
||||
}
|
||||
|
||||
if (lastCredential.redemptionTime >= oneWeekOut) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
startDay: lastCredential.redemptionTime + 1,
|
||||
endDay: oneWeekOut,
|
||||
};
|
||||
}
|
||||
|
||||
export function sortCredentials(
|
||||
data: CredentialsDataType
|
||||
): CredentialsDataType {
|
||||
return sortBy(data, (item: GroupCredentialType) => item.redemptionTime);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue