"Fossies" - the Fresh Open Source Software Archive  

Source code changes of the file "ts/textsecure/WebAPI.ts" between
Signal-Desktop-1.35.2.tar.gz and Signal-Desktop-1.36.1.tar.gz

About: Signal-Desktop is a cross-platform encrypted messaging service (also available for mobile devices).

WebAPI.ts  (Signal-Desktop-1.35.2):WebAPI.ts  (Signal-Desktop-1.36.1)
import { w3cwebsocket as WebSocket } from 'websocket'; import { w3cwebsocket as WebSocket } from 'websocket';
import fetch, { Response } from 'node-fetch'; import fetch, { Response } from 'node-fetch';
import ProxyAgent from 'proxy-agent'; import ProxyAgent from 'proxy-agent';
import { Agent } from 'https'; import { Agent } from 'https';
import { escapeRegExp } from 'lodash'; import pProps from 'p-props';
import {
compact,
Dictionary,
escapeRegExp,
mapValues,
zipObject,
} from 'lodash';
import { createVerify } from 'crypto';
import { Long } from '../window.d';
import { pki } from 'node-forge';
import is from '@sindresorhus/is'; import is from '@sindresorhus/is';
import { isPackIdValid, redactPackId } from '../../js/modules/stickers'; import { isPackIdValid, redactPackId } from '../../js/modules/stickers';
import { getRandomValue } from '../Crypto';
import MessageSender from './SendMessage'; import MessageSender from './SendMessage';
import {
arrayBufferToBase64,
base64ToArrayBuffer,
bytesFromHexString,
bytesFromString,
concatenateBytes,
constantTimeEqual,
decryptAesGcm,
encryptCdsDiscoveryRequest,
getBytes,
getRandomValue,
splitUuids,
} from '../Crypto';
import { getUserAgent } from '../util/getUserAgent';
import PQueue from 'p-queue'; import PQueue from 'p-queue';
import { v4 as getGuid } from 'uuid'; import { v4 as getGuid } from 'uuid';
import { import {
AvatarUploadAttributesClass,
GroupChangeClass,
GroupChangesClass,
GroupClass,
StorageServiceCallOptionsType, StorageServiceCallOptionsType,
StorageServiceCredentials, StorageServiceCredentials,
} from '../textsecure.d'; } from '../textsecure.d';
type SgxConstantsType = {
SGX_FLAGS_INITTED: Long;
SGX_FLAGS_DEBUG: Long;
SGX_FLAGS_MODE64BIT: Long;
SGX_FLAGS_PROVISION_KEY: Long;
SGX_FLAGS_EINITTOKEN_KEY: Long;
SGX_FLAGS_RESERVED: Long;
SGX_XFRM_LEGACY: Long;
SGX_XFRM_AVX: Long;
SGX_XFRM_RESERVED: Long;
};
let sgxConstantCache: SgxConstantsType | null = null;
function makeLong(value: string): Long {
return window.dcodeIO.Long.fromString(value);
}
function getSgxConstants() {
if (sgxConstantCache) {
return sgxConstantCache;
}
sgxConstantCache = {
SGX_FLAGS_INITTED: makeLong('x0000000000000001L'),
SGX_FLAGS_DEBUG: makeLong('x0000000000000002L'),
SGX_FLAGS_MODE64BIT: makeLong('x0000000000000004L'),
SGX_FLAGS_PROVISION_KEY: makeLong('x0000000000000004L'),
SGX_FLAGS_EINITTOKEN_KEY: makeLong('x0000000000000004L'),
SGX_FLAGS_RESERVED: makeLong('xFFFFFFFFFFFFFFC8L'),
SGX_XFRM_LEGACY: makeLong('x0000000000000003L'),
SGX_XFRM_AVX: makeLong('x0000000000000006L'),
SGX_XFRM_RESERVED: makeLong('xFFFFFFFFFFFFFFF8L'),
};
return sgxConstantCache;
}
// tslint:disable no-bitwise // tslint:disable no-bitwise
function _btoa(str: any) { function _btoa(str: any) {
let buffer; let buffer;
if (str instanceof Buffer) { if (str instanceof Buffer) {
buffer = str; buffer = str;
} else { } else {
buffer = Buffer.from(str.toString(), 'binary'); buffer = Buffer.from(str.toString(), 'binary');
} }
skipping to change at line 183 skipping to change at line 247
} }
return true; return true;
} }
function _createSocket( function _createSocket(
url: string, url: string,
{ {
certificateAuthority, certificateAuthority,
proxyUrl, proxyUrl,
}: { certificateAuthority: string; proxyUrl?: string } version,
}: { certificateAuthority: string; proxyUrl?: string; version: string }
) { ) {
let requestOptions; let requestOptions;
if (proxyUrl) { if (proxyUrl) {
requestOptions = { requestOptions = {
ca: certificateAuthority, ca: certificateAuthority,
agent: new ProxyAgent(proxyUrl), agent: new ProxyAgent(proxyUrl),
}; };
} else { } else {
requestOptions = { requestOptions = {
ca: certificateAuthority, ca: certificateAuthority,
}; };
} }
const headers = {
return new WebSocket(url, undefined, undefined, undefined, requestOptions, { 'User-Agent': getUserAgent(version),
};
return new WebSocket(url, undefined, undefined, headers, requestOptions, {
maxReceivedFrameSize: 0x210000, maxReceivedFrameSize: 0x210000,
}); });
} }
const FIVE_MINUTES = 1000 * 60 * 5; const FIVE_MINUTES = 1000 * 60 * 5;
type AgentCacheType = { type AgentCacheType = {
[name: string]: { [name: string]: {
timestamp: number; timestamp: number;
agent: ProxyAgent | Agent; agent: ProxyAgent | Agent;
skipping to change at line 227 skipping to change at line 294
return null; return null;
} }
type HeaderListType = { [name: string]: string }; type HeaderListType = { [name: string]: string };
type HTTPCodeType = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; type HTTPCodeType = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
type RedactUrl = (url: string) => string; type RedactUrl = (url: string) => string;
type PromiseAjaxOptionsType = { type PromiseAjaxOptionsType = {
accessKey?: string; accessKey?: string;
basicAuth?: string;
certificateAuthority?: string; certificateAuthority?: string;
contentType?: string; contentType?: string;
data?: ArrayBuffer | Buffer | string; data?: ArrayBuffer | Buffer | string;
headers?: HeaderListType; headers?: HeaderListType;
host?: string; host?: string;
password?: string; password?: string;
path?: string; path?: string;
proxyUrl?: string; proxyUrl?: string;
redactUrl?: RedactUrl; redactUrl?: RedactUrl;
redirect?: 'error' | 'follow' | 'manual'; redirect?: 'error' | 'follow' | 'manual';
responseType?: 'json' | 'arraybuffer' | 'arraybufferwithdetails'; responseType?:
| 'json'
| 'jsonwithdetails'
| 'arraybuffer'
| 'arraybufferwithdetails';
stack?: string; stack?: string;
timeout?: number; timeout?: number;
type: HTTPCodeType; type: HTTPCodeType;
unauthenticated?: boolean; unauthenticated?: boolean;
user?: string; user?: string;
validateResponse?: any; validateResponse?: any;
version: string; version: string;
}; };
type JSONWithDetailsType = {
data: any;
contentType: string | null;
response: Response;
};
type ArrayBufferWithDetailsType = {
data: ArrayBuffer;
contentType: string | null;
response: Response;
};
// tslint:disable-next-line max-func-body-length // tslint:disable-next-line max-func-body-length
async function _promiseAjax( async function _promiseAjax(
providedUrl: string | null, providedUrl: string | null,
options: PromiseAjaxOptionsType options: PromiseAjaxOptionsType
): Promise<any> { ): Promise<any> {
// tslint:disable-next-line max-func-body-length // tslint:disable-next-line max-func-body-length
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const url = providedUrl || `${options.host}/${options.path}`; const url = providedUrl || `${options.host}/${options.path}`;
const unauthLabel = options.unauthenticated ? ' (unauth)' : ''; const unauthLabel = options.unauthenticated ? ' (unauth)' : '';
skipping to change at line 290 skipping to change at line 373
: new Agent({ keepAlive: true }), : new Agent({ keepAlive: true }),
timestamp: Date.now(), timestamp: Date.now(),
}; };
} }
const { agent } = agents[cacheKey]; const { agent } = agents[cacheKey];
const fetchOptions = { const fetchOptions = {
method: options.type, method: options.type,
body: options.data, body: options.data,
headers: { headers: {
'User-Agent': `Signal Desktop ${options.version}`, 'User-Agent': getUserAgent(options.version),
'X-Signal-Agent': 'OWD', 'X-Signal-Agent': 'OWD',
...options.headers, ...options.headers,
} as HeaderListType, } as HeaderListType,
redirect: options.redirect, redirect: options.redirect,
agent, agent,
// We patched node-fetch to add the ca param; its type definitions don't h ave it // We patched node-fetch to add the ca param; its type definitions don't h ave it
// @ts-ignore // @ts-ignore
ca: options.certificateAuthority, ca: options.certificateAuthority,
timeout, timeout,
}; };
if (fetchOptions.body instanceof ArrayBuffer) { if (fetchOptions.body instanceof ArrayBuffer) {
// node-fetch doesn't support ArrayBuffer, only node Buffer // node-fetch doesn't support ArrayBuffer, only node Buffer
const contentLength = fetchOptions.body.byteLength; const contentLength = fetchOptions.body.byteLength;
fetchOptions.body = Buffer.from(fetchOptions.body); fetchOptions.body = Buffer.from(fetchOptions.body);
// node-fetch doesn't set content-length like S3 requires // node-fetch doesn't set content-length like S3 requires
fetchOptions.headers['Content-Length'] = contentLength.toString(); fetchOptions.headers['Content-Length'] = contentLength.toString();
} }
const { accessKey, unauthenticated } = options; const { accessKey, basicAuth, unauthenticated } = options;
if (unauthenticated) { if (basicAuth) {
fetchOptions.headers.Authorization = `Basic ${basicAuth}`;
} else if (unauthenticated) {
if (!accessKey) { if (!accessKey) {
throw new Error( throw new Error(
'_promiseAjax: mode is aunathenticated, but accessKey was not provided ' '_promiseAjax: mode is aunathenticated, but accessKey was not provided '
); );
} }
// Access key is already a Base64 string // Access key is already a Base64 string
fetchOptions.headers['Unidentified-Access-Key'] = accessKey; fetchOptions.headers['Unidentified-Access-Key'] = accessKey;
} else if (options.user && options.password) { } else if (options.user && options.password) {
const user = _getString(options.user); const user = _getString(options.user);
const password = _getString(options.password); const password = _getString(options.password);
skipping to change at line 334 skipping to change at line 419
fetchOptions.headers.Authorization = `Basic ${auth}`; fetchOptions.headers.Authorization = `Basic ${auth}`;
} }
if (options.contentType) { if (options.contentType) {
fetchOptions.headers['Content-Type'] = options.contentType; fetchOptions.headers['Content-Type'] = options.contentType;
} }
fetch(url, fetchOptions) fetch(url, fetchOptions)
// tslint:disable-next-line max-func-body-length // tslint:disable-next-line max-func-body-length
.then(async response => { .then(async response => {
// Build expired!
if (response.status === 499) {
window.log.error('Error: build expired');
window.storage.put('remoteBuildExpiration', Date.now());
window.reduxActions.expiration.hydrateExpirationStatus(true);
}
let resultPromise; let resultPromise;
if ( if (
options.responseType === 'json' && (options.responseType === 'json' ||
options.responseType === 'jsonwithdetails') &&
response.headers.get('Content-Type') === 'application/json' response.headers.get('Content-Type') === 'application/json'
) { ) {
resultPromise = response.json(); resultPromise = response.json();
} else if ( } else if (
options.responseType === 'arraybuffer' || options.responseType === 'arraybuffer' ||
options.responseType === 'arraybufferwithdetails' options.responseType === 'arraybufferwithdetails'
) { ) {
resultPromise = response.buffer(); resultPromise = response.buffer();
} else { } else {
resultPromise = response.textConverted(); resultPromise = response.textConverted();
} }
// tslint:disable-next-line max-func-body-length
return resultPromise.then(result => { return resultPromise.then(result => {
if ( if (
options.responseType === 'arraybuffer' || options.responseType === 'arraybuffer' ||
options.responseType === 'arraybufferwithdetails' options.responseType === 'arraybufferwithdetails'
) { ) {
// tslint:disable-next-line no-parameter-reassignment // tslint:disable-next-line no-parameter-reassignment
result = result.buffer.slice( result = result.buffer.slice(
result.byteOffset, result.byteOffset,
// tslint:disable-next-line: restrict-plus-operands // tslint:disable-next-line: restrict-plus-operands
result.byteOffset + result.byteLength result.byteOffset + result.byteLength
); );
} }
if (options.responseType === 'json') { if (
options.responseType === 'json' ||
options.responseType === 'jsonwithdetails'
) {
if (options.validateResponse) { if (options.validateResponse) {
if (!_validateResponse(result, options.validateResponse)) { if (!_validateResponse(result, options.validateResponse)) {
if (options.redactUrl) { if (options.redactUrl) {
window.log.info( window.log.info(
options.type, options.type,
options.redactUrl(url), options.redactUrl(url),
response.status, response.status,
'Error' 'Error'
); );
} else { } else {
skipping to change at line 399 skipping to change at line 496
window.log.info( window.log.info(
options.type, options.type,
options.redactUrl(url), options.redactUrl(url),
response.status, response.status,
'Success' 'Success'
); );
} else { } else {
window.log.info(options.type, url, response.status, 'Success'); window.log.info(options.type, url, response.status, 'Success');
} }
if (options.responseType === 'arraybufferwithdetails') { if (options.responseType === 'arraybufferwithdetails') {
resolve({ const fullResult: ArrayBufferWithDetailsType = {
data: result, data: result,
contentType: getContentType(response), contentType: getContentType(response),
response, response,
}); };
resolve(fullResult);
return; return;
} }
if (options.responseType === 'jsonwithdetails') {
const fullResult: JSONWithDetailsType = {
data: result,
contentType: getContentType(response),
response,
};
resolve(fullResult);
return;
}
resolve(result); resolve(result);
return; return;
} }
if (options.redactUrl) { if (options.redactUrl) {
window.log.info( window.log.info(
options.type, options.type,
options.redactUrl(url), options.redactUrl(url),
response.status, response.status,
skipping to change at line 502 skipping to change at line 613
e.stack += `\nOriginal stack:\n${stack}`; e.stack += `\nOriginal stack:\n${stack}`;
if (response) { if (response) {
e.response = response; e.response = response;
} }
return e; return e;
} }
const URL_CALLS = { const URL_CALLS = {
accounts: 'v1/accounts', accounts: 'v1/accounts',
updateDeviceName: 'v1/accounts/name',
removeSignalingKey: 'v1/accounts/signaling_key',
getIceServers: 'v1/accounts/turn',
attachmentId: 'v2/attachments/form/upload', attachmentId: 'v2/attachments/form/upload',
attestation: 'v1/attestation',
config: 'v1/config',
deliveryCert: 'v1/certificate/delivery', deliveryCert: 'v1/certificate/delivery',
devices: 'v1/devices', devices: 'v1/devices',
directoryAuth: 'v1/directory/auth',
discovery: 'v1/discovery',
getGroupAvatarUpload: '/v1/groups/avatar/form',
getGroupCredentials: 'v1/certificate/group',
getIceServers: 'v1/accounts/turn',
getStickerPackUpload: 'v1/sticker/pack/form',
groupLog: 'v1/groups/logs',
groups: 'v1/groups',
keys: 'v2/keys', keys: 'v2/keys',
messages: 'v1/messages', messages: 'v1/messages',
profile: 'v1/profile', profile: 'v1/profile',
registerCapabilities: 'v1/devices/capabilities', registerCapabilities: 'v1/devices/capabilities',
removeSignalingKey: 'v1/accounts/signaling_key',
signed: 'v2/keys/signed', signed: 'v2/keys/signed',
storageManifest: 'v1/storage/manifest', storageManifest: 'v1/storage/manifest',
storageModify: 'v1/storage/', storageModify: 'v1/storage/',
storageRead: 'v1/storage/read', storageRead: 'v1/storage/read',
storageToken: 'v1/storage/auth', storageToken: 'v1/storage/auth',
supportUnauthenticatedDelivery: 'v1/devices/unauthenticated_delivery', supportUnauthenticatedDelivery: 'v1/devices/unauthenticated_delivery',
getStickerPackUpload: 'v1/sticker/pack/form', updateDeviceName: 'v1/accounts/name',
whoami: 'v1/accounts/whoami', whoami: 'v1/accounts/whoami',
config: 'v1/config',
}; };
type InitializeOptionsType = { type InitializeOptionsType = {
url: string; url: string;
storageUrl: string; storageUrl: string;
directoryEnclaveId: string;
directoryTrustAnchor: string;
directoryUrl: string;
cdnUrlObject: { cdnUrlObject: {
readonly '0': string; readonly '0': string;
readonly [propName: string]: string; readonly [propName: string]: string;
}; };
certificateAuthority: string; certificateAuthority: string;
contentProxyUrl: string; contentProxyUrl: string;
proxyUrl: string; proxyUrl: string;
version: string; version: string;
}; };
type ConnectParametersType = { type ConnectParametersType = {
username: string; username: string;
password: string; password: string;
}; };
type MessageType = any; type MessageType = any;
type AjaxOptionsType = { type AjaxOptionsType = {
accessKey?: string; accessKey?: string;
basicAuth?: string;
call: keyof typeof URL_CALLS; call: keyof typeof URL_CALLS;
contentType?: string; contentType?: string;
data?: ArrayBuffer | Buffer | string; data?: ArrayBuffer | Buffer | string;
host?: string; host?: string;
httpType: HTTPCodeType; httpType: HTTPCodeType;
jsonData?: any; jsonData?: any;
password?: string; password?: string;
redactUrl?: RedactUrl; redactUrl?: RedactUrl;
responseType?: 'json' | 'arraybuffer' | 'arraybufferwithdetails'; responseType?: 'json' | 'arraybuffer' | 'arraybufferwithdetails';
schema?: any; schema?: any;
skipping to change at line 568 skipping to change at line 690
username?: string; username?: string;
validateResponse?: any; validateResponse?: any;
}; };
export type WebAPIConnectType = { export type WebAPIConnectType = {
connect: (options: ConnectParametersType) => WebAPIType; connect: (options: ConnectParametersType) => WebAPIType;
}; };
type StickerPackManifestType = any; type StickerPackManifestType = any;
export type GroupCredentialType = {
credential: string;
redemptionTime: number;
};
export type GroupCredentialsType = {
groupPublicParamsHex: string;
authCredentialPresentationHex: string;
};
export type GroupLogResponseType = {
currentRevision?: number;
start?: number;
end?: number;
changes: GroupChangesClass;
};
export type WebAPIType = { export type WebAPIType = {
confirmCode: ( confirmCode: (
number: string, number: string,
code: string, code: string,
newPassword: string, newPassword: string,
registrationId: number, registrationId: number,
deviceName?: string | null, deviceName?: string | null,
options?: { accessKey?: ArrayBuffer } options?: { accessKey?: ArrayBuffer }
) => Promise<any>; ) => Promise<any>;
createGroup: (
group: GroupClass,
options: GroupCredentialsType
) => Promise<void>;
getAttachment: (cdnKey: string, cdnNumber: number) => Promise<any>; getAttachment: (cdnKey: string, cdnNumber: number) => Promise<any>;
getAvatar: (path: string) => Promise<any>; getAvatar: (path: string) => Promise<any>;
getDevices: () => Promise<any>; getDevices: () => Promise<any>;
getGroup: (options: GroupCredentialsType) => Promise<GroupClass>;
getGroupAvatar: (key: string) => Promise<ArrayBuffer>;
getGroupCredentials: (
startDay: number,
endDay: number
) => Promise<Array<GroupCredentialType>>;
getGroupLog: (
startVersion: number,
options: GroupCredentialsType
) => Promise<GroupLogResponseType>;
getIceServers: () => Promise<any>; getIceServers: () => Promise<any>;
getKeysForIdentifier: ( getKeysForIdentifier: (
identifier: string, identifier: string,
deviceId?: number deviceId?: number
) => Promise<ServerKeysType>; ) => Promise<ServerKeysType>;
getKeysForIdentifierUnauth: ( getKeysForIdentifierUnauth: (
identifier: string, identifier: string,
deviceId?: number, deviceId?: number,
options?: { accessKey?: string } options?: { accessKey?: string }
) => Promise<ServerKeysType>; ) => Promise<ServerKeysType>;
skipping to change at line 614 skipping to change at line 765
profileKeyCredentialRequest?: string; profileKeyCredentialRequest?: string;
} }
) => Promise<any>; ) => Promise<any>;
getProvisioningSocket: () => WebSocket; getProvisioningSocket: () => WebSocket;
getSenderCertificate: (withUuid?: boolean) => Promise<any>; getSenderCertificate: (withUuid?: boolean) => Promise<any>;
getSticker: (packId: string, stickerId: string) => Promise<any>; getSticker: (packId: string, stickerId: string) => Promise<any>;
getStickerPackManifest: (packId: string) => Promise<StickerPackManifestType>; getStickerPackManifest: (packId: string) => Promise<StickerPackManifestType>;
getStorageCredentials: MessageSender['getStorageCredentials']; getStorageCredentials: MessageSender['getStorageCredentials'];
getStorageManifest: MessageSender['getStorageManifest']; getStorageManifest: MessageSender['getStorageManifest'];
getStorageRecords: MessageSender['getStorageRecords']; getStorageRecords: MessageSender['getStorageRecords'];
getUuidsForE164s: (
e164s: ReadonlyArray<string>
) => Promise<Dictionary<string | null>>;
makeProxiedRequest: ( makeProxiedRequest: (
targetUrl: string, targetUrl: string,
options?: ProxiedRequestOptionsType options?: ProxiedRequestOptionsType
) => Promise<any>; ) => Promise<any>;
modifyGroup: (
changes: GroupChangeClass.Actions,
options: GroupCredentialsType
) => Promise<GroupChangeClass>;
modifyStorageRecords: MessageSender['modifyStorageRecords'];
putAttachment: (encryptedBin: ArrayBuffer) => Promise<any>; putAttachment: (encryptedBin: ArrayBuffer) => Promise<any>;
registerCapabilities: (capabilities: any) => Promise<void>; registerCapabilities: (capabilities: Dictionary<boolean>) => Promise<void>;
putStickers: ( putStickers: (
encryptedManifest: ArrayBuffer, encryptedManifest: ArrayBuffer,
encryptedStickers: Array<ArrayBuffer>, encryptedStickers: Array<ArrayBuffer>,
onProgress?: () => void onProgress?: () => void
) => Promise<string>; ) => Promise<string>;
registerKeys: (genKeys: KeysType) => Promise<void>; registerKeys: (genKeys: KeysType) => Promise<void>;
registerSupportForUnauthenticatedDelivery: () => Promise<any>; registerSupportForUnauthenticatedDelivery: () => Promise<any>;
removeSignalingKey: () => Promise<void>; removeSignalingKey: () => Promise<void>;
requestVerificationSMS: (number: string) => Promise<any>; requestVerificationSMS: (number: string) => Promise<any>;
requestVerificationVoice: (number: string) => Promise<any>; requestVerificationVoice: (number: string) => Promise<any>;
skipping to change at line 647 skipping to change at line 806
sendMessagesUnauth: ( sendMessagesUnauth: (
destination: string, destination: string,
messageArray: Array<MessageType>, messageArray: Array<MessageType>,
timestamp: number, timestamp: number,
silent?: boolean, silent?: boolean,
online?: boolean, online?: boolean,
options?: { accessKey?: string } options?: { accessKey?: string }
) => Promise<void>; ) => Promise<void>;
setSignedPreKey: (signedPreKey: SignedPreKeyType) => Promise<void>; setSignedPreKey: (signedPreKey: SignedPreKeyType) => Promise<void>;
updateDeviceName: (deviceName: string) => Promise<void>; updateDeviceName: (deviceName: string) => Promise<void>;
uploadGroupAvatar: (
avatarData: ArrayBuffer,
options: GroupCredentialsType
) => Promise<string>;
whoami: () => Promise<any>; whoami: () => Promise<any>;
getConfig: () => Promise<Array<{ name: string; enabled: boolean }>>; getConfig: () => Promise<
Array<{ name: string; enabled: boolean; value: string | null }>
>;
}; };
export type SignedPreKeyType = { export type SignedPreKeyType = {
keyId: number; keyId: number;
publicKey: ArrayBuffer; publicKey: ArrayBuffer;
signature: ArrayBuffer; signature: ArrayBuffer;
}; };
export type KeysType = { export type KeysType = {
identityKey: ArrayBuffer; identityKey: ArrayBuffer;
skipping to change at line 694 skipping to change at line 859
returnArrayBuffer?: boolean; returnArrayBuffer?: boolean;
start?: number; start?: number;
end?: number; end?: number;
}; };
// We first set up the data that won't change during this session of the app // We first set up the data that won't change during this session of the app
// tslint:disable-next-line max-func-body-length // tslint:disable-next-line max-func-body-length
export function initialize({ export function initialize({
url, url,
storageUrl, storageUrl,
directoryEnclaveId,
directoryTrustAnchor,
directoryUrl,
cdnUrlObject, cdnUrlObject,
certificateAuthority, certificateAuthority,
contentProxyUrl, contentProxyUrl,
proxyUrl, proxyUrl,
version, version,
}: InitializeOptionsType): WebAPIConnectType { }: InitializeOptionsType): WebAPIConnectType {
if (!is.string(url)) { if (!is.string(url)) {
throw new Error('WebAPI.initialize: Invalid server url'); throw new Error('WebAPI.initialize: Invalid server url');
} }
if (!is.string(storageUrl)) { if (!is.string(storageUrl)) {
throw new Error('WebAPI.initialize: Invalid storageUrl'); throw new Error('WebAPI.initialize: Invalid storageUrl');
} }
if (!is.string(directoryEnclaveId)) {
throw new Error('WebAPI.initialize: Invalid directory enclave id');
}
if (!is.string(directoryTrustAnchor)) {
throw new Error('WebAPI.initialize: Invalid directory enclave id');
}
if (!is.string(directoryUrl)) {
throw new Error('WebAPI.initialize: Invalid directory url');
}
if (!is.object(cdnUrlObject)) { if (!is.object(cdnUrlObject)) {
throw new Error('WebAPI.initialize: Invalid cdnUrlObject'); throw new Error('WebAPI.initialize: Invalid cdnUrlObject');
} }
if (!is.string(cdnUrlObject['0'])) { if (!is.string(cdnUrlObject['0'])) {
throw new Error('WebAPI.initialize: Missing CDN 0 configuration'); throw new Error('WebAPI.initialize: Missing CDN 0 configuration');
} }
if (!is.string(cdnUrlObject['2'])) { if (!is.string(cdnUrlObject['2'])) {
throw new Error('WebAPI.initialize: Missing CDN 2 configuration'); throw new Error('WebAPI.initialize: Missing CDN 2 configuration');
} }
if (!is.string(certificateAuthority)) { if (!is.string(certificateAuthority)) {
skipping to change at line 742 skipping to change at line 919
// exposed to the browser context, ensuring that it can't connect to arbitra ry // exposed to the browser context, ensuring that it can't connect to arbitra ry
// locations. // locations.
// tslint:disable-next-line max-func-body-length // tslint:disable-next-line max-func-body-length
function connect({ function connect({
username: initialUsername, username: initialUsername,
password: initialPassword, password: initialPassword,
}: ConnectParametersType) { }: ConnectParametersType) {
let username = initialUsername; let username = initialUsername;
let password = initialPassword; let password = initialPassword;
const PARSE_RANGE_HEADER = /\/(\d+)$/; const PARSE_RANGE_HEADER = /\/(\d+)$/;
const PARSE_GROUP_LOG_RANGE_HEADER = /$versions (\d{1,10})-(\d{1,10})\/(d{1, 10})/;
// Thanks, function hoisting! // Thanks, function hoisting!
return { return {
confirmCode, confirmCode,
createGroup,
getAttachment, getAttachment,
getAvatar, getAvatar,
getConfig,
getDevices, getDevices,
getGroup,
getGroupAvatar,
getGroupCredentials,
getGroupLog,
getIceServers, getIceServers,
getKeysForIdentifier, getKeysForIdentifier,
getKeysForIdentifierUnauth, getKeysForIdentifierUnauth,
getMessageSocket, getMessageSocket,
getMyKeys, getMyKeys,
getProfile, getProfile,
getProfileUnauth, getProfileUnauth,
getProvisioningSocket, getProvisioningSocket,
getSenderCertificate, getSenderCertificate,
getSticker, getSticker,
getStickerPackManifest, getStickerPackManifest,
getStorageCredentials, getStorageCredentials,
getStorageManifest, getStorageManifest,
getStorageRecords, getStorageRecords,
getUuidsForE164s,
makeProxiedRequest, makeProxiedRequest,
modifyGroup,
modifyStorageRecords,
putAttachment, putAttachment,
registerCapabilities,
putStickers, putStickers,
registerCapabilities,
registerKeys, registerKeys,
registerSupportForUnauthenticatedDelivery, registerSupportForUnauthenticatedDelivery,
removeSignalingKey, removeSignalingKey,
requestVerificationSMS, requestVerificationSMS,
requestVerificationVoice, requestVerificationVoice,
sendMessages, sendMessages,
sendMessagesUnauth, sendMessagesUnauth,
setSignedPreKey, setSignedPreKey,
updateDeviceName, updateDeviceName,
uploadGroupAvatar,
whoami, whoami,
getConfig,
}; };
async function _ajax(param: AjaxOptionsType): Promise<any> { async function _ajax(param: AjaxOptionsType): Promise<any> {
if (!param.urlParameters) { if (!param.urlParameters) {
param.urlParameters = ''; param.urlParameters = '';
} }
return _outerAjax(null, { return _outerAjax(null, {
basicAuth: param.basicAuth,
certificateAuthority, certificateAuthority,
contentType: param.contentType || 'application/json; charset=utf-8', contentType: param.contentType || 'application/json; charset=utf-8',
data: param.data || (param.jsonData && _jsonThing(param.jsonData)), data: param.data || (param.jsonData && _jsonThing(param.jsonData)),
host: param.host || url, host: param.host || url,
password: param.password || password, password: param.password || password,
path: URL_CALLS[param.call] + param.urlParameters, path: URL_CALLS[param.call] + param.urlParameters,
proxyUrl, proxyUrl,
responseType: param.responseType, responseType: param.responseType,
timeout: param.timeout, timeout: param.timeout,
type: param.httpType, type: param.httpType,
skipping to change at line 849 skipping to change at line 1037
async function whoami() { async function whoami() {
return _ajax({ return _ajax({
call: 'whoami', call: 'whoami',
httpType: 'GET', httpType: 'GET',
responseType: 'json', responseType: 'json',
}); });
} }
async function getConfig() { async function getConfig() {
type ResType = { type ResType = {
config: Array<{ name: string; enabled: boolean }>; config: Array<{ name: string; enabled: boolean; value: string | null }>;
}; };
const res: ResType = await _ajax({ const res: ResType = await _ajax({
call: 'config', call: 'config',
httpType: 'GET', httpType: 'GET',
responseType: 'json', responseType: 'json',
}); });
return res.config.filter(({ name }: { name: string }) => return res.config.filter(({ name }: { name: string }) =>
name.startsWith('desktop.') name.startsWith('desktop.')
); );
skipping to change at line 916 skipping to change at line 1104
call: 'storageRead', call: 'storageRead',
contentType: 'application/x-protobuf', contentType: 'application/x-protobuf',
data, data,
host: storageUrl, host: storageUrl,
httpType: 'PUT', httpType: 'PUT',
responseType: 'arraybuffer', responseType: 'arraybuffer',
...credentials, ...credentials,
}); });
} }
async function modifyStorageRecords(
data: ArrayBuffer,
options: StorageServiceCallOptionsType = {}
): Promise<ArrayBuffer> {
const { credentials } = options;
return _ajax({
call: 'storageModify',
contentType: 'application/x-protobuf',
data,
host: storageUrl,
httpType: 'PUT',
// If we run into a conflict, the current manifest is returned -
// it will will be an ArrayBuffer at the response key on the Error
responseType: 'arraybuffer',
...credentials,
});
}
async function registerSupportForUnauthenticatedDelivery() { async function registerSupportForUnauthenticatedDelivery() {
return _ajax({ return _ajax({
call: 'supportUnauthenticatedDelivery', call: 'supportUnauthenticatedDelivery',
httpType: 'PUT', httpType: 'PUT',
responseType: 'json', responseType: 'json',
}); });
} }
async function registerCapabilities(capabilities: any) { async function registerCapabilities(capabilities: Dictionary<boolean>) {
return _ajax({ return _ajax({
call: 'registerCapabilities', call: 'registerCapabilities',
httpType: 'PUT', httpType: 'PUT',
jsonData: { capabilities }, jsonData: capabilities,
}); });
} }
function getProfileUrl( function getProfileUrl(
identifier: string, identifier: string,
profileKeyVersion?: string, profileKeyVersion?: string,
profileKeyCredentialRequest?: string profileKeyCredentialRequest?: string
) { ) {
let profileUrl = `/${identifier}`; let profileUrl = `/${identifier}`;
skipping to change at line 1052 skipping to change at line 1259
async function confirmCode( async function confirmCode(
number: string, number: string,
code: string, code: string,
newPassword: string, newPassword: string,
registrationId: number, registrationId: number,
deviceName?: string | null, deviceName?: string | null,
options: { accessKey?: ArrayBuffer } = {} options: { accessKey?: ArrayBuffer } = {}
) { ) {
const { accessKey } = options; const { accessKey } = options;
const jsonData: any = { const jsonData: any = {
// tslint:disable-next-line: no-suspicious-comment capabilities: {
// TODO: uncomment this once we want to start registering UUID support gv2: true,
// capabilities: { },
// uuid: true,
// },
fetchesMessages: true, fetchesMessages: true,
name: deviceName ? deviceName : undefined, name: deviceName ? deviceName : undefined,
registrationId, registrationId,
supportsSms: false, supportsSms: false,
unidentifiedAccessKey: accessKey unidentifiedAccessKey: accessKey
? _btoa(_getString(accessKey)) ? _btoa(_getString(accessKey))
: undefined, : undefined,
unrestrictedUnidentifiedAccess: false, unrestrictedUnidentifiedAccess: false,
}; };
skipping to change at line 1578 skipping to change at line 1783
redirect: 'follow', redirect: 'follow',
redactUrl: () => '[REDACTED_URL]', redactUrl: () => '[REDACTED_URL]',
headers, headers,
version, version,
}); });
if (!returnArrayBuffer) { if (!returnArrayBuffer) {
return result; return result;
} }
const { response } = result; const { response } = result as ArrayBufferWithDetailsType;
if (!response.headers || !response.headers.get) { if (!response.headers || !response.headers.get) {
throw new Error('makeProxiedRequest: Problem retrieving header value'); throw new Error('makeProxiedRequest: Problem retrieving header value');
} }
const range = response.headers.get('content-range'); const range = response.headers.get('content-range');
const match = PARSE_RANGE_HEADER.exec(range); const match = PARSE_RANGE_HEADER.exec(range || '');
if (!match || !match[1]) { if (!match || !match[1]) {
throw new Error( throw new Error(
`makeProxiedRequest: Unable to parse total size from ${range}` `makeProxiedRequest: Unable to parse total size from ${range}`
); );
} }
const totalSize = parseInt(match[1], 10); const totalSize = parseInt(match[1], 10);
return { return {
totalSize, totalSize,
result, result,
}; };
} }
// Groups
function generateGroupAuth(
groupPublicParamsHex: string,
authCredentialPresentationHex: string
) {
return _btoa(`${groupPublicParamsHex}:${authCredentialPresentationHex}`);
}
type CredentialResponseType = {
credentials: Array<GroupCredentialType>;
};
async function getGroupCredentials(
startDay: number,
endDay: number
): Promise<Array<GroupCredentialType>> {
const response: CredentialResponseType = await _ajax({
call: 'getGroupCredentials',
urlParameters: `/${startDay}/${endDay}`,
httpType: 'GET',
responseType: 'json',
});
return response.credentials;
}
function verifyAttributes(attributes: AvatarUploadAttributesClass) {
const {
key,
credential,
acl,
algorithm,
date,
policy,
signature,
} = attributes;
if (
!key ||
!credential ||
!acl ||
!algorithm ||
!date ||
!policy ||
!signature
) {
throw new Error(
'verifyAttributes: Missing value from AvatarUploadAttributes'
);
}
return {
key,
credential,
acl,
algorithm,
date,
policy,
signature,
};
}
async function uploadGroupAvatar(
avatarData: ArrayBuffer,
options: GroupCredentialsType
): Promise<string> {
const basicAuth = generateGroupAuth(
options.groupPublicParamsHex,
options.authCredentialPresentationHex
);
const response: ArrayBuffer = await _ajax({
basicAuth,
call: 'getGroupAvatarUpload',
httpType: 'GET',
responseType: 'arraybuffer',
host: storageUrl,
});
const attributes = window.textsecure.protobuf.AvatarUploadAttributes.decod
e(
response
);
const verified = verifyAttributes(attributes);
const { key } = verified;
const manifestParams = makePutParams(verified, avatarData);
await _outerAjax(`${cdnUrlObject['0']}/`, {
...manifestParams,
certificateAuthority,
proxyUrl,
timeout: 0,
type: 'POST',
version,
});
return key;
}
async function getGroupAvatar(key: string): Promise<ArrayBuffer> {
return _outerAjax(`${cdnUrlObject['0']}/${key}`, {
certificateAuthority,
proxyUrl,
responseType: 'arraybuffer',
timeout: 0,
type: 'GET',
version,
});
}
async function createGroup(
group: GroupClass,
options: GroupCredentialsType
): Promise<void> {
const basicAuth = generateGroupAuth(
options.groupPublicParamsHex,
options.authCredentialPresentationHex
);
const data = group.toArrayBuffer();
await _ajax({
basicAuth,
call: 'groups',
httpType: 'PUT',
data,
host: storageUrl,
});
}
async function getGroup(
options: GroupCredentialsType
): Promise<GroupClass> {
const basicAuth = generateGroupAuth(
options.groupPublicParamsHex,
options.authCredentialPresentationHex
);
const response: ArrayBuffer = await _ajax({
basicAuth,
call: 'groups',
httpType: 'GET',
contentType: 'application/x-protobuf',
responseType: 'arraybuffer',
host: storageUrl,
});
return window.textsecure.protobuf.Group.decode(response);
}
async function modifyGroup(
changes: GroupChangeClass.Actions,
options: GroupCredentialsType
): Promise<GroupChangeClass> {
const basicAuth = generateGroupAuth(
options.groupPublicParamsHex,
options.authCredentialPresentationHex
);
const data = changes.toArrayBuffer();
const response: ArrayBuffer = await _ajax({
basicAuth,
call: 'groups',
httpType: 'PATCH',
data,
contentType: 'application/x-protobuf',
responseType: 'arraybuffer',
host: storageUrl,
});
return window.textsecure.protobuf.GroupChange.decode(response);
}
async function getGroupLog(
startVersion: number,
options: GroupCredentialsType
): Promise<GroupLogResponseType> {
const basicAuth = generateGroupAuth(
options.groupPublicParamsHex,
options.authCredentialPresentationHex
);
const withDetails: ArrayBufferWithDetailsType = await _ajax({
basicAuth,
call: 'groupLog',
urlParameters: `/${startVersion}`,
httpType: 'GET',
contentType: 'application/x-protobuf',
responseType: 'arraybufferwithdetails',
host: storageUrl,
});
const { data, response } = withDetails;
const changes = window.textsecure.protobuf.GroupChanges.decode(data);
if (response && response.status === 206) {
const range = response.headers.get('Content-Range');
const match = PARSE_GROUP_LOG_RANGE_HEADER.exec(range || '');
const start = match ? parseInt(match[0], 10) : undefined;
const end = match ? parseInt(match[1], 10) : undefined;
const currentRevision = match ? parseInt(match[2], 10) : undefined;
if (
match &&
is.number(start) &&
is.number(end) &&
is.number(currentRevision)
) {
return {
changes,
start,
end,
currentRevision,
};
}
}
return {
changes,
};
}
function getMessageSocket() { function getMessageSocket() {
window.log.info('opening message socket', url); window.log.info('opening message socket', url);
const fixedScheme = url const fixedScheme = url
.replace('https://', 'wss://') .replace('https://', 'wss://')
// tslint:disable-next-line no-http-string // tslint:disable-next-line no-http-string
.replace('http://', 'ws://'); .replace('http://', 'ws://');
const login = encodeURIComponent(username); const login = encodeURIComponent(username);
const pass = encodeURIComponent(password); const pass = encodeURIComponent(password);
const clientVersion = encodeURIComponent(version); const clientVersion = encodeURIComponent(version);
return _createSocket( return _createSocket(
`${fixedScheme}/v1/websocket/?login=${login}&password=${pass}&agent=OWD& version=${clientVersion}`, `${fixedScheme}/v1/websocket/?login=${login}&password=${pass}&agent=OWD& version=${clientVersion}`,
{ certificateAuthority, proxyUrl } { certificateAuthority, proxyUrl, version }
); );
} }
function getProvisioningSocket() { function getProvisioningSocket() {
window.log.info('opening provisioning socket', url); window.log.info('opening provisioning socket', url);
const fixedScheme = url const fixedScheme = url
.replace('https://', 'wss://') .replace('https://', 'wss://')
// tslint:disable-next-line no-http-string // tslint:disable-next-line no-http-string
.replace('http://', 'ws://'); .replace('http://', 'ws://');
const clientVersion = encodeURIComponent(version); const clientVersion = encodeURIComponent(version);
return _createSocket( return _createSocket(
`${fixedScheme}/v1/websocket/provisioning/?agent=OWD&version=${clientVer sion}`, `${fixedScheme}/v1/websocket/provisioning/?agent=OWD&version=${clientVer sion}`,
{ certificateAuthority, proxyUrl } { certificateAuthority, proxyUrl, version }
);
}
async function getDirectoryAuth(): Promise<{
username: string;
password: string;
}> {
return _ajax({
call: 'directoryAuth',
httpType: 'GET',
responseType: 'json',
});
}
function validateAttestationQuote({
serverStaticPublic,
quote,
}: {
serverStaticPublic: ArrayBuffer;
quote: ArrayBuffer;
}) {
const SGX_CONSTANTS = getSgxConstants();
const byteBuffer = window.dcodeIO.ByteBuffer.wrap(
quote,
'binary',
window.dcodeIO.ByteBuffer.LITTLE_ENDIAN
); );
const quoteVersion = byteBuffer.readShort(0) & 0xffff;
if (quoteVersion < 0 || quoteVersion > 2) {
throw new Error(`Unknown version ${quoteVersion}`);
}
const miscSelect = new Uint8Array(getBytes(quote, 64, 4));
if (!miscSelect.every(byte => byte === 0)) {
throw new Error('Quote miscSelect invalid!');
}
const reserved1 = new Uint8Array(getBytes(quote, 68, 28));
if (!reserved1.every(byte => byte === 0)) {
throw new Error('Quote reserved1 invalid!');
}
const flags = byteBuffer.readLong(96);
if (
flags.and(SGX_CONSTANTS.SGX_FLAGS_RESERVED).notEquals(0) ||
flags.and(SGX_CONSTANTS.SGX_FLAGS_INITTED).equals(0) ||
flags.and(SGX_CONSTANTS.SGX_FLAGS_MODE64BIT).equals(0)
) {
throw new Error(`Quote flags invalid ${flags.toString()}`);
}
const xfrm = byteBuffer.readLong(104);
if (xfrm.and(SGX_CONSTANTS.SGX_XFRM_RESERVED).notEquals(0)) {
throw new Error(`Quote xfrm invalid ${xfrm}`);
}
const mrenclave = new Uint8Array(getBytes(quote, 112, 32));
const enclaveIdBytes = new Uint8Array(
bytesFromHexString(directoryEnclaveId)
);
if (!mrenclave.every((byte, index) => byte === enclaveIdBytes[index])) {
throw new Error('Quote mrenclave invalid!');
}
const reserved2 = new Uint8Array(getBytes(quote, 144, 32));
if (!reserved2.every(byte => byte === 0)) {
throw new Error('Quote reserved2 invalid!');
}
const reportData = new Uint8Array(getBytes(quote, 368, 64));
const serverStaticPublicBytes = new Uint8Array(serverStaticPublic);
if (
!reportData.every((byte, index) => {
if (index >= 32) {
return byte === 0;
}
return byte === serverStaticPublicBytes[index];
})
) {
throw new Error('Quote report_data invalid!');
}
const reserved3 = new Uint8Array(getBytes(quote, 208, 96));
if (!reserved3.every(byte => byte === 0)) {
throw new Error('Quote reserved3 invalid!');
}
const reserved4 = new Uint8Array(getBytes(quote, 308, 60));
if (!reserved4.every(byte => byte === 0)) {
throw new Error('Quote reserved4 invalid!');
}
const signatureLength = byteBuffer.readInt(432) & 0xffff_ffff;
if (signatureLength !== quote.byteLength - 436) {
throw new Error(`Bad signatureLength ${signatureLength}`);
}
// const signature = Uint8Array.from(getBytes(quote, 436, signatureLength)
);
}
function validateAttestationSignatureBody(
signatureBody: {
timestamp: string;
version: number;
isvEnclaveQuoteBody: string;
isvEnclaveQuoteStatus: string;
},
encodedQuote: string
) {
// Parse timestamp as UTC
const { timestamp } = signatureBody;
const utcTimestamp = timestamp.endsWith('Z')
? timestamp
: `${timestamp}Z`;
const signatureTime = new Date(utcTimestamp).getTime();
const now = Date.now();
if (signatureBody.version !== 3) {
throw new Error('Attestation signature invalid version!');
}
if (!encodedQuote.startsWith(signatureBody.isvEnclaveQuoteBody)) {
throw new Error('Attestion signature mismatches quote!');
}
if (signatureBody.isvEnclaveQuoteStatus !== 'OK') {
throw new Error('Attestation signature status not "OK"!');
}
if (signatureTime < now - 24 * 60 * 60 * 1000) {
throw new Error('Attestation signature timestamp older than 24 hours!');
}
}
async function validateAttestationSignature(
signature: ArrayBuffer,
signatureBody: string,
certificates: string
) {
const CERT_PREFIX = '-----BEGIN CERTIFICATE-----';
const pem = compact(
certificates.split(CERT_PREFIX).map(match => {
if (!match) {
return null;
}
return `${CERT_PREFIX}${match}`;
})
);
if (pem.length < 2) {
throw new Error(
`validateAttestationSignature: Expect two or more entries; got ${pem.l
ength}`
);
}
const verify = createVerify('RSA-SHA256');
verify.update(Buffer.from(bytesFromString(signatureBody)));
const isValid = verify.verify(pem[0], Buffer.from(signature));
if (!isValid) {
throw new Error('Validation of signature across signatureBody failed!');
}
const caStore = pki.createCaStore([directoryTrustAnchor]);
const chain = compact(pem.map(cert => pki.certificateFromPem(cert)));
const isChainValid = pki.verifyCertificateChain(caStore, chain);
if (!isChainValid) {
throw new Error('Validation of certificate chain failed!');
}
const leafCert = chain[0];
const fieldCN = leafCert.subject.getField('CN');
if (
!fieldCN ||
fieldCN.value !== 'Intel SGX Attestation Report Signing'
) {
throw new Error('Leaf cert CN field had unexpected value');
}
const fieldO = leafCert.subject.getField('O');
if (!fieldO || fieldO.value !== 'Intel Corporation') {
throw new Error('Leaf cert O field had unexpected value');
}
const fieldL = leafCert.subject.getField('L');
if (!fieldL || fieldL.value !== 'Santa Clara') {
throw new Error('Leaf cert L field had unexpected value');
}
const fieldST = leafCert.subject.getField('ST');
if (!fieldST || fieldST.value !== 'CA') {
throw new Error('Leaf cert ST field had unexpected value');
}
const fieldC = leafCert.subject.getField('C');
if (!fieldC || fieldC.value !== 'US') {
throw new Error('Leaf cert C field had unexpected value');
}
}
// tslint:disable-next-line max-func-body-length
async function putRemoteAttestation(auth: {
username: string;
password: string;
}) {
const keyPair = await window.libsignal.externalCurveAsync.generateKeyPair(
);
const { privKey, pubKey } = keyPair;
// Remove first "key type" byte from public key
const slicedPubKey = pubKey.slice(1);
const pubKeyBase64 = arrayBufferToBase64(slicedPubKey);
// Do request
const data = JSON.stringify({ clientPublic: pubKeyBase64 });
const result: JSONWithDetailsType = await _outerAjax(null, {
certificateAuthority,
type: 'PUT',
contentType: 'application/json; charset=utf-8',
host: directoryUrl,
path: `${URL_CALLS.attestation}/${directoryEnclaveId}`,
user: auth.username,
password: auth.password,
responseType: 'jsonwithdetails',
data,
version,
});
const { data: responseBody, response } = result;
const attestationsLength = Object.keys(responseBody.attestations).length;
if (attestationsLength > 3) {
throw new Error(
'Got more than three attestations from the Contact Discovery Service'
);
}
if (attestationsLength < 1) {
throw new Error(
'Got no attestations from the Contact Discovery Service'
);
}
const cookie = response.headers.get('set-cookie');
// Decode response
return {
cookie,
attestations: await pProps(
responseBody.attestations,
async attestation => {
const decoded = { ...attestation };
[
'ciphertext',
'iv',
'quote',
'serverEphemeralPublic',
'serverStaticPublic',
'signature',
'tag',
].forEach(prop => {
decoded[prop] = base64ToArrayBuffer(decoded[prop]);
});
// Validate response
validateAttestationQuote(decoded);
validateAttestationSignatureBody(
JSON.parse(decoded.signatureBody),
attestation.quote
);
await validateAttestationSignature(
decoded.signature,
decoded.signatureBody,
decoded.certificates
);
// Derive key
const ephemeralToEphemeral = await window.libsignal.externalCurveAsy
nc.calculateAgreement(
decoded.serverEphemeralPublic,
privKey
);
const ephemeralToStatic = await window.libsignal.externalCurveAsync.
calculateAgreement(
decoded.serverStaticPublic,
privKey
);
const masterSecret = concatenateBytes(
ephemeralToEphemeral,
ephemeralToStatic
);
const publicKeys = concatenateBytes(
slicedPubKey,
decoded.serverEphemeralPublic,
decoded.serverStaticPublic
);
const [
clientKey,
serverKey,
] = await window.libsignal.HKDF.deriveSecrets(
masterSecret,
publicKeys
);
// Decrypt ciphertext into requestId
const requestId = await decryptAesGcm(
serverKey,
decoded.iv,
concatenateBytes(decoded.ciphertext, decoded.tag)
);
return { clientKey, serverKey, requestId };
}
),
};
}
async function getUuidsForE164s(
e164s: ReadonlyArray<string>
): Promise<Dictionary<string | null>> {
const directoryAuth = await getDirectoryAuth();
const attestationResult = await putRemoteAttestation(directoryAuth);
// Encrypt data for discovery
const data = await encryptCdsDiscoveryRequest(
attestationResult.attestations,
e164s
);
const { cookie } = attestationResult;
// Send discovery request
const discoveryResponse: {
requestId: string;
iv: string;
data: string;
mac: string;
} = await _outerAjax(null, {
certificateAuthority,
type: 'PUT',
headers: cookie
? {
cookie,
}
: undefined,
contentType: 'application/json; charset=utf-8',
host: directoryUrl,
path: `${URL_CALLS.discovery}/${directoryEnclaveId}`,
user: directoryAuth.username,
password: directoryAuth.password,
responseType: 'json',
data: JSON.stringify(data),
version,
});
// Decode discovery request response
const decodedDiscoveryResponse: {
[K in keyof typeof discoveryResponse]: ArrayBuffer;
} = mapValues(discoveryResponse, value => {
return base64ToArrayBuffer(value);
}) as any;
const returnedAttestation = Object.values(
attestationResult.attestations
).find(at =>
constantTimeEqual(at.requestId, decodedDiscoveryResponse.requestId)
);
if (!returnedAttestation) {
throw new Error('No known attestations returned from CDS');
}
// Decrypt discovery response
const decryptedDiscoveryData = await decryptAesGcm(
returnedAttestation.serverKey,
decodedDiscoveryResponse.iv,
concatenateBytes(
decodedDiscoveryResponse.data,
decodedDiscoveryResponse.mac
)
);
// Process and return result
const uuids = splitUuids(decryptedDiscoveryData);
if (uuids.length !== e164s.length) {
throw new Error(
'Returned set of UUIDs did not match returned set of e164s!'
);
}
return zipObject(e164s, uuids);
} }
} }
} }
 End of changes. 59 change blocks. 
34 lines changed or deleted 844 lines changed or added

Home  |  About  |  Features  |  All  |  Newest  |  Dox  |  Diffs  |  RSS Feeds  |  Screenshots  |  Comments  |  Imprint  |  Privacy  |  HTTP(S)