Support for Contact Discovery Service

This commit is contained in:
Ken Powers 2020-09-03 21:25:19 -04:00 committed by Scott Nonnenberg
parent f6dcf91dbf
commit 8290881bd8
21 changed files with 961 additions and 79 deletions

View File

@ -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"

View File

@ -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"

View File

@ -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();

View File

@ -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);

View File

@ -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.

View File

@ -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;
}

View File

@ -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));
};

View File

@ -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'),

View File

@ -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",

View File

@ -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,

View File

@ -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,

View File

@ -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<string>
) {
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;
}

12
ts/libsignal.d.ts vendored
View File

@ -20,6 +20,16 @@ export type LibSignalType = {
) => Promise<void>;
getRandomBytes: (size: number) => ArrayBuffer;
};
externalCurveAsync: {
calculateAgreement: (
pubKey: ArrayBuffer,
privKey: ArrayBuffer
) => Promise<ArrayBuffer>;
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<Array<ArrayBuffer>>;
};
worker: {

View File

@ -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<any>;
failoverIdentifiers: Array<any>;
unidentifiedDeliveries: Array<any>;
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
);
}

View File

@ -53,6 +53,10 @@ export type CallbackResultType = {
errors?: Array<any>;
unidentifiedDeliveries?: Array<any>;
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<CallbackResultType> {
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<CallbackResultType> =>
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<string>) {
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<CallbackResultType> {
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<CallbackResultType> {
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: [],
});
}

View File

@ -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<string>
) => Promise<Dictionary<string | null>>;
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<string>
): Promise<Dictionary<string | null>> {
const directoryAuth = await getDirectoryAuth();
const attestationResult = await putRemoteAttestation(directoryAuth);
// Encrypt data for discovery
const data = await encryptCdsDiscoveryRequest(
attestationResult.attestations,
e164s
);
const { cookie } = attestationResult;
// Send discovery request
const discoveryResponse: {
requestId: string;
iv: string;
data: string;
mac: string;
} = await _outerAjax(null, {
certificateAuthority,
type: 'PUT',
headers: cookie
? {
cookie,
}
: undefined,
contentType: 'application/json; charset=utf-8',
host: directoryUrl,
path: `${URL_CALLS.discovery}/${directoryEnclaveId}`,
user: directoryAuth.username,
password: directoryAuth.password,
responseType: 'json',
data: JSON.stringify(data),
version,
});
// Decode discovery request response
const decodedDiscoveryResponse: {
[K in keyof typeof discoveryResponse]: ArrayBuffer;
} = mapValues(discoveryResponse, value => {
return base64ToArrayBuffer(value);
}) as any;
const returnedAttestation = Object.values(
attestationResult.attestations
).find(at =>
constantTimeEqual(at.requestId, decodedDiscoveryResponse.requestId)
);
if (!returnedAttestation) {
throw new Error('No known attestations returned from CDS');
}
// Decrypt discovery response
const decryptedDiscoveryData = await decryptAesGcm(
returnedAttestation.serverKey,
decodedDiscoveryResponse.iv,
concatenateBytes(
decodedDiscoveryResponse.data,
decodedDiscoveryResponse.mac
)
);
// Process and return result
const uuids = splitUuids(decryptedDiscoveryData);
if (uuids.length !== e164s.length) {
throw new Error(
'Returned set of UUIDs did not match returned set of e164s!'
);
}
return zipObject(e164s, uuids);
}
}
}

View File

@ -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.

View File

@ -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"
}
]

View File

@ -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

24
ts/window.d.ts vendored
View File

@ -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;
}

View File

@ -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"