Implement endorsements for group send

This commit is contained in:
Jamie Kyle 2024-09-06 10:52:19 -07:00 committed by GitHub
parent 5f82c82803
commit 24536e1342
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 816 additions and 269 deletions

View file

@ -281,20 +281,21 @@ export default class OutgoingMessage {
async getKeysForServiceId(
serviceId: ServiceIdString,
updateDevices?: Array<number>
updateDevices: Array<number> | null
): Promise<void> {
const { sendMetadata } = this;
const info =
sendMetadata && sendMetadata[serviceId]
? sendMetadata[serviceId]
: { accessKey: undefined };
: { accessKey: null };
const { accessKey } = info;
const { accessKeyFailed } = await getKeysForServiceId(
serviceId,
this.server,
updateDevices,
accessKey
updateDevices ?? null,
accessKey,
null
);
if (accessKeyFailed && !this.failoverServiceIds.includes(serviceId)) {
this.failoverServiceIds.push(serviceId);
@ -607,8 +608,8 @@ export default class OutgoingMessage {
return p.then(async () => {
const resetDevices =
error.code === 410
? response.staleDevices
: response.missingDevices;
? (response.staleDevices ?? null)
: (response.missingDevices ?? null);
return this.getKeysForServiceId(serviceId, resetDevices).then(
// We continue to retry as long as the error code was 409; the assumption is
// that we'll request new device info and the next request will succeed.
@ -677,7 +678,7 @@ export default class OutgoingMessage {
serviceId,
});
if (deviceIds.length === 0) {
await this.getKeysForServiceId(serviceId);
await this.getKeysForServiceId(serviceId, null);
}
await this.reloadDevicesAndSend(serviceId, true)();
} catch (error) {

View file

@ -72,6 +72,7 @@ import { SECOND } from '../util/durations';
import { safeParseNumber } from '../util/numbers';
import { isStagingServer } from '../util/isStagingServer';
import type { IWebSocketResource } from './WebsocketResources';
import type { GroupSendToken } from '../types/GroupSendEndorsements';
// Note: this will break some code that expects to be able to use err.response when a
// web request fails, because it will force it to text. But it is very useful for
@ -184,10 +185,12 @@ type PromiseAjaxOptionsType = {
| {
unauthenticated?: false;
accessKey?: string;
groupSendToken?: GroupSendToken;
}
| {
unauthenticated: true;
accessKey: undefined | string;
groupSendToken: undefined | GroupSendToken;
}
);
@ -330,11 +333,13 @@ async function _promiseAjax(
fetchOptions.headers['Content-Length'] = contentLength.toString();
}
const { accessKey, basicAuth, unauthenticated } = options;
const { accessKey, basicAuth, groupSendToken, unauthenticated } = options;
if (basicAuth) {
fetchOptions.headers.Authorization = `Basic ${basicAuth}`;
} else if (unauthenticated) {
if (accessKey) {
if (groupSendToken != null) {
fetchOptions.headers['Group-Send-Token'] = Bytes.toBase64(groupSendToken);
} else if (accessKey != null) {
// Access key is already a Base64 string
fetchOptions.headers['Unidentified-Access-Key'] = accessKey;
}
@ -708,10 +713,12 @@ type AjaxOptionsType = {
| {
unauthenticated?: false;
accessKey?: string;
groupSendToken?: GroupSendToken;
}
| {
unauthenticated: true;
accessKey: undefined | string;
groupSendToken: undefined | GroupSendToken;
}
);
@ -1279,7 +1286,7 @@ export type WebAPIType = {
getKeysForServiceIdUnauth: (
serviceId: ServiceIdString,
deviceId?: number,
options?: { accessKey?: string }
options?: { accessKey?: string; groupSendToken?: GroupSendToken }
) => Promise<ServerKeysType>;
getMyKeyCounts: (serviceIdKind: ServiceIdKind) => Promise<ServerKeyCountType>;
getOnboardingStoryManifest: () => Promise<{
@ -1398,7 +1405,8 @@ export type WebAPIType = {
) => Promise<void>;
sendWithSenderKey: (
payload: Uint8Array,
accessKeys: Uint8Array,
accessKeys: Uint8Array | undefined,
groupSendToken: GroupSendToken | undefined,
timestamp: number,
options: {
online?: boolean;
@ -1868,6 +1876,7 @@ export function initialize({
version,
unauthenticated: param.unauthenticated,
accessKey: param.accessKey,
groupSendToken: param.groupSendToken,
abortSignal: param.abortSignal,
};
@ -2204,6 +2213,7 @@ export function initialize({
redactUrl: _createRedactor(hashBase64),
unauthenticated: true,
accessKey: undefined,
groupSendToken: undefined,
})
);
}
@ -2246,6 +2256,7 @@ export function initialize({
responseType: 'json',
unauthenticated: true,
accessKey,
groupSendToken: undefined,
redactUrl: _createRedactor(
serviceId,
profileKeyVersion,
@ -2418,6 +2429,7 @@ export function initialize({
responseType: 'json',
unauthenticated: true,
accessKey: undefined,
groupSendToken: undefined,
})
);
}
@ -2454,6 +2466,7 @@ export function initialize({
},
unauthenticated: true,
accessKey: undefined,
groupSendToken: undefined,
})
);
@ -2469,6 +2482,7 @@ export function initialize({
},
unauthenticated: true,
accessKey: undefined,
groupSendToken: undefined,
})
);
@ -2491,6 +2505,7 @@ export function initialize({
},
unauthenticated: true,
accessKey: undefined,
groupSendToken: undefined,
})
);
@ -2506,6 +2521,7 @@ export function initialize({
urlParameters: `/${serviceId}`,
unauthenticated: true,
accessKey: undefined,
groupSendToken: undefined,
});
return true;
} catch (error) {
@ -2595,6 +2611,7 @@ export function initialize({
},
unauthenticated: true,
accessKey: undefined,
groupSendToken: undefined,
})
);
@ -2801,6 +2818,7 @@ export function initialize({
httpType: 'GET',
unauthenticated: true,
accessKey: undefined,
groupSendToken: undefined,
headers,
responseType: 'json',
});
@ -2836,6 +2854,7 @@ export function initialize({
httpType: 'GET',
unauthenticated: true,
accessKey: undefined,
groupSendToken: undefined,
headers,
responseType: 'json',
});
@ -2887,6 +2906,7 @@ export function initialize({
httpType: 'GET',
unauthenticated: true,
accessKey: undefined,
groupSendToken: undefined,
headers,
responseType: 'json',
});
@ -2900,6 +2920,7 @@ export function initialize({
httpType: 'POST',
unauthenticated: true,
accessKey: undefined,
groupSendToken: undefined,
headers,
});
}
@ -2931,6 +2952,7 @@ export function initialize({
httpType: 'GET',
unauthenticated: true,
accessKey: undefined,
groupSendToken: undefined,
headers,
urlParameters: `?cdn=${cdn}`,
responseType: 'json',
@ -2962,6 +2984,7 @@ export function initialize({
httpType: 'PUT',
unauthenticated: true,
accessKey: undefined,
groupSendToken: undefined,
headers,
jsonData: {
backupIdPublicKey: Bytes.toBase64(backupIdPublicKey),
@ -2978,6 +3001,7 @@ export function initialize({
httpType: 'PUT',
unauthenticated: true,
accessKey: undefined,
groupSendToken: undefined,
headers,
responseType: 'json',
jsonData: {
@ -3018,6 +3042,7 @@ export function initialize({
httpType: 'POST',
unauthenticated: true,
accessKey: undefined,
groupSendToken: undefined,
headers,
jsonData: {
mediaToDelete: mediaToDelete.map(({ cdn, mediaId }) => {
@ -3047,6 +3072,7 @@ export function initialize({
httpType: 'GET',
unauthenticated: true,
accessKey: undefined,
groupSendToken: undefined,
headers,
responseType: 'json',
urlParameters: `?${params.join('&')}`,
@ -3194,7 +3220,10 @@ export function initialize({
async function getKeysForServiceIdUnauth(
serviceId: ServiceIdString,
deviceId?: number,
{ accessKey }: { accessKey?: string } = {}
{
accessKey,
groupSendToken,
}: { accessKey?: string; groupSendToken?: GroupSendToken } = {}
) {
const keys = (await _ajax({
call: 'keys',
@ -3204,6 +3233,7 @@ export function initialize({
validateResponse: { identityKey: 'string', devices: 'object' },
unauthenticated: true,
accessKey,
groupSendToken,
})) as ServerKeyResponseType;
return handleKeys(keys);
}
@ -3239,6 +3269,7 @@ export function initialize({
responseType: 'json',
unauthenticated: true,
accessKey,
groupSendToken: undefined,
});
}
@ -3274,7 +3305,8 @@ export function initialize({
async function sendWithSenderKey(
data: Uint8Array,
accessKeys: Uint8Array,
accessKeys: Uint8Array | undefined,
groupSendToken: GroupSendToken | undefined,
timestamp: number,
{
online,
@ -3298,7 +3330,8 @@ export function initialize({
urlParameters: `?ts=${timestamp}${onlineParam}${urgentParam}${storyParam}`,
responseType: 'json',
unauthenticated: true,
accessKey: Bytes.toBase64(accessKeys),
accessKey: accessKeys != null ? Bytes.toBase64(accessKeys) : undefined,
groupSendToken,
});
const parseResult = multiRecipient200ResponseSchema.safeParse(response);
if (parseResult.success) {
@ -4240,6 +4273,7 @@ export function initialize({
responseType: 'json',
unauthenticated: true,
accessKey: undefined,
groupSendToken: undefined,
redactUrl: _createRedactor(formattedId),
});

View file

@ -23,18 +23,22 @@ import type { ServiceIdString } from '../types/ServiceId';
import type { ServerKeysType, WebAPIType } from './WebAPI';
import * as log from '../logging/log';
import { isRecord } from '../util/isRecord';
import type { GroupSendToken } from '../types/GroupSendEndorsements';
import { onFailedToSendWithEndorsements } from '../util/groupSendEndorsements';
export async function getKeysForServiceId(
serviceId: ServiceIdString,
server: WebAPIType,
devicesToUpdate?: Array<number>,
accessKey?: string
devicesToUpdate: Array<number> | null,
accessKey: string | null,
groupSendToken: GroupSendToken | null
): Promise<{ accessKeyFailed?: boolean }> {
try {
const { keys, accessKeyFailed } = await getServerKeys(
serviceId,
server,
accessKey
accessKey,
groupSendToken
);
await handleServerKeys(serviceId, keys, devicesToUpdate);
@ -53,44 +57,67 @@ export async function getKeysForServiceId(
}
}
function isUnauthorizedError(error: unknown) {
return (
isRecord(error) &&
typeof error.code === 'number' &&
(error.code === 401 || error.code === 403)
);
}
async function getServerKeys(
serviceId: ServiceIdString,
server: WebAPIType,
accessKey?: string
): Promise<{ accessKeyFailed?: boolean; keys: ServerKeysType }> {
try {
if (!accessKey) {
return {
keys: await server.getKeysForServiceId(serviceId),
};
}
accessKey: string | null,
groupSendToken: GroupSendToken | null
): Promise<{ accessKeyFailed: boolean; keys: ServerKeysType }> {
// Return true only when attempted with access key
let accessKeyFailed = false;
return {
keys: await server.getKeysForServiceIdUnauth(serviceId, undefined, {
accessKey,
}),
};
} catch (error: unknown) {
if (
accessKey &&
isRecord(error) &&
typeof error.code === 'number' &&
(error.code === 401 || error.code === 403)
) {
return {
accessKeyFailed: true,
keys: await server.getKeysForServiceId(serviceId),
};
if (accessKey != null) {
// Try the access key first
try {
const keys = await server.getKeysForServiceIdUnauth(
serviceId,
undefined,
{ accessKey }
);
return { keys, accessKeyFailed };
} catch (error) {
accessKeyFailed = true;
if (!isUnauthorizedError(error)) {
throw error;
}
}
throw error;
}
if (groupSendToken != null) {
try {
const keys = await server.getKeysForServiceIdUnauth(
serviceId,
undefined,
{ groupSendToken }
);
return { keys, accessKeyFailed };
} catch (error) {
if (!isUnauthorizedError(error)) {
throw error;
} else {
onFailedToSendWithEndorsements(error);
}
}
}
return {
keys: await server.getKeysForServiceId(serviceId),
accessKeyFailed,
};
}
async function handleServerKeys(
serviceId: ServiceIdString,
response: ServerKeysType,
devicesToUpdate?: Array<number>
devicesToUpdate: Array<number> | null
): Promise<void> {
const ourAci = window.textsecure.storage.user.getCheckedAci();
const sessionStore = new Sessions({ ourServiceId: ourAci });
@ -100,10 +127,7 @@ async function handleServerKeys(
response.devices.map(async device => {
const { deviceId, registrationId, pqPreKey, preKey, signedPreKey } =
device;
if (
devicesToUpdate !== undefined &&
!devicesToUpdate.includes(deviceId)
) {
if (devicesToUpdate != null && !devicesToUpdate.includes(deviceId)) {
return;
}