mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-08-29 09:25:57 +01:00
Adopt libsignal list backup media endpoint
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@signalapp/mock-server': major
|
||||
---
|
||||
|
||||
Moves backup listMedia to GRPC
|
||||
@@ -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
|
||||
//
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user