New sticker creator button

This commit is contained in:
Fedor Indutny 2023-02-27 14:34:43 -08:00 committed by GitHub
parent 85adb39d31
commit fad0529080
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 442 additions and 11 deletions

View file

@ -34,6 +34,7 @@ const JITTER = 5 * durations.SECOND;
export type SocketManagerOptions = Readonly<{
url: string;
artCreatorUrl: string;
certificateAuthority: string;
version: string;
proxyUrl?: string;
@ -276,6 +277,27 @@ export class SocketManager extends EventListener {
}).getResult();
}
// Creates new WebSocket for Art Creator provisioning
public async connectExternalSocket({
url,
extraHeaders,
}: {
url: string;
extraHeaders?: Record<string, string>;
}): Promise<WebSocket> {
return connectWebSocket({
name: 'art-creator-provisioning',
url,
version: this.options.version,
proxyAgent: this.proxyAgent,
extraHeaders,
createResource(socket: WebSocket): WebSocket {
return socket;
},
}).getResult();
}
// Fetch-compatible wrapper around underlying unauthenticated/authenticated
// websocket resources. This wrapper supports only limited number of features
// of node-fetch despite being API compatible.

View file

@ -15,6 +15,7 @@ import PQueue from 'p-queue';
import { v4 as getGuid } from 'uuid';
import { z } from 'zod';
import type { Readable } from 'stream';
import type { connection as WebSocket } from 'websocket';
import { assertDev, strictAssert } from '../util/assert';
import { isRecord } from '../util/isRecord';
@ -477,7 +478,6 @@ const URL_CALLS = {
config: 'v1/config',
deliveryCert: 'v1/certificate/delivery',
devices: 'v1/devices',
directoryAuth: 'v1/directory/auth',
directoryAuthV2: 'v2/directory/auth',
discovery: 'v1/discovery',
getGroupAvatarUpload: 'v1/groups/avatar/form',
@ -486,6 +486,7 @@ const URL_CALLS = {
getOnboardingStoryManifest:
'dynamic/desktop/stories/onboarding/manifest.json',
getStickerPackUpload: 'v1/sticker/pack/form',
getArtAuth: 'v1/art/auth',
groupLog: 'v1/groups/logs',
groupJoinedAtVersion: 'v1/groups/joined_at_version',
groups: 'v1/groups',
@ -537,7 +538,6 @@ const WEBSOCKET_CALLS = new Set<keyof typeof URL_CALLS>([
'supportUnauthenticatedDelivery',
// Directory
'directoryAuth',
'directoryAuthV2',
// Storage
@ -552,6 +552,7 @@ type InitializeOptionsType = {
storageUrl: string;
updatesUrl: string;
resourcesUrl: string;
artCreatorUrl: string;
cdnUrlObject: {
readonly '0': string;
readonly [propName: string]: string;
@ -831,6 +832,13 @@ export type ReportMessageOptionsType = Readonly<{
token?: string;
}>;
const artAuthZod = z.object({
username: z.string(),
password: z.string(),
});
export type ArtAuthType = z.infer<typeof artAuthZod>;
export type WebAPIType = {
startRegistration(): unknown;
finishRegistration(baton: unknown): void;
@ -848,6 +856,7 @@ export type WebAPIType = {
version: string,
imageFiles: Array<string>
) => Promise<Array<Uint8Array>>;
getArtAuth: () => Promise<ArtAuthType>;
getAttachment: (cdnKey: string, cdnNumber?: number) => Promise<Uint8Array>;
getAvatar: (path: string) => Promise<Uint8Array>;
getDevices: () => Promise<GetDevicesResultType>;
@ -901,6 +910,7 @@ export type WebAPIType = {
getProvisioningResource: (
handler: IRequestHandler
) => Promise<WebSocketResource>;
getArtProvisioningSocket: (token: string) => Promise<WebSocket>;
getSenderCertificate: (
withUuid?: boolean
) => Promise<GetSenderCertificateResultType>;
@ -1073,6 +1083,7 @@ export function initialize({
storageUrl,
updatesUrl,
resourcesUrl,
artCreatorUrl,
directoryConfig,
cdnUrlObject,
certificateAuthority,
@ -1092,6 +1103,9 @@ export function initialize({
if (!isString(resourcesUrl)) {
throw new Error('WebAPI.initialize: Invalid updatesUrl (general)');
}
if (!isString(artCreatorUrl)) {
throw new Error('WebAPI.initialize: Invalid artCreatorUrl');
}
if (!isObject(cdnUrlObject)) {
throw new Error('WebAPI.initialize: Invalid cdnUrlObject');
}
@ -1139,6 +1153,7 @@ export function initialize({
const socketManager = new SocketManager({
url,
artCreatorUrl,
certificateAuthority,
version,
proxyUrl,
@ -1224,6 +1239,8 @@ export function initialize({
fetchLinkPreviewMetadata,
finishRegistration,
getAccountForUsername,
getArtAuth,
getArtProvisioningSocket,
getAttachment,
getAvatar,
getBadgeImageFile,
@ -2982,6 +2999,15 @@ export function initialize({
return socketManager.getProvisioningResource(handler);
}
function getArtProvisioningSocket(token: string): Promise<WebSocket> {
return socketManager.connectExternalSocket({
url: `${artCreatorUrl}/api/socket?token=${token}`,
extraHeaders: {
origin: artCreatorUrl,
},
});
}
async function cdsLookup({
e164s,
acis = [],
@ -2995,5 +3021,19 @@ export function initialize({
returnAcisWithoutUaks,
});
}
//
// Art
//
async function getArtAuth(): Promise<ArtAuthType> {
const response = await _ajax({
call: 'getArtAuth',
httpType: 'GET',
responseType: 'json',
});
return artAuthZod.parse(response);
}
}
}

View file

@ -24,7 +24,7 @@ export type IResource = {
export type ConnectOptionsType<Resource extends IResource> = Readonly<{
name: string;
url: string;
certificateAuthority: string;
certificateAuthority?: string;
version: string;
proxyAgent?: ReturnType<typeof ProxyAgent>;
timeout?: number;

View file

@ -0,0 +1,59 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { promisify } from 'util';
import { SignalService as Proto } from '../protobuf';
import { calculateAgreement, generateKeyPair } from '../Curve';
import { encryptAttachment, deriveSecrets } from '../Crypto';
import * as Bytes from '../Bytes';
const PROVISIONING_INFO = 'Art Service Provisioning Message';
export type AuthorizeArtCreatorOptionsType = Readonly<{
token: string;
pubKeyBase64: string;
}>;
export async function authorizeArtCreator({
token,
pubKeyBase64: theirPubKeyBase64,
}: AuthorizeArtCreatorOptionsType): Promise<void> {
const { server } = window.textsecure;
if (!server) {
throw new Error('Server not ready');
}
const auth = await server.getArtAuth();
const ourKeys = generateKeyPair();
const theirPubKey = Bytes.fromBase64(theirPubKeyBase64);
const secret = calculateAgreement(theirPubKey, ourKeys.privKey);
const [aesKey, macKey] = deriveSecrets(
secret,
new Uint8Array(64),
Bytes.fromString(PROVISIONING_INFO)
);
const keys = Bytes.concatenate([aesKey, macKey]);
const { ciphertext } = encryptAttachment(
Proto.ArtProvisioningMessage.encode({
...auth,
}).finish(),
keys
);
const envelope = Proto.ArtProvisioningEnvelope.encode({
publicKey: ourKeys.pubKey,
ciphertext,
}).finish();
const socket = await server.getArtProvisioningSocket(token);
try {
await promisify(socket.sendBytes).call(socket, Buffer.from(envelope));
} finally {
socket.close(1000, 'goodbye');
}
}