Support for Contact Discovery Service
This commit is contained in:
parent
f6dcf91dbf
commit
8290881bd8
21 changed files with 961 additions and 79 deletions
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
};
|
||||
|
||||
|
|
3
main.js
3
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'),
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
144
ts/Crypto.ts
144
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<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
12
ts/libsignal.d.ts
vendored
|
@ -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: {
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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: [],
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
|
@ -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
24
ts/window.d.ts
vendored
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
55
yarn.lock
55
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"
|
||||
|
|
Loading…
Reference in a new issue