Add v2 implementation of CDS HSM
This commit is contained in:
parent
56a8e79413
commit
b4b65c4f00
8 changed files with 180 additions and 31 deletions
|
@ -6,7 +6,7 @@
|
|||
"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",
|
||||
"directoryV2Url": "https://cdsh.staging.signal.org",
|
||||
"directoryV2PublicKey": "2fe57da347cd62431528daac5fbb290730fff684afc4cfc2ed90995f58cb3b74",
|
||||
"directoryV2CodeHash": "ec58c0d7561de8d5657f3a4b22a635eaa305204e9359dcc80a99dfd0c5f1cbf2",
|
||||
"directoryV2CodeHash": "8c8025d787b4e7da35047c342a96c24a7119fd23ed9a3a774454a06315b4852a",
|
||||
"cdn": {
|
||||
"0": "https://cdn-staging.signal.org",
|
||||
"2": "https://cdn2-staging.signal.org"
|
||||
|
|
40
protos/ContactDiscovery.proto
Normal file
40
protos/ContactDiscovery.proto
Normal file
|
@ -0,0 +1,40 @@
|
|||
// Copyright 2021-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
message CDSClientResponse {
|
||||
// Each triple is an 8-byte e164, a 16-byte PNI, and a 16-byte ACI.
|
||||
// If the e164 was not found, PNI and ACI are all zeros. If the PNI
|
||||
// was found but the ACI was not, the PNI will be non-zero and the ACI
|
||||
// will be all zeros. ACI will be returned if one of the returned
|
||||
// PNIs has an ACI/UAK pair that matches.
|
||||
//
|
||||
// Should the request be successful (IE: a successful status returned),
|
||||
// |e164_pni_aci_triple| will always equal |e164| of the request,
|
||||
// so the entire marshalled size of the response will be (2+32)*|e164|,
|
||||
// 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;
|
||||
|
||||
// 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
|
||||
// closure of type 4008 (RESOURCE_EXHAUSTED). Should they retry exactly
|
||||
// the same request after the provided number of seconds has passed,
|
||||
// we expect it should work.
|
||||
optional int32 retry_after_secs = 2;
|
||||
}
|
|
@ -12,7 +12,7 @@ import { calculateAgreement, generateKeyPair } from './Curve';
|
|||
import * as log from './logging/log';
|
||||
import { HashType, CipherType } from './types/Crypto';
|
||||
import { ProfileDecryptError } from './types/errors';
|
||||
import { UUID } from './types/UUID';
|
||||
import { UUID, UUID_BYTE_SIZE } from './types/UUID';
|
||||
import type { UUIDStringType } from './types/UUID';
|
||||
|
||||
export { HashType, CipherType };
|
||||
|
@ -458,7 +458,7 @@ export function uuidToBytes(uuid: string): Uint8Array {
|
|||
}
|
||||
|
||||
export function bytesToUuid(bytes: Uint8Array): undefined | UUIDStringType {
|
||||
if (bytes.byteLength !== 16) {
|
||||
if (bytes.byteLength !== UUID_BYTE_SIZE) {
|
||||
log.warn(
|
||||
'bytesToUuid: received an Uint8Array of invalid length. ' +
|
||||
'Returning undefined'
|
||||
|
@ -475,8 +475,8 @@ export function bytesToUuid(bytes: Uint8Array): undefined | UUIDStringType {
|
|||
|
||||
export function splitUuids(buffer: Uint8Array): Array<UUIDStringType | null> {
|
||||
const uuids = [];
|
||||
for (let i = 0; i < buffer.byteLength; i += 16) {
|
||||
const bytes = getBytes(buffer, i, 16);
|
||||
for (let i = 0; i < buffer.byteLength; i += UUID_BYTE_SIZE) {
|
||||
const bytes = getBytes(buffer, i, UUID_BYTE_SIZE);
|
||||
const hex = Bytes.toHex(bytes);
|
||||
const chunks = [
|
||||
hex.substring(0, 8),
|
||||
|
|
|
@ -7,12 +7,15 @@ import type { connection as WebSocket } from 'websocket';
|
|||
import Long from 'long';
|
||||
|
||||
import { strictAssert } from '../util/assert';
|
||||
import { dropNull } from '../util/dropNull';
|
||||
import { explodePromise } from '../util/explodePromise';
|
||||
import * as durations from '../util/durations';
|
||||
import type { UUIDStringType } from '../types/UUID';
|
||||
import { UUID_BYTE_SIZE } from '../types/UUID';
|
||||
import * as Bytes from '../Bytes';
|
||||
import * as Timers from '../Timers';
|
||||
import { splitUuids } from '../Crypto';
|
||||
import { uuidToBytes, bytesToUuid } from '../Crypto';
|
||||
import { SignalService as Proto } from '../protobuf';
|
||||
|
||||
enum State {
|
||||
Handshake,
|
||||
|
@ -22,6 +25,8 @@ enum State {
|
|||
|
||||
export type CDSRequestOptionsType = Readonly<{
|
||||
e164s: ReadonlyArray<string>;
|
||||
acis: ReadonlyArray<UUIDStringType>;
|
||||
accessKeys: ReadonlyArray<string>;
|
||||
auth: CDSAuthType;
|
||||
timeout?: number;
|
||||
}>;
|
||||
|
@ -31,11 +36,26 @@ export type CDSAuthType = Readonly<{
|
|||
password: string;
|
||||
}>;
|
||||
|
||||
export type CDSSocketDictionaryEntryType = Readonly<{
|
||||
aci: UUIDStringType | undefined;
|
||||
pni: UUIDStringType | undefined;
|
||||
}>;
|
||||
|
||||
export type CDSSocketDictionaryType = Readonly<
|
||||
Record<string, CDSSocketDictionaryEntryType>
|
||||
>;
|
||||
|
||||
export type CDSSocketResponseType = Readonly<{
|
||||
dictionary: CDSSocketDictionaryType;
|
||||
retryAfterSecs?: number;
|
||||
}>;
|
||||
|
||||
const HANDSHAKE_TIMEOUT = 10 * durations.SECOND;
|
||||
const REQUEST_TIMEOUT = 10 * durations.SECOND;
|
||||
const VERSION = new Uint8Array([0x01]);
|
||||
const VERSION = new Uint8Array([0x02]);
|
||||
const USERNAME_LENGTH = 32;
|
||||
const PASSWORD_LENGTH = 31;
|
||||
const E164_BYTE_SIZE = 8;
|
||||
|
||||
export class CDSSocket extends EventEmitter {
|
||||
private state = State.Handshake;
|
||||
|
@ -96,9 +116,11 @@ export class CDSSocket extends EventEmitter {
|
|||
|
||||
public async request({
|
||||
e164s,
|
||||
acis,
|
||||
accessKeys,
|
||||
auth,
|
||||
timeout = REQUEST_TIMEOUT,
|
||||
}: CDSRequestOptionsType): Promise<ReadonlyArray<UUIDStringType | null>> {
|
||||
}: CDSRequestOptionsType): Promise<CDSSocketResponseType> {
|
||||
await this.finishedHandshake;
|
||||
strictAssert(
|
||||
this.state === State.Established,
|
||||
|
@ -116,15 +138,30 @@ export class CDSSocket extends EventEmitter {
|
|||
'Invalid password length'
|
||||
);
|
||||
|
||||
const request = Bytes.concatenate([
|
||||
VERSION,
|
||||
strictAssert(
|
||||
acis.length === accessKeys.length,
|
||||
`Number of ACIs ${acis.length} is different ` +
|
||||
`from number of access keys ${accessKeys.length}`
|
||||
);
|
||||
const aciUakPair = new Array<Uint8Array>();
|
||||
for (let i = 0; i < acis.length; i += 1) {
|
||||
aciUakPair.push(
|
||||
Bytes.concatenate([
|
||||
uuidToBytes(acis[i]),
|
||||
Bytes.fromBase64(accessKeys[i]),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
const request = Proto.CDSClientRequest.encode({
|
||||
username,
|
||||
password,
|
||||
...e164s.map(e164 => {
|
||||
e164: e164s.map(e164 => {
|
||||
// Long.fromString handles numbers with or without a leading '+'
|
||||
return new Uint8Array(Long.fromString(e164).toBytesBE());
|
||||
}),
|
||||
]);
|
||||
aciUakPair,
|
||||
}).finish();
|
||||
|
||||
const { promise, resolve, reject } = explodePromise<Buffer>();
|
||||
|
||||
|
@ -133,7 +170,7 @@ export class CDSSocket extends EventEmitter {
|
|||
}, timeout);
|
||||
|
||||
this.socket.sendBytes(
|
||||
this.enclaveClient.establishedSend(Buffer.from(request))
|
||||
this.enclaveClient.establishedSend(Buffer.concat([VERSION, request]))
|
||||
);
|
||||
|
||||
this.requestQueue.push(resolve);
|
||||
|
@ -141,11 +178,41 @@ export class CDSSocket extends EventEmitter {
|
|||
this.requestQueue.length === 1,
|
||||
'Concurrent use of CDS shold not happen'
|
||||
);
|
||||
const uuids = await promise;
|
||||
|
||||
const responseBytes = await promise;
|
||||
Timers.clearTimeout(timer);
|
||||
|
||||
return splitUuids(uuids);
|
||||
const response = Proto.CDSClientResponse.decode(responseBytes);
|
||||
|
||||
const dictionary: Record<string, CDSSocketDictionaryEntryType> =
|
||||
Object.create(null);
|
||||
|
||||
for (const tripleBytes of response.e164PniAciTriple ?? []) {
|
||||
strictAssert(
|
||||
tripleBytes.length === UUID_BYTE_SIZE * 2 + E164_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 e164 = `+${Long.fromBytesBE(Array.from(e164Bytes)).toString()}`;
|
||||
const pni = bytesToUuid(pniBytes);
|
||||
const aci = bytesToUuid(aciBytes);
|
||||
|
||||
dictionary[e164] = { pni, aci };
|
||||
}
|
||||
|
||||
return {
|
||||
dictionary,
|
||||
retryAfterSecs: dropNull(response.retryAfterSecs),
|
||||
};
|
||||
}
|
||||
|
||||
// EventEmitter types
|
||||
|
|
|
@ -8,10 +8,14 @@ import type { connection as WebSocket } from 'websocket';
|
|||
import * as Bytes from '../Bytes';
|
||||
import { prefixPublicKey } from '../Curve';
|
||||
import type { AbortableProcess } from '../util/AbortableProcess';
|
||||
import * as durations from '../util/durations';
|
||||
import { sleep } from '../util/sleep';
|
||||
import * as log from '../logging/log';
|
||||
import type { UUIDStringType } from '../types/UUID';
|
||||
import { CDSSocket } from './CDSSocket';
|
||||
import type { CDSRequestOptionsType } from './CDSSocket';
|
||||
import type {
|
||||
CDSRequestOptionsType,
|
||||
CDSSocketDictionaryType,
|
||||
} from './CDSSocket';
|
||||
import { connect as connectWebSocket } from './WebSocket';
|
||||
|
||||
export type CDSSocketManagerOptionsType = Readonly<{
|
||||
|
@ -23,6 +27,8 @@ export type CDSSocketManagerOptionsType = Readonly<{
|
|||
version: string;
|
||||
}>;
|
||||
|
||||
export type CDSResponseType = CDSSocketDictionaryType;
|
||||
|
||||
export class CDSSocketManager {
|
||||
private readonly publicKey: PublicKey;
|
||||
|
||||
|
@ -30,6 +36,8 @@ export class CDSSocketManager {
|
|||
|
||||
private readonly proxyAgent?: ReturnType<typeof ProxyAgent>;
|
||||
|
||||
private retryAfter?: number;
|
||||
|
||||
constructor(private readonly options: CDSSocketManagerOptionsType) {
|
||||
this.publicKey = PublicKey.deserialize(
|
||||
Buffer.from(prefixPublicKey(Bytes.fromHex(options.publicKey)))
|
||||
|
@ -42,13 +50,29 @@ export class CDSSocketManager {
|
|||
|
||||
public async request(
|
||||
options: CDSRequestOptionsType
|
||||
): Promise<ReadonlyArray<UUIDStringType | null>> {
|
||||
): Promise<CDSResponseType> {
|
||||
if (this.retryAfter !== undefined) {
|
||||
const delay = Math.max(0, this.retryAfter - Date.now());
|
||||
|
||||
log.info(`CDSSocketManager: waiting ${delay}ms before retrying`);
|
||||
await sleep(delay);
|
||||
}
|
||||
|
||||
log.info('CDSSocketManager: connecting socket');
|
||||
const socket = await this.connect().getResult();
|
||||
log.info('CDSSocketManager: connected socket');
|
||||
|
||||
try {
|
||||
return await socket.request(options);
|
||||
const { dictionary, retryAfterSecs = 0 } = await socket.request(options);
|
||||
|
||||
if (retryAfterSecs > 0) {
|
||||
this.retryAfter = Math.max(
|
||||
this.retryAfter ?? Date.now(),
|
||||
Date.now() + retryAfterSecs * durations.SECOND
|
||||
);
|
||||
}
|
||||
|
||||
return dictionary;
|
||||
} finally {
|
||||
log.info('CDSSocketManager: closing socket');
|
||||
socket.close(3000, 'Normal');
|
||||
|
|
|
@ -41,6 +41,7 @@ import type {
|
|||
SendLogCallbackType,
|
||||
} from './OutgoingMessage';
|
||||
import OutgoingMessage from './OutgoingMessage';
|
||||
import type { CDSResponseType } from './CDSSocketManager';
|
||||
import * as Bytes from '../Bytes';
|
||||
import { getRandomBytes, getZeroes, encryptAttachment } from '../Crypto';
|
||||
import type {
|
||||
|
@ -2074,9 +2075,15 @@ export default class MessageSender {
|
|||
}
|
||||
|
||||
async getUuidsForE164sV2(
|
||||
numbers: ReadonlyArray<string>
|
||||
): Promise<Dictionary<UUIDStringType | null>> {
|
||||
return this.server.getUuidsForE164sV2(numbers);
|
||||
e164s: ReadonlyArray<string>,
|
||||
acis: ReadonlyArray<UUIDStringType>,
|
||||
accessKeys: ReadonlyArray<string>
|
||||
): Promise<CDSResponseType> {
|
||||
return this.server.getUuidsForE164sV2({
|
||||
e164s,
|
||||
acis,
|
||||
accessKeys,
|
||||
});
|
||||
}
|
||||
|
||||
async getAvatar(path: string): Promise<ReturnType<WebAPIType['getAvatar']>> {
|
||||
|
|
|
@ -55,6 +55,7 @@ import type {
|
|||
StorageServiceCredentials,
|
||||
} from '../textsecure.d';
|
||||
import { SocketManager } from './SocketManager';
|
||||
import type { CDSResponseType } from './CDSSocketManager';
|
||||
import { CDSSocketManager } from './CDSSocketManager';
|
||||
import type WebSocketResource from './WebsocketResources';
|
||||
import { SignalService as Proto } from '../protobuf';
|
||||
|
@ -758,6 +759,12 @@ export type ConfirmCodeResultType = Readonly<{
|
|||
deviceId?: number;
|
||||
}>;
|
||||
|
||||
export type GetUuidsForE164sV2OptionsType = Readonly<{
|
||||
e164s: ReadonlyArray<string>;
|
||||
acis: ReadonlyArray<UUIDStringType>;
|
||||
accessKeys: ReadonlyArray<string>;
|
||||
}>;
|
||||
|
||||
export type WebAPIType = {
|
||||
confirmCode: (
|
||||
number: string,
|
||||
|
@ -840,8 +847,8 @@ export type WebAPIType = {
|
|||
e164s: ReadonlyArray<string>
|
||||
) => Promise<Dictionary<UUIDStringType | null>>;
|
||||
getUuidsForE164sV2: (
|
||||
e164s: ReadonlyArray<string>
|
||||
) => Promise<Dictionary<UUIDStringType | null>>;
|
||||
options: GetUuidsForE164sV2OptionsType
|
||||
) => Promise<CDSResponseType>;
|
||||
fetchLinkPreviewMetadata: (
|
||||
href: string,
|
||||
abortSignal: AbortSignal
|
||||
|
@ -3005,17 +3012,19 @@ export function initialize({
|
|||
return zipObject(e164s, uuids);
|
||||
}
|
||||
|
||||
async function getUuidsForE164sV2(
|
||||
e164s: ReadonlyArray<string>
|
||||
): Promise<Dictionary<UUIDStringType | null>> {
|
||||
async function getUuidsForE164sV2({
|
||||
e164s,
|
||||
acis,
|
||||
accessKeys,
|
||||
}: GetUuidsForE164sV2OptionsType): Promise<CDSResponseType> {
|
||||
const auth = await getDirectoryAuthV2();
|
||||
|
||||
const uuids = await cdsSocketManager.request({
|
||||
return cdsSocketManager.request({
|
||||
auth,
|
||||
e164s,
|
||||
acis,
|
||||
accessKeys,
|
||||
});
|
||||
|
||||
return zipObject(e164s, uuids);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,8 @@ export enum UUIDKind {
|
|||
Unknown = 'Unknown',
|
||||
}
|
||||
|
||||
export const UUID_BYTE_SIZE = 16;
|
||||
|
||||
export const isValidUuid = (value: unknown): value is UUIDStringType =>
|
||||
typeof value === 'string' &&
|
||||
/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i.test(
|
||||
|
|
Loading…
Add table
Reference in a new issue