Support for Contact Discovery Service
This commit is contained in:
parent
f6dcf91dbf
commit
8290881bd8
|
@ -1,6 +1,9 @@
|
||||||
{
|
{
|
||||||
"serverUrl": "https://textsecure-service-staging.whispersystems.org",
|
"serverUrl": "https://textsecure-service-staging.whispersystems.org",
|
||||||
"storageUrl": "https://storage-staging.signal.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": {
|
"cdn": {
|
||||||
"0": "https://cdn-staging.signal.org",
|
"0": "https://cdn-staging.signal.org",
|
||||||
"2": "https://cdn2-staging.signal.org"
|
"2": "https://cdn2-staging.signal.org"
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"serverUrl": "https://textsecure-service.whispersystems.org",
|
"serverUrl": "https://textsecure-service.whispersystems.org",
|
||||||
"storageUrl": "https://storage.signal.org",
|
"storageUrl": "https://storage.signal.org",
|
||||||
|
"directoryUrl": "https://api.directory.signal.org",
|
||||||
"cdn": {
|
"cdn": {
|
||||||
"0": "https://cdn.signal.org",
|
"0": "https://cdn.signal.org",
|
||||||
"2": "https://cdn2.signal.org"
|
"2": "https://cdn2.signal.org"
|
||||||
|
|
|
@ -230,10 +230,14 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
let messageReceiver;
|
let messageReceiver;
|
||||||
|
let preMessageReceiverStatus;
|
||||||
window.getSocketStatus = () => {
|
window.getSocketStatus = () => {
|
||||||
if (messageReceiver) {
|
if (messageReceiver) {
|
||||||
return messageReceiver.getStatus();
|
return messageReceiver.getStatus();
|
||||||
}
|
}
|
||||||
|
if (_.isNumber(preMessageReceiverStatus)) {
|
||||||
|
return preMessageReceiverStatus;
|
||||||
|
}
|
||||||
return -1;
|
return -1;
|
||||||
};
|
};
|
||||||
Whisper.events = _.clone(Backbone.Events);
|
Whisper.events = _.clone(Backbone.Events);
|
||||||
|
@ -1633,6 +1637,8 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
preMessageReceiverStatus = WebSocket.CONNECTING;
|
||||||
|
|
||||||
if (messageReceiver) {
|
if (messageReceiver) {
|
||||||
await messageReceiver.stopProcessing();
|
await messageReceiver.stopProcessing();
|
||||||
|
|
||||||
|
@ -1647,6 +1653,52 @@
|
||||||
const PASSWORD = storage.get('password');
|
const PASSWORD = storage.get('password');
|
||||||
const mySignalingKey = storage.get('signaling_key');
|
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;
|
connectCount += 1;
|
||||||
const options = {
|
const options = {
|
||||||
retryCached: connectCount === 1,
|
retryCached: connectCount === 1,
|
||||||
|
@ -1667,6 +1719,8 @@
|
||||||
);
|
);
|
||||||
window.textsecure.messageReceiver = messageReceiver;
|
window.textsecure.messageReceiver = messageReceiver;
|
||||||
|
|
||||||
|
preMessageReceiverStatus = null;
|
||||||
|
|
||||||
function addQueuedEventListener(name, handler) {
|
function addQueuedEventListener(name, handler) {
|
||||||
messageReceiver.addEventListener(name, (...args) =>
|
messageReceiver.addEventListener(name, (...args) =>
|
||||||
eventHandlerQueue.add(async () => {
|
eventHandlerQueue.add(async () => {
|
||||||
|
@ -1709,11 +1763,6 @@
|
||||||
logger: window.log,
|
logger: window.log,
|
||||||
});
|
});
|
||||||
|
|
||||||
window.textsecure.messaging = new textsecure.MessageSender(
|
|
||||||
USERNAME || OLD_USERNAME,
|
|
||||||
PASSWORD
|
|
||||||
);
|
|
||||||
|
|
||||||
if (connectCount === 1) {
|
if (connectCount === 1) {
|
||||||
window.Signal.Stickers.downloadQueuedPacks();
|
window.Signal.Stickers.downloadQueuedPacks();
|
||||||
await window.textsecure.messaging.sendRequestKeySyncMessage();
|
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() {
|
isBlocked() {
|
||||||
const uuid = this.get('uuid');
|
const uuid = this.get('uuid');
|
||||||
if (uuid) {
|
if (uuid) {
|
||||||
|
@ -1258,6 +1291,11 @@
|
||||||
if (c.id === me) {
|
if (c.id === me) {
|
||||||
return null;
|
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();
|
return c.getSendTarget();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -1422,7 +1460,7 @@
|
||||||
});
|
});
|
||||||
Whisper.Reactions.onReaction(reactionModel);
|
Whisper.Reactions.onReaction(reactionModel);
|
||||||
|
|
||||||
const destination = this.get('e164');
|
const destination = this.getSendTarget();
|
||||||
const recipients = this.getRecipients();
|
const recipients = this.getRecipients();
|
||||||
|
|
||||||
let profileKey;
|
let profileKey;
|
||||||
|
@ -1717,7 +1755,8 @@
|
||||||
if (result) {
|
if (result) {
|
||||||
await this.handleMessageSendResult(
|
await this.handleMessageSendResult(
|
||||||
result.failoverIdentifiers,
|
result.failoverIdentifiers,
|
||||||
result.unidentifiedDeliveries
|
result.unidentifiedDeliveries,
|
||||||
|
result.discoveredIdentifierPairs
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
@ -1727,7 +1766,8 @@
|
||||||
if (result) {
|
if (result) {
|
||||||
await this.handleMessageSendResult(
|
await this.handleMessageSendResult(
|
||||||
result.failoverIdentifiers,
|
result.failoverIdentifiers,
|
||||||
result.unidentifiedDeliveries
|
result.unidentifiedDeliveries,
|
||||||
|
result.discoveredIdentifierPairs
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
throw result;
|
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(
|
await Promise.all(
|
||||||
(failoverIdentifiers || []).map(async identifier => {
|
(failoverIdentifiers || []).map(async identifier => {
|
||||||
const conversation = ConversationController.get(identifier);
|
const conversation = ConversationController.get(identifier);
|
||||||
|
|
|
@ -1716,6 +1716,14 @@
|
||||||
|
|
||||||
let promises = [];
|
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) {
|
if (result instanceof Error) {
|
||||||
this.saveErrors(result);
|
this.saveErrors(result);
|
||||||
if (result.name === 'SignedPreKeyRotationError') {
|
if (result.name === 'SignedPreKeyRotationError') {
|
||||||
|
@ -1728,6 +1736,24 @@
|
||||||
if (result.successfulIdentifiers.length > 0) {
|
if (result.successfulIdentifiers.length > 0) {
|
||||||
const sentTo = this.get('sent_to') || [];
|
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
|
// In groups, we don't treat unregistered users as a user-visible
|
||||||
// error. The message will look successful, but the details
|
// error. The message will look successful, but the details
|
||||||
// screen will show that we didn't send to these unregistered users.
|
// screen will show that we didn't send to these unregistered users.
|
||||||
|
|
|
@ -157,7 +157,9 @@
|
||||||
this.onEmpty();
|
this.onEmpty();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// We also replicate empty here
|
window.log.warn(
|
||||||
|
'startConnectionListener: Found unexpected socket status; calling onEmpty() manually.'
|
||||||
|
);
|
||||||
this.onEmpty();
|
this.onEmpty();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -25564,7 +25564,7 @@ var Internal = Internal || {};
|
||||||
|
|
||||||
|
|
||||||
// HKDF for TextSecure has a bit of additional handling - salts always end up being 32 bytes
|
// 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));
|
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'),
|
buildExpiration: config.get('buildExpiration'),
|
||||||
serverUrl: config.get('serverUrl'),
|
serverUrl: config.get('serverUrl'),
|
||||||
storageUrl: config.get('storageUrl'),
|
storageUrl: config.get('storageUrl'),
|
||||||
|
directoryUrl: config.get('directoryUrl'),
|
||||||
|
directoryEnclaveId: config.get('directoryEnclaveId'),
|
||||||
|
directoryTrustAnchor: config.get('directoryTrustAnchor'),
|
||||||
cdnUrl0: config.get('cdn').get('0'),
|
cdnUrl0: config.get('cdn').get('0'),
|
||||||
cdnUrl2: config.get('cdn').get('2'),
|
cdnUrl2: config.get('cdn').get('2'),
|
||||||
certificateAuthority: config.get('certificateAuthority'),
|
certificateAuthority: config.get('certificateAuthority'),
|
||||||
|
|
|
@ -99,10 +99,12 @@
|
||||||
"moment": "2.21.0",
|
"moment": "2.21.0",
|
||||||
"mustache": "2.3.0",
|
"mustache": "2.3.0",
|
||||||
"node-fetch": "2.6.0",
|
"node-fetch": "2.6.0",
|
||||||
|
"node-forge": "0.10.0",
|
||||||
"node-gyp": "5.0.3",
|
"node-gyp": "5.0.3",
|
||||||
"normalize-path": "3.0.0",
|
"normalize-path": "3.0.0",
|
||||||
"os-locale": "3.0.1",
|
"os-locale": "3.0.1",
|
||||||
"p-map": "2.1.0",
|
"p-map": "2.1.0",
|
||||||
|
"p-props": "4.0.0",
|
||||||
"p-queue": "6.2.1",
|
"p-queue": "6.2.1",
|
||||||
"pify": "3.0.0",
|
"pify": "3.0.0",
|
||||||
"protobufjs": "6.8.6",
|
"protobufjs": "6.8.6",
|
||||||
|
@ -169,10 +171,12 @@
|
||||||
"@types/js-yaml": "3.12.0",
|
"@types/js-yaml": "3.12.0",
|
||||||
"@types/linkify-it": "2.1.0",
|
"@types/linkify-it": "2.1.0",
|
||||||
"@types/lodash": "4.14.106",
|
"@types/lodash": "4.14.106",
|
||||||
|
"@types/long": "4.0.1",
|
||||||
"@types/memoizee": "0.4.2",
|
"@types/memoizee": "0.4.2",
|
||||||
"@types/mkdirp": "0.5.2",
|
"@types/mkdirp": "0.5.2",
|
||||||
"@types/mocha": "5.0.0",
|
"@types/mocha": "5.0.0",
|
||||||
"@types/node-fetch": "2.5.7",
|
"@types/node-fetch": "2.5.7",
|
||||||
|
"@types/node-forge": "0.9.5",
|
||||||
"@types/normalize-path": "3.0.0",
|
"@types/normalize-path": "3.0.0",
|
||||||
"@types/pify": "3.0.2",
|
"@types/pify": "3.0.2",
|
||||||
"@types/react": "16.8.5",
|
"@types/react": "16.8.5",
|
||||||
|
|
|
@ -330,6 +330,9 @@ try {
|
||||||
window.WebAPI = window.textsecure.WebAPI.initialize({
|
window.WebAPI = window.textsecure.WebAPI.initialize({
|
||||||
url: config.serverUrl,
|
url: config.serverUrl,
|
||||||
storageUrl: config.storageUrl,
|
storageUrl: config.storageUrl,
|
||||||
|
directoryUrl: config.directoryUrl,
|
||||||
|
directoryEnclaveId: config.directoryEnclaveId,
|
||||||
|
directoryTrustAnchor: config.directoryTrustAnchor,
|
||||||
cdnUrlObject: {
|
cdnUrlObject: {
|
||||||
'0': config.cdnUrl0,
|
'0': config.cdnUrl0,
|
||||||
'2': config.cdnUrl2,
|
'2': config.cdnUrl2,
|
||||||
|
|
|
@ -35,6 +35,9 @@ const { initialize: initializeWebAPI } = require('../ts/textsecure/WebAPI');
|
||||||
const WebAPI = initializeWebAPI({
|
const WebAPI = initializeWebAPI({
|
||||||
url: config.serverUrl,
|
url: config.serverUrl,
|
||||||
storageUrl: config.storageUrl,
|
storageUrl: config.storageUrl,
|
||||||
|
directoryUrl: config.directoryUrl,
|
||||||
|
directoryEnclaveId: config.directoryEnclaveId,
|
||||||
|
directoryTrustAnchor: config.directoryTrustAnchor,
|
||||||
cdnUrlObject: {
|
cdnUrlObject: {
|
||||||
'0': config.cdnUrl0,
|
'0': config.cdnUrl0,
|
||||||
'2': config.cdnUrl2,
|
'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
|
// Yep, we're doing some bitwise stuff in an encryption-related file
|
||||||
// tslint:disable no-bitwise
|
// tslint:disable no-bitwise
|
||||||
|
|
||||||
|
@ -11,7 +13,7 @@ export function typedArrayToArrayBuffer(typedArray: Uint8Array): ArrayBuffer {
|
||||||
const { buffer, byteOffset, byteLength } = typedArray;
|
const { buffer, byteOffset, byteLength } = typedArray;
|
||||||
|
|
||||||
// tslint:disable-next-line no-unnecessary-type-assertion
|
// 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) {
|
export function arrayBufferToBase64(arrayBuffer: ArrayBuffer) {
|
||||||
|
@ -173,7 +175,7 @@ export async function decryptFile(
|
||||||
data: ArrayBuffer
|
data: ArrayBuffer
|
||||||
) {
|
) {
|
||||||
const ephemeralPublicKey = getFirstBytes(data, PUB_KEY_LENGTH);
|
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(
|
const agreement = await window.libsignal.Curve.async.calculateAgreement(
|
||||||
ephemeralPublicKey,
|
ephemeralPublicKey,
|
||||||
staticPrivateKey
|
staticPrivateKey
|
||||||
|
@ -201,7 +203,7 @@ export async function deriveStorageItemKey(
|
||||||
export async function deriveAccessKey(profileKey: ArrayBuffer) {
|
export async function deriveAccessKey(profileKey: ArrayBuffer) {
|
||||||
const iv = getZeroes(12);
|
const iv = getZeroes(12);
|
||||||
const plaintext = getZeroes(16);
|
const plaintext = getZeroes(16);
|
||||||
const accessKey = await _encrypt_aes_gcm(profileKey, iv, plaintext);
|
const accessKey = await encryptAesGcm(profileKey, iv, plaintext);
|
||||||
|
|
||||||
return getFirstBytes(accessKey, 16);
|
return getFirstBytes(accessKey, 16);
|
||||||
}
|
}
|
||||||
|
@ -253,12 +255,12 @@ export async function decryptSymmetric(key: ArrayBuffer, data: ArrayBuffer) {
|
||||||
const iv = getZeroes(IV_LENGTH);
|
const iv = getZeroes(IV_LENGTH);
|
||||||
|
|
||||||
const nonce = getFirstBytes(data, NONCE_LENGTH);
|
const nonce = getFirstBytes(data, NONCE_LENGTH);
|
||||||
const cipherText = _getBytes(
|
const cipherText = getBytes(
|
||||||
data,
|
data,
|
||||||
NONCE_LENGTH,
|
NONCE_LENGTH,
|
||||||
data.byteLength - NONCE_LENGTH - MAC_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 cipherKey = await hmacSha256(key, nonce);
|
||||||
const macKey = await hmacSha256(key, cipherKey);
|
const macKey = await hmacSha256(key, cipherKey);
|
||||||
|
@ -413,15 +415,18 @@ export async function decryptAesCtr(
|
||||||
return plaintext;
|
return plaintext;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function _encrypt_aes_gcm(
|
export async function encryptAesGcm(
|
||||||
key: ArrayBuffer,
|
key: ArrayBuffer,
|
||||||
iv: ArrayBuffer,
|
iv: ArrayBuffer,
|
||||||
plaintext: ArrayBuffer
|
plaintext: ArrayBuffer,
|
||||||
|
additionalData?: ArrayBuffer
|
||||||
) {
|
) {
|
||||||
const algorithm = {
|
const algorithm = {
|
||||||
name: 'AES-GCM',
|
name: 'AES-GCM',
|
||||||
iv,
|
iv,
|
||||||
|
...(additionalData ? { additionalData } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const extractable = false;
|
const extractable = false;
|
||||||
|
|
||||||
const cryptoKey = await crypto.subtle.importKey(
|
const cryptoKey = await crypto.subtle.importKey(
|
||||||
|
@ -435,6 +440,37 @@ export async function _encrypt_aes_gcm(
|
||||||
return crypto.subtle.encrypt(algorithm, cryptoKey, plaintext);
|
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
|
// Utility
|
||||||
|
|
||||||
export function getRandomBytes(n: number) {
|
export function getRandomBytes(n: number) {
|
||||||
|
@ -550,9 +586,7 @@ export function getFirstBytes(data: ArrayBuffer, n: number) {
|
||||||
return typedArrayToArrayBuffer(source.subarray(0, n));
|
return typedArrayToArrayBuffer(source.subarray(0, n));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Internal-only
|
export function getBytes(
|
||||||
|
|
||||||
export function _getBytes(
|
|
||||||
data: ArrayBuffer | Uint8Array,
|
data: ArrayBuffer | Uint8Array,
|
||||||
start: number,
|
start: number,
|
||||||
n: number
|
n: number
|
||||||
|
@ -561,3 +595,93 @@ export function _getBytes(
|
||||||
|
|
||||||
return typedArrayToArrayBuffer(source.subarray(start, start + n));
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -20,6 +20,16 @@ export type LibSignalType = {
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
getRandomBytes: (size: number) => ArrayBuffer;
|
getRandomBytes: (size: number) => ArrayBuffer;
|
||||||
};
|
};
|
||||||
|
externalCurveAsync: {
|
||||||
|
calculateAgreement: (
|
||||||
|
pubKey: ArrayBuffer,
|
||||||
|
privKey: ArrayBuffer
|
||||||
|
) => Promise<ArrayBuffer>;
|
||||||
|
generateKeyPair: () => Promise<{
|
||||||
|
privKey: ArrayBuffer;
|
||||||
|
pubKey: ArrayBuffer;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
KeyHelper: {
|
KeyHelper: {
|
||||||
generateIdentityKeyPair: () => Promise<{
|
generateIdentityKeyPair: () => Promise<{
|
||||||
privKey: ArrayBuffer;
|
privKey: ArrayBuffer;
|
||||||
|
@ -56,7 +66,7 @@ export type LibSignalType = {
|
||||||
packKey: ArrayBuffer,
|
packKey: ArrayBuffer,
|
||||||
salt: ArrayBuffer,
|
salt: ArrayBuffer,
|
||||||
// The string is a bit crazy, but ProvisioningCipher currently passes in a string
|
// The string is a bit crazy, but ProvisioningCipher currently passes in a string
|
||||||
info: ArrayBuffer | string
|
info?: ArrayBuffer | string
|
||||||
) => Promise<Array<ArrayBuffer>>;
|
) => Promise<Array<ArrayBuffer>>;
|
||||||
};
|
};
|
||||||
worker: {
|
worker: {
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
SendMessageNetworkError,
|
SendMessageNetworkError,
|
||||||
UnregisteredUserError,
|
UnregisteredUserError,
|
||||||
} from './Errors';
|
} from './Errors';
|
||||||
|
import { isValidNumber } from '../types/PhoneNumber';
|
||||||
|
|
||||||
type OutgoingMessageOptionsType = SendOptionsType & {
|
type OutgoingMessageOptionsType = SendOptionsType & {
|
||||||
online?: boolean;
|
online?: boolean;
|
||||||
|
@ -34,6 +35,10 @@ export default class OutgoingMessage {
|
||||||
successfulIdentifiers: Array<any>;
|
successfulIdentifiers: Array<any>;
|
||||||
failoverIdentifiers: Array<any>;
|
failoverIdentifiers: Array<any>;
|
||||||
unidentifiedDeliveries: Array<any>;
|
unidentifiedDeliveries: Array<any>;
|
||||||
|
discoveredIdentifierPairs: Array<{
|
||||||
|
e164: string;
|
||||||
|
uuid: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
sendMetadata?: SendMetadataType;
|
sendMetadata?: SendMetadataType;
|
||||||
senderCertificate?: ArrayBuffer;
|
senderCertificate?: ArrayBuffer;
|
||||||
|
@ -68,6 +73,7 @@ export default class OutgoingMessage {
|
||||||
this.successfulIdentifiers = [];
|
this.successfulIdentifiers = [];
|
||||||
this.failoverIdentifiers = [];
|
this.failoverIdentifiers = [];
|
||||||
this.unidentifiedDeliveries = [];
|
this.unidentifiedDeliveries = [];
|
||||||
|
this.discoveredIdentifierPairs = [];
|
||||||
|
|
||||||
const { sendMetadata, senderCertificate, online } = options || ({} as any);
|
const { sendMetadata, senderCertificate, online } = options || ({} as any);
|
||||||
this.sendMetadata = sendMetadata;
|
this.sendMetadata = sendMetadata;
|
||||||
|
@ -82,6 +88,7 @@ export default class OutgoingMessage {
|
||||||
failoverIdentifiers: this.failoverIdentifiers,
|
failoverIdentifiers: this.failoverIdentifiers,
|
||||||
errors: this.errors,
|
errors: this.errors,
|
||||||
unidentifiedDeliveries: this.unidentifiedDeliveries,
|
unidentifiedDeliveries: this.unidentifiedDeliveries,
|
||||||
|
discoveredIdentifierPairs: this.discoveredIdentifierPairs,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -564,8 +571,39 @@ export default class OutgoingMessage {
|
||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendToIdentifier(identifier: string) {
|
async sendToIdentifier(providedIdentifier: string) {
|
||||||
|
let identifier = providedIdentifier;
|
||||||
try {
|
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(
|
const updateDevices = await this.getStaleDeviceIdsForIdentifier(
|
||||||
identifier
|
identifier
|
||||||
);
|
);
|
||||||
|
@ -583,7 +621,7 @@ export default class OutgoingMessage {
|
||||||
} else {
|
} else {
|
||||||
this.registerError(
|
this.registerError(
|
||||||
identifier,
|
identifier,
|
||||||
`Failed to retrieve new device keys for number ${identifier}`,
|
`Failed to retrieve new device keys for identifier ${identifier}`,
|
||||||
error
|
error
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,6 +53,10 @@ export type CallbackResultType = {
|
||||||
errors?: Array<any>;
|
errors?: Array<any>;
|
||||||
unidentifiedDeliveries?: Array<any>;
|
unidentifiedDeliveries?: Array<any>;
|
||||||
dataMessage?: ArrayBuffer;
|
dataMessage?: ArrayBuffer;
|
||||||
|
discoveredIdentifierPairs: Array<{
|
||||||
|
e164: string;
|
||||||
|
uuid: string | null;
|
||||||
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PreviewType = {
|
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 message = new Message(attrs);
|
||||||
const silent = false;
|
const silent = false;
|
||||||
|
|
||||||
|
@ -474,7 +481,7 @@ export default class MessageSender {
|
||||||
this.uploadLinkPreviews(message),
|
this.uploadLinkPreviews(message),
|
||||||
this.uploadSticker(message),
|
this.uploadSticker(message),
|
||||||
]).then(
|
]).then(
|
||||||
async () =>
|
async (): Promise<CallbackResultType> =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
this.sendMessageProto(
|
this.sendMessageProto(
|
||||||
message.timestamp,
|
message.timestamp,
|
||||||
|
@ -697,6 +704,10 @@ export default class MessageSender {
|
||||||
return this.server.getProfile(number, options);
|
return this.server.getProfile(number, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getUuidsForE164s(numbers: Array<string>) {
|
||||||
|
return this.server.getUuidsForE164s(numbers);
|
||||||
|
}
|
||||||
|
|
||||||
async getAvatar(path: string) {
|
async getAvatar(path: string) {
|
||||||
return this.server.getAvatar(path);
|
return this.server.getAvatar(path);
|
||||||
}
|
}
|
||||||
|
@ -1439,7 +1450,7 @@ export default class MessageSender {
|
||||||
expireTimer: number | undefined,
|
expireTimer: number | undefined,
|
||||||
profileKey?: ArrayBuffer,
|
profileKey?: ArrayBuffer,
|
||||||
options?: SendOptionsType
|
options?: SendOptionsType
|
||||||
) {
|
): Promise<CallbackResultType> {
|
||||||
const myE164 = window.textsecure.storage.user.getNumber();
|
const myE164 = window.textsecure.storage.user.getNumber();
|
||||||
const myUuid = window.textsecure.storage.user.getNumber();
|
const myUuid = window.textsecure.storage.user.getNumber();
|
||||||
const attrs = {
|
const attrs = {
|
||||||
|
@ -1466,6 +1477,7 @@ export default class MessageSender {
|
||||||
errors: [],
|
errors: [],
|
||||||
unidentifiedDeliveries: [],
|
unidentifiedDeliveries: [],
|
||||||
dataMessage: await this.getMessageProtoObj(attrs),
|
dataMessage: await this.getMessageProtoObj(attrs),
|
||||||
|
discoveredIdentifierPairs: [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1611,7 +1623,7 @@ export default class MessageSender {
|
||||||
timestamp: number,
|
timestamp: number,
|
||||||
profileKey?: ArrayBuffer,
|
profileKey?: ArrayBuffer,
|
||||||
options?: SendOptionsType
|
options?: SendOptionsType
|
||||||
) {
|
): Promise<CallbackResultType> {
|
||||||
const myNumber = window.textsecure.storage.user.getNumber();
|
const myNumber = window.textsecure.storage.user.getNumber();
|
||||||
const myUuid = window.textsecure.storage.user.getUuid();
|
const myUuid = window.textsecure.storage.user.getUuid();
|
||||||
const recipients = groupIdentifiers.filter(
|
const recipients = groupIdentifiers.filter(
|
||||||
|
@ -1637,6 +1649,7 @@ export default class MessageSender {
|
||||||
errors: [],
|
errors: [],
|
||||||
unidentifiedDeliveries: [],
|
unidentifiedDeliveries: [],
|
||||||
dataMessage: await this.getMessageProtoObj(attrs),
|
dataMessage: await this.getMessageProtoObj(attrs),
|
||||||
|
discoveredIdentifierPairs: [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,12 +2,34 @@ import { w3cwebsocket as WebSocket } from 'websocket';
|
||||||
import fetch, { Response } from 'node-fetch';
|
import fetch, { Response } from 'node-fetch';
|
||||||
import ProxyAgent from 'proxy-agent';
|
import ProxyAgent from 'proxy-agent';
|
||||||
import { Agent } from 'https';
|
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 is from '@sindresorhus/is';
|
||||||
import { isPackIdValid, redactPackId } from '../../js/modules/stickers';
|
import { isPackIdValid, redactPackId } from '../../js/modules/stickers';
|
||||||
import { getRandomValue } from '../Crypto';
|
|
||||||
import MessageSender from './SendMessage';
|
import MessageSender from './SendMessage';
|
||||||
|
import {
|
||||||
|
arrayBufferToBase64,
|
||||||
|
base64ToArrayBuffer,
|
||||||
|
bytesFromHexString,
|
||||||
|
bytesFromString,
|
||||||
|
concatenateBytes,
|
||||||
|
constantTimeEqual,
|
||||||
|
decryptAesGcm,
|
||||||
|
encryptCdsDiscoveryRequest,
|
||||||
|
getBytes,
|
||||||
|
getRandomValue,
|
||||||
|
splitUuids,
|
||||||
|
} from '../Crypto';
|
||||||
|
|
||||||
import PQueue from 'p-queue';
|
import PQueue from 'p-queue';
|
||||||
import { v4 as getGuid } from 'uuid';
|
import { v4 as getGuid } from 'uuid';
|
||||||
|
@ -17,6 +39,43 @@ import {
|
||||||
StorageServiceCredentials,
|
StorageServiceCredentials,
|
||||||
} from '../textsecure.d';
|
} 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
|
// tslint:disable no-bitwise
|
||||||
|
|
||||||
function _btoa(str: any) {
|
function _btoa(str: any) {
|
||||||
|
@ -234,7 +293,11 @@ type PromiseAjaxOptionsType = {
|
||||||
proxyUrl?: string;
|
proxyUrl?: string;
|
||||||
redactUrl?: RedactUrl;
|
redactUrl?: RedactUrl;
|
||||||
redirect?: 'error' | 'follow' | 'manual';
|
redirect?: 'error' | 'follow' | 'manual';
|
||||||
responseType?: 'json' | 'arraybuffer' | 'arraybufferwithdetails';
|
responseType?:
|
||||||
|
| 'json'
|
||||||
|
| 'jsonwithdetails'
|
||||||
|
| 'arraybuffer'
|
||||||
|
| 'arraybufferwithdetails';
|
||||||
stack?: string;
|
stack?: string;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
type: HTTPCodeType;
|
type: HTTPCodeType;
|
||||||
|
@ -244,6 +307,12 @@ type PromiseAjaxOptionsType = {
|
||||||
version: string;
|
version: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type JSONWithDetailsType = {
|
||||||
|
data: any;
|
||||||
|
contentType: string;
|
||||||
|
response: Response;
|
||||||
|
};
|
||||||
|
|
||||||
// tslint:disable-next-line max-func-body-length
|
// tslint:disable-next-line max-func-body-length
|
||||||
async function _promiseAjax(
|
async function _promiseAjax(
|
||||||
providedUrl: string | null,
|
providedUrl: string | null,
|
||||||
|
@ -333,7 +402,8 @@ async function _promiseAjax(
|
||||||
.then(async response => {
|
.then(async response => {
|
||||||
let resultPromise;
|
let resultPromise;
|
||||||
if (
|
if (
|
||||||
options.responseType === 'json' &&
|
(options.responseType === 'json' ||
|
||||||
|
options.responseType === 'jsonwithdetails') &&
|
||||||
response.headers.get('Content-Type') === 'application/json'
|
response.headers.get('Content-Type') === 'application/json'
|
||||||
) {
|
) {
|
||||||
resultPromise = response.json();
|
resultPromise = response.json();
|
||||||
|
@ -358,7 +428,10 @@ async function _promiseAjax(
|
||||||
result.byteOffset + result.byteLength
|
result.byteOffset + result.byteLength
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (options.responseType === 'json') {
|
if (
|
||||||
|
options.responseType === 'json' ||
|
||||||
|
options.responseType === 'jsonwithdetails'
|
||||||
|
) {
|
||||||
if (options.validateResponse) {
|
if (options.validateResponse) {
|
||||||
if (!_validateResponse(result, options.validateResponse)) {
|
if (!_validateResponse(result, options.validateResponse)) {
|
||||||
if (options.redactUrl) {
|
if (options.redactUrl) {
|
||||||
|
@ -395,7 +468,10 @@ async function _promiseAjax(
|
||||||
} else {
|
} else {
|
||||||
window.log.info(options.type, url, response.status, 'Success');
|
window.log.info(options.type, url, response.status, 'Success');
|
||||||
}
|
}
|
||||||
if (options.responseType === 'arraybufferwithdetails') {
|
if (
|
||||||
|
options.responseType === 'arraybufferwithdetails' ||
|
||||||
|
options.responseType === 'jsonwithdetails'
|
||||||
|
) {
|
||||||
resolve({
|
resolve({
|
||||||
data: result,
|
data: result,
|
||||||
contentType: getContentType(response),
|
contentType: getContentType(response),
|
||||||
|
@ -518,11 +594,18 @@ const URL_CALLS = {
|
||||||
getStickerPackUpload: 'v1/sticker/pack/form',
|
getStickerPackUpload: 'v1/sticker/pack/form',
|
||||||
whoami: 'v1/accounts/whoami',
|
whoami: 'v1/accounts/whoami',
|
||||||
config: 'v1/config',
|
config: 'v1/config',
|
||||||
|
directoryAuth: 'v1/directory/auth',
|
||||||
|
// CDS endpoints
|
||||||
|
attestation: 'v1/attestation',
|
||||||
|
discovery: 'v1/discovery',
|
||||||
};
|
};
|
||||||
|
|
||||||
type InitializeOptionsType = {
|
type InitializeOptionsType = {
|
||||||
url: string;
|
url: string;
|
||||||
storageUrl: string;
|
storageUrl: string;
|
||||||
|
directoryEnclaveId: string;
|
||||||
|
directoryTrustAnchor: string;
|
||||||
|
directoryUrl: string;
|
||||||
cdnUrlObject: {
|
cdnUrlObject: {
|
||||||
readonly '0': string;
|
readonly '0': string;
|
||||||
readonly [propName: string]: string;
|
readonly [propName: string]: string;
|
||||||
|
@ -611,6 +694,9 @@ export type WebAPIType = {
|
||||||
getStorageCredentials: MessageSender['getStorageCredentials'];
|
getStorageCredentials: MessageSender['getStorageCredentials'];
|
||||||
getStorageManifest: MessageSender['getStorageManifest'];
|
getStorageManifest: MessageSender['getStorageManifest'];
|
||||||
getStorageRecords: MessageSender['getStorageRecords'];
|
getStorageRecords: MessageSender['getStorageRecords'];
|
||||||
|
getUuidsForE164s: (
|
||||||
|
e164s: ReadonlyArray<string>
|
||||||
|
) => Promise<Dictionary<string | null>>;
|
||||||
makeProxiedRequest: (
|
makeProxiedRequest: (
|
||||||
targetUrl: string,
|
targetUrl: string,
|
||||||
options?: ProxiedRequestOptionsType
|
options?: ProxiedRequestOptionsType
|
||||||
|
@ -691,6 +777,9 @@ export type ProxiedRequestOptionsType = {
|
||||||
export function initialize({
|
export function initialize({
|
||||||
url,
|
url,
|
||||||
storageUrl,
|
storageUrl,
|
||||||
|
directoryEnclaveId,
|
||||||
|
directoryTrustAnchor,
|
||||||
|
directoryUrl,
|
||||||
cdnUrlObject,
|
cdnUrlObject,
|
||||||
certificateAuthority,
|
certificateAuthority,
|
||||||
contentProxyUrl,
|
contentProxyUrl,
|
||||||
|
@ -703,6 +792,15 @@ export function initialize({
|
||||||
if (!is.string(storageUrl)) {
|
if (!is.string(storageUrl)) {
|
||||||
throw new Error('WebAPI.initialize: Invalid 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)) {
|
if (!is.object(cdnUrlObject)) {
|
||||||
throw new Error('WebAPI.initialize: Invalid cdnUrlObject');
|
throw new Error('WebAPI.initialize: Invalid cdnUrlObject');
|
||||||
}
|
}
|
||||||
|
@ -760,6 +858,7 @@ export function initialize({
|
||||||
getStorageCredentials,
|
getStorageCredentials,
|
||||||
getStorageManifest,
|
getStorageManifest,
|
||||||
getStorageRecords,
|
getStorageRecords,
|
||||||
|
getUuidsForE164s,
|
||||||
makeProxiedRequest,
|
makeProxiedRequest,
|
||||||
putAttachment,
|
putAttachment,
|
||||||
registerCapabilities,
|
registerCapabilities,
|
||||||
|
@ -1626,5 +1725,382 @@ export function initialize({
|
||||||
{ certificateAuthority, proxyUrl }
|
{ 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, {
|
export const format = memoizee(_format, {
|
||||||
primitive: true,
|
primitive: true,
|
||||||
// Convert the arguments to a unique string, required for primitive mode.
|
// Convert the arguments to a unique string, required for primitive mode.
|
||||||
|
|
|
@ -203,30 +203,6 @@
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-03-25T15:45:04.024Z"
|
"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(",
|
"rule": "jQuery-append(",
|
||||||
"path": "js/modules/debuglogs.js",
|
"path": "js/modules/debuglogs.js",
|
||||||
|
@ -566,7 +542,7 @@
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " if (e && this.$(e.target).closest('.placeholder').length) {",
|
"line": " if (e && this.$(e.target).closest('.placeholder').length) {",
|
||||||
"lineNumber": 190,
|
"lineNumber": 192,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-05-28T17:42:35.329Z",
|
"updated": "2020-05-28T17:42:35.329Z",
|
||||||
"reasonDetail": "Known DOM elements"
|
"reasonDetail": "Known DOM elements"
|
||||||
|
@ -575,7 +551,7 @@
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " this.$('#header, .gutter').addClass('inactive');",
|
"line": " this.$('#header, .gutter').addClass('inactive');",
|
||||||
"lineNumber": 194,
|
"lineNumber": 196,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-05-28T17:42:35.329Z",
|
"updated": "2020-05-28T17:42:35.329Z",
|
||||||
"reasonDetail": "Hardcoded selector"
|
"reasonDetail": "Hardcoded selector"
|
||||||
|
@ -584,25 +560,25 @@
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " this.$('.conversation-stack').addClass('inactive');",
|
"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,
|
"lineNumber": 200,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-05-28T17:42:35.329Z",
|
"updated": "2020-05-28T17:42:35.329Z",
|
||||||
"reasonDetail": "Hardcoded selector"
|
"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-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " if (e && this.$(e.target).closest('.capture-audio').length > 0) {",
|
"line": " if (e && this.$(e.target).closest('.capture-audio').length > 0) {",
|
||||||
"lineNumber": 220,
|
"lineNumber": 222,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-05-29T18:29:18.234Z",
|
"updated": "2020-05-29T18:29:18.234Z",
|
||||||
"reasonDetail": "Known DOM elements"
|
"reasonDetail": "Known DOM elements"
|
||||||
|
@ -611,7 +587,7 @@
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " this.$('.conversation:first .recorder').trigger('close');",
|
"line": " this.$('.conversation:first .recorder').trigger('close');",
|
||||||
"lineNumber": 223,
|
"lineNumber": 225,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-05-29T18:29:18.234Z",
|
"updated": "2020-05-29T18:29:18.234Z",
|
||||||
"reasonDetail": "Hardcoded selector"
|
"reasonDetail": "Hardcoded selector"
|
||||||
|
@ -12971,5 +12947,21 @@
|
||||||
"lineNumber": 51,
|
"lineNumber": 51,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2020-04-05T23:45:16.746Z"
|
"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
|
// High-traffic files in our project
|
||||||
'^js/models/messages.js',
|
'^js/models/messages.js',
|
||||||
|
'^js/models/conversations.js',
|
||||||
'^js/views/conversation_view.js',
|
'^js/views/conversation_view.js',
|
||||||
'^js/background.js',
|
'^js/background.js',
|
||||||
'^ts/Crypto.js',
|
'^ts/Crypto.js',
|
||||||
|
@ -280,7 +281,7 @@ const excludedFiles = [
|
||||||
'^node_modules/dotenv-webpack/.+',
|
'^node_modules/dotenv-webpack/.+',
|
||||||
'^node_modules/follow-redirects/.+', // Used by webpack-dev-server
|
'^node_modules/follow-redirects/.+', // Used by webpack-dev-server
|
||||||
'^node_modules/html-webpack-plugin/.+',
|
'^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/portfinder/.+',
|
||||||
'^node_modules/renderkid/.+', // Used by html-webpack-plugin
|
'^node_modules/renderkid/.+', // Used by html-webpack-plugin
|
||||||
'^node_modules/spdy-transport/.+', // Used by webpack-dev-server
|
'^node_modules/spdy-transport/.+', // Used by webpack-dev-server
|
||||||
|
|
|
@ -25,6 +25,8 @@ import { ConversationController } from './ConversationController';
|
||||||
import { SendOptionsType } from './textsecure/SendMessage';
|
import { SendOptionsType } from './textsecure/SendMessage';
|
||||||
import Data from './sql/Client';
|
import Data from './sql/Client';
|
||||||
|
|
||||||
|
export { Long } from 'long';
|
||||||
|
|
||||||
type TaskResultType = any;
|
type TaskResultType = any;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@ -101,9 +103,14 @@ declare global {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DCodeIOType = {
|
export type DCodeIOType = {
|
||||||
ByteBuffer: typeof ByteBufferClass;
|
ByteBuffer: typeof ByteBufferClass & {
|
||||||
Long: {
|
BIG_ENDIAN: number;
|
||||||
|
LITTLE_ENDIAN: number;
|
||||||
|
Long: DCodeIOType['Long'];
|
||||||
|
};
|
||||||
|
Long: Long & {
|
||||||
fromBits: (low: number, high: number, unsigned: boolean) => number;
|
fromBits: (low: number, high: number, unsigned: boolean) => number;
|
||||||
|
fromString: (str: string) => Long;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -138,8 +145,13 @@ export class SecretSessionCipherClass {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ByteBufferClass {
|
export class ByteBufferClass {
|
||||||
constructor(value?: any, encoding?: string);
|
constructor(value?: any, littleEndian?: number);
|
||||||
static wrap: (value: any, type?: string) => ByteBufferClass;
|
static wrap: (
|
||||||
|
value: any,
|
||||||
|
encoding?: string,
|
||||||
|
littleEndian?: number
|
||||||
|
) => ByteBufferClass;
|
||||||
|
buffer: ArrayBuffer;
|
||||||
toString: (type: string) => string;
|
toString: (type: string) => string;
|
||||||
toArrayBuffer: () => ArrayBuffer;
|
toArrayBuffer: () => ArrayBuffer;
|
||||||
toBinary: () => string;
|
toBinary: () => string;
|
||||||
|
@ -147,7 +159,11 @@ export class ByteBufferClass {
|
||||||
append: (data: ArrayBuffer) => void;
|
append: (data: ArrayBuffer) => void;
|
||||||
limit: number;
|
limit: number;
|
||||||
offset: 0;
|
offset: 0;
|
||||||
|
readInt: (offset: number) => number;
|
||||||
|
readLong: (offset: number) => Long;
|
||||||
|
readShort: (offset: number) => number;
|
||||||
readVarint32: () => number;
|
readVarint32: () => number;
|
||||||
|
writeLong: (l: Long) => void;
|
||||||
skip: (length: number) => void;
|
skip: (length: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
55
yarn.lock
55
yarn.lock
|
@ -2177,9 +2177,9 @@
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/fs-extra@^8.1.0":
|
"@types/fs-extra@^8.1.0":
|
||||||
version "8.1.0"
|
version "8.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-8.1.0.tgz#1114834b53c3914806cd03b3304b37b3bd221a4d"
|
resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-8.1.1.tgz#1e49f22d09aa46e19b51c0b013cb63d0d923a068"
|
||||||
integrity sha512-UoOfVEzAUpeSPmjm7h1uk5MH6KZma2z2O7a75onTGjnNvAvMVrPzPL/vBbT65iIGHWj6rokwfmYcmxmlSf2uwg==
|
integrity sha512-TcUlBem321DFQzBNuz8p0CLLKp0VvF/XH9E4KHNmgwyp4E3AfgI5cjiIVZWlbfThBop2qxFIh4+LeY6hVWWZ2w==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
@ -2289,6 +2289,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.106.tgz#6093e9a02aa567ddecfe9afadca89e53e5dce4dd"
|
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.106.tgz#6093e9a02aa567ddecfe9afadca89e53e5dce4dd"
|
||||||
integrity sha512-tOSvCVrvSqFZ4A/qrqqm6p37GZoawsZtoR0SJhlF7EonNZUgrn8FfT+RNQ11h+NUpMt6QVe36033f3qEKBwfWA==
|
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":
|
"@types/long@^3.0.32":
|
||||||
version "3.0.32"
|
version "3.0.32"
|
||||||
resolved "https://registry.yarnpkg.com/@types/long/-/long-3.0.32.tgz#f4e5af31e9e9b196d8e5fca8a5e2e20aa3d60b69"
|
resolved "https://registry.yarnpkg.com/@types/long/-/long-3.0.32.tgz#f4e5af31e9e9b196d8e5fca8a5e2e20aa3d60b69"
|
||||||
|
@ -2329,6 +2334,13 @@
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
form-data "^3.0.0"
|
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@*":
|
"@types/node@*":
|
||||||
version "11.12.2"
|
version "11.12.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-11.12.2.tgz#d7f302e74b10e9801d52852137f652d9ee235da8"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-11.12.2.tgz#d7f302e74b10e9801d52852137f652d9ee235da8"
|
||||||
|
@ -2900,6 +2912,14 @@ agent-base@^4.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
es6-promisify "^5.0.0"
|
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":
|
"airbnb-js-shims@^1 || ^2":
|
||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/airbnb-js-shims/-/airbnb-js-shims-2.2.0.tgz#46e1d9d9516f704ef736de76a3b6d484df9a96d8"
|
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:
|
dependencies:
|
||||||
source-map "~0.6.0"
|
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:
|
cli-boxes@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143"
|
resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143"
|
||||||
|
@ -8935,6 +8960,11 @@ indent-string@^2.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
repeating "^2.0.0"
|
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:
|
indexes-of@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
|
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"
|
encoding "^0.1.11"
|
||||||
is-stream "^1.0.1"
|
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:
|
node-forge@0.7.5:
|
||||||
version "0.7.5"
|
version "0.7.5"
|
||||||
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.5.tgz#6c152c345ce11c52f465c2abd957e8639cd674df"
|
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"
|
resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175"
|
||||||
integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==
|
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:
|
p-queue@6.2.1:
|
||||||
version "6.2.1"
|
version "6.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-6.2.1.tgz#809a832046451b2240a0a8e48b4fa18192b22b64"
|
resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-6.2.1.tgz#809a832046451b2240a0a8e48b4fa18192b22b64"
|
||||||
|
|
Loading…
Reference in New Issue