From 8290881bd8576dc4a036100fcb9985b945d02a0e Mon Sep 17 00:00:00 2001 From: Ken Powers Date: Thu, 3 Sep 2020 21:25:19 -0400 Subject: [PATCH] Support for Contact Discovery Service --- config/default.json | 3 + config/production.json | 1 + js/background.js | 59 +++- js/models/conversations.js | 61 +++- js/models/messages.js | 26 ++ js/views/inbox_view.js | 4 +- libtextsecure/libsignal-protocol.js | 2 +- main.js | 3 + package.json | 4 + preload.js | 3 + sticker-creator/preload.js | 3 + ts/Crypto.ts | 144 +++++++- ts/libsignal.d.ts | 12 +- ts/textsecure/OutgoingMessage.ts | 42 ++- ts/textsecure/SendMessage.ts | 21 +- ts/textsecure/WebAPI.ts | 488 +++++++++++++++++++++++++++- ts/types/PhoneNumber.ts | 16 + ts/util/lint/exceptions.json | 66 ++-- ts/util/lint/linter.ts | 3 +- ts/window.d.ts | 24 +- yarn.lock | 55 +++- 21 files changed, 961 insertions(+), 79 deletions(-) diff --git a/config/default.json b/config/default.json index 44f1dc4da..b61a567d8 100644 --- a/config/default.json +++ b/config/default.json @@ -1,6 +1,9 @@ { "serverUrl": "https://textsecure-service-staging.whispersystems.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", "cdn": { "0": "https://cdn-staging.signal.org", "2": "https://cdn2-staging.signal.org" diff --git a/config/production.json b/config/production.json index ee0d3e144..eb0d37a03 100644 --- a/config/production.json +++ b/config/production.json @@ -1,6 +1,7 @@ { "serverUrl": "https://textsecure-service.whispersystems.org", "storageUrl": "https://storage.signal.org", + "directoryUrl": "https://api.directory.signal.org", "cdn": { "0": "https://cdn.signal.org", "2": "https://cdn2.signal.org" diff --git a/js/background.js b/js/background.js index 77c7187a9..f1605c9dc 100644 --- a/js/background.js +++ b/js/background.js @@ -230,10 +230,14 @@ }); let messageReceiver; + let preMessageReceiverStatus; window.getSocketStatus = () => { if (messageReceiver) { return messageReceiver.getStatus(); } + if (_.isNumber(preMessageReceiverStatus)) { + return preMessageReceiverStatus; + } return -1; }; Whisper.events = _.clone(Backbone.Events); @@ -1633,6 +1637,8 @@ return; } + preMessageReceiverStatus = WebSocket.CONNECTING; + if (messageReceiver) { await messageReceiver.stopProcessing(); @@ -1647,6 +1653,52 @@ const PASSWORD = storage.get('password'); const mySignalingKey = storage.get('signaling_key'); + window.textsecure.messaging = new textsecure.MessageSender( + USERNAME || OLD_USERNAME, + PASSWORD + ); + + try { + if (connectCount === 0) { + const lonelyE164s = window + .getConversations() + .filter( + c => + c.isPrivate() && + c.get('e164') && + !c.get('uuid') && + !c.isEverUnregistered() + ) + .map(c => c.get('e164')); + + if (lonelyE164s.length > 0) { + const lookup = await textsecure.messaging.getUuidsForE164s( + lonelyE164s + ); + const e164s = Object.keys(lookup); + e164s.forEach(e164 => { + const uuid = lookup[e164]; + if (!uuid) { + const byE164 = window.ConversationController.get(e164); + if (byE164) { + byE164.setUnregistered(); + } + } + window.ConversationController.ensureContactIds({ + e164, + uuid, + highTrust: true, + }); + }); + } + } + } catch (error) { + window.log.error( + 'Error fetching UUIDs for lonely e164s:', + error && error.stack ? error.stack : error + ); + } + connectCount += 1; const options = { retryCached: connectCount === 1, @@ -1667,6 +1719,8 @@ ); window.textsecure.messageReceiver = messageReceiver; + preMessageReceiverStatus = null; + function addQueuedEventListener(name, handler) { messageReceiver.addEventListener(name, (...args) => eventHandlerQueue.add(async () => { @@ -1709,11 +1763,6 @@ logger: window.log, }); - window.textsecure.messaging = new textsecure.MessageSender( - USERNAME || OLD_USERNAME, - PASSWORD - ); - if (connectCount === 1) { window.Signal.Stickers.downloadQueuedPacks(); await window.textsecure.messaging.sendRequestKeySyncMessage(); diff --git a/js/models/conversations.js b/js/models/conversations.js index d1223d7d6..dae198687 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -184,6 +184,39 @@ ); }, + isEverUnregistered() { + return Boolean(this.get('discoveredUnregisteredAt')); + }, + isUnregistered() { + const now = Date.now(); + const sixHoursAgo = now - 1000 * 60 * 60 * 6; + const discoveredUnregisteredAt = this.get('discoveredUnregisteredAt'); + + if (discoveredUnregisteredAt && discoveredUnregisteredAt > sixHoursAgo) { + return true; + } + + return false; + }, + setUnregistered() { + window.log.info( + `Conversation ${this.idForLogging()} is now unregistered` + ); + this.set({ + discoveredUnregisteredAt: Date.now(), + }); + window.Signal.Data.updateConversation(this.attributes); + }, + setRegistered() { + window.log.info( + `Conversation ${this.idForLogging()} is registered once again` + ); + this.set({ + discoveredUnregisteredAt: undefined, + }); + window.Signal.Data.updateConversation(this.attributes); + }, + isBlocked() { const uuid = this.get('uuid'); if (uuid) { @@ -1258,6 +1291,11 @@ if (c.id === me) { return null; } + // We don't want to even attempt a send if we have recently discovered that they + // are unregistered. + if (c.isUnregistered()) { + return null; + } return c.getSendTarget(); }) ); @@ -1422,7 +1460,7 @@ }); Whisper.Reactions.onReaction(reactionModel); - const destination = this.get('e164'); + const destination = this.getSendTarget(); const recipients = this.getRecipients(); let profileKey; @@ -1717,7 +1755,8 @@ if (result) { await this.handleMessageSendResult( result.failoverIdentifiers, - result.unidentifiedDeliveries + result.unidentifiedDeliveries, + result.discoveredIdentifierPairs ); } return result; @@ -1727,7 +1766,8 @@ if (result) { await this.handleMessageSendResult( result.failoverIdentifiers, - result.unidentifiedDeliveries + result.unidentifiedDeliveries, + result.discoveredIdentifierPairs ); } throw result; @@ -1735,7 +1775,20 @@ ); }, - async handleMessageSendResult(failoverIdentifiers, unidentifiedDeliveries) { + async handleMessageSendResult( + failoverIdentifiers, + unidentifiedDeliveries, + discoveredIdentifierPairs + ) { + discoveredIdentifierPairs.forEach(item => { + const { uuid, e164 } = item; + window.ConversationController.ensureContactIds({ + uuid, + e164, + highTrust: true, + }); + }); + await Promise.all( (failoverIdentifiers || []).map(async identifier => { const conversation = ConversationController.get(identifier); diff --git a/js/models/messages.js b/js/models/messages.js index d71e1e8a7..cd7ec895c 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -1716,6 +1716,14 @@ let promises = []; + // If we successfully sent to a user, we can remove our unregistered flag. + result.successfulIdentifiers.forEach(identifier => { + const c = ConversationController.get(identifier); + if (c && c.isEverUnregistered()) { + c.setRegistered(); + } + }); + if (result instanceof Error) { this.saveErrors(result); if (result.name === 'SignedPreKeyRotationError') { @@ -1728,6 +1736,24 @@ if (result.successfulIdentifiers.length > 0) { const sentTo = this.get('sent_to') || []; + // If we just found out that we couldn't send to a user because they are no + // longer registered, we will update our unregistered flag. In groups we + // will not event try to send to them for 6 hours. And we will never try + // to fetch them on startup again. + // The way to discover registration once more is: + // 1) any attempt to send to them in 1:1 conversation + // 2) the six-hour time period has passed and we send in a group again + const unregisteredUserErrors = _.filter( + result.errors, + error => error.name === 'UnregisteredUserError' + ); + unregisteredUserErrors.forEach(error => { + const c = ConversationController.get(error.identifier); + if (c) { + c.setUnregistered(); + } + }); + // In groups, we don't treat unregistered users as a user-visible // error. The message will look successful, but the details // screen will show that we didn't send to these unregistered users. diff --git a/js/views/inbox_view.js b/js/views/inbox_view.js index 4747142c9..e3f109619 100644 --- a/js/views/inbox_view.js +++ b/js/views/inbox_view.js @@ -157,7 +157,9 @@ this.onEmpty(); break; default: - // We also replicate empty here + window.log.warn( + 'startConnectionListener: Found unexpected socket status; calling onEmpty() manually.' + ); this.onEmpty(); break; } diff --git a/libtextsecure/libsignal-protocol.js b/libtextsecure/libsignal-protocol.js index bfb4b7c37..64edc5955 100644 --- a/libtextsecure/libsignal-protocol.js +++ b/libtextsecure/libsignal-protocol.js @@ -25564,7 +25564,7 @@ var Internal = Internal || {}; // HKDF for TextSecure has a bit of additional handling - salts always end up being 32 bytes - Internal.HKDF = function(input, salt, info) { + Internal.HKDF = function(input, salt, info = new ArrayBuffer()) { return Internal.crypto.HKDF(input, salt, util.toArrayBuffer(info)); }; diff --git a/main.js b/main.js index 76ee427e6..6c286f8c9 100644 --- a/main.js +++ b/main.js @@ -188,6 +188,9 @@ function prepareURL(pathSegments, moreKeys) { buildExpiration: config.get('buildExpiration'), serverUrl: config.get('serverUrl'), storageUrl: config.get('storageUrl'), + directoryUrl: config.get('directoryUrl'), + directoryEnclaveId: config.get('directoryEnclaveId'), + directoryTrustAnchor: config.get('directoryTrustAnchor'), cdnUrl0: config.get('cdn').get('0'), cdnUrl2: config.get('cdn').get('2'), certificateAuthority: config.get('certificateAuthority'), diff --git a/package.json b/package.json index 313a8d9ff..3373b966b 100644 --- a/package.json +++ b/package.json @@ -99,10 +99,12 @@ "moment": "2.21.0", "mustache": "2.3.0", "node-fetch": "2.6.0", + "node-forge": "0.10.0", "node-gyp": "5.0.3", "normalize-path": "3.0.0", "os-locale": "3.0.1", "p-map": "2.1.0", + "p-props": "4.0.0", "p-queue": "6.2.1", "pify": "3.0.0", "protobufjs": "6.8.6", @@ -169,10 +171,12 @@ "@types/js-yaml": "3.12.0", "@types/linkify-it": "2.1.0", "@types/lodash": "4.14.106", + "@types/long": "4.0.1", "@types/memoizee": "0.4.2", "@types/mkdirp": "0.5.2", "@types/mocha": "5.0.0", "@types/node-fetch": "2.5.7", + "@types/node-forge": "0.9.5", "@types/normalize-path": "3.0.0", "@types/pify": "3.0.2", "@types/react": "16.8.5", diff --git a/preload.js b/preload.js index 0a491faef..86ddd8b1e 100644 --- a/preload.js +++ b/preload.js @@ -330,6 +330,9 @@ try { window.WebAPI = window.textsecure.WebAPI.initialize({ url: config.serverUrl, storageUrl: config.storageUrl, + directoryUrl: config.directoryUrl, + directoryEnclaveId: config.directoryEnclaveId, + directoryTrustAnchor: config.directoryTrustAnchor, cdnUrlObject: { '0': config.cdnUrl0, '2': config.cdnUrl2, diff --git a/sticker-creator/preload.js b/sticker-creator/preload.js index ea65fdf7a..586a29b1a 100644 --- a/sticker-creator/preload.js +++ b/sticker-creator/preload.js @@ -35,6 +35,9 @@ const { initialize: initializeWebAPI } = require('../ts/textsecure/WebAPI'); const WebAPI = initializeWebAPI({ url: config.serverUrl, storageUrl: config.storageUrl, + directoryUrl: config.directoryUrl, + directoryEnclaveId: config.directoryEnclaveId, + directoryTrustAnchor: config.directoryTrustAnchor, cdnUrlObject: { '0': config.cdnUrl0, '2': config.cdnUrl2, diff --git a/ts/Crypto.ts b/ts/Crypto.ts index 933d7e69f..e015bb3b7 100644 --- a/ts/Crypto.ts +++ b/ts/Crypto.ts @@ -1,3 +1,5 @@ +import pProps from 'p-props'; + // Yep, we're doing some bitwise stuff in an encryption-related file // tslint:disable no-bitwise @@ -11,7 +13,7 @@ export function typedArrayToArrayBuffer(typedArray: Uint8Array): ArrayBuffer { const { buffer, byteOffset, byteLength } = typedArray; // tslint:disable-next-line no-unnecessary-type-assertion - return buffer.slice(byteOffset, byteLength + byteOffset) as ArrayBuffer; + return buffer.slice(byteOffset, byteLength + byteOffset) as typeof typedArray; } export function arrayBufferToBase64(arrayBuffer: ArrayBuffer) { @@ -173,7 +175,7 @@ export async function decryptFile( data: ArrayBuffer ) { const ephemeralPublicKey = getFirstBytes(data, PUB_KEY_LENGTH); - const ciphertext = _getBytes(data, PUB_KEY_LENGTH, data.byteLength); + const ciphertext = getBytes(data, PUB_KEY_LENGTH, data.byteLength); const agreement = await window.libsignal.Curve.async.calculateAgreement( ephemeralPublicKey, staticPrivateKey @@ -201,7 +203,7 @@ export async function deriveStorageItemKey( export async function deriveAccessKey(profileKey: ArrayBuffer) { const iv = getZeroes(12); const plaintext = getZeroes(16); - const accessKey = await _encrypt_aes_gcm(profileKey, iv, plaintext); + const accessKey = await encryptAesGcm(profileKey, iv, plaintext); return getFirstBytes(accessKey, 16); } @@ -253,12 +255,12 @@ export async function decryptSymmetric(key: ArrayBuffer, data: ArrayBuffer) { const iv = getZeroes(IV_LENGTH); const nonce = getFirstBytes(data, NONCE_LENGTH); - const cipherText = _getBytes( + const cipherText = getBytes( data, NONCE_LENGTH, data.byteLength - NONCE_LENGTH - MAC_LENGTH ); - const theirMac = _getBytes(data, data.byteLength - MAC_LENGTH, MAC_LENGTH); + const theirMac = getBytes(data, data.byteLength - MAC_LENGTH, MAC_LENGTH); const cipherKey = await hmacSha256(key, nonce); const macKey = await hmacSha256(key, cipherKey); @@ -413,15 +415,18 @@ export async function decryptAesCtr( return plaintext; } -export async function _encrypt_aes_gcm( +export async function encryptAesGcm( key: ArrayBuffer, iv: ArrayBuffer, - plaintext: ArrayBuffer + plaintext: ArrayBuffer, + additionalData?: ArrayBuffer ) { const algorithm = { name: 'AES-GCM', iv, + ...(additionalData ? { additionalData } : {}), }; + const extractable = false; const cryptoKey = await crypto.subtle.importKey( @@ -435,6 +440,37 @@ export async function _encrypt_aes_gcm( return crypto.subtle.encrypt(algorithm, cryptoKey, plaintext); } +export async function decryptAesGcm( + key: ArrayBuffer, + iv: ArrayBuffer, + ciphertext: ArrayBuffer, + additionalData?: ArrayBuffer +) { + const algorithm = { + name: 'AES-GCM', + iv, + ...(additionalData ? { additionalData } : {}), + tagLength: 128, + }; + + const extractable = false; + const cryptoKey = await crypto.subtle.importKey( + 'raw', + key, + algorithm as any, + extractable, + ['decrypt'] + ); + + return crypto.subtle.decrypt(algorithm, cryptoKey, ciphertext); +} + +// Hashing + +export async function sha256(data: ArrayBuffer) { + return crypto.subtle.digest('SHA-256', data); +} + // Utility export function getRandomBytes(n: number) { @@ -550,9 +586,7 @@ export function getFirstBytes(data: ArrayBuffer, n: number) { return typedArrayToArrayBuffer(source.subarray(0, n)); } -// Internal-only - -export function _getBytes( +export function getBytes( data: ArrayBuffer | Uint8Array, start: number, n: number @@ -561,3 +595,93 @@ export function _getBytes( return typedArrayToArrayBuffer(source.subarray(start, start + n)); } + +function _getMacAndData(ciphertext: ArrayBuffer) { + const dataLength = ciphertext.byteLength - MAC_LENGTH; + const data = getBytes(ciphertext, 0, dataLength); + const mac = getBytes(ciphertext, dataLength, MAC_LENGTH); + + return { data, mac }; +} + +export async function encryptCdsDiscoveryRequest( + attestations: { + [key: string]: { clientKey: ArrayBuffer; requestId: ArrayBuffer }; + }, + phoneNumbers: ReadonlyArray +) { + const nonce = getRandomBytes(32); + const numbersArray = new window.dcodeIO.ByteBuffer( + phoneNumbers.length * 8, + window.dcodeIO.ByteBuffer.BIG_ENDIAN + ); + phoneNumbers.forEach(number => { + // Long.fromString handles numbers with or without a leading '+' + numbersArray.writeLong(window.dcodeIO.ByteBuffer.Long.fromString(number)); + }); + const queryDataPlaintext = concatenateBytes(nonce, numbersArray.buffer); + const queryDataKey = getRandomBytes(32); + const commitment = await sha256(queryDataPlaintext); + const iv = getRandomBytes(12); + const queryDataCiphertext = await encryptAesGcm( + queryDataKey, + iv, + queryDataPlaintext + ); + const { + data: queryDataCiphertextData, + mac: queryDataCiphertextMac, + } = _getMacAndData(queryDataCiphertext); + + const envelopes = await pProps( + attestations, + async ({ clientKey, requestId }) => { + const envelopeIv = getRandomBytes(12); + const ciphertext = await encryptAesGcm( + clientKey, + envelopeIv, + queryDataKey, + requestId + ); + const { data, mac } = _getMacAndData(ciphertext); + + return { + requestId: arrayBufferToBase64(requestId), + data: arrayBufferToBase64(data), + iv: arrayBufferToBase64(envelopeIv), + mac: arrayBufferToBase64(mac), + }; + } + ); + + return { + addressCount: phoneNumbers.length, + commitment: arrayBufferToBase64(commitment), + data: arrayBufferToBase64(queryDataCiphertextData), + iv: arrayBufferToBase64(iv), + mac: arrayBufferToBase64(queryDataCiphertextMac), + envelopes, + }; +} + +export function splitUuids(arrayBuffer: ArrayBuffer) { + const uuids = []; + for (let i = 0; i < arrayBuffer.byteLength; i += 16) { + const bytes = getBytes(arrayBuffer, i, 16); + const hex = arrayBufferToHex(bytes); + const chunks = [ + hex.substring(0, 8), + hex.substring(8, 12), + hex.substring(12, 16), + hex.substring(16, 20), + hex.substring(20), + ]; + const uuid = chunks.join('-'); + if (uuid !== '00000000-0000-0000-0000-000000000000') { + uuids.push(uuid); + } else { + uuids.push(null); + } + } + return uuids; +} diff --git a/ts/libsignal.d.ts b/ts/libsignal.d.ts index 8a0032191..40daa7f90 100644 --- a/ts/libsignal.d.ts +++ b/ts/libsignal.d.ts @@ -20,6 +20,16 @@ export type LibSignalType = { ) => Promise; getRandomBytes: (size: number) => ArrayBuffer; }; + externalCurveAsync: { + calculateAgreement: ( + pubKey: ArrayBuffer, + privKey: ArrayBuffer + ) => Promise; + generateKeyPair: () => Promise<{ + privKey: ArrayBuffer; + pubKey: ArrayBuffer; + }>; + }; KeyHelper: { generateIdentityKeyPair: () => Promise<{ privKey: ArrayBuffer; @@ -56,7 +66,7 @@ export type LibSignalType = { packKey: ArrayBuffer, salt: ArrayBuffer, // The string is a bit crazy, but ProvisioningCipher currently passes in a string - info: ArrayBuffer | string + info?: ArrayBuffer | string ) => Promise>; }; worker: { diff --git a/ts/textsecure/OutgoingMessage.ts b/ts/textsecure/OutgoingMessage.ts index ff9fb3ccf..c05529d31 100644 --- a/ts/textsecure/OutgoingMessage.ts +++ b/ts/textsecure/OutgoingMessage.ts @@ -15,6 +15,7 @@ import { SendMessageNetworkError, UnregisteredUserError, } from './Errors'; +import { isValidNumber } from '../types/PhoneNumber'; type OutgoingMessageOptionsType = SendOptionsType & { online?: boolean; @@ -34,6 +35,10 @@ export default class OutgoingMessage { successfulIdentifiers: Array; failoverIdentifiers: Array; unidentifiedDeliveries: Array; + discoveredIdentifierPairs: Array<{ + e164: string; + uuid: string; + }>; sendMetadata?: SendMetadataType; senderCertificate?: ArrayBuffer; @@ -68,6 +73,7 @@ export default class OutgoingMessage { this.successfulIdentifiers = []; this.failoverIdentifiers = []; this.unidentifiedDeliveries = []; + this.discoveredIdentifierPairs = []; const { sendMetadata, senderCertificate, online } = options || ({} as any); this.sendMetadata = sendMetadata; @@ -82,6 +88,7 @@ export default class OutgoingMessage { failoverIdentifiers: this.failoverIdentifiers, errors: this.errors, unidentifiedDeliveries: this.unidentifiedDeliveries, + discoveredIdentifierPairs: this.discoveredIdentifierPairs, }); } } @@ -564,8 +571,39 @@ export default class OutgoingMessage { return promise; } - async sendToIdentifier(identifier: string) { + async sendToIdentifier(providedIdentifier: string) { + let identifier = providedIdentifier; try { + if (window.isValidGuid(identifier)) { + // We're good! + } else if (isValidNumber(identifier)) { + if (!window.textsecure.messaging) { + throw new Error( + 'sendToIdentifier: window.textsecure.messaging is not available!' + ); + } + const lookup = await window.textsecure.messaging.getUuidsForE164s([ + identifier, + ]); + const uuid = lookup[identifier]; + if (uuid) { + this.discoveredIdentifierPairs.push({ + uuid, + e164: identifier, + }); + identifier = uuid; + } else { + throw new UnregisteredUserError( + identifier, + new Error('User is not registered') + ); + } + } else { + throw new Error( + `sendToIdentifier: identifier ${identifier} was neither a UUID or E164` + ); + } + const updateDevices = await this.getStaleDeviceIdsForIdentifier( identifier ); @@ -583,7 +621,7 @@ export default class OutgoingMessage { } else { this.registerError( identifier, - `Failed to retrieve new device keys for number ${identifier}`, + `Failed to retrieve new device keys for identifier ${identifier}`, error ); } diff --git a/ts/textsecure/SendMessage.ts b/ts/textsecure/SendMessage.ts index bd3af3c5e..6d61b7579 100644 --- a/ts/textsecure/SendMessage.ts +++ b/ts/textsecure/SendMessage.ts @@ -53,6 +53,10 @@ export type CallbackResultType = { errors?: Array; unidentifiedDeliveries?: Array; dataMessage?: ArrayBuffer; + discoveredIdentifierPairs: Array<{ + e164: string; + uuid: string | null; + }>; }; type PreviewType = { @@ -464,7 +468,10 @@ export default class MessageSender { }); } - async sendMessage(attrs: MessageOptionsType, options?: SendOptionsType) { + async sendMessage( + attrs: MessageOptionsType, + options?: SendOptionsType + ): Promise { const message = new Message(attrs); const silent = false; @@ -474,7 +481,7 @@ export default class MessageSender { this.uploadLinkPreviews(message), this.uploadSticker(message), ]).then( - async () => + async (): Promise => new Promise((resolve, reject) => { this.sendMessageProto( message.timestamp, @@ -697,6 +704,10 @@ export default class MessageSender { return this.server.getProfile(number, options); } + async getUuidsForE164s(numbers: Array) { + return this.server.getUuidsForE164s(numbers); + } + async getAvatar(path: string) { return this.server.getAvatar(path); } @@ -1439,7 +1450,7 @@ export default class MessageSender { expireTimer: number | undefined, profileKey?: ArrayBuffer, options?: SendOptionsType - ) { + ): Promise { const myE164 = window.textsecure.storage.user.getNumber(); const myUuid = window.textsecure.storage.user.getNumber(); const attrs = { @@ -1466,6 +1477,7 @@ export default class MessageSender { errors: [], unidentifiedDeliveries: [], dataMessage: await this.getMessageProtoObj(attrs), + discoveredIdentifierPairs: [], }); } @@ -1611,7 +1623,7 @@ export default class MessageSender { timestamp: number, profileKey?: ArrayBuffer, options?: SendOptionsType - ) { + ): Promise { const myNumber = window.textsecure.storage.user.getNumber(); const myUuid = window.textsecure.storage.user.getUuid(); const recipients = groupIdentifiers.filter( @@ -1637,6 +1649,7 @@ export default class MessageSender { errors: [], unidentifiedDeliveries: [], dataMessage: await this.getMessageProtoObj(attrs), + discoveredIdentifierPairs: [], }); } diff --git a/ts/textsecure/WebAPI.ts b/ts/textsecure/WebAPI.ts index d67d1eff4..398288232 100644 --- a/ts/textsecure/WebAPI.ts +++ b/ts/textsecure/WebAPI.ts @@ -2,12 +2,34 @@ import { w3cwebsocket as WebSocket } from 'websocket'; import fetch, { Response } from 'node-fetch'; import ProxyAgent from 'proxy-agent'; 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 { isPackIdValid, redactPackId } from '../../js/modules/stickers'; -import { getRandomValue } from '../Crypto'; import MessageSender from './SendMessage'; +import { + arrayBufferToBase64, + base64ToArrayBuffer, + bytesFromHexString, + bytesFromString, + concatenateBytes, + constantTimeEqual, + decryptAesGcm, + encryptCdsDiscoveryRequest, + getBytes, + getRandomValue, + splitUuids, +} from '../Crypto'; import PQueue from 'p-queue'; import { v4 as getGuid } from 'uuid'; @@ -17,6 +39,43 @@ import { StorageServiceCredentials, } 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 function _btoa(str: any) { @@ -234,7 +293,11 @@ type PromiseAjaxOptionsType = { proxyUrl?: string; redactUrl?: RedactUrl; redirect?: 'error' | 'follow' | 'manual'; - responseType?: 'json' | 'arraybuffer' | 'arraybufferwithdetails'; + responseType?: + | 'json' + | 'jsonwithdetails' + | 'arraybuffer' + | 'arraybufferwithdetails'; stack?: string; timeout?: number; type: HTTPCodeType; @@ -244,6 +307,12 @@ type PromiseAjaxOptionsType = { version: string; }; +type JSONWithDetailsType = { + data: any; + contentType: string; + response: Response; +}; + // tslint:disable-next-line max-func-body-length async function _promiseAjax( providedUrl: string | null, @@ -333,7 +402,8 @@ async function _promiseAjax( .then(async response => { let resultPromise; if ( - options.responseType === 'json' && + (options.responseType === 'json' || + options.responseType === 'jsonwithdetails') && response.headers.get('Content-Type') === 'application/json' ) { resultPromise = response.json(); @@ -358,7 +428,10 @@ async function _promiseAjax( result.byteOffset + result.byteLength ); } - if (options.responseType === 'json') { + if ( + options.responseType === 'json' || + options.responseType === 'jsonwithdetails' + ) { if (options.validateResponse) { if (!_validateResponse(result, options.validateResponse)) { if (options.redactUrl) { @@ -395,7 +468,10 @@ async function _promiseAjax( } else { window.log.info(options.type, url, response.status, 'Success'); } - if (options.responseType === 'arraybufferwithdetails') { + if ( + options.responseType === 'arraybufferwithdetails' || + options.responseType === 'jsonwithdetails' + ) { resolve({ data: result, contentType: getContentType(response), @@ -518,11 +594,18 @@ const URL_CALLS = { getStickerPackUpload: 'v1/sticker/pack/form', whoami: 'v1/accounts/whoami', config: 'v1/config', + directoryAuth: 'v1/directory/auth', + // CDS endpoints + attestation: 'v1/attestation', + discovery: 'v1/discovery', }; type InitializeOptionsType = { url: string; storageUrl: string; + directoryEnclaveId: string; + directoryTrustAnchor: string; + directoryUrl: string; cdnUrlObject: { readonly '0': string; readonly [propName: string]: string; @@ -611,6 +694,9 @@ export type WebAPIType = { getStorageCredentials: MessageSender['getStorageCredentials']; getStorageManifest: MessageSender['getStorageManifest']; getStorageRecords: MessageSender['getStorageRecords']; + getUuidsForE164s: ( + e164s: ReadonlyArray + ) => Promise>; makeProxiedRequest: ( targetUrl: string, options?: ProxiedRequestOptionsType @@ -691,6 +777,9 @@ export type ProxiedRequestOptionsType = { export function initialize({ url, storageUrl, + directoryEnclaveId, + directoryTrustAnchor, + directoryUrl, cdnUrlObject, certificateAuthority, contentProxyUrl, @@ -703,6 +792,15 @@ export function initialize({ if (!is.string(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)) { throw new Error('WebAPI.initialize: Invalid cdnUrlObject'); } @@ -760,6 +858,7 @@ export function initialize({ getStorageCredentials, getStorageManifest, getStorageRecords, + getUuidsForE164s, makeProxiedRequest, putAttachment, registerCapabilities, @@ -1626,5 +1725,382 @@ export function initialize({ { certificateAuthority, proxyUrl } ); } + + 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.length}` + ); + } + + 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.externalCurveAsync.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 + ): Promise> { + 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); + } } } diff --git a/ts/types/PhoneNumber.ts b/ts/types/PhoneNumber.ts index 48fd5b83a..75504a6fc 100644 --- a/ts/types/PhoneNumber.ts +++ b/ts/types/PhoneNumber.ts @@ -22,6 +22,22 @@ function _format( } } +export function isValidNumber( + phoneNumber: string, + options?: { + regionCode?: string; + } +): boolean { + const { regionCode } = options || { regionCode: undefined }; + try { + const parsedNumber = instance.parse(phoneNumber, regionCode); + + return instance.isValidNumber(parsedNumber); + } catch (error) { + return false; + } +} + export const format = memoizee(_format, { primitive: true, // Convert the arguments to a unique string, required for primitive mode. diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index 84dff9dfe..f9bcbb751 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -203,30 +203,6 @@ "reasonCategory": "usageTrusted", "updated": "2020-03-25T15:45:04.024Z" }, - { - "rule": "jQuery-load(", - "path": "js/models/conversations.js", - "line": " // but the full ConversationController.load() sequence isn't complete. So, we", - "lineNumber": 465, - "reasonCategory": "exampleCode", - "updated": "2020-08-11T21:28:50.868Z" - }, - { - "rule": "jQuery-load(", - "path": "js/models/conversations.js", - "line": " // don't cache props on create, but we do later when load() calls generateProps()", - "lineNumber": 466, - "reasonCategory": "exampleCode", - "updated": "2020-08-11T21:28:50.868Z" - }, - { - "rule": "jQuery-wrap(", - "path": "js/models/conversations.js", - "line": " await wrap(", - "lineNumber": 691, - "reasonCategory": "falseMatch", - "updated": "2020-06-09T20:26:46.515Z" - }, { "rule": "jQuery-append(", "path": "js/modules/debuglogs.js", @@ -566,7 +542,7 @@ "rule": "jQuery-$(", "path": "js/views/inbox_view.js", "line": " if (e && this.$(e.target).closest('.placeholder').length) {", - "lineNumber": 190, + "lineNumber": 192, "reasonCategory": "usageTrusted", "updated": "2020-05-28T17:42:35.329Z", "reasonDetail": "Known DOM elements" @@ -575,7 +551,7 @@ "rule": "jQuery-$(", "path": "js/views/inbox_view.js", "line": " this.$('#header, .gutter').addClass('inactive');", - "lineNumber": 194, + "lineNumber": 196, "reasonCategory": "usageTrusted", "updated": "2020-05-28T17:42:35.329Z", "reasonDetail": "Hardcoded selector" @@ -584,25 +560,25 @@ "rule": "jQuery-$(", "path": "js/views/inbox_view.js", "line": " this.$('.conversation-stack').addClass('inactive');", - "lineNumber": 198, - "reasonCategory": "usageTrusted", - "updated": "2020-05-28T17:42:35.329Z", - "reasonDetail": "Hardcoded selector" - }, - { - "rule": "jQuery-$(", - "path": "js/views/inbox_view.js", - "line": " this.$('.conversation:first .menu').trigger('close');", "lineNumber": 200, "reasonCategory": "usageTrusted", "updated": "2020-05-28T17:42:35.329Z", "reasonDetail": "Hardcoded selector" }, + { + "rule": "jQuery-$(", + "path": "js/views/inbox_view.js", + "line": " this.$('.conversation:first .menu').trigger('close');", + "lineNumber": 202, + "reasonCategory": "usageTrusted", + "updated": "2020-05-28T17:42:35.329Z", + "reasonDetail": "Hardcoded selector" + }, { "rule": "jQuery-$(", "path": "js/views/inbox_view.js", "line": " if (e && this.$(e.target).closest('.capture-audio').length > 0) {", - "lineNumber": 220, + "lineNumber": 222, "reasonCategory": "usageTrusted", "updated": "2020-05-29T18:29:18.234Z", "reasonDetail": "Known DOM elements" @@ -611,7 +587,7 @@ "rule": "jQuery-$(", "path": "js/views/inbox_view.js", "line": " this.$('.conversation:first .recorder').trigger('close');", - "lineNumber": 223, + "lineNumber": 225, "reasonCategory": "usageTrusted", "updated": "2020-05-29T18:29:18.234Z", "reasonDetail": "Hardcoded selector" @@ -12971,5 +12947,21 @@ "lineNumber": 51, "reasonCategory": "falseMatch", "updated": "2020-04-05T23:45:16.746Z" + }, + { + "rule": "jQuery-wrap(", + "path": "ts/textsecure/WebAPI.js", + "line": " const byteBuffer = window.dcodeIO.ByteBuffer.wrap(quote, 'binary', window.dcodeIO.ByteBuffer.LITTLE_ENDIAN);", + "lineNumber": 1049, + "reasonCategory": "falseMatch", + "updated": "2020-09-04T00:33:28.532Z" + }, + { + "rule": "jQuery-wrap(", + "path": "ts/textsecure/WebAPI.ts", + "line": " const byteBuffer = window.dcodeIO.ByteBuffer.wrap(", + "lineNumber": 1748, + "reasonCategory": "falseMatch", + "updated": "2020-09-04T00:33:28.532Z" } ] \ No newline at end of file diff --git a/ts/util/lint/linter.ts b/ts/util/lint/linter.ts index cdf7690ce..45e2c0aec 100644 --- a/ts/util/lint/linter.ts +++ b/ts/util/lint/linter.ts @@ -54,6 +54,7 @@ const excludedFiles = [ // High-traffic files in our project '^js/models/messages.js', + '^js/models/conversations.js', '^js/views/conversation_view.js', '^js/background.js', '^ts/Crypto.js', @@ -280,7 +281,7 @@ const excludedFiles = [ '^node_modules/dotenv-webpack/.+', '^node_modules/follow-redirects/.+', // Used by webpack-dev-server '^node_modules/html-webpack-plugin/.+', - '^node_modules/node-forge/.+', // Used by webpack-dev-server + '^node_modules/selfsigned/.+', // Used by webpack-dev-server '^node_modules/portfinder/.+', '^node_modules/renderkid/.+', // Used by html-webpack-plugin '^node_modules/spdy-transport/.+', // Used by webpack-dev-server diff --git a/ts/window.d.ts b/ts/window.d.ts index 6a16d886e..27d863334 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -25,6 +25,8 @@ import { ConversationController } from './ConversationController'; import { SendOptionsType } from './textsecure/SendMessage'; import Data from './sql/Client'; +export { Long } from 'long'; + type TaskResultType = any; declare global { @@ -101,9 +103,14 @@ declare global { } export type DCodeIOType = { - ByteBuffer: typeof ByteBufferClass; - Long: { + ByteBuffer: typeof ByteBufferClass & { + BIG_ENDIAN: number; + LITTLE_ENDIAN: number; + Long: DCodeIOType['Long']; + }; + Long: Long & { fromBits: (low: number, high: number, unsigned: boolean) => number; + fromString: (str: string) => Long; }; }; @@ -138,8 +145,13 @@ export class SecretSessionCipherClass { } export class ByteBufferClass { - constructor(value?: any, encoding?: string); - static wrap: (value: any, type?: string) => ByteBufferClass; + constructor(value?: any, littleEndian?: number); + static wrap: ( + value: any, + encoding?: string, + littleEndian?: number + ) => ByteBufferClass; + buffer: ArrayBuffer; toString: (type: string) => string; toArrayBuffer: () => ArrayBuffer; toBinary: () => string; @@ -147,7 +159,11 @@ export class ByteBufferClass { append: (data: ArrayBuffer) => void; limit: number; offset: 0; + readInt: (offset: number) => number; + readLong: (offset: number) => Long; + readShort: (offset: number) => number; readVarint32: () => number; + writeLong: (l: Long) => void; skip: (length: number) => void; } diff --git a/yarn.lock b/yarn.lock index 6754184e0..c80ba04f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2177,9 +2177,9 @@ "@types/node" "*" "@types/fs-extra@^8.1.0": - version "8.1.0" - resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-8.1.0.tgz#1114834b53c3914806cd03b3304b37b3bd221a4d" - integrity sha512-UoOfVEzAUpeSPmjm7h1uk5MH6KZma2z2O7a75onTGjnNvAvMVrPzPL/vBbT65iIGHWj6rokwfmYcmxmlSf2uwg== + version "8.1.1" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-8.1.1.tgz#1e49f22d09aa46e19b51c0b013cb63d0d923a068" + integrity sha512-TcUlBem321DFQzBNuz8p0CLLKp0VvF/XH9E4KHNmgwyp4E3AfgI5cjiIVZWlbfThBop2qxFIh4+LeY6hVWWZ2w== dependencies: "@types/node" "*" @@ -2289,6 +2289,11 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.106.tgz#6093e9a02aa567ddecfe9afadca89e53e5dce4dd" integrity sha512-tOSvCVrvSqFZ4A/qrqqm6p37GZoawsZtoR0SJhlF7EonNZUgrn8FfT+RNQ11h+NUpMt6QVe36033f3qEKBwfWA== +"@types/long@4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" + integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w== + "@types/long@^3.0.32": version "3.0.32" resolved "https://registry.yarnpkg.com/@types/long/-/long-3.0.32.tgz#f4e5af31e9e9b196d8e5fca8a5e2e20aa3d60b69" @@ -2329,6 +2334,13 @@ "@types/node" "*" form-data "^3.0.0" +"@types/node-forge@0.9.5": + version "0.9.5" + resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-0.9.5.tgz#648231d79da197216290429020698d4e767365a0" + integrity sha512-rrN3xfA/oZIzwOnO3d2wRQz7UdeVkmMMPjWUCfpPTPuKFVb3D6G10LuiVHYYmvrivBBLMx4m0P/FICoDbNZUMA== + dependencies: + "@types/node" "*" + "@types/node@*": version "11.12.2" resolved "https://registry.yarnpkg.com/@types/node/-/node-11.12.2.tgz#d7f302e74b10e9801d52852137f652d9ee235da8" @@ -2900,6 +2912,14 @@ agent-base@^4.3.0: dependencies: es6-promisify "^5.0.0" +aggregate-error@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.0.1.tgz#db2fe7246e536f40d9b5442a39e117d7dd6a24e0" + integrity sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + "airbnb-js-shims@^1 || ^2": version "2.2.0" resolved "https://registry.yarnpkg.com/airbnb-js-shims/-/airbnb-js-shims-2.2.0.tgz#46e1d9d9516f704ef736de76a3b6d484df9a96d8" @@ -4728,6 +4748,11 @@ clean-css@4.2.x, clean-css@^4.2.1: dependencies: source-map "~0.6.0" +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + cli-boxes@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143" @@ -8935,6 +8960,11 @@ indent-string@^2.1.0: dependencies: repeating "^2.0.0" +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + indexes-of@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" @@ -11136,6 +11166,11 @@ node-fetch@^1.0.1: encoding "^0.1.11" is-stream "^1.0.1" +node-forge@0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" + integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA== + node-forge@0.7.5: version "0.7.5" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.5.tgz#6c152c345ce11c52f465c2abd957e8639cd674df" @@ -11865,6 +11900,20 @@ p-map@2.1.0, p-map@^2.0.0: resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + +p-props@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-props/-/p-props-4.0.0.tgz#f37c877a9a722057833e1dc38d43edf3906b3437" + integrity sha512-3iKFbPdoPG7Ne3cMA53JnjPsTMaIzE9gxKZnvKJJivTAeqLEZPBu6zfi6DYq9AsH1nYycWmo3sWCNI8Kz6T2Zg== + dependencies: + p-map "^4.0.0" + p-queue@6.2.1: version "6.2.1" resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-6.2.1.tgz#809a832046451b2240a0a8e48b4fa18192b22b64"