Adopt libsignal list backup media endpoint

This commit is contained in:
trevor-signal
2026-08-25 10:02:04 -04:00
committed by GitHub
parent 4ae4fd1187
commit b60e076b6a
8 changed files with 103 additions and 121 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@signalapp/mock-server': major
---
Moves backup listMedia to GRPC
+43 -1
View File
@@ -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)),
);
@@ -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
//
+3 -4
View File
@@ -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,
+10 -12
View File
@@ -70,12 +70,12 @@ export class BackupAPI {
}
async #refreshType(type: BackupCredentialType): Promise<void> {
const auth = (await this.#credentials.getForToday(type)).backupAuth;
const auth = await this.#credentials.getForToday(type);
return refreshBackup({ auth });
}
public async getMessageBackupInfo(): Promise<GetMessageBackupInfoResponseType> {
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<GetMediaBackupInfoResponseType> {
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<void> {
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<AttachmentUploadFormType> {
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<BackupListMediaResponseType> {
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<BackupsSubscriptionType> {
+12 -32
View File
@@ -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<BackupSignedPresentationType> {
): Promise<BackupAuth> {
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<BackupPresentationHeadersType> {
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<BackupLevel> {
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
+30 -36
View File
@@ -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<BackupListMediaResponseType> {
const params = new Array<string>();
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,
};
});
}
-7
View File
@@ -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<GetBackupCDNCredentialsResponseType>;
retrievedAtMs: number;