Use new CDS implementation in staging
This commit is contained in:
parent
5774fdef9f
commit
0c8c332805
11 changed files with 284 additions and 130 deletions
11
app/main.ts
11
app/main.ts
|
@ -308,12 +308,15 @@ function prepareUrl(
|
|||
serverUrl: config.get<string>('serverUrl'),
|
||||
storageUrl: config.get<string>('storageUrl'),
|
||||
updatesUrl: config.get<string>('updatesUrl'),
|
||||
directoryUrl: config.get<string>('directoryUrl'),
|
||||
directoryEnclaveId: config.get<string>('directoryEnclaveId'),
|
||||
directoryTrustAnchor: config.get<string>('directoryTrustAnchor'),
|
||||
directoryVersion: config.get<number | undefined>('directoryVersion') || 1,
|
||||
directoryUrl: config.get<string | null>('directoryUrl') || undefined,
|
||||
directoryEnclaveId:
|
||||
config.get<string | null>('directoryEnclaveId') || undefined,
|
||||
directoryTrustAnchor:
|
||||
config.get<string | null>('directoryTrustAnchor') || undefined,
|
||||
directoryV2Url: config.get<string>('directoryV2Url'),
|
||||
directoryV2PublicKey: config.get<string>('directoryV2PublicKey'),
|
||||
directoryV2CodeHash: config.get<string>('directoryV2CodeHash'),
|
||||
directoryV2CodeHashes: config.get<string>('directoryV2CodeHashes'),
|
||||
cdnUrl0: config.get<ConfigType>('cdn').get<string>('0'),
|
||||
cdnUrl2: config.get<ConfigType>('cdn').get<string>('2'),
|
||||
certificateAuthority: config.get<string>('certificateAuthority'),
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
{
|
||||
"serverUrl": "https://chat.staging.signal.org",
|
||||
"storageUrl": "https://storage-staging.signal.org",
|
||||
"directoryUrl": "https://api-staging.directory.signal.org",
|
||||
"directoryEnclaveId": "c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15",
|
||||
"directoryTrustAnchor": "-----BEGIN CERTIFICATE-----\nMIIFSzCCA7OgAwIBAgIJANEHdl0yo7CUMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV\nBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV\nBAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0\nYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwIBcNMTYxMTE0MTUzNzMxWhgPMjA0OTEy\nMzEyMzU5NTlaMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwL\nU2FudGEgQ2xhcmExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQD\nDCdJbnRlbCBTR1ggQXR0ZXN0YXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwggGiMA0G\nCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCfPGR+tXc8u1EtJzLA10Feu1Wg+p7e\nLmSRmeaCHbkQ1TF3Nwl3RmpqXkeGzNLd69QUnWovYyVSndEMyYc3sHecGgfinEeh\nrgBJSEdsSJ9FpaFdesjsxqzGRa20PYdnnfWcCTvFoulpbFR4VBuXnnVLVzkUvlXT\nL/TAnd8nIZk0zZkFJ7P5LtePvykkar7LcSQO85wtcQe0R1Raf/sQ6wYKaKmFgCGe\nNpEJUmg4ktal4qgIAxk+QHUxQE42sxViN5mqglB0QJdUot/o9a/V/mMeH8KvOAiQ\nbyinkNndn+Bgk5sSV5DFgF0DffVqmVMblt5p3jPtImzBIH0QQrXJq39AT8cRwP5H\nafuVeLHcDsRp6hol4P+ZFIhu8mmbI1u0hH3W/0C2BuYXB5PC+5izFFh/nP0lc2Lf\n6rELO9LZdnOhpL1ExFOq9H/B8tPQ84T3Sgb4nAifDabNt/zu6MmCGo5U8lwEFtGM\nRoOaX4AS+909x00lYnmtwsDVWv9vBiJCXRsCAwEAAaOByTCBxjBgBgNVHR8EWTBX\nMFWgU6BRhk9odHRwOi8vdHJ1c3RlZHNlcnZpY2VzLmludGVsLmNvbS9jb250ZW50\nL0NSTC9TR1gvQXR0ZXN0YXRpb25SZXBvcnRTaWduaW5nQ0EuY3JsMB0GA1UdDgQW\nBBR4Q3t2pn680K9+QjfrNXw7hwFRPDAfBgNVHSMEGDAWgBR4Q3t2pn680K9+Qjfr\nNXw7hwFRPDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkq\nhkiG9w0BAQsFAAOCAYEAeF8tYMXICvQqeXYQITkV2oLJsp6J4JAqJabHWxYJHGir\nIEqucRiJSSx+HjIJEUVaj8E0QjEud6Y5lNmXlcjqRXaCPOqK0eGRz6hi+ripMtPZ\nsFNaBwLQVV905SDjAzDzNIDnrcnXyB4gcDFCvwDFKKgLRjOB/WAqgscDUoGq5ZVi\nzLUzTqiQPmULAQaB9c6Oti6snEFJiCQ67JLyW/E83/frzCmO5Ru6WjU4tmsmy8Ra\nUd4APK0wZTGtfPXU7w+IBdG5Ez0kE1qzxGQaL4gINJ1zMyleDnbuS8UicjJijvqA\n152Sq049ESDz+1rRGc2NVEqh1KaGXmtXvqxXcTB+Ljy5Bw2ke0v8iGngFBPqCTVB\n3op5KBG3RjbF6RRSzwzuWfL7QErNC8WEy5yDVARzTA5+xmBc388v9Dm21HGfcC8O\nDD+gT9sSpssq0ascmvH49MOgjt1yoysLtdCtJW/9FZpoOypaHx0R+mJTLwPXVMrv\nDaVzWh5aiEx+idkSGMnX\n-----END CERTIFICATE-----\n",
|
||||
"directoryVersion": 2,
|
||||
"directoryUrl": null,
|
||||
"directoryEnclaveId": null,
|
||||
"directoryTrustAnchor": null,
|
||||
"directoryV2Url": "https://cdsh.staging.signal.org",
|
||||
"directoryV2PublicKey": "2fe57da347cd62431528daac5fbb290730fff684afc4cfc2ed90995f58cb3b74",
|
||||
"directoryV2CodeHash": "8c8025d787b4e7da35047c342a96c24a7119fd23ed9a3a774454a06315b4852a",
|
||||
"directoryV2CodeHashes": [
|
||||
"2f79dc6c1599b71c70fc2d14f3ea2e3bc65134436eb87011c88845b137af673a"
|
||||
],
|
||||
"cdn": {
|
||||
"0": "https://cdn-staging.signal.org",
|
||||
"2": "https://cdn2-staging.signal.org"
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
{
|
||||
"serverUrl": "https://chat.signal.org",
|
||||
"storageUrl": "https://storage.signal.org",
|
||||
"directoryVersion": 1,
|
||||
"directoryUrl": "https://api.directory.signal.org",
|
||||
"directoryEnclaveId": "c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15",
|
||||
"directoryTrustAnchor": "-----BEGIN CERTIFICATE-----\nMIIFSzCCA7OgAwIBAgIJANEHdl0yo7CUMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV\nBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV\nBAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0\nYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwIBcNMTYxMTE0MTUzNzMxWhgPMjA0OTEy\nMzEyMzU5NTlaMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwL\nU2FudGEgQ2xhcmExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQD\nDCdJbnRlbCBTR1ggQXR0ZXN0YXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwggGiMA0G\nCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCfPGR+tXc8u1EtJzLA10Feu1Wg+p7e\nLmSRmeaCHbkQ1TF3Nwl3RmpqXkeGzNLd69QUnWovYyVSndEMyYc3sHecGgfinEeh\nrgBJSEdsSJ9FpaFdesjsxqzGRa20PYdnnfWcCTvFoulpbFR4VBuXnnVLVzkUvlXT\nL/TAnd8nIZk0zZkFJ7P5LtePvykkar7LcSQO85wtcQe0R1Raf/sQ6wYKaKmFgCGe\nNpEJUmg4ktal4qgIAxk+QHUxQE42sxViN5mqglB0QJdUot/o9a/V/mMeH8KvOAiQ\nbyinkNndn+Bgk5sSV5DFgF0DffVqmVMblt5p3jPtImzBIH0QQrXJq39AT8cRwP5H\nafuVeLHcDsRp6hol4P+ZFIhu8mmbI1u0hH3W/0C2BuYXB5PC+5izFFh/nP0lc2Lf\n6rELO9LZdnOhpL1ExFOq9H/B8tPQ84T3Sgb4nAifDabNt/zu6MmCGo5U8lwEFtGM\nRoOaX4AS+909x00lYnmtwsDVWv9vBiJCXRsCAwEAAaOByTCBxjBgBgNVHR8EWTBX\nMFWgU6BRhk9odHRwOi8vdHJ1c3RlZHNlcnZpY2VzLmludGVsLmNvbS9jb250ZW50\nL0NSTC9TR1gvQXR0ZXN0YXRpb25SZXBvcnRTaWduaW5nQ0EuY3JsMB0GA1UdDgQW\nBBR4Q3t2pn680K9+QjfrNXw7hwFRPDAfBgNVHSMEGDAWgBR4Q3t2pn680K9+Qjfr\nNXw7hwFRPDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkq\nhkiG9w0BAQsFAAOCAYEAeF8tYMXICvQqeXYQITkV2oLJsp6J4JAqJabHWxYJHGir\nIEqucRiJSSx+HjIJEUVaj8E0QjEud6Y5lNmXlcjqRXaCPOqK0eGRz6hi+ripMtPZ\nsFNaBwLQVV905SDjAzDzNIDnrcnXyB4gcDFCvwDFKKgLRjOB/WAqgscDUoGq5ZVi\nzLUzTqiQPmULAQaB9c6Oti6snEFJiCQ67JLyW/E83/frzCmO5Ru6WjU4tmsmy8Ra\nUd4APK0wZTGtfPXU7w+IBdG5Ez0kE1qzxGQaL4gINJ1zMyleDnbuS8UicjJijvqA\n152Sq049ESDz+1rRGc2NVEqh1KaGXmtXvqxXcTB+Ljy5Bw2ke0v8iGngFBPqCTVB\n3op5KBG3RjbF6RRSzwzuWfL7QErNC8WEy5yDVARzTA5+xmBc388v9Dm21HGfcC8O\nDD+gT9sSpssq0ascmvH49MOgjt1yoysLtdCtJW/9FZpoOypaHx0R+mJTLwPXVMrv\nDaVzWh5aiEx+idkSGMnX\n-----END CERTIFICATE-----\n",
|
||||
"cdn": {
|
||||
"0": "https://cdn.signal.org",
|
||||
"2": "https://cdn2.signal.org"
|
||||
|
|
|
@ -376,12 +376,13 @@ try {
|
|||
url: config.serverUrl,
|
||||
storageUrl: config.storageUrl,
|
||||
updatesUrl: config.updatesUrl,
|
||||
directoryVersion: parseInt(config.directoryVersion, 10),
|
||||
directoryUrl: config.directoryUrl,
|
||||
directoryEnclaveId: config.directoryEnclaveId,
|
||||
directoryTrustAnchor: config.directoryTrustAnchor,
|
||||
directoryV2Url: config.directoryV2Url,
|
||||
directoryV2PublicKey: config.directoryV2PublicKey,
|
||||
directoryV2CodeHash: config.directoryV2CodeHash,
|
||||
directoryV2CodeHashes: (config.directoryV2CodeHashes || '').split(','),
|
||||
cdnUrlObject: {
|
||||
0: config.cdnUrl0,
|
||||
2: config.cdnUrl2,
|
||||
|
|
|
@ -4,16 +4,23 @@
|
|||
package signalservice;
|
||||
|
||||
message CDSClientRequest {
|
||||
// From Signal /v2/directory/auth
|
||||
optional bytes username = 1;
|
||||
optional bytes password = 2;
|
||||
|
||||
// Each e164 is a big-endian uint64 (8 bytes).
|
||||
repeated bytes e164 = 3;
|
||||
|
||||
// Each ACI/UAK pair is a 32-byte buffer, containing the 16-byte ACI followed
|
||||
// by its 16-byte UAK.
|
||||
repeated bytes aci_uak_pair = 4;
|
||||
optional bytes aci_uak_pairs = 1;
|
||||
|
||||
// Each E164 is an 8-byte big-endian number, as 8 bytes.
|
||||
optional bytes prev_e164s = 2;
|
||||
optional bytes new_e164s = 3;
|
||||
optional bytes discard_e164s = 4;
|
||||
|
||||
// If true, the client has more pairs or e164s to send. If false or unset,
|
||||
// this is the client's last request, and processing should commence.
|
||||
optional bool has_more = 5;
|
||||
|
||||
// If set, a token which allows rate limiting to discount the e164s in
|
||||
// the request's prev_e164s, only counting new_e164s. If not set, then
|
||||
// rate limiting considers both prev_e164s' and new_e164s' size.
|
||||
optional bytes token = 6;
|
||||
}
|
||||
|
||||
message CDSClientResponse {
|
||||
|
@ -29,7 +36,7 @@ message CDSClientResponse {
|
|||
// where the additional 2 bytes are the id/type/length additions of the
|
||||
// protobuf marshaling added to each byte array. This avoids any data
|
||||
// leakage based on the size of the encrypted output.
|
||||
repeated bytes e164_pni_aci_triple = 1;
|
||||
optional bytes e164_pni_aci_triples = 1;
|
||||
|
||||
// If the user has run out of quota for lookups, they will receive
|
||||
// a response with just the following field set, followed by a websocket
|
||||
|
@ -37,4 +44,9 @@ message CDSClientResponse {
|
|||
// the same request after the provided number of seconds has passed,
|
||||
// we expect it should work.
|
||||
optional int32 retry_after_secs = 2;
|
||||
|
||||
// A token which allows subsequent calls' rate limiting to discount the
|
||||
// e164s sent up in this request, only counting those in the next
|
||||
// request's new_e164s.
|
||||
optional bytes token = 3;
|
||||
}
|
||||
|
|
|
@ -56,12 +56,13 @@ const WebAPI = initializeWebAPI({
|
|||
url: config.serverUrl,
|
||||
storageUrl: config.storageUrl,
|
||||
updatesUrl: config.updatesUrl,
|
||||
directoryVersion: parseInt(config.directoryVersion, 10),
|
||||
directoryUrl: config.directoryUrl,
|
||||
directoryEnclaveId: config.directoryEnclaveId,
|
||||
directoryTrustAnchor: config.directoryTrustAnchor,
|
||||
directoryV2Url: config.directoryV2Url,
|
||||
directoryV2PublicKey: config.directoryV2PublicKey,
|
||||
directoryV2CodeHash: config.directoryV2CodeHash,
|
||||
directoryV2CodeHashes: (config.directoryV2CodeHashes || '').split(','),
|
||||
cdnUrlObject: {
|
||||
0: config.cdnUrl0,
|
||||
2: config.cdnUrl2,
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { noop } from 'lodash';
|
||||
import { Readable } from 'stream';
|
||||
import type { HsmEnclaveClient } from '@signalapp/signal-client';
|
||||
import type { connection as WebSocket } from 'websocket';
|
||||
import Long from 'long';
|
||||
|
@ -10,6 +12,7 @@ import { strictAssert } from '../util/assert';
|
|||
import { dropNull } from '../util/dropNull';
|
||||
import { explodePromise } from '../util/explodePromise';
|
||||
import * as durations from '../util/durations';
|
||||
import * as log from '../logging/log';
|
||||
import type { UUIDStringType } from '../types/UUID';
|
||||
import { UUID_BYTE_SIZE } from '../types/UUID';
|
||||
import * as Bytes from '../Bytes';
|
||||
|
@ -23,13 +26,24 @@ enum State {
|
|||
Closed,
|
||||
}
|
||||
|
||||
export type CDSRequestOptionsType = Readonly<{
|
||||
e164s: ReadonlyArray<string>;
|
||||
acis: ReadonlyArray<UUIDStringType>;
|
||||
accessKeys: ReadonlyArray<string>;
|
||||
auth: CDSAuthType;
|
||||
timeout?: number;
|
||||
}>;
|
||||
export type CDSRequestOptionsType = Readonly<
|
||||
{
|
||||
auth: CDSAuthType;
|
||||
e164s: ReadonlyArray<string>;
|
||||
timeout?: number;
|
||||
} & (
|
||||
| {
|
||||
version: 1;
|
||||
acis?: undefined;
|
||||
accessKeys?: undefined;
|
||||
}
|
||||
| {
|
||||
version: 2;
|
||||
acis: ReadonlyArray<UUIDStringType>;
|
||||
accessKeys: ReadonlyArray<string>;
|
||||
}
|
||||
)
|
||||
>;
|
||||
|
||||
export type CDSAuthType = Readonly<{
|
||||
username: string;
|
||||
|
@ -50,19 +64,23 @@ export type CDSSocketResponseType = Readonly<{
|
|||
retryAfterSecs?: number;
|
||||
}>;
|
||||
|
||||
const MAX_E164_COUNT = 5000;
|
||||
const HANDSHAKE_TIMEOUT = 10 * durations.SECOND;
|
||||
const REQUEST_TIMEOUT = 10 * durations.SECOND;
|
||||
const VERSION = new Uint8Array([0x02]);
|
||||
const USERNAME_LENGTH = 32;
|
||||
const PASSWORD_LENGTH = 31;
|
||||
const E164_BYTE_SIZE = 8;
|
||||
const TRIPLE_BYTE_SIZE = UUID_BYTE_SIZE * 2 + E164_BYTE_SIZE;
|
||||
|
||||
export class CDSSocket extends EventEmitter {
|
||||
private state = State.Handshake;
|
||||
|
||||
private readonly finishedHandshake: Promise<void>;
|
||||
|
||||
private readonly requestQueue = new Array<(buffer: Buffer) => void>();
|
||||
private readonly responseStream = new Readable({
|
||||
read: noop,
|
||||
|
||||
// Don't coalesce separate websocket messages
|
||||
objectMode: true,
|
||||
});
|
||||
|
||||
constructor(
|
||||
private readonly socket: WebSocket,
|
||||
|
@ -93,15 +111,25 @@ export class CDSSocket extends EventEmitter {
|
|||
return;
|
||||
}
|
||||
|
||||
const requestHandler = this.requestQueue.shift();
|
||||
strictAssert(
|
||||
requestHandler !== undefined,
|
||||
'No handler for incoming CDS data'
|
||||
);
|
||||
|
||||
requestHandler(this.enclaveClient.establishedRecv(binaryData));
|
||||
try {
|
||||
this.responseStream.push(
|
||||
this.enclaveClient.establishedRecv(binaryData)
|
||||
);
|
||||
} catch (error) {
|
||||
this.responseStream.destroy(error);
|
||||
}
|
||||
});
|
||||
socket.on('close', (code, reason) => {
|
||||
if (this.state === State.Established) {
|
||||
if (code === 1000) {
|
||||
this.responseStream.push(null);
|
||||
} else {
|
||||
this.responseStream.destroy(
|
||||
new Error(`Socket closed with code ${code} and reason ${reason}`)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.state = State.Closed;
|
||||
this.emit('close', code, reason);
|
||||
});
|
||||
|
@ -115,37 +143,32 @@ export class CDSSocket extends EventEmitter {
|
|||
}
|
||||
|
||||
public async request({
|
||||
e164s,
|
||||
acis,
|
||||
accessKeys,
|
||||
auth,
|
||||
version,
|
||||
timeout = REQUEST_TIMEOUT,
|
||||
e164s,
|
||||
acis = [],
|
||||
accessKeys = [],
|
||||
}: CDSRequestOptionsType): Promise<CDSSocketResponseType> {
|
||||
strictAssert(
|
||||
e164s.length < MAX_E164_COUNT,
|
||||
'CDSSocket does not support paging. Use this for one-off requests'
|
||||
);
|
||||
|
||||
log.info('CDSSocket.request(): awaiting handshake');
|
||||
await this.finishedHandshake;
|
||||
strictAssert(
|
||||
this.state === State.Established,
|
||||
'Connection not established'
|
||||
);
|
||||
|
||||
const username = Bytes.fromString(auth.username);
|
||||
const password = Bytes.fromString(auth.password);
|
||||
strictAssert(
|
||||
username.length === USERNAME_LENGTH,
|
||||
'Invalid username length'
|
||||
);
|
||||
strictAssert(
|
||||
password.length === PASSWORD_LENGTH,
|
||||
'Invalid password length'
|
||||
);
|
||||
|
||||
strictAssert(
|
||||
acis.length === accessKeys.length,
|
||||
`Number of ACIs ${acis.length} is different ` +
|
||||
`from number of access keys ${accessKeys.length}`
|
||||
);
|
||||
const aciUakPair = new Array<Uint8Array>();
|
||||
const aciUakPairs = new Array<Uint8Array>();
|
||||
for (let i = 0; i < acis.length; i += 1) {
|
||||
aciUakPair.push(
|
||||
aciUakPairs.push(
|
||||
Bytes.concatenate([
|
||||
uuidToBytes(acis[i]),
|
||||
Bytes.fromBase64(accessKeys[i]),
|
||||
|
@ -154,64 +177,55 @@ export class CDSSocket extends EventEmitter {
|
|||
}
|
||||
|
||||
const request = Proto.CDSClientRequest.encode({
|
||||
username,
|
||||
password,
|
||||
e164: e164s.map(e164 => {
|
||||
// Long.fromString handles numbers with or without a leading '+'
|
||||
return new Uint8Array(Long.fromString(e164).toBytesBE());
|
||||
}),
|
||||
aciUakPair,
|
||||
newE164s: Buffer.concat(
|
||||
e164s.map(e164 => {
|
||||
// Long.fromString handles numbers with or without a leading '+'
|
||||
return new Uint8Array(Long.fromString(e164).toBytesBE());
|
||||
})
|
||||
),
|
||||
aciUakPairs: Buffer.concat(aciUakPairs),
|
||||
}).finish();
|
||||
|
||||
const { promise, resolve, reject } = explodePromise<Buffer>();
|
||||
|
||||
const timer = Timers.setTimeout(() => {
|
||||
reject(new Error('CDS request timed out'));
|
||||
this.responseStream.destroy(new Error('CDS request timed out'));
|
||||
}, timeout);
|
||||
|
||||
log.info(`CDSSocket.request(): sending version=${version} request`);
|
||||
this.socket.sendBytes(
|
||||
this.enclaveClient.establishedSend(Buffer.concat([VERSION, request]))
|
||||
this.enclaveClient.establishedSend(
|
||||
Buffer.concat([Buffer.from([version]), request])
|
||||
)
|
||||
);
|
||||
|
||||
this.requestQueue.push(resolve);
|
||||
strictAssert(
|
||||
this.requestQueue.length === 1,
|
||||
'Concurrent use of CDS shold not happen'
|
||||
);
|
||||
const responseBytes = await promise;
|
||||
Timers.clearTimeout(timer);
|
||||
const resultMap: Map<string, CDSSocketDictionaryEntryType> = new Map();
|
||||
let retryAfterSecs: number | undefined;
|
||||
|
||||
const response = Proto.CDSClientResponse.decode(responseBytes);
|
||||
for await (const message of this.responseStream) {
|
||||
log.info('CDSSocket.request(): processing response message');
|
||||
|
||||
const dictionary: Record<string, CDSSocketDictionaryEntryType> =
|
||||
Object.create(null);
|
||||
const response = Proto.CDSClientResponse.decode(message);
|
||||
const newRetryAfterSecs = dropNull(response.retryAfterSecs);
|
||||
|
||||
for (const tripleBytes of response.e164PniAciTriple ?? []) {
|
||||
strictAssert(
|
||||
tripleBytes.length === UUID_BYTE_SIZE * 2 + E164_BYTE_SIZE,
|
||||
'Invalid size of CDS response triple'
|
||||
);
|
||||
decodeSingleResponse(resultMap, response);
|
||||
|
||||
let offset = 0;
|
||||
const e164Bytes = tripleBytes.slice(offset, offset + E164_BYTE_SIZE);
|
||||
offset += E164_BYTE_SIZE;
|
||||
|
||||
const pniBytes = tripleBytes.slice(offset, offset + UUID_BYTE_SIZE);
|
||||
offset += UUID_BYTE_SIZE;
|
||||
|
||||
const aciBytes = tripleBytes.slice(offset, offset + UUID_BYTE_SIZE);
|
||||
offset += UUID_BYTE_SIZE;
|
||||
|
||||
const e164 = `+${Long.fromBytesBE(Array.from(e164Bytes)).toString()}`;
|
||||
const pni = bytesToUuid(pniBytes);
|
||||
const aci = bytesToUuid(aciBytes);
|
||||
|
||||
dictionary[e164] = { pni, aci };
|
||||
if (newRetryAfterSecs) {
|
||||
retryAfterSecs = Math.max(newRetryAfterSecs, retryAfterSecs ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
const result: Record<string, CDSSocketDictionaryEntryType> =
|
||||
Object.create(null);
|
||||
|
||||
for (const [key, value] of resultMap) {
|
||||
result[key] = value;
|
||||
}
|
||||
|
||||
log.info('CDSSocket.request(): done');
|
||||
Timers.clearTimeout(timer);
|
||||
|
||||
return {
|
||||
dictionary,
|
||||
retryAfterSecs: dropNull(response.retryAfterSecs),
|
||||
dictionary: result,
|
||||
retryAfterSecs,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -239,3 +253,44 @@ export class CDSSocket extends EventEmitter {
|
|||
return super.emit(type, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
function decodeSingleResponse(
|
||||
resultMap: Map<string, CDSSocketDictionaryEntryType>,
|
||||
response: Proto.CDSClientResponse
|
||||
): void {
|
||||
for (
|
||||
let i = 0;
|
||||
i < response.e164PniAciTriples.length;
|
||||
i += TRIPLE_BYTE_SIZE
|
||||
) {
|
||||
const tripleBytes = response.e164PniAciTriples.slice(
|
||||
i,
|
||||
i + TRIPLE_BYTE_SIZE
|
||||
);
|
||||
strictAssert(
|
||||
tripleBytes.length === TRIPLE_BYTE_SIZE,
|
||||
'Invalid size of CDS response triple'
|
||||
);
|
||||
|
||||
let offset = 0;
|
||||
const e164Bytes = tripleBytes.slice(offset, offset + E164_BYTE_SIZE);
|
||||
offset += E164_BYTE_SIZE;
|
||||
|
||||
const pniBytes = tripleBytes.slice(offset, offset + UUID_BYTE_SIZE);
|
||||
offset += UUID_BYTE_SIZE;
|
||||
|
||||
const aciBytes = tripleBytes.slice(offset, offset + UUID_BYTE_SIZE);
|
||||
offset += UUID_BYTE_SIZE;
|
||||
|
||||
const e164Long = Long.fromBytesBE(Array.from(e164Bytes));
|
||||
if (e164Long.isZero()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const e164 = `+${e164Long.toString()}`;
|
||||
const pni = bytesToUuid(pniBytes);
|
||||
const aci = bytesToUuid(aciBytes);
|
||||
|
||||
resultMap.set(e164, { pni, aci });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,10 +9,12 @@ import * as Bytes from '../Bytes';
|
|||
import { prefixPublicKey } from '../Curve';
|
||||
import type { AbortableProcess } from '../util/AbortableProcess';
|
||||
import * as durations from '../util/durations';
|
||||
import { getBasicAuth } from '../util/getBasicAuth';
|
||||
import { sleep } from '../util/sleep';
|
||||
import * as log from '../logging/log';
|
||||
import { CDSSocket } from './CDSSocket';
|
||||
import type {
|
||||
CDSAuthType,
|
||||
CDSRequestOptionsType,
|
||||
CDSSocketDictionaryType,
|
||||
} from './CDSSocket';
|
||||
|
@ -21,7 +23,7 @@ import { connect as connectWebSocket } from './WebSocket';
|
|||
export type CDSSocketManagerOptionsType = Readonly<{
|
||||
url: string;
|
||||
publicKey: string;
|
||||
codeHash: string;
|
||||
codeHashes: ReadonlyArray<string>;
|
||||
certificateAuthority: string;
|
||||
proxyUrl?: string;
|
||||
version: string;
|
||||
|
@ -32,7 +34,7 @@ export type CDSResponseType = CDSSocketDictionaryType;
|
|||
export class CDSSocketManager {
|
||||
private readonly publicKey: PublicKey;
|
||||
|
||||
private readonly codeHash: Buffer;
|
||||
private readonly codeHashes: Array<Buffer>;
|
||||
|
||||
private readonly proxyAgent?: ReturnType<typeof ProxyAgent>;
|
||||
|
||||
|
@ -42,7 +44,9 @@ export class CDSSocketManager {
|
|||
this.publicKey = PublicKey.deserialize(
|
||||
Buffer.from(prefixPublicKey(Bytes.fromHex(options.publicKey)))
|
||||
);
|
||||
this.codeHash = Buffer.from(Bytes.fromHex(options.codeHash));
|
||||
this.codeHashes = options.codeHashes.map(hash =>
|
||||
Buffer.from(Bytes.fromHex(hash))
|
||||
);
|
||||
if (options.proxyUrl) {
|
||||
this.proxyAgent = new ProxyAgent(options.proxyUrl);
|
||||
}
|
||||
|
@ -58,8 +62,10 @@ export class CDSSocketManager {
|
|||
await sleep(delay);
|
||||
}
|
||||
|
||||
const { auth } = options;
|
||||
|
||||
log.info('CDSSocketManager: connecting socket');
|
||||
const socket = await this.connect().getResult();
|
||||
const socket = await this.connect(auth).getResult();
|
||||
log.info('CDSSocketManager: connected socket');
|
||||
|
||||
try {
|
||||
|
@ -79,16 +85,14 @@ export class CDSSocketManager {
|
|||
}
|
||||
}
|
||||
|
||||
private connect(): AbortableProcess<CDSSocket> {
|
||||
const enclaveClient = HsmEnclaveClient.new(this.publicKey, [this.codeHash]);
|
||||
private connect(auth: CDSAuthType): AbortableProcess<CDSSocket> {
|
||||
const enclaveClient = HsmEnclaveClient.new(this.publicKey, this.codeHashes);
|
||||
|
||||
const {
|
||||
publicKey: publicKeyHex,
|
||||
codeHash: codeHashHex,
|
||||
version,
|
||||
} = this.options;
|
||||
const { publicKey: publicKeyHex, codeHashes, version } = this.options;
|
||||
|
||||
const url = `${this.options.url}/discovery/${publicKeyHex}/${codeHashHex}`;
|
||||
const url = `${
|
||||
this.options.url
|
||||
}/discovery/${publicKeyHex}/${codeHashes.join(',')}`;
|
||||
|
||||
return connectWebSocket<CDSSocket>({
|
||||
name: 'CDSSocket',
|
||||
|
@ -96,6 +100,9 @@ export class CDSSocketManager {
|
|||
version,
|
||||
proxyAgent: this.proxyAgent,
|
||||
certificateAuthority: this.options.certificateAuthority,
|
||||
extraHeaders: {
|
||||
authorization: getBasicAuth(auth),
|
||||
},
|
||||
|
||||
createResource: (socket: WebSocket): CDSSocket => {
|
||||
return new CDSSocket(socket, enclaveClient);
|
||||
|
|
|
@ -34,6 +34,7 @@ import { getUserAgent } from '../util/getUserAgent';
|
|||
import { getStreamWithTimeout } from '../util/getStreamWithTimeout';
|
||||
import { formatAcceptLanguageHeader } from '../util/userLanguages';
|
||||
import { toWebSafeBase64 } from '../util/webSafeBase64';
|
||||
import { getBasicAuth } from '../util/getBasicAuth';
|
||||
import type { SocketStatus } from '../types/SocketStatus';
|
||||
import { toLogFormat } from '../types/errors';
|
||||
import { isPackIdValid, redactPackId } from '../types/Stickers';
|
||||
|
@ -338,10 +339,10 @@ async function _promiseAjax(
|
|||
fetchOptions.headers['Unidentified-Access-Key'] = accessKey;
|
||||
}
|
||||
} else if (options.user && options.password) {
|
||||
const auth = Bytes.toBase64(
|
||||
Bytes.fromString(`${options.user}:${options.password}`)
|
||||
);
|
||||
fetchOptions.headers.Authorization = `Basic ${auth}`;
|
||||
fetchOptions.headers.Authorization = getBasicAuth({
|
||||
username: options.user,
|
||||
password: options.password,
|
||||
});
|
||||
}
|
||||
|
||||
if (options.contentType) {
|
||||
|
@ -596,12 +597,13 @@ type InitializeOptionsType = {
|
|||
url: string;
|
||||
storageUrl: string;
|
||||
updatesUrl: string;
|
||||
directoryEnclaveId: string;
|
||||
directoryTrustAnchor: string;
|
||||
directoryUrl: string;
|
||||
directoryVersion: number;
|
||||
directoryUrl?: string;
|
||||
directoryEnclaveId?: string;
|
||||
directoryTrustAnchor?: string;
|
||||
directoryV2Url: string;
|
||||
directoryV2PublicKey: string;
|
||||
directoryV2CodeHash: string;
|
||||
directoryV2CodeHashes: ReadonlyArray<string>;
|
||||
cdnUrlObject: {
|
||||
readonly '0': string;
|
||||
readonly [propName: string]: string;
|
||||
|
@ -999,12 +1001,13 @@ export function initialize({
|
|||
url,
|
||||
storageUrl,
|
||||
updatesUrl,
|
||||
directoryVersion,
|
||||
directoryUrl,
|
||||
directoryEnclaveId,
|
||||
directoryTrustAnchor,
|
||||
directoryUrl,
|
||||
directoryV2Url,
|
||||
directoryV2PublicKey,
|
||||
directoryV2CodeHash,
|
||||
directoryV2CodeHashes,
|
||||
cdnUrlObject,
|
||||
certificateAuthority,
|
||||
contentProxyUrl,
|
||||
|
@ -1020,14 +1023,26 @@ export function initialize({
|
|||
if (!is.string(updatesUrl)) {
|
||||
throw new Error('WebAPI.initialize: Invalid updatesUrl');
|
||||
}
|
||||
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 (directoryVersion === 1) {
|
||||
if (!is.string(directoryEnclaveId)) {
|
||||
throw new Error('WebAPI.initialize: Invalid directory enclave id');
|
||||
}
|
||||
if (!is.string(directoryTrustAnchor)) {
|
||||
throw new Error('WebAPI.initialize: Invalid directory trust anchor');
|
||||
}
|
||||
if (!is.string(directoryUrl)) {
|
||||
throw new Error('WebAPI.initialize: Invalid directory url');
|
||||
}
|
||||
} else {
|
||||
if (directoryEnclaveId) {
|
||||
throw new Error('WebAPI.initialize: Invalid directory enclave id');
|
||||
}
|
||||
if (directoryTrustAnchor) {
|
||||
throw new Error('WebAPI.initialize: Invalid directory trust anchor');
|
||||
}
|
||||
if (directoryUrl) {
|
||||
throw new Error('WebAPI.initialize: Invalid directory url');
|
||||
}
|
||||
}
|
||||
if (!is.string(directoryV2Url)) {
|
||||
throw new Error('WebAPI.initialize: Invalid directory V2 url');
|
||||
|
@ -1035,7 +1050,7 @@ export function initialize({
|
|||
if (!is.string(directoryV2PublicKey)) {
|
||||
throw new Error('WebAPI.initialize: Invalid directory V2 public key');
|
||||
}
|
||||
if (!is.string(directoryV2CodeHash)) {
|
||||
if (!is.array(directoryV2CodeHashes)) {
|
||||
throw new Error('WebAPI.initialize: Invalid directory V2 code hash');
|
||||
}
|
||||
if (!is.object(cdnUrlObject)) {
|
||||
|
@ -1104,7 +1119,7 @@ export function initialize({
|
|||
const cdsSocketManager = new CDSSocketManager({
|
||||
url: directoryV2Url,
|
||||
publicKey: directoryV2PublicKey,
|
||||
codeHash: directoryV2CodeHash,
|
||||
codeHashes: directoryV2CodeHashes,
|
||||
certificateAuthority,
|
||||
version,
|
||||
proxyUrl,
|
||||
|
@ -2723,6 +2738,7 @@ export function initialize({
|
|||
username: string;
|
||||
password: string;
|
||||
}> {
|
||||
strictAssert(directoryVersion === 1, 'Legacy CDS should not be used');
|
||||
return (await _ajax({
|
||||
call: 'directoryAuth',
|
||||
httpType: 'GET',
|
||||
|
@ -2748,6 +2764,9 @@ export function initialize({
|
|||
serverStaticPublic: Uint8Array;
|
||||
quote: Uint8Array;
|
||||
}) {
|
||||
strictAssert(directoryVersion === 1, 'Legacy CDS should not be used');
|
||||
strictAssert(directoryEnclaveId, 'Legacy CDS needs directoryEnclaveId');
|
||||
|
||||
const SGX_CONSTANTS = getSgxConstants();
|
||||
const quote = Buffer.from(quoteBytes);
|
||||
|
||||
|
@ -2835,6 +2854,8 @@ export function initialize({
|
|||
},
|
||||
encodedQuote: string
|
||||
) {
|
||||
strictAssert(directoryVersion === 1, 'Legacy CDS should not be used');
|
||||
|
||||
// Parse timestamp as UTC
|
||||
const { timestamp } = signatureBody;
|
||||
const utcTimestamp = timestamp.endsWith('Z')
|
||||
|
@ -2862,6 +2883,12 @@ export function initialize({
|
|||
signatureBody: string,
|
||||
certificates: string
|
||||
) {
|
||||
strictAssert(directoryVersion === 1, 'Legacy CDS should not be used');
|
||||
strictAssert(
|
||||
directoryTrustAnchor,
|
||||
'Legacy CDS needs directoryTrustAnchor'
|
||||
);
|
||||
|
||||
const CERT_PREFIX = '-----BEGIN CERTIFICATE-----';
|
||||
const pem = compact(
|
||||
certificates.split(CERT_PREFIX).map(match => {
|
||||
|
@ -2922,6 +2949,8 @@ export function initialize({
|
|||
username: string;
|
||||
password: string;
|
||||
}) {
|
||||
strictAssert(directoryVersion === 1, 'Legacy CDS should not be used');
|
||||
|
||||
const keyPair = generateKeyPair();
|
||||
const { privKey, pubKey } = keyPair;
|
||||
// Remove first "key type" byte from public key
|
||||
|
@ -3051,7 +3080,7 @@ export function initialize({
|
|||
};
|
||||
}
|
||||
|
||||
async function getUuidsForE164s(
|
||||
async function getLegacyUuidsForE164s(
|
||||
e164s: ReadonlyArray<string>
|
||||
): Promise<Dictionary<UUIDStringType | null>> {
|
||||
const directoryAuth = await getDirectoryAuth();
|
||||
|
@ -3127,6 +3156,24 @@ export function initialize({
|
|||
return zipObject(e164s, uuids);
|
||||
}
|
||||
|
||||
async function getUuidsForE164s(
|
||||
e164s: ReadonlyArray<string>
|
||||
): Promise<Dictionary<UUIDStringType | null>> {
|
||||
if (directoryVersion === 1) {
|
||||
return getLegacyUuidsForE164s(e164s);
|
||||
}
|
||||
|
||||
const auth = await getDirectoryAuthV2();
|
||||
|
||||
const dictionary = await cdsSocketManager.request({
|
||||
version: 1,
|
||||
auth,
|
||||
e164s,
|
||||
});
|
||||
|
||||
return mapValues(dictionary, value => value.aci ?? null);
|
||||
}
|
||||
|
||||
async function getUuidsForE164sV2({
|
||||
e164s,
|
||||
acis,
|
||||
|
@ -3135,6 +3182,7 @@ export function initialize({
|
|||
const auth = await getDirectoryAuthV2();
|
||||
|
||||
return cdsSocketManager.request({
|
||||
version: 2,
|
||||
auth,
|
||||
e164s,
|
||||
acis,
|
||||
|
|
|
@ -28,6 +28,7 @@ export type ConnectOptionsType<Resource extends IResource> = Readonly<{
|
|||
version: string;
|
||||
proxyAgent?: ReturnType<typeof ProxyAgent>;
|
||||
timeout?: number;
|
||||
extraHeaders?: Record<string, string>;
|
||||
|
||||
createResource(socket: WebSocket): Resource;
|
||||
}>;
|
||||
|
@ -38,6 +39,7 @@ export function connect<Resource extends IResource>({
|
|||
certificateAuthority,
|
||||
version,
|
||||
proxyAgent,
|
||||
extraHeaders = {},
|
||||
timeout = TEN_SECONDS,
|
||||
createResource,
|
||||
}: ConnectOptionsType<Resource>): AbortableProcess<Resource> {
|
||||
|
@ -46,6 +48,7 @@ export function connect<Resource extends IResource>({
|
|||
.replace('http://', 'ws://');
|
||||
|
||||
const headers = {
|
||||
...extraHeaders,
|
||||
'User-Agent': getUserAgent(version),
|
||||
};
|
||||
const client = new WebSocketClient({
|
||||
|
|
18
ts/util/getBasicAuth.ts
Normal file
18
ts/util/getBasicAuth.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
// Copyright 2020-2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { fromString, toBase64 } from '../Bytes';
|
||||
|
||||
export type GetBasicAuthOptionsType = Readonly<{
|
||||
username: string;
|
||||
password: string;
|
||||
}>;
|
||||
|
||||
export function getBasicAuth({
|
||||
username,
|
||||
password,
|
||||
}: GetBasicAuthOptionsType): string {
|
||||
const auth = toBase64(fromString(`${username}:${password}`));
|
||||
|
||||
return `Basic ${auth}`;
|
||||
}
|
Loading…
Reference in a new issue