From b60e076b6a62c8c6a1bdb335b3e834b52bb6ea00 Mon Sep 17 00:00:00 2001 From: trevor-signal <131492920+trevor-signal@users.noreply.github.com> Date: Tue, 25 Aug 2026 10:02:04 -0400 Subject: [PATCH] Adopt libsignal list backup media endpoint --- .changeset/olive-pugs-repeat.md | 5 ++ packages/mock-server/src/server/grpc.ts | 44 ++++++++++++- .../mock-server/src/server/ws/connection.ts | 29 -------- ts/jobs/AttachmentBackupManager.preload.ts | 7 +- ts/services/backups/api.preload.ts | 22 +++---- ts/services/backups/credentials.preload.ts | 44 ++++--------- ts/textsecure/WebAPI.preload.ts | 66 +++++++++---------- ts/types/backups.node.ts | 7 -- 8 files changed, 103 insertions(+), 121 deletions(-) create mode 100644 .changeset/olive-pugs-repeat.md diff --git a/.changeset/olive-pugs-repeat.md b/.changeset/olive-pugs-repeat.md new file mode 100644 index 0000000000..b980d5a41c --- /dev/null +++ b/.changeset/olive-pugs-repeat.md @@ -0,0 +1,5 @@ +--- +'@signalapp/mock-server': major +--- + +Moves backup listMedia to GRPC diff --git a/packages/mock-server/src/server/grpc.ts b/packages/mock-server/src/server/grpc.ts index 79131b4da1..0dbe3f553d 100644 --- a/packages/mock-server/src/server/grpc.ts +++ b/packages/mock-server/src/server/grpc.ts @@ -31,10 +31,11 @@ import { BackupAuthError, BackupInfo, BackupMediaBatchResult, + BackupMediaList, Server, } from './base'; import { parsePassword } from './common'; -import { toURLSafeBase64 } from '../util'; +import { fromURLSafeBase64, toURLSafeBase64 } from '../util'; const debug = createDebug('mock:grpc'); @@ -620,6 +621,46 @@ export const createHandler = (server: Server): RequestHandler => { }, ); + const onListBackupMedia = grpcRoute( + 'org.signal.chat.backup.BackupsAnonymous/ListMedia', + async ({ signedPresentation, cursor, limit }) => { + assert(signedPresentation != null); + assert(limit > 0, 'Missing or invalid limit'); + + let list: BackupMediaList; + try { + list = await server.listBackupMedia(signedPresentation, { + cursor: cursor ?? undefined, + limit, + }); + } catch (error) { + if (error instanceof BackupAuthError) { + return { + response: { failedAuthentication: { description: error.message } }, + }; + } + throw error; + } + + return { + response: { + listResult: { + page: list.storedMediaObjects.map( + ({ cdn, mediaId, objectLength }) => ({ + cdn, + mediaId: fromURLSafeBase64(mediaId), + length: BigInt(objectLength), + }), + ), + backupDir: list.backupDir, + mediaDir: list.mediaDir, + cursor: list.cursor ?? null, + }, + }, + }; + }, + ); + const onReserveUsername = authenticatedGrpcRoute( 'org.signal.chat.account.Accounts/ReserveUsernameHash', async ({ usernameHashes }, device) => { @@ -720,6 +761,7 @@ export const createHandler = (server: Server): RequestHandler => { onGetMessageBackupInfo, onGetMediaBackupInfo, onCopyBackupMedia, + onListBackupMedia, ...ALL_METHODS.map((method) => method('/*', notFoundAfterAuth)), ); diff --git a/packages/mock-server/src/server/ws/connection.ts b/packages/mock-server/src/server/ws/connection.ts index 605437658e..ba8d5b5289 100644 --- a/packages/mock-server/src/server/ws/connection.ts +++ b/packages/mock-server/src/server/ws/connection.ts @@ -19,7 +19,6 @@ import { signalservice as Proto } from '../../../protos/compiled'; import { Device } from '../../data/device'; import { AtomicLinkingDataSchema, - BackupHeadersSchema, CreateCallLinkAuthSchema, CreateVerificationSessionSchema, DeviceKeysSchema, @@ -901,34 +900,6 @@ export class Connection extends Service { }), ); - this.router.get( - '/v1/archives/media', - async (_params, _body, headers, query = {}) => { - if (this.device) { - return [400, { error: 'Extraneous authentication' }]; - } - - if (typeof query.limit !== 'string') { - return [400, { error: 'Missing limit param' }]; - } - - const limit = parseInt(query.limit, 10); - if (limit <= 0) { - return [400, { error: 'Invalid limit' }]; - } - - const cursor = query.cursor; - - return [ - 200, - await this.server.listBackupMedia( - BackupHeadersSchema.parse(headers), - { cursor: cursor != null ? String(cursor) : undefined, limit }, - ), - ]; - }, - ); - // // Keepalive // diff --git a/ts/jobs/AttachmentBackupManager.preload.ts b/ts/jobs/AttachmentBackupManager.preload.ts index 4707e68e61..42108366c8 100644 --- a/ts/jobs/AttachmentBackupManager.preload.ts +++ b/ts/jobs/AttachmentBackupManager.preload.ts @@ -594,10 +594,9 @@ async function copyToBackupTier({ mediaTier: MediaTier.STANDARD, }); - const { backupAuth } = - await dependencies.backupsService.credentials.getForToday( - BackupCredentialType.Media - ); + const backupAuth = await dependencies.backupsService.credentials.getForToday( + BackupCredentialType.Media + ); const outcomes = await dependencies.copyBackupMedia({ auth: backupAuth, diff --git a/ts/services/backups/api.preload.ts b/ts/services/backups/api.preload.ts index a575978553..5b5a2e84a9 100644 --- a/ts/services/backups/api.preload.ts +++ b/ts/services/backups/api.preload.ts @@ -70,12 +70,12 @@ export class BackupAPI { } async #refreshType(type: BackupCredentialType): Promise { - const auth = (await this.#credentials.getForToday(type)).backupAuth; + const auth = await this.#credentials.getForToday(type); return refreshBackup({ auth }); } public async getMessageBackupInfo(): Promise { - const { backupAuth } = await this.#credentials.getForToday( + const backupAuth = await this.#credentials.getForToday( BackupCredentialType.Messages ); const backupInfo = await getMessageBackupInfo({ auth: backupAuth }); @@ -84,7 +84,7 @@ export class BackupAPI { } public async getMediaBackupInfo(): Promise { - const { backupAuth } = await this.#credentials.getForToday( + const backupAuth = await this.#credentials.getForToday( BackupCredentialType.Media ); const backupInfo = await getMediaBackupInfo({ auth: backupAuth }); @@ -109,7 +109,7 @@ export class BackupAPI { } public async upload(filePath: string, fileSize: number): Promise { - const { backupAuth } = await this.#credentials.getForToday( + const backupAuth = await this.#credentials.getForToday( BackupCredentialType.Messages ); @@ -202,7 +202,7 @@ export class BackupAPI { public async getMediaUploadForm( uploadSize: number ): Promise { - const { backupAuth } = await this.#credentials.getForToday( + const backupAuth = await this.#credentials.getForToday( BackupCredentialType.Media ); @@ -216,13 +216,11 @@ export class BackupAPI { cursor?: string; limit: number; }): Promise { - return backupListMedia({ - headers: await this.#credentials.getHeadersForToday( - BackupCredentialType.Media - ), - cursor, - limit, - }); + const backupAuth = await this.#credentials.getForToday( + BackupCredentialType.Media + ); + + return backupListMedia({ auth: backupAuth, cursor, limit }); } public async getSubscriptionInfo(): Promise { diff --git a/ts/services/backups/credentials.preload.ts b/ts/services/backups/credentials.preload.ts index 594aec5009..18b7275630 100644 --- a/ts/services/backups/credentials.preload.ts +++ b/ts/services/backups/credentials.preload.ts @@ -10,6 +10,7 @@ import { GenericServerPublicParams, } from '@signalapp/libsignal-client/zkgroup.js'; import { type BackupKey } from '@signalapp/libsignal-client/dist/AccountKeys.js'; +import type { BackupAuth } from '@signalapp/libsignal-client/dist/net'; import lodashFp from 'lodash/fp.js'; import * as Bytes from '../../Bytes.std.ts'; @@ -26,8 +27,6 @@ import { missingCaseError } from '../../util/missingCaseError.std.ts'; import { type BackupCdnReadCredentialType, type BackupCredentialWrapperType, - type BackupPresentationHeadersType, - type BackupSignedPresentationType, BackupCredentialType, } from '../../types/backups.node.ts'; import { HTTPError } from '../../types/HTTPError.std.ts'; @@ -94,7 +93,7 @@ export class BackupCredentials { public async getForToday( credentialType: BackupCredentialType - ): Promise { + ): Promise { const now = toDayMillis(Date.now()); let signatureKey: PrivateKey; @@ -134,45 +133,25 @@ export class BackupCredentials { Bytes.fromBase64(window.getBackupServerPublicParams()) ); - const presentation = cred.present(serverPublicParams).serialize(); - const signature = signatureKey.sign(presentation); - - const headers = { - 'X-Signal-ZK-Auth': Bytes.toBase64(presentation), - 'X-Signal-ZK-Auth-Signature': Bytes.toBase64(signature), - }; - - const info = { - headers, - level: result.level, - // For libsignal APIs - backupAuth: { - credential: cred, - serverKeys: serverPublicParams, - signingKey: signatureKey, - }, + const backupAuth: BackupAuth = { + credential: cred, + serverKeys: serverPublicParams, + signingKey: signatureKey, }; if (itemStorage.get(storageKey)) { - return info; + return backupAuth; } log.warn(`uploading signature key (${storageKey})`); await setBackupSignatureKey({ - auth: info.backupAuth, + auth: backupAuth, }); await itemStorage.put(storageKey, true); - return info; - } - - public async getHeadersForToday( - credentialType: BackupCredentialType - ): Promise { - const { headers } = await this.getForToday(credentialType); - return headers; + return backupAuth; } public async getCDNReadCredentials( @@ -196,7 +175,7 @@ export class BackupCredentials { return cachedCredentials.credentials; } - const { backupAuth } = await this.getForToday(credentialType); + const backupAuth = await this.getForToday(credentialType); const newCredentials = await getBackupCDNCredentials({ auth: backupAuth, @@ -408,7 +387,8 @@ export class BackupCredentials { public async getBackupLevel( credentialType: BackupCredentialType ): Promise { - return (await this.getForToday(credentialType)).level; + const backupAuth = await this.getForToday(credentialType); + return backupAuth.credential.getBackupLevel(); } // Called when backup tier changes or when userChanged event diff --git a/ts/textsecure/WebAPI.preload.ts b/ts/textsecure/WebAPI.preload.ts index eaf3ca5040..879d00d691 100644 --- a/ts/textsecure/WebAPI.preload.ts +++ b/ts/textsecure/WebAPI.preload.ts @@ -803,7 +803,6 @@ const CHAT_CALLS = { multiRecipient: 'v1/messages/multi_recipient', phoneNumberDiscoverability: 'v2/accounts/phone_number_discoverability', profile: 'v1/profile', - backupMedia: 'v1/archives/media', backupMediaDelete: 'v1/archives/media/delete', callLinkCreateAuth: 'v1/call-link/create-auth', callQualitySurvey: 'v1/call_quality_survey', @@ -1297,27 +1296,21 @@ export type CopyBackupMediaOptionsType = Readonly<{ }>; export type BackupListMediaOptionsType = Readonly<{ - headers: BackupPresentationHeadersType; + auth: BackupAuth; cursor?: string; limit: number; }>; -export const backupListMediaResponseSchema = z.object({ - storedMediaObjects: z - .object({ - cdn: z.number(), - mediaId: z.string(), - objectLength: z.number(), - }) - .array(), - backupDir: z.string(), - mediaDir: z.string(), - cursor: z.string().nullish(), -}); - -export type BackupListMediaResponseType = z.infer< - typeof backupListMediaResponseSchema ->; +export type BackupListMediaResponseType = Readonly<{ + storedMediaObjects: ReadonlyArray<{ + cdn: number; + mediaId: string; + objectLength: number; + }>; + backupDir: string; + mediaDir: string; + cursor?: string; +}>; export type BackupDeleteMediaItemType = Readonly<{ cdn: number; @@ -3417,28 +3410,29 @@ export async function copyBackupMedia({ } export async function backupListMedia({ - headers, + auth, cursor, limit, }: BackupListMediaOptionsType): Promise { - const params = new Array(); + return _retry(async () => { + const unauthChat = await socketManager.getUnauthenticatedApi(); + const { + items, + backupDir, + mediaDir, + cursor: nextCursor, + } = await unauthChat.listBackupMedia({ auth, cursor, limit }); - if (cursor != null) { - params.push(`cursor=${encodeURIComponent(cursor)}`); - } - params.push(`limit=${limit}`); - - return _ajax({ - host: 'chatService', - call: 'backupMedia', - httpType: 'GET', - unauthenticated: true, - accessKey: undefined, - groupSendToken: undefined, - headers, - responseType: 'json', - urlParameters: `?${params.join('&')}`, - zodSchema: backupListMediaResponseSchema, + return { + storedMediaObjects: items.map(({ cdn, mediaId, objectLength }) => ({ + cdn, + mediaId: Bytes.toBase64url(mediaId), + objectLength: Number(objectLength), + })), + backupDir, + mediaDir, + cursor: nextCursor, + }; }); } diff --git a/ts/types/backups.node.ts b/ts/types/backups.node.ts index a3022c407a..089db34e2a 100644 --- a/ts/types/backups.node.ts +++ b/ts/types/backups.node.ts @@ -4,7 +4,6 @@ import type { BackupLevel } from '@signalapp/libsignal-client/dist/zkgroup/index.js'; import { BackupCredentialType } from '@signalapp/libsignal-client/dist/zkgroup/index.js'; import type { GetBackupCDNCredentialsResponseType } from '../textsecure/WebAPI.preload.ts'; -import type { BackupAuth } from '@signalapp/libsignal-client/dist/net.js'; export { BackupCredentialType }; @@ -20,12 +19,6 @@ export type BackupPresentationHeadersType = Readonly<{ 'X-Signal-ZK-Auth-Signature': string; }>; -export type BackupSignedPresentationType = Readonly<{ - headers: BackupPresentationHeadersType; - level: BackupLevel; - backupAuth: BackupAuth; -}>; - export type BackupCdnReadCredentialType = Readonly<{ credentials: Readonly; retrievedAtMs: number;