From b757031f0bbfb9beb25441ab91728b2a07685045 Mon Sep 17 00:00:00 2001 From: Jamie Kyle <113370520+jamiebuilds-signal@users.noreply.github.com> Date: Mon, 6 May 2024 16:19:09 -0700 Subject: [PATCH] Switch to the /v2/ storage-service endpoints for group operations --- package.json | 2 +- protos/Groups.proto | 23 ++++++++++++++---- ts/groups.ts | 16 +++++++++---- ts/textsecure/SendMessage.ts | 10 ++++---- ts/textsecure/WebAPI.ts | 45 ++++++++++++++++++++++-------------- yarn.lock | 8 +++---- 6 files changed, 68 insertions(+), 36 deletions(-) diff --git a/package.json b/package.json index 610d9d28efc..5f299508406 100644 --- a/package.json +++ b/package.json @@ -211,7 +211,7 @@ "@formatjs/intl": "2.6.7", "@indutny/rezip-electron": "1.3.1", "@mixer/parallel-prettier": "2.0.3", - "@signalapp/mock-server": "6.3.0", + "@signalapp/mock-server": "6.4.1", "@storybook/addon-a11y": "7.4.5", "@storybook/addon-actions": "7.4.5", "@storybook/addon-controls": "7.4.5", diff --git a/protos/Groups.proto b/protos/Groups.proto index 1101a238b51..dd8f6a59540 100644 --- a/protos/Groups.proto +++ b/protos/Groups.proto @@ -220,6 +220,19 @@ message GroupChange { uint32 changeEpoch = 3; // Allows clients to decide whether their change logic can successfully apply this diff } +// External credentials + +message ExternalGroupCredential { + string token = 1; +} + +// API responses + +message GroupResponse { + Group group = 1; + bytes groupSendEndorsementResponse = 2; +} + message GroupChanges { message GroupChangeState { GroupChange groupChange = 1; @@ -227,6 +240,12 @@ message GroupChanges { } repeated GroupChangeState groupChanges = 1; + bytes groupSendEndorsementResponse = 2; +} + +message GroupChangeResponse { + GroupChange groupChange = 1; + bytes groupSendEndorsementResponse = 2; } message GroupAttributeBlob { @@ -238,10 +257,6 @@ message GroupAttributeBlob { } } -message GroupExternalCredential { - string token = 1; -} - message GroupInviteLink { message GroupInviteLinkContentsV1 { bytes groupMasterKey = 1; diff --git a/ts/groups.ts b/ts/groups.ts index 45a0f089c99..7ead5031802 100644 --- a/ts/groups.ts +++ b/ts/groups.ts @@ -1445,7 +1445,7 @@ async function uploadGroupChange({ actions: Proto.GroupChange.IActions; group: ConversationAttributesType; inviteLinkPassword?: string; -}): Promise { +}): Promise { const logId = idForLogging(group.groupId); // Ensure we have the credentials we need before attempting GroupsV2 operations @@ -1551,11 +1551,13 @@ export async function modifyGroupV2({ } // Upload. If we don't have permission, the server will return an error here. - const groupChange = await uploadGroupChange({ + const groupChangeResponse = await uploadGroupChange({ actions, inviteLinkPassword, group: conversation.attributes, }); + const { groupChange } = groupChangeResponse; + strictAssert(groupChange, 'missing groupChange'); const groupChangeBuffer = Proto.GroupChange.encode(groupChange).finish(); @@ -2755,12 +2757,13 @@ export async function respondToGroupV2Migration({ `respondToGroupV2Migration/${logId}: Failed to access log endpoint; fetching full group state` ); try { - firstGroupState = await makeRequestWithTemporalRetry({ + const groupResponse = await makeRequestWithTemporalRetry({ logId: `getGroup/${logId}`, publicParams, secretParams, request: (sender, options) => sender.getGroup(options), }); + firstGroupState = groupResponse.group; } catch (secondError) { if (secondError.code === GROUP_ACCESS_DENIED_CODE) { log.info( @@ -3669,13 +3672,16 @@ async function updateGroupViaState({ throw new Error('updateGroupViaState: group was missing publicParams!'); } - const groupState = await makeRequestWithTemporalRetry({ + const groupResponse = await makeRequestWithTemporalRetry({ logId: `getGroup/${logId}`, publicParams, secretParams, request: (sender, requestOptions) => sender.getGroup(requestOptions), }); + const groupState = groupResponse.group; + strictAssert(groupState, 'Group state must be present'); + const decryptedGroupState = decryptGroupState( groupState, secretParams, @@ -3809,7 +3815,7 @@ async function updateGroupViaLogs({ // `integrateGroupChanges`. let revisionToFetch = isNumber(currentRevision) ? currentRevision : undefined; - let response; + let response: GroupLogResponseType; const changes: Array = []; do { // eslint-disable-next-line no-await-in-loop diff --git a/ts/textsecure/SendMessage.ts b/ts/textsecure/SendMessage.ts index 69eb1aaae3b..d3e0e02323f 100644 --- a/ts/textsecure/SendMessage.ts +++ b/ts/textsecure/SendMessage.ts @@ -2153,7 +2153,7 @@ export default class MessageSender { async createGroup( group: Readonly, options: Readonly - ): Promise { + ): Promise { return this.server.createGroup(group, options); } @@ -2166,7 +2166,7 @@ export default class MessageSender { async getGroup( options: Readonly - ): Promise { + ): Promise { return this.server.getGroup(options); } @@ -2192,7 +2192,7 @@ export default class MessageSender { changes: Readonly, options: Readonly, inviteLinkBase64?: string - ): Promise { + ): Promise { return this.server.modifyGroup(changes, options, inviteLinkBase64); } @@ -2243,8 +2243,8 @@ export default class MessageSender { async getGroupMembershipToken( options: Readonly - ): Promise { - return this.server.getGroupExternalCredential(options); + ): Promise { + return this.server.getExternalGroupCredential(options); } public async sendChallengeResponse( diff --git a/ts/textsecure/WebAPI.ts b/ts/textsecure/WebAPI.ts index c8bb5f45c25..ca1ff44f0e8 100644 --- a/ts/textsecure/WebAPI.ts +++ b/ts/textsecure/WebAPI.ts @@ -542,9 +542,9 @@ const URL_CALLS = { getBackupCDNCredentials: 'v1/archives/auth/read', getBackupUploadForm: 'v1/archives/upload/form', getBackupMediaUploadForm: 'v1/archives/media/upload/form', - groupLog: 'v1/groups/logs', + groupLog: 'v2/groups/logs', groupJoinedAtVersion: 'v1/groups/joined_at_version', - groups: 'v1/groups', + groups: 'v2/groups', groupsViaLink: 'v1/groups/join/', groupToken: 'v1/groups/token', keys: 'v2/keys', @@ -719,6 +719,7 @@ export type GroupLogResponseType = { start?: number; end?: number; changes: Proto.GroupChanges; + groupSendEndorsementResponse: Uint8Array | null; }; export type ProfileRequestDataType = { @@ -1153,7 +1154,7 @@ export type WebAPIType = { createGroup: ( group: Proto.IGroup, options: GroupCredentialsType - ) => Promise; + ) => Promise; deleteUsername: (abortSignal?: AbortSignal) => Promise; downloadOnboardingStories: ( version: string, @@ -1178,7 +1179,7 @@ export type WebAPIType = { ) => Promise; getAvatar: (path: string) => Promise; getHasSubscription: (subscriberId: Uint8Array) => Promise; - getGroup: (options: GroupCredentialsType) => Promise; + getGroup: (options: GroupCredentialsType) => Promise; getGroupFromLink: ( inviteLinkPassword: string | undefined, auth: GroupCredentialsType @@ -1187,9 +1188,9 @@ export type WebAPIType = { getGroupCredentials: ( options: GetGroupCredentialsOptionsType ) => Promise; - getGroupExternalCredential: ( + getExternalGroupCredential: ( options: GroupCredentialsType - ) => Promise; + ) => Promise; getGroupLog: ( options: GetGroupLogOptionsType, credentials: GroupCredentialsType @@ -1259,7 +1260,7 @@ export type WebAPIType = { changes: Proto.GroupChange.IActions, options: GroupCredentialsType, inviteLinkBase64?: string - ) => Promise; + ) => Promise; modifyStorageRecords: MessageSender['modifyStorageRecords']; postBatchIdentityCheck: ( elements: VerifyServiceIdRequestType @@ -1669,7 +1670,7 @@ export function initialize({ getGroup, getGroupAvatar, getGroupCredentials, - getGroupExternalCredential, + getExternalGroupCredential, getGroupFromLink, getGroupLog, getHasSubscription, @@ -3695,9 +3696,9 @@ export function initialize({ return response; } - async function getGroupExternalCredential( + async function getExternalGroupCredential( options: GroupCredentialsType - ): Promise { + ): Promise { const basicAuth = generateGroupAuth( options.groupPublicParamsHex, options.authCredentialPresentationHex @@ -3713,7 +3714,7 @@ export function initialize({ disableSessionResumption: true, }); - return Proto.GroupExternalCredential.decode(response); + return Proto.ExternalGroupCredential.decode(response); } function verifyAttributes(attributes: Proto.IAvatarUploadAttributes) { @@ -3817,14 +3818,14 @@ export function initialize({ async function createGroup( group: Proto.IGroup, options: GroupCredentialsType - ): Promise { + ): Promise { const basicAuth = generateGroupAuth( options.groupPublicParamsHex, options.authCredentialPresentationHex ); const data = Proto.Group.encode(group).finish(); - await _ajax({ + const response = await _ajax({ basicAuth, call: 'groups', contentType: 'application/x-protobuf', @@ -3832,12 +3833,15 @@ export function initialize({ host: storageUrl, disableSessionResumption: true, httpType: 'PUT', + responseType: 'bytes', }); + + return Proto.GroupResponse.decode(response); } async function getGroup( options: GroupCredentialsType - ): Promise { + ): Promise { const basicAuth = generateGroupAuth( options.groupPublicParamsHex, options.authCredentialPresentationHex @@ -3853,7 +3857,7 @@ export function initialize({ responseType: 'bytes', }); - return Proto.Group.decode(response); + return Proto.GroupResponse.decode(response); } async function getGroupFromLink( @@ -3889,7 +3893,7 @@ export function initialize({ changes: Proto.GroupChange.IActions, options: GroupCredentialsType, inviteLinkBase64?: string - ): Promise { + ): Promise { const basicAuth = generateGroupAuth( options.groupPublicParamsHex, options.authCredentialPresentationHex @@ -3916,7 +3920,7 @@ export function initialize({ : undefined, }); - return Proto.GroupChange.decode(response); + return Proto.GroupChangeResponse.decode(response); } async function getGroupLog( @@ -3966,6 +3970,10 @@ export function initialize({ disableSessionResumption: true, httpType: 'GET', responseType: 'byteswithdetails', + headers: { + // TODO(jamie): To be implmented in DESKTOP-699 + 'Cached-Send-Endorsements': '0', + }, urlParameters: `/${startVersion}?` + `includeFirstState=${Boolean(includeFirstState)}&` + @@ -3974,6 +3982,7 @@ export function initialize({ }); const { data, response } = withDetails; const changes = Proto.GroupChanges.decode(data); + const { groupSendEndorsementResponse } = changes; if (response && response.status === 206) { const range = response.headers.get('Content-Range'); @@ -3994,12 +4003,14 @@ export function initialize({ start, end, currentRevision, + groupSendEndorsementResponse, }; } } return { changes, + groupSendEndorsementResponse, }; } diff --git a/yarn.lock b/yarn.lock index 939a66ce92d..74f22be9c6c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4001,10 +4001,10 @@ type-fest "^3.5.0" uuid "^8.3.0" -"@signalapp/mock-server@6.3.0": - version "6.3.0" - resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-6.3.0.tgz#5715fc1ff4517310caacc767ab4790530f11c673" - integrity sha512-mC4QXqS7+MH1p3U7kTuUqJsFUHfBZ6wemuzvQvhk3+4bRoc77Wynu1uIN0WRLhx/faOGwBkSiAWNiLhQt0Vscw== +"@signalapp/mock-server@6.4.1": + version "6.4.1" + resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-6.4.1.tgz#b49700f8d43b0c76d3f02820dd3b3da82a910f12" + integrity sha512-is75JwGL2CjLJ3NakMxw6rkgx379aKc3n328lSaiwLKVgBpuG/ms8wF3fNALxFstKoMl41lPzooOMWeqm+ubVQ== dependencies: "@signalapp/libsignal-client" "^0.42.0" debug "^4.3.2"