From bb15cfc622227caf2d9e9de045ba0d0fbc914eb0 Mon Sep 17 00:00:00 2001 From: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com> Date: Fri, 12 Nov 2021 21:45:30 +0100 Subject: [PATCH] Additional protocol changes for CDS v2 --- config/default.json | 4 ++-- ts/Curve.ts | 11 +++++++--- ts/textsecure/CDSSocket.ts | 35 ++++++++++++++++++++++++++----- ts/textsecure/CDSSocketManager.ts | 16 +++++++------- ts/textsecure/WebAPI.ts | 20 +++++++++++++++++- 5 files changed, 66 insertions(+), 20 deletions(-) diff --git a/config/default.json b/config/default.json index e477f8d944..a7ab665f32 100644 --- a/config/default.json +++ b/config/default.json @@ -5,8 +5,8 @@ "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", "directoryV2Url": "https://cdsh.staging.signal.org", - "directoryV2PublicKey": "052fe57da347cd62431528daac5fbb290730fff684afc4cfc2ed90995f58cb3b74", - "directoryV2CodeHash": "ec31a51880d19a5e9e0fed404740c1a3ff53a553125564b745acce475f0fded8", + "directoryV2PublicKey": "2fe57da347cd62431528daac5fbb290730fff684afc4cfc2ed90995f58cb3b74", + "directoryV2CodeHash": "ec58c0d7561de8d5657f3a4b22a635eaa305204e9359dcc80a99dfd0c5f1cbf2", "cdn": { "0": "https://cdn-staging.signal.org", "2": "https://cdn2-staging.signal.org" diff --git a/ts/Curve.ts b/ts/Curve.ts index f5067b1e8f..22412a8209 100644 --- a/ts/Curve.ts +++ b/ts/Curve.ts @@ -91,6 +91,13 @@ export function createKeyPair(incomingKey: Uint8Array): KeyPairType { }; } +export function prefixPublicKey(pubKey: Uint8Array): Uint8Array { + return Bytes.concatenate([ + new Uint8Array([0x05]), + validatePubKeyFormat(pubKey), + ]); +} + export function calculateAgreement( pubKey: Uint8Array, privKey: Uint8Array @@ -98,9 +105,7 @@ export function calculateAgreement( const privKeyBuffer = Buffer.from(privKey); const pubKeyObj = client.PublicKey.deserialize( - Buffer.from( - Bytes.concatenate([new Uint8Array([0x05]), validatePubKeyFormat(pubKey)]) - ) + Buffer.from(prefixPublicKey(pubKey)) ); const privKeyObj = client.PrivateKey.deserialize(privKeyBuffer); const sharedSecret = privKeyObj.agree(pubKeyObj); diff --git a/ts/textsecure/CDSSocket.ts b/ts/textsecure/CDSSocket.ts index 790a877384..a6de405ce7 100644 --- a/ts/textsecure/CDSSocket.ts +++ b/ts/textsecure/CDSSocket.ts @@ -20,8 +20,22 @@ enum State { Closed, } +export type CDSRequestOptionsType = Readonly<{ + e164s: ReadonlyArray; + auth: CDSAuthType; + timeout?: number; +}>; + +export type CDSAuthType = Readonly<{ + username: string; + password: string; +}>; + const HANDSHAKE_TIMEOUT = 10 * durations.SECOND; const REQUEST_TIMEOUT = 10 * durations.SECOND; +const VERSION = new Uint8Array([0x01]); +const USERNAME_LENGTH = 32; +const PASSWORD_LENGTH = 31; export class CDSSocket extends EventEmitter { private state = State.Handshake; @@ -82,19 +96,30 @@ export class CDSSocket extends EventEmitter { public async request({ e164s, + auth, timeout = REQUEST_TIMEOUT, - }: { - e164s: ReadonlyArray; - timeout?: number; - }): Promise> { + }: CDSRequestOptionsType): Promise> { 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' + ); + const request = Bytes.concatenate([ - new Uint8Array([0x01]), + VERSION, + username, + password, ...e164s.map(e164 => { // Long.fromString handles numbers with or without a leading '+' return new Uint8Array(Long.fromString(e164).toBytesBE()); diff --git a/ts/textsecure/CDSSocketManager.ts b/ts/textsecure/CDSSocketManager.ts index 55e30bd999..983459007e 100644 --- a/ts/textsecure/CDSSocketManager.ts +++ b/ts/textsecure/CDSSocketManager.ts @@ -6,10 +6,12 @@ import { HsmEnclaveClient, PublicKey } from '@signalapp/signal-client'; import type { connection as WebSocket } from 'websocket'; import * as Bytes from '../Bytes'; +import { prefixPublicKey } from '../Curve'; import type { AbortableProcess } from '../util/AbortableProcess'; import * as log from '../logging/log'; import type { UUIDStringType } from '../types/UUID'; import { CDSSocket } from './CDSSocket'; +import type { CDSRequestOptionsType } from './CDSSocket'; import { connect as connectWebSocket } from './WebSocket'; export type CDSSocketManagerOptionsType = Readonly<{ @@ -30,7 +32,7 @@ export class CDSSocketManager { constructor(private readonly options: CDSSocketManagerOptionsType) { this.publicKey = PublicKey.deserialize( - Buffer.from(Bytes.fromHex(options.publicKey)) + Buffer.from(prefixPublicKey(Bytes.fromHex(options.publicKey))) ); this.codeHash = Buffer.from(Bytes.fromHex(options.codeHash)); if (options.proxyUrl) { @@ -38,19 +40,15 @@ export class CDSSocketManager { } } - public async request({ - e164s, - timeout, - }: { - e164s: ReadonlyArray; - timeout?: number; - }): Promise> { + public async request( + options: CDSRequestOptionsType + ): Promise> { log.info('CDSSocketManager: connecting socket'); const socket = await this.connect().getResult(); log.info('CDSSocketManager: connected socket'); try { - return await socket.request({ e164s, timeout }); + return await socket.request(options); } finally { log.info('CDSSocketManager: closing socket'); socket.close(3000, 'Normal'); diff --git a/ts/textsecure/WebAPI.ts b/ts/textsecure/WebAPI.ts index 370718192e..edba5469d9 100644 --- a/ts/textsecure/WebAPI.ts +++ b/ts/textsecure/WebAPI.ts @@ -504,6 +504,7 @@ const URL_CALLS = { deliveryCert: 'v1/certificate/delivery', devices: 'v1/devices', directoryAuth: 'v1/directory/auth', + directoryAuthV2: 'v2/directory/auth', discovery: 'v1/discovery', getGroupAvatarUpload: 'v1/groups/avatar/form', getGroupCredentials: 'v1/certificate/group', @@ -556,6 +557,7 @@ const WEBSOCKET_CALLS = new Set([ // Directory 'directoryAuth', + 'directoryAuthV2', // Storage 'storageToken', @@ -2475,6 +2477,17 @@ export function initialize({ })) as { username: string; password: string }; } + async function getDirectoryAuthV2(): Promise<{ + username: string; + password: string; + }> { + return (await _ajax({ + call: 'directoryAuthV2', + httpType: 'GET', + responseType: 'json', + })) as { username: string; password: string }; + } + function validateAttestationQuote({ serverStaticPublic, quote: quoteBytes, @@ -2864,7 +2877,12 @@ export function initialize({ async function getUuidsForE164sV2( e164s: ReadonlyArray ): Promise> { - const uuids = await cdsSocketManager.request({ e164s }); + const auth = await getDirectoryAuthV2(); + + const uuids = await cdsSocketManager.request({ + auth, + e164s, + }); return zipObject(e164s, uuids); }