parent
817cf5ed03
commit
a7d78c0e9b
38 changed files with 2996 additions and 789 deletions
|
@ -798,6 +798,11 @@
|
||||||
"message":
|
"message":
|
||||||
"Are you sure? Clicking 'delete' will permanently remove this message from this device only."
|
"Are you sure? Clicking 'delete' will permanently remove this message from this device only."
|
||||||
},
|
},
|
||||||
|
"unidentifiedDelivery": {
|
||||||
|
"message": "Unidentified Delivery",
|
||||||
|
"description":
|
||||||
|
"Label shown on the message detail screen for messages sent or received with Unidentified Delivery enabled"
|
||||||
|
},
|
||||||
"deleteThisMessage": {
|
"deleteThisMessage": {
|
||||||
"message": "Delete this message"
|
"message": "Delete this message"
|
||||||
},
|
},
|
||||||
|
|
|
@ -325,6 +325,7 @@ const SCHEMA_VERSIONS = [
|
||||||
updateToSchemaVersion2,
|
updateToSchemaVersion2,
|
||||||
updateToSchemaVersion3,
|
updateToSchemaVersion3,
|
||||||
updateToSchemaVersion4,
|
updateToSchemaVersion4,
|
||||||
|
// version 5 was dropped
|
||||||
];
|
];
|
||||||
|
|
||||||
async function updateSchema(instance) {
|
async function updateSchema(instance) {
|
||||||
|
|
|
@ -6,5 +6,6 @@
|
||||||
"buildExpiration": 0,
|
"buildExpiration": 0,
|
||||||
"certificateAuthority":
|
"certificateAuthority":
|
||||||
"-----BEGIN CERTIFICATE-----\nMIID7zCCAtegAwIBAgIJAIm6LatK5PNiMA0GCSqGSIb3DQEBBQUAMIGNMQswCQYD\nVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5j\naXNjbzEdMBsGA1UECgwUT3BlbiBXaGlzcGVyIFN5c3RlbXMxHTAbBgNVBAsMFE9w\nZW4gV2hpc3BlciBTeXN0ZW1zMRMwEQYDVQQDDApUZXh0U2VjdXJlMB4XDTEzMDMy\nNTIyMTgzNVoXDTIzMDMyMzIyMTgzNVowgY0xCzAJBgNVBAYTAlVTMRMwEQYDVQQI\nDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMR0wGwYDVQQKDBRP\ncGVuIFdoaXNwZXIgU3lzdGVtczEdMBsGA1UECwwUT3BlbiBXaGlzcGVyIFN5c3Rl\nbXMxEzARBgNVBAMMClRleHRTZWN1cmUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw\nggEKAoIBAQDBSWBpOCBDF0i4q2d4jAXkSXUGpbeWugVPQCjaL6qD9QDOxeW1afvf\nPo863i6Crq1KDxHpB36EwzVcjwLkFTIMeo7t9s1FQolAt3mErV2U0vie6Ves+yj6\ngrSfxwIDAcdsKmI0a1SQCZlr3Q1tcHAkAKFRxYNawADyps5B+Zmqcgf653TXS5/0\nIPPQLocLn8GWLwOYNnYfBvILKDMItmZTtEbucdigxEA9mfIvvHADEbteLtVgwBm9\nR5vVvtwrD6CCxI3pgH7EH7kMP0Od93wLisvn1yhHY7FuYlrkYqdkMvWUrKoASVw4\njb69vaeJCUdU+HCoXOSP1PQcL6WenNCHAgMBAAGjUDBOMB0GA1UdDgQWBBQBixjx\nP/s5GURuhYa+lGUypzI8kDAfBgNVHSMEGDAWgBQBixjxP/s5GURuhYa+lGUypzI8\nkDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQB+Hr4hC56m0LvJAu1R\nK6NuPDbTMEN7/jMojFHxH4P3XPFfupjR+bkDq0pPOU6JjIxnrD1XD/EVmTTaTVY5\niOheyv7UzJOefb2pLOc9qsuvI4fnaESh9bhzln+LXxtCrRPGhkxA1IMIo3J/s2WF\n/KVYZyciu6b4ubJ91XPAuBNZwImug7/srWvbpk0hq6A6z140WTVSKtJG7EP41kJe\n/oF4usY5J7LPkxK3LWzMJnb5EIJDmRvyH8pyRwWg6Qm6qiGFaI4nL8QU4La1x2en\n4DGXRaLMPRwjELNgQPodR38zoCMuA8gHZfZYYoZ7D7Q1wNUiVHcxuFrEeBaYJbLE\nrwLV\n-----END CERTIFICATE-----\n",
|
"-----BEGIN CERTIFICATE-----\nMIID7zCCAtegAwIBAgIJAIm6LatK5PNiMA0GCSqGSIb3DQEBBQUAMIGNMQswCQYD\nVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5j\naXNjbzEdMBsGA1UECgwUT3BlbiBXaGlzcGVyIFN5c3RlbXMxHTAbBgNVBAsMFE9w\nZW4gV2hpc3BlciBTeXN0ZW1zMRMwEQYDVQQDDApUZXh0U2VjdXJlMB4XDTEzMDMy\nNTIyMTgzNVoXDTIzMDMyMzIyMTgzNVowgY0xCzAJBgNVBAYTAlVTMRMwEQYDVQQI\nDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMR0wGwYDVQQKDBRP\ncGVuIFdoaXNwZXIgU3lzdGVtczEdMBsGA1UECwwUT3BlbiBXaGlzcGVyIFN5c3Rl\nbXMxEzARBgNVBAMMClRleHRTZWN1cmUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw\nggEKAoIBAQDBSWBpOCBDF0i4q2d4jAXkSXUGpbeWugVPQCjaL6qD9QDOxeW1afvf\nPo863i6Crq1KDxHpB36EwzVcjwLkFTIMeo7t9s1FQolAt3mErV2U0vie6Ves+yj6\ngrSfxwIDAcdsKmI0a1SQCZlr3Q1tcHAkAKFRxYNawADyps5B+Zmqcgf653TXS5/0\nIPPQLocLn8GWLwOYNnYfBvILKDMItmZTtEbucdigxEA9mfIvvHADEbteLtVgwBm9\nR5vVvtwrD6CCxI3pgH7EH7kMP0Od93wLisvn1yhHY7FuYlrkYqdkMvWUrKoASVw4\njb69vaeJCUdU+HCoXOSP1PQcL6WenNCHAgMBAAGjUDBOMB0GA1UdDgQWBBQBixjx\nP/s5GURuhYa+lGUypzI8kDAfBgNVHSMEGDAWgBQBixjxP/s5GURuhYa+lGUypzI8\nkDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQB+Hr4hC56m0LvJAu1R\nK6NuPDbTMEN7/jMojFHxH4P3XPFfupjR+bkDq0pPOU6JjIxnrD1XD/EVmTTaTVY5\niOheyv7UzJOefb2pLOc9qsuvI4fnaESh9bhzln+LXxtCrRPGhkxA1IMIo3J/s2WF\n/KVYZyciu6b4ubJ91XPAuBNZwImug7/srWvbpk0hq6A6z140WTVSKtJG7EP41kJe\n/oF4usY5J7LPkxK3LWzMJnb5EIJDmRvyH8pyRwWg6Qm6qiGFaI4nL8QU4La1x2en\n4DGXRaLMPRwjELNgQPodR38zoCMuA8gHZfZYYoZ7D7Q1wNUiVHcxuFrEeBaYJbLE\nrwLV\n-----END CERTIFICATE-----\n",
|
||||||
"import": false
|
"import": false,
|
||||||
|
"serverTrustRoot": "BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{
|
{
|
||||||
"serverUrl": "https://textsecure-service.whispersystems.org",
|
"serverUrl": "https://textsecure-service.whispersystems.org",
|
||||||
"cdnUrl": "https://cdn.signal.org"
|
"cdnUrl": "https://cdn.signal.org",
|
||||||
|
"serverTrustRoot": "BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF"
|
||||||
}
|
}
|
||||||
|
|
149
js/background.js
149
js/background.js
|
@ -1,14 +1,15 @@
|
||||||
/* global Backbone: false */
|
/* global
|
||||||
/* global $: false */
|
$,
|
||||||
|
_,
|
||||||
/* global dcodeIO: false */
|
Backbone,
|
||||||
/* global ConversationController: false */
|
ConversationController,
|
||||||
/* global getAccountManager: false */
|
getAccountManager,
|
||||||
/* global Signal: false */
|
Signal,
|
||||||
/* global storage: false */
|
storage,
|
||||||
/* global textsecure: false */
|
textsecure,
|
||||||
/* global Whisper: false */
|
WebAPI
|
||||||
/* global _: false */
|
Whisper,
|
||||||
|
*/
|
||||||
|
|
||||||
// eslint-disable-next-line func-names
|
// eslint-disable-next-line func-names
|
||||||
(async function() {
|
(async function() {
|
||||||
|
@ -553,7 +554,16 @@
|
||||||
window.log.info('listening for registration events');
|
window.log.info('listening for registration events');
|
||||||
Whisper.events.on('registration_done', () => {
|
Whisper.events.on('registration_done', () => {
|
||||||
window.log.info('handling registration event');
|
window.log.info('handling registration event');
|
||||||
|
|
||||||
|
// listeners
|
||||||
Whisper.RotateSignedPreKeyListener.init(Whisper.events, newVersion);
|
Whisper.RotateSignedPreKeyListener.init(Whisper.events, newVersion);
|
||||||
|
window.Signal.RefreshSenderCertificate.initialize({
|
||||||
|
events: Whisper.events,
|
||||||
|
storage,
|
||||||
|
navigator,
|
||||||
|
logger: window.log,
|
||||||
|
});
|
||||||
|
|
||||||
connect(true);
|
connect(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -570,7 +580,15 @@
|
||||||
window.log.info('Import was interrupted, showing import error screen');
|
window.log.info('Import was interrupted, showing import error screen');
|
||||||
appView.openImporter();
|
appView.openImporter();
|
||||||
} else if (Whisper.Registration.everDone()) {
|
} else if (Whisper.Registration.everDone()) {
|
||||||
|
// listeners
|
||||||
Whisper.RotateSignedPreKeyListener.init(Whisper.events, newVersion);
|
Whisper.RotateSignedPreKeyListener.init(Whisper.events, newVersion);
|
||||||
|
window.Signal.RefreshSenderCertificate.initialize({
|
||||||
|
events: Whisper.events,
|
||||||
|
storage,
|
||||||
|
navigator,
|
||||||
|
logger: window.log,
|
||||||
|
});
|
||||||
|
|
||||||
connect();
|
connect();
|
||||||
appView.openInbox({
|
appView.openInbox({
|
||||||
initialLoadComplete,
|
initialLoadComplete,
|
||||||
|
@ -713,6 +731,7 @@
|
||||||
connectCount += 1;
|
connectCount += 1;
|
||||||
const options = {
|
const options = {
|
||||||
retryCached: connectCount === 1,
|
retryCached: connectCount === 1,
|
||||||
|
serverTrustRoot: window.getServerTrustRoot(),
|
||||||
};
|
};
|
||||||
|
|
||||||
Whisper.Notifications.disable(); // avoid notification flood until empty
|
Whisper.Notifications.disable(); // avoid notification flood until empty
|
||||||
|
@ -755,14 +774,33 @@
|
||||||
window.getSyncRequest();
|
window.getSyncRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const udSupportKey = 'hasRegisterSupportForUnauthenticatedDelivery';
|
||||||
|
if (!storage.get(udSupportKey)) {
|
||||||
|
const server = WebAPI.connect({ username: USERNAME, password: PASSWORD });
|
||||||
|
try {
|
||||||
|
await server.registerSupportForUnauthenticatedDelivery();
|
||||||
|
storage.put(udSupportKey, true);
|
||||||
|
} catch (error) {
|
||||||
|
window.log.error(
|
||||||
|
'Error: Unable to register for unauthenticated delivery support.',
|
||||||
|
error && error.stack ? error.stack : error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const deviceId = textsecure.storage.user.getDeviceId();
|
const deviceId = textsecure.storage.user.getDeviceId();
|
||||||
|
const ourNumber = textsecure.storage.user.getNumber();
|
||||||
const { sendRequestConfigurationSyncMessage } = textsecure.messaging;
|
const { sendRequestConfigurationSyncMessage } = textsecure.messaging;
|
||||||
const status = await Signal.Startup.syncReadReceiptConfiguration({
|
const status = await Signal.Startup.syncReadReceiptConfiguration({
|
||||||
|
ourNumber,
|
||||||
deviceId,
|
deviceId,
|
||||||
sendRequestConfigurationSyncMessage,
|
sendRequestConfigurationSyncMessage,
|
||||||
storage,
|
storage,
|
||||||
|
prepareForSend: ConversationController.prepareForSend.bind(
|
||||||
|
ConversationController
|
||||||
|
),
|
||||||
});
|
});
|
||||||
window.log.info('Sync read receipt configuration status:', status);
|
window.log.info('Sync configuration status:', status);
|
||||||
|
|
||||||
if (firstRun === true && deviceId !== '1') {
|
if (firstRun === true && deviceId !== '1') {
|
||||||
const hasThemeSetting = Boolean(storage.get('theme-setting'));
|
const hasThemeSetting = Boolean(storage.get('theme-setting'));
|
||||||
|
@ -786,14 +824,17 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
if (Whisper.Import.isComplete()) {
|
if (Whisper.Import.isComplete()) {
|
||||||
textsecure.messaging
|
const { wrap, sendOptions } = ConversationController.prepareForSend(
|
||||||
.sendRequestConfigurationSyncMessage()
|
textsecure.storage.user.getNumber()
|
||||||
.catch(error => {
|
);
|
||||||
window.log.error(
|
wrap(
|
||||||
'Import complete, but failed to send sync message',
|
textsecure.messaging.sendRequestConfigurationSyncMessage(sendOptions)
|
||||||
error && error.stack ? error.stack : error
|
).catch(error => {
|
||||||
);
|
window.log.error(
|
||||||
});
|
'Import complete, but failed to send sync message',
|
||||||
|
error && error.stack ? error.stack : error
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -838,7 +879,20 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function onConfiguration(ev) {
|
function onConfiguration(ev) {
|
||||||
storage.put('read-receipt-setting', ev.configuration.readReceipts);
|
const { configuration } = ev;
|
||||||
|
|
||||||
|
storage.put('read-receipt-setting', configuration.readReceipts);
|
||||||
|
|
||||||
|
const { unidentifiedDeliveryIndicators } = configuration;
|
||||||
|
if (
|
||||||
|
unidentifiedDeliveryIndicators === true ||
|
||||||
|
unidentifiedDeliveryIndicators === false
|
||||||
|
) {
|
||||||
|
storage.put(
|
||||||
|
'unidentifiedDeliveryIndicators',
|
||||||
|
unidentifiedDeliveryIndicators
|
||||||
|
);
|
||||||
|
}
|
||||||
ev.confirm();
|
ev.confirm();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -881,10 +935,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
if (details.profileKey) {
|
if (details.profileKey) {
|
||||||
const profileKey = dcodeIO.ByteBuffer.wrap(details.profileKey).toString(
|
const profileKey = window.Signal.Crypto.arrayBufferToBase64(
|
||||||
'base64'
|
details.profileKey
|
||||||
);
|
);
|
||||||
conversation.set({ profileKey });
|
conversation.setProfileKey(profileKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof details.blocked !== 'undefined') {
|
if (typeof details.blocked !== 'undefined') {
|
||||||
|
@ -1052,7 +1106,7 @@
|
||||||
return handleProfileUpdate({ data, confirm, messageDescriptor });
|
return handleProfileUpdate({ data, confirm, messageDescriptor });
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = createMessage(data);
|
const message = await createMessage(data);
|
||||||
const isDuplicate = await isMessageDuplicate(message);
|
const isDuplicate = await isMessageDuplicate(message);
|
||||||
if (isDuplicate) {
|
if (isDuplicate) {
|
||||||
window.log.warn('Received duplicate message', message.idForLogging());
|
window.log.warn('Received duplicate message', message.idForLogging());
|
||||||
|
@ -1191,15 +1245,27 @@
|
||||||
|
|
||||||
function createSentMessage(data) {
|
function createSentMessage(data) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
let sentTo = [];
|
||||||
|
|
||||||
|
if (data.unidentifiedStatus && data.unidentifiedStatus.length) {
|
||||||
|
sentTo = data.unidentifiedStatus.map(item => item.destination);
|
||||||
|
const unidentified = _.filter(data.unidentifiedStatus, item =>
|
||||||
|
Boolean(item.unidentified)
|
||||||
|
);
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
data.unidentifiedDeliveries = unidentified.map(item => item.destination);
|
||||||
|
}
|
||||||
|
|
||||||
return new Whisper.Message({
|
return new Whisper.Message({
|
||||||
source: textsecure.storage.user.getNumber(),
|
source: textsecure.storage.user.getNumber(),
|
||||||
sourceDevice: data.device,
|
sourceDevice: data.device,
|
||||||
sent_at: data.timestamp,
|
sent_at: data.timestamp,
|
||||||
|
sent_to: sentTo,
|
||||||
received_at: now,
|
received_at: now,
|
||||||
conversationId: data.destination,
|
conversationId: data.destination,
|
||||||
type: 'outgoing',
|
type: 'outgoing',
|
||||||
sent: true,
|
sent: true,
|
||||||
|
unidentifiedDeliveries: data.unidentifiedDeliveries || [],
|
||||||
expirationStartTimestamp: Math.min(
|
expirationStartTimestamp: Math.min(
|
||||||
data.expirationStartTimestamp || data.timestamp || Date.now(),
|
data.expirationStartTimestamp || data.timestamp || Date.now(),
|
||||||
Date.now()
|
Date.now()
|
||||||
|
@ -1227,17 +1293,46 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function initIncomingMessage(data) {
|
async function initIncomingMessage(data, options = {}) {
|
||||||
|
const { isError } = options;
|
||||||
|
|
||||||
const message = new Whisper.Message({
|
const message = new Whisper.Message({
|
||||||
source: data.source,
|
source: data.source,
|
||||||
sourceDevice: data.sourceDevice,
|
sourceDevice: data.sourceDevice,
|
||||||
sent_at: data.timestamp,
|
sent_at: data.timestamp,
|
||||||
received_at: data.receivedAt || Date.now(),
|
received_at: data.receivedAt || Date.now(),
|
||||||
conversationId: data.source,
|
conversationId: data.source,
|
||||||
|
unidentifiedDeliveryReceived: data.unidentifiedDeliveryReceived,
|
||||||
type: 'incoming',
|
type: 'incoming',
|
||||||
unread: 1,
|
unread: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// If we don't return early here, we can get into infinite error loops. So, no
|
||||||
|
// delivery receipts for sealed sender errors.
|
||||||
|
if (isError || !data.unidentifiedDeliveryReceived) {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { wrap, sendOptions } = ConversationController.prepareForSend(
|
||||||
|
data.source
|
||||||
|
);
|
||||||
|
await wrap(
|
||||||
|
textsecure.messaging.sendDeliveryReceipt(
|
||||||
|
data.source,
|
||||||
|
data.timestamp,
|
||||||
|
sendOptions
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
window.log.error(
|
||||||
|
`Failed to send delivery receipt to ${data.source} for message ${
|
||||||
|
data.timestamp
|
||||||
|
}:`,
|
||||||
|
error && error.stack ? error.stack : error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1322,7 +1417,7 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const envelope = ev.proto;
|
const envelope = ev.proto;
|
||||||
const message = initIncomingMessage(envelope);
|
const message = await initIncomingMessage(envelope, { isError: true });
|
||||||
|
|
||||||
await message.saveErrors(error || new Error('Error was null'));
|
await message.saveErrors(error || new Error('Error was null'));
|
||||||
const id = message.get('conversationId');
|
const id = message.get('conversationId');
|
||||||
|
|
|
@ -181,6 +181,16 @@
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
prepareForSend(id) {
|
||||||
|
// id is either a group id or an individual user's id
|
||||||
|
const conversation = this.get(id);
|
||||||
|
const sendOptions = conversation && conversation.getSendOptions();
|
||||||
|
const wrap = conversation
|
||||||
|
? conversation.wrapSend.bind(conversation)
|
||||||
|
: promise => promise;
|
||||||
|
|
||||||
|
return { wrap, sendOptions };
|
||||||
|
},
|
||||||
async getAllGroupsInvolvingId(id) {
|
async getAllGroupsInvolvingId(id) {
|
||||||
const groups = await window.Signal.Data.getAllGroupsInvolvingId(id, {
|
const groups = await window.Signal.Data.getAllGroupsInvolvingId(id, {
|
||||||
ConversationCollection: Whisper.ConversationCollection,
|
ConversationCollection: Whisper.ConversationCollection,
|
||||||
|
|
|
@ -22848,7 +22848,7 @@ function _memset(ptr, value, num) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
while ((ptr|0) < (stop4|0)) {
|
while ((ptr|0) < (stop4|0)) {
|
||||||
HEAP32[ptr>>2]=value4;
|
HEAP32[((ptr)>>2)]=value4;
|
||||||
ptr = (ptr+4)|0;
|
ptr = (ptr+4)|0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22904,7 +22904,7 @@ function _memcpy(dest, src, num) {
|
||||||
num = (num-1)|0;
|
num = (num-1)|0;
|
||||||
}
|
}
|
||||||
while ((num|0) >= 4) {
|
while ((num|0) >= 4) {
|
||||||
HEAP32[dest>>2]=((HEAP32[src>>2])|0);
|
HEAP32[((dest)>>2)]=((HEAP32[((src)>>2)])|0);
|
||||||
dest = (dest+4)|0;
|
dest = (dest+4)|0;
|
||||||
src = (src+4)|0;
|
src = (src+4)|0;
|
||||||
num = (num-4)|0;
|
num = (num-4)|0;
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
/* global _: false */
|
/* global _: false */
|
||||||
/* global Backbone: false */
|
/* global Backbone: false */
|
||||||
/* global dcodeIO: false */
|
|
||||||
/* global libphonenumber: false */
|
/* global libphonenumber: false */
|
||||||
|
|
||||||
/* global ConversationController: false */
|
/* global ConversationController: false */
|
||||||
|
@ -33,8 +32,6 @@
|
||||||
deleteAttachmentData,
|
deleteAttachmentData,
|
||||||
} = window.Signal.Migrations;
|
} = window.Signal.Migrations;
|
||||||
|
|
||||||
// TODO: Factor out private and group subclasses of Conversation
|
|
||||||
|
|
||||||
const COLORS = [
|
const COLORS = [
|
||||||
'red',
|
'red',
|
||||||
'deep_orange',
|
'deep_orange',
|
||||||
|
@ -324,9 +321,20 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
sendVerifySyncMessage(number, state) {
|
sendVerifySyncMessage(number, state) {
|
||||||
|
// Because syncVerification sends a (null) message to the target of the verify and
|
||||||
|
// a sync message to our own devices, we need to send the accessKeys down for both
|
||||||
|
// contacts. So we merge their sendOptions.
|
||||||
|
const { sendOptions } = ConversationController.prepareForSend(
|
||||||
|
this.ourNumber
|
||||||
|
);
|
||||||
|
const recipientSendOptions = this.getSendOptions();
|
||||||
|
const options = Object.assign({}, sendOptions, recipientSendOptions);
|
||||||
|
|
||||||
const promise = textsecure.storage.protocol.loadIdentityKey(number);
|
const promise = textsecure.storage.protocol.loadIdentityKey(number);
|
||||||
return promise.then(key =>
|
return promise.then(key =>
|
||||||
textsecure.messaging.syncVerification(number, state, key)
|
this.wrapSend(
|
||||||
|
textsecure.messaging.syncVerification(number, state, key, options)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
isVerified() {
|
isVerified() {
|
||||||
|
@ -754,20 +762,118 @@
|
||||||
messageWithSchema.attachments.map(loadAttachmentData)
|
messageWithSchema.attachments.map(loadAttachmentData)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const options = this.getSendOptions();
|
||||||
return message.send(
|
return message.send(
|
||||||
sendFunction(
|
this.wrapSend(
|
||||||
destination,
|
sendFunction(
|
||||||
body,
|
destination,
|
||||||
attachmentsWithData,
|
body,
|
||||||
quote,
|
attachmentsWithData,
|
||||||
now,
|
quote,
|
||||||
expireTimer,
|
now,
|
||||||
profileKey
|
expireTimer,
|
||||||
|
profileKey,
|
||||||
|
options
|
||||||
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
wrapSend(promise) {
|
||||||
|
return promise.then(
|
||||||
|
async result => {
|
||||||
|
// success
|
||||||
|
if (
|
||||||
|
result &&
|
||||||
|
result.failoverNumbers &&
|
||||||
|
result.failoverNumbers.length
|
||||||
|
) {
|
||||||
|
await this.handleFailover(result.failoverNumbers);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
async result => {
|
||||||
|
// failure
|
||||||
|
if (
|
||||||
|
result &&
|
||||||
|
result.failoverNumbers &&
|
||||||
|
result.failoverNumbers.length
|
||||||
|
) {
|
||||||
|
await this.handleFailover(result.failoverNumbers);
|
||||||
|
}
|
||||||
|
throw result;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleFailover(numberArray) {
|
||||||
|
return Promise.all(
|
||||||
|
(numberArray || []).map(async number => {
|
||||||
|
const conversation = ConversationController.get(number);
|
||||||
|
if (conversation && conversation.get('unidentifiedDelivery')) {
|
||||||
|
window.log.info(
|
||||||
|
`Marking unidentifiedDelivery false for conversation ${conversation.idForLogging()}`
|
||||||
|
);
|
||||||
|
conversation.set({ unidentifiedDelivery: false });
|
||||||
|
await window.Signal.Data.updateConversation(
|
||||||
|
conversation.id,
|
||||||
|
conversation.attributes,
|
||||||
|
{ Conversation: Whisper.Conversation }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
getSendOptions() {
|
||||||
|
const senderCertificate = storage.get('senderCertificate');
|
||||||
|
const numberInfo = this.getNumberInfo();
|
||||||
|
|
||||||
|
return {
|
||||||
|
senderCertificate,
|
||||||
|
numberInfo,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getNumberInfo() {
|
||||||
|
const UD = 'unidentifiedDelivery';
|
||||||
|
const UNRESTRICTED_UD = 'unidentifiedDeliveryUnrestricted';
|
||||||
|
|
||||||
|
// We don't want to enable unidentified delivery for send unless it is
|
||||||
|
// also enabled for our own account.
|
||||||
|
const me = ConversationController.getOrCreate(this.ourNumber, 'private');
|
||||||
|
if (!me.get(UD) && !me.get(UNRESTRICTED_UD)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isPrivate()) {
|
||||||
|
const accessKey = this.get('accessKey');
|
||||||
|
const unidentifiedDelivery = this.get(UD);
|
||||||
|
const unrestricted = this.get(UNRESTRICTED_UD);
|
||||||
|
|
||||||
|
if (!unidentifiedDelivery && !unrestricted) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
[this.id]: {
|
||||||
|
accessKey:
|
||||||
|
accessKey && !unrestricted
|
||||||
|
? accessKey
|
||||||
|
: window.Signal.Crypto.arrayBufferToBase64(
|
||||||
|
window.Signal.Crypto.getRandomBytes(16)
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const infoArray = this.contactCollection.map(conversation =>
|
||||||
|
conversation.getNumberInfo()
|
||||||
|
);
|
||||||
|
return Object.assign({}, ...infoArray);
|
||||||
|
},
|
||||||
|
|
||||||
async updateLastMessage() {
|
async updateLastMessage() {
|
||||||
if (!this.id) {
|
if (!this.id) {
|
||||||
return;
|
return;
|
||||||
|
@ -901,14 +1007,17 @@
|
||||||
if (this.get('profileSharing')) {
|
if (this.get('profileSharing')) {
|
||||||
profileKey = storage.get('profileKey');
|
profileKey = storage.get('profileKey');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sendOptions = this.getSendOptions();
|
||||||
const promise = sendFunc(
|
const promise = sendFunc(
|
||||||
this.get('id'),
|
this.get('id'),
|
||||||
this.get('expireTimer'),
|
this.get('expireTimer'),
|
||||||
message.get('sent_at'),
|
message.get('sent_at'),
|
||||||
profileKey
|
profileKey,
|
||||||
|
sendOptions
|
||||||
);
|
);
|
||||||
|
|
||||||
await message.send(promise);
|
await message.send(this.wrapSend(promise));
|
||||||
|
|
||||||
return message;
|
return message;
|
||||||
},
|
},
|
||||||
|
@ -935,7 +1044,12 @@
|
||||||
});
|
});
|
||||||
message.set({ id });
|
message.set({ id });
|
||||||
|
|
||||||
message.send(textsecure.messaging.resetSession(this.id, now));
|
const options = this.getSendOptions();
|
||||||
|
message.send(
|
||||||
|
this.wrapSend(
|
||||||
|
textsecure.messaging.resetSession(this.id, now, options)
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -962,12 +1076,16 @@
|
||||||
});
|
});
|
||||||
message.set({ id });
|
message.set({ id });
|
||||||
|
|
||||||
|
const options = this.getSendOptions();
|
||||||
message.send(
|
message.send(
|
||||||
textsecure.messaging.updateGroup(
|
this.wrapSend(
|
||||||
this.id,
|
textsecure.messaging.updateGroup(
|
||||||
this.get('name'),
|
this.id,
|
||||||
this.get('avatar'),
|
this.get('name'),
|
||||||
this.get('members')
|
this.get('avatar'),
|
||||||
|
this.get('members'),
|
||||||
|
options
|
||||||
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -993,7 +1111,10 @@
|
||||||
});
|
});
|
||||||
message.set({ id });
|
message.set({ id });
|
||||||
|
|
||||||
message.send(textsecure.messaging.leaveGroup(this.id));
|
const options = this.getSendOptions();
|
||||||
|
message.send(
|
||||||
|
this.wrapSend(textsecure.messaging.leaveGroup(this.id, options))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1054,14 +1175,32 @@
|
||||||
read = read.filter(item => !item.hasErrors);
|
read = read.filter(item => !item.hasErrors);
|
||||||
|
|
||||||
if (read.length && options.sendReadReceipts) {
|
if (read.length && options.sendReadReceipts) {
|
||||||
window.log.info('Sending', read.length, 'read receipts');
|
window.log.info(`Sending ${read.length} read receipts`);
|
||||||
await textsecure.messaging.syncReadMessages(read);
|
// Because syncReadMessages sends to our other devices, and sendReadReceipts goes
|
||||||
|
// to a contact, we need accessKeys for both.
|
||||||
|
const prep = ConversationController.prepareForSend(this.ourNumber);
|
||||||
|
const recipientSendOptions = this.getSendOptions();
|
||||||
|
const sendOptions = Object.assign(
|
||||||
|
{},
|
||||||
|
prep.sendOptions,
|
||||||
|
recipientSendOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.wrapSend(
|
||||||
|
textsecure.messaging.syncReadMessages(read, sendOptions)
|
||||||
|
);
|
||||||
|
|
||||||
if (storage.get('read-receipt-setting')) {
|
if (storage.get('read-receipt-setting')) {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
_.map(_.groupBy(read, 'sender'), async (receipts, sender) => {
|
_.map(_.groupBy(read, 'sender'), async (receipts, sender) => {
|
||||||
const timestamps = _.map(receipts, 'timestamp');
|
const timestamps = _.map(receipts, 'timestamp');
|
||||||
await textsecure.messaging.sendReadReceipts(sender, timestamps);
|
await this.wrapSend(
|
||||||
|
textsecure.messaging.sendReadReceipts(
|
||||||
|
sender,
|
||||||
|
timestamps,
|
||||||
|
sendOptions
|
||||||
|
)
|
||||||
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1092,13 +1231,56 @@
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const c = await ConversationController.getOrCreateAndWait(id, 'private');
|
||||||
const profile = await textsecure.messaging.getProfile(id);
|
|
||||||
const identityKey = dcodeIO.ByteBuffer.wrap(
|
|
||||||
profile.identityKey,
|
|
||||||
'base64'
|
|
||||||
).toArrayBuffer();
|
|
||||||
|
|
||||||
|
// Because we're no longer using Backbone-integrated saves, we need to manually
|
||||||
|
// clear the changed fields here so our hasChanged() check is useful.
|
||||||
|
c.changed = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (c.get('profileKey') && !c.get('accessKey')) {
|
||||||
|
const profileKeyBuffer = window.Signal.Crypto.base64ToArrayBuffer(
|
||||||
|
c.get('profileKey')
|
||||||
|
);
|
||||||
|
const buffer = await window.Signal.Crypto.deriveAccessKey(
|
||||||
|
profileKeyBuffer
|
||||||
|
);
|
||||||
|
c.set({
|
||||||
|
accessKey: window.Signal.Crypto.arrayBufferToBase64(buffer),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstProfileFetch = !c.get('hasFetchedProfile');
|
||||||
|
const accessKey = c.get('accessKey');
|
||||||
|
|
||||||
|
let profile;
|
||||||
|
if (c.get('unidentifiedDelivery') || firstProfileFetch) {
|
||||||
|
try {
|
||||||
|
profile = await textsecure.messaging.getProfile(id, {
|
||||||
|
accessKey:
|
||||||
|
accessKey ||
|
||||||
|
window.Signal.Crypto.arrayBufferToBase64(
|
||||||
|
window.window.Signal.Crypto.getRandomBytes(16)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 401 || error.code === 403) {
|
||||||
|
c.set({
|
||||||
|
unidentifiedDelivery: false,
|
||||||
|
unidentifiedDeliveryUnrestricted: false,
|
||||||
|
});
|
||||||
|
profile = await textsecure.messaging.getProfile(id);
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
profile = await textsecure.messaging.getProfile(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const identityKey = window.Signal.Crypto.base64ToArrayBuffer(
|
||||||
|
profile.identityKey
|
||||||
|
);
|
||||||
const changed = await textsecure.storage.protocol.saveIdentity(
|
const changed = await textsecure.storage.protocol.saveIdentity(
|
||||||
`${id}.1`,
|
`${id}.1`,
|
||||||
identityKey,
|
identityKey,
|
||||||
|
@ -1116,49 +1298,68 @@
|
||||||
await sessionCipher.closeOpenSessionForDevice();
|
await sessionCipher.closeOpenSessionForDevice();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
c.set({
|
||||||
const c = ConversationController.get(id);
|
hasFetchedProfile: true,
|
||||||
|
});
|
||||||
|
|
||||||
// Because we're no longer using Backbone-integrated saves, we need to manually
|
if (
|
||||||
// clear the changed fields here so our hasChanged() check is useful.
|
profile.unrestrictedUnidentifiedAccess &&
|
||||||
c.changed = {};
|
profile.unidentifiedAccess
|
||||||
await c.setProfileName(profile.name);
|
) {
|
||||||
await c.setProfileAvatar(profile.avatar);
|
c.set({
|
||||||
|
unidentifiedDelivery: true,
|
||||||
|
unidentifiedDeliveryUnrestricted: true,
|
||||||
|
});
|
||||||
|
} else if (accessKey && profile.unidentifiedAccess) {
|
||||||
|
const haveCorrectKey = await window.Signal.Crypto.verifyAccessKey(
|
||||||
|
window.Signal.Crypto.base64ToArrayBuffer(accessKey),
|
||||||
|
window.Signal.Crypto.base64ToArrayBuffer(profile.unidentifiedAccess)
|
||||||
|
);
|
||||||
|
|
||||||
if (c.hasChanged()) {
|
window.log.info(
|
||||||
await window.Signal.Data.updateConversation(id, c.attributes, {
|
`Setting unidentifiedDelivery to ${haveCorrectKey} for conversation ${c.idForLogging()}`
|
||||||
Conversation: Whisper.Conversation,
|
);
|
||||||
});
|
c.set({
|
||||||
}
|
unidentifiedDelivery: haveCorrectKey,
|
||||||
} catch (e) {
|
unidentifiedDeliveryUnrestricted: false,
|
||||||
if (e.name === 'ProfileDecryptError') {
|
});
|
||||||
// probably the profile key has changed.
|
} else {
|
||||||
window.log.error(
|
c.set({
|
||||||
'decryptProfile error:',
|
unidentifiedDelivery: false,
|
||||||
id,
|
unidentifiedDeliveryUnrestricted: false,
|
||||||
e && e.stack ? e.stack : e
|
});
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await c.setProfileName(profile.name);
|
||||||
|
|
||||||
|
// This might throw if we can't pull the avatar down, so we do it last
|
||||||
|
await c.setProfileAvatar(profile.avatar);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.log.error(
|
window.log.error(
|
||||||
'getProfile error:',
|
'getProfile error:',
|
||||||
|
id,
|
||||||
error && error.stack ? error.stack : error
|
error && error.stack ? error.stack : error
|
||||||
);
|
);
|
||||||
|
} finally {
|
||||||
|
if (c.hasChanged()) {
|
||||||
|
await window.Signal.Data.updateConversation(id, c.attributes, {
|
||||||
|
Conversation: Whisper.Conversation,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async setProfileName(encryptedName) {
|
async setProfileName(encryptedName) {
|
||||||
|
if (!encryptedName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const key = this.get('profileKey');
|
const key = this.get('profileKey');
|
||||||
if (!key) {
|
if (!key) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// decode
|
// decode
|
||||||
const keyBuffer = dcodeIO.ByteBuffer.wrap(key, 'base64').toArrayBuffer();
|
const keyBuffer = window.Signal.Crypto.base64ToArrayBuffer(key);
|
||||||
const data = dcodeIO.ByteBuffer.wrap(
|
const data = window.Signal.Crypto.base64ToArrayBuffer(encryptedName);
|
||||||
encryptedName,
|
|
||||||
'base64'
|
|
||||||
).toArrayBuffer();
|
|
||||||
|
|
||||||
// decrypt
|
// decrypt
|
||||||
const decrypted = await textsecure.crypto.decryptProfileName(
|
const decrypted = await textsecure.crypto.decryptProfileName(
|
||||||
|
@ -1167,10 +1368,10 @@
|
||||||
);
|
);
|
||||||
|
|
||||||
// encode
|
// encode
|
||||||
const name = dcodeIO.ByteBuffer.wrap(decrypted).toString('utf8');
|
const profileName = window.Signal.Crypto.stringFromBytes(decrypted);
|
||||||
|
|
||||||
// set
|
// set
|
||||||
this.set({ profileName: name });
|
this.set({ profileName });
|
||||||
},
|
},
|
||||||
async setProfileAvatar(avatarPath) {
|
async setProfileAvatar(avatarPath) {
|
||||||
if (!avatarPath) {
|
if (!avatarPath) {
|
||||||
|
@ -1182,7 +1383,7 @@
|
||||||
if (!key) {
|
if (!key) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const keyBuffer = dcodeIO.ByteBuffer.wrap(key, 'base64').toArrayBuffer();
|
const keyBuffer = window.Signal.Crypto.base64ToArrayBuffer(key);
|
||||||
|
|
||||||
// decrypt
|
// decrypt
|
||||||
const decrypted = await textsecure.crypto.decryptProfile(
|
const decrypted = await textsecure.crypto.decryptProfile(
|
||||||
|
@ -1204,9 +1405,20 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async setProfileKey(profileKey) {
|
async setProfileKey(profileKey) {
|
||||||
// profileKey is now being saved as a string
|
// profileKey is a string so we can compare it directly
|
||||||
if (this.get('profileKey') !== profileKey) {
|
if (this.get('profileKey') !== profileKey) {
|
||||||
this.set({ profileKey });
|
const profileKeyBuffer = window.Signal.Crypto.base64ToArrayBuffer(
|
||||||
|
profileKey
|
||||||
|
);
|
||||||
|
const accessKeyBuffer = await window.Signal.Crypto.deriveAccessKey(
|
||||||
|
profileKeyBuffer
|
||||||
|
);
|
||||||
|
const accessKey = window.Signal.Crypto.arrayBufferToBase64(
|
||||||
|
accessKeyBuffer
|
||||||
|
);
|
||||||
|
|
||||||
|
this.set({ profileKey, accessKey });
|
||||||
|
|
||||||
await window.Signal.Data.updateConversation(this.id, this.attributes, {
|
await window.Signal.Data.updateConversation(this.id, this.attributes, {
|
||||||
Conversation: Whisper.Conversation,
|
Conversation: Whisper.Conversation,
|
||||||
});
|
});
|
||||||
|
|
|
@ -599,10 +599,25 @@
|
||||||
: null,
|
: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
isUnidentifiedDelivery(contactId, lookup) {
|
||||||
|
if (this.isIncoming()) {
|
||||||
|
return this.get('unidentifiedDeliveryReceived');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Boolean(lookup[contactId]);
|
||||||
|
},
|
||||||
getPropsForMessageDetail() {
|
getPropsForMessageDetail() {
|
||||||
const newIdentity = i18n('newIdentity');
|
const newIdentity = i18n('newIdentity');
|
||||||
const OUTGOING_KEY_ERROR = 'OutgoingIdentityKeyError';
|
const OUTGOING_KEY_ERROR = 'OutgoingIdentityKeyError';
|
||||||
|
|
||||||
|
const unidentifiedLookup = (
|
||||||
|
this.get('unidentifiedDeliveries') || []
|
||||||
|
).reduce((accumulator, item) => {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
accumulator[item] = true;
|
||||||
|
return accumulator;
|
||||||
|
}, Object.create(null));
|
||||||
|
|
||||||
// Older messages don't have the recipients included on the message, so we fall
|
// Older messages don't have the recipients included on the message, so we fall
|
||||||
// back to the conversation's current recipients
|
// back to the conversation's current recipients
|
||||||
const phoneNumbers = this.isIncoming()
|
const phoneNumbers = this.isIncoming()
|
||||||
|
@ -628,12 +643,16 @@
|
||||||
const isOutgoingKeyError = Boolean(
|
const isOutgoingKeyError = Boolean(
|
||||||
_.find(errorsForContact, error => error.name === OUTGOING_KEY_ERROR)
|
_.find(errorsForContact, error => error.name === OUTGOING_KEY_ERROR)
|
||||||
);
|
);
|
||||||
|
const isUnidentifiedDelivery =
|
||||||
|
storage.get('unidentifiedDeliveryIndicators') &&
|
||||||
|
this.isUnidentifiedDelivery(id, unidentifiedLookup);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...this.findAndFormatContact(id),
|
...this.findAndFormatContact(id),
|
||||||
status: this.getStatus(id),
|
status: this.getStatus(id),
|
||||||
errors: errorsForContact,
|
errors: errorsForContact,
|
||||||
isOutgoingKeyError,
|
isOutgoingKeyError,
|
||||||
|
isUnidentifiedDelivery,
|
||||||
onSendAnyway: () =>
|
onSendAnyway: () =>
|
||||||
this.trigger('force-send', {
|
this.trigger('force-send', {
|
||||||
contact: this.findContact(id),
|
contact: this.findContact(id),
|
||||||
|
@ -696,11 +715,12 @@
|
||||||
const quoteWithData = await loadQuoteData(this.get('quote'));
|
const quoteWithData = await loadQuoteData(this.get('quote'));
|
||||||
|
|
||||||
const conversation = this.getConversation();
|
const conversation = this.getConversation();
|
||||||
|
const options = conversation.getSendOptions();
|
||||||
|
|
||||||
let promise;
|
let promise;
|
||||||
|
|
||||||
if (conversation.isPrivate()) {
|
if (conversation.isPrivate()) {
|
||||||
const [number] = numbers;
|
const [number] = numbers;
|
||||||
|
|
||||||
promise = textsecure.messaging.sendMessageToNumber(
|
promise = textsecure.messaging.sendMessageToNumber(
|
||||||
number,
|
number,
|
||||||
this.get('body'),
|
this.get('body'),
|
||||||
|
@ -708,28 +728,33 @@
|
||||||
quoteWithData,
|
quoteWithData,
|
||||||
this.get('sent_at'),
|
this.get('sent_at'),
|
||||||
this.get('expireTimer'),
|
this.get('expireTimer'),
|
||||||
profileKey
|
profileKey,
|
||||||
|
options
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Because this is a partial group send, we manually construct the request like
|
// Because this is a partial group send, we manually construct the request like
|
||||||
// sendMessageToGroup does.
|
// sendMessageToGroup does.
|
||||||
promise = textsecure.messaging.sendMessage({
|
|
||||||
recipients: numbers,
|
promise = textsecure.messaging.sendMessage(
|
||||||
body: this.get('body'),
|
{
|
||||||
timestamp: this.get('sent_at'),
|
recipients: numbers,
|
||||||
attachments: attachmentsWithData,
|
body: this.get('body'),
|
||||||
quote: quoteWithData,
|
timestamp: this.get('sent_at'),
|
||||||
needsSync: !this.get('synced'),
|
attachments: attachmentsWithData,
|
||||||
expireTimer: this.get('expireTimer'),
|
quote: quoteWithData,
|
||||||
profileKey,
|
needsSync: !this.get('synced'),
|
||||||
group: {
|
expireTimer: this.get('expireTimer'),
|
||||||
id: this.get('conversationId'),
|
profileKey,
|
||||||
type: textsecure.protobuf.GroupContext.Type.DELIVER,
|
group: {
|
||||||
|
id: this.get('conversationId'),
|
||||||
|
type: textsecure.protobuf.GroupContext.Type.DELIVER,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
options
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.send(promise);
|
return this.send(conversation.wrapSend(promise));
|
||||||
},
|
},
|
||||||
isReplayableError(e) {
|
isReplayableError(e) {
|
||||||
return (
|
return (
|
||||||
|
@ -752,6 +777,9 @@
|
||||||
);
|
);
|
||||||
const quoteWithData = await loadQuoteData(this.get('quote'));
|
const quoteWithData = await loadQuoteData(this.get('quote'));
|
||||||
|
|
||||||
|
const { wrap, sendOptions } = ConversationController.prepareForSend(
|
||||||
|
number
|
||||||
|
);
|
||||||
const promise = textsecure.messaging.sendMessageToNumber(
|
const promise = textsecure.messaging.sendMessageToNumber(
|
||||||
number,
|
number,
|
||||||
this.get('body'),
|
this.get('body'),
|
||||||
|
@ -759,10 +787,11 @@
|
||||||
quoteWithData,
|
quoteWithData,
|
||||||
this.get('sent_at'),
|
this.get('sent_at'),
|
||||||
this.get('expireTimer'),
|
this.get('expireTimer'),
|
||||||
profileKey
|
profileKey,
|
||||||
|
sendOptions
|
||||||
);
|
);
|
||||||
|
|
||||||
this.send(promise);
|
this.send(wrap(promise));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
removeOutgoingErrors(number) {
|
removeOutgoingErrors(number) {
|
||||||
|
@ -860,11 +889,13 @@
|
||||||
sent_to: _.union(sentTo, result.successfulNumbers),
|
sent_to: _.union(sentTo, result.successfulNumbers),
|
||||||
sent: true,
|
sent: true,
|
||||||
expirationStartTimestamp: Date.now(),
|
expirationStartTimestamp: Date.now(),
|
||||||
|
unidentifiedDeliveries: result.unidentifiedDeliveries,
|
||||||
});
|
});
|
||||||
|
|
||||||
await window.Signal.Data.saveMessage(this.attributes, {
|
await window.Signal.Data.saveMessage(this.attributes, {
|
||||||
Message: Whisper.Message,
|
Message: Whisper.Message,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.trigger('sent', this);
|
this.trigger('sent', this);
|
||||||
this.sendSyncMessage();
|
this.sendSyncMessage();
|
||||||
})
|
})
|
||||||
|
@ -909,6 +940,7 @@
|
||||||
sent_to: _.union(sentTo, result.successfulNumbers),
|
sent_to: _.union(sentTo, result.successfulNumbers),
|
||||||
sent: true,
|
sent: true,
|
||||||
expirationStartTimestamp,
|
expirationStartTimestamp,
|
||||||
|
unidentifiedDeliveries: result.unidentifiedDeliveries,
|
||||||
});
|
});
|
||||||
promises.push(this.sendSyncMessage());
|
promises.push(this.sendSyncMessage());
|
||||||
} else {
|
} else {
|
||||||
|
@ -950,28 +982,36 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
sendSyncMessage() {
|
sendSyncMessage() {
|
||||||
|
const ourNumber = textsecure.storage.user.getNumber();
|
||||||
|
const { wrap, sendOptions } = ConversationController.prepareForSend(
|
||||||
|
ourNumber
|
||||||
|
);
|
||||||
|
|
||||||
this.syncPromise = this.syncPromise || Promise.resolve();
|
this.syncPromise = this.syncPromise || Promise.resolve();
|
||||||
this.syncPromise = this.syncPromise.then(() => {
|
this.syncPromise = this.syncPromise.then(() => {
|
||||||
const dataMessage = this.get('dataMessage');
|
const dataMessage = this.get('dataMessage');
|
||||||
if (this.get('synced') || !dataMessage) {
|
if (this.get('synced') || !dataMessage) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
return textsecure.messaging
|
return wrap(
|
||||||
.sendSyncMessage(
|
textsecure.messaging.sendSyncMessage(
|
||||||
dataMessage,
|
dataMessage,
|
||||||
this.get('sent_at'),
|
this.get('sent_at'),
|
||||||
this.get('destination'),
|
this.get('destination'),
|
||||||
this.get('expirationStartTimestamp')
|
this.get('expirationStartTimestamp'),
|
||||||
|
this.get('sent_to'),
|
||||||
|
this.get('unidentifiedDeliveries'),
|
||||||
|
sendOptions
|
||||||
)
|
)
|
||||||
.then(() => {
|
).then(() => {
|
||||||
this.set({
|
this.set({
|
||||||
synced: true,
|
synced: true,
|
||||||
dataMessage: null,
|
dataMessage: null,
|
||||||
});
|
|
||||||
return window.Signal.Data.saveMessage(this.attributes, {
|
|
||||||
Message: Whisper.Message,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
return window.Signal.Data.saveMessage(this.attributes, {
|
||||||
|
Message: Whisper.Message,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1238,7 +1278,7 @@
|
||||||
if (source === textsecure.storage.user.getNumber()) {
|
if (source === textsecure.storage.user.getNumber()) {
|
||||||
conversation.set({ profileSharing: true });
|
conversation.set({ profileSharing: true });
|
||||||
} else if (conversation.isPrivate()) {
|
} else if (conversation.isPrivate()) {
|
||||||
conversation.set({ profileKey });
|
conversation.setProfileKey(profileKey);
|
||||||
} else {
|
} else {
|
||||||
ConversationController.getOrCreateAndWait(source, 'private').then(
|
ConversationController.getOrCreateAndWait(source, 'private').then(
|
||||||
sender => {
|
sender => {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
/* global Signal: false */
|
/* global Signal: false */
|
||||||
/* global Whisper: false */
|
/* global Whisper: false */
|
||||||
/* global dcodeIO: false */
|
|
||||||
/* global _: false */
|
/* global _: false */
|
||||||
/* global textsecure: false */
|
/* global textsecure: false */
|
||||||
/* global i18n: false */
|
/* global i18n: false */
|
||||||
|
@ -48,7 +47,7 @@ function stringify(object) {
|
||||||
object[key] = {
|
object[key] = {
|
||||||
type: 'ArrayBuffer',
|
type: 'ArrayBuffer',
|
||||||
encoding: 'base64',
|
encoding: 'base64',
|
||||||
data: dcodeIO.ByteBuffer.wrap(val).toString('base64'),
|
data: crypto.arrayBufferToBase64(val),
|
||||||
};
|
};
|
||||||
} else if (val instanceof Object) {
|
} else if (val instanceof Object) {
|
||||||
object[key] = stringify(val);
|
object[key] = stringify(val);
|
||||||
|
@ -70,7 +69,7 @@ function unstringify(object) {
|
||||||
val.encoding === 'base64' &&
|
val.encoding === 'base64' &&
|
||||||
typeof val.data === 'string'
|
typeof val.data === 'string'
|
||||||
) {
|
) {
|
||||||
object[key] = dcodeIO.ByteBuffer.wrap(val.data, 'base64').toArrayBuffer();
|
object[key] = crypto.base64ToArrayBuffer(val.data);
|
||||||
} else if (val instanceof Object) {
|
} else if (val instanceof Object) {
|
||||||
object[key] = unstringify(object[key]);
|
object[key] = unstringify(object[key]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,39 +1,82 @@
|
||||||
/* eslint-env browser */
|
/* eslint-env browser */
|
||||||
|
/* global dcodeIO */
|
||||||
|
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase, no-bitwise */
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
encryptSymmetric,
|
arrayBufferToBase64,
|
||||||
decryptSymmetric,
|
base64ToArrayBuffer,
|
||||||
|
bytesFromString,
|
||||||
|
concatenateBytes,
|
||||||
constantTimeEqual,
|
constantTimeEqual,
|
||||||
|
decryptAesCtr,
|
||||||
|
decryptSymmetric,
|
||||||
|
deriveAccessKey,
|
||||||
|
encryptAesCtr,
|
||||||
|
encryptSymmetric,
|
||||||
|
fromEncodedBinaryToArrayBuffer,
|
||||||
|
getAccessKeyVerifier,
|
||||||
|
getRandomBytes,
|
||||||
|
getViewOfArrayBuffer,
|
||||||
|
getZeroes,
|
||||||
|
highBitsToInt,
|
||||||
|
hmacSha256,
|
||||||
|
intsToByteHighAndLow,
|
||||||
|
splitBytes,
|
||||||
|
stringFromBytes,
|
||||||
|
trimBytes,
|
||||||
|
verifyAccessKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// High-level Operations
|
||||||
|
|
||||||
|
async function deriveAccessKey(profileKey) {
|
||||||
|
const iv = getZeroes(12);
|
||||||
|
const plaintext = getZeroes(16);
|
||||||
|
const accessKey = await _encrypt_aes_gcm(profileKey, iv, plaintext);
|
||||||
|
return _getFirstBytes(accessKey, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAccessKeyVerifier(accessKey) {
|
||||||
|
const plaintext = getZeroes(32);
|
||||||
|
const hmac = await hmacSha256(accessKey, plaintext);
|
||||||
|
|
||||||
|
return hmac;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifyAccessKey(accessKey, theirVerifier) {
|
||||||
|
const ourVerifier = await getAccessKeyVerifier(accessKey);
|
||||||
|
|
||||||
|
if (constantTimeEqual(ourVerifier, theirVerifier)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const IV_LENGTH = 16;
|
const IV_LENGTH = 16;
|
||||||
const MAC_LENGTH = 16;
|
const MAC_LENGTH = 16;
|
||||||
const NONCE_LENGTH = 16;
|
const NONCE_LENGTH = 16;
|
||||||
|
|
||||||
async function encryptSymmetric(key, plaintext) {
|
async function encryptSymmetric(key, plaintext) {
|
||||||
const iv = _getZeros(IV_LENGTH);
|
const iv = getZeroes(IV_LENGTH);
|
||||||
const nonce = _getRandomBytes(NONCE_LENGTH);
|
const nonce = getRandomBytes(NONCE_LENGTH);
|
||||||
|
|
||||||
const cipherKey = await _hmac_SHA256(key, nonce);
|
const cipherKey = await hmacSha256(key, nonce);
|
||||||
const macKey = await _hmac_SHA256(key, cipherKey);
|
const macKey = await hmacSha256(key, cipherKey);
|
||||||
|
|
||||||
const cipherText = await _encrypt_aes256_CBC_PKCSPadding(
|
const cipherText = await _encrypt_aes256_CBC_PKCSPadding(
|
||||||
cipherKey,
|
cipherKey,
|
||||||
iv,
|
iv,
|
||||||
plaintext
|
plaintext
|
||||||
);
|
);
|
||||||
const mac = _getFirstBytes(
|
const mac = _getFirstBytes(await hmacSha256(macKey, cipherText), MAC_LENGTH);
|
||||||
await _hmac_SHA256(macKey, cipherText),
|
|
||||||
MAC_LENGTH
|
|
||||||
);
|
|
||||||
|
|
||||||
return _concatData([nonce, cipherText, mac]);
|
return concatenateBytes(nonce, cipherText, mac);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function decryptSymmetric(key, data) {
|
async function decryptSymmetric(key, data) {
|
||||||
const iv = _getZeros(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(
|
||||||
|
@ -43,11 +86,11 @@ async function decryptSymmetric(key, data) {
|
||||||
);
|
);
|
||||||
const theirMac = _getBytes(data, data.byteLength - MAC_LENGTH, MAC_LENGTH);
|
const theirMac = _getBytes(data, data.byteLength - MAC_LENGTH, MAC_LENGTH);
|
||||||
|
|
||||||
const cipherKey = await _hmac_SHA256(key, nonce);
|
const cipherKey = await hmacSha256(key, nonce);
|
||||||
const macKey = await _hmac_SHA256(key, cipherKey);
|
const macKey = await hmacSha256(key, cipherKey);
|
||||||
|
|
||||||
const ourMac = _getFirstBytes(
|
const ourMac = _getFirstBytes(
|
||||||
await _hmac_SHA256(macKey, cipherText),
|
await hmacSha256(macKey, cipherText),
|
||||||
MAC_LENGTH
|
MAC_LENGTH
|
||||||
);
|
);
|
||||||
if (!constantTimeEqual(theirMac, ourMac)) {
|
if (!constantTimeEqual(theirMac, ourMac)) {
|
||||||
|
@ -73,56 +116,135 @@ function constantTimeEqual(left, right) {
|
||||||
return result === 0;
|
return result === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _hmac_SHA256(key, data) {
|
// Encryption
|
||||||
|
|
||||||
|
async function hmacSha256(key, plaintext) {
|
||||||
|
const algorithm = {
|
||||||
|
name: 'HMAC',
|
||||||
|
hash: 'SHA-256',
|
||||||
|
};
|
||||||
const extractable = false;
|
const extractable = false;
|
||||||
|
|
||||||
const cryptoKey = await window.crypto.subtle.importKey(
|
const cryptoKey = await window.crypto.subtle.importKey(
|
||||||
'raw',
|
'raw',
|
||||||
key,
|
key,
|
||||||
{ name: 'HMAC', hash: { name: 'SHA-256' } },
|
algorithm,
|
||||||
extractable,
|
extractable,
|
||||||
['sign']
|
['sign']
|
||||||
);
|
);
|
||||||
|
|
||||||
return window.crypto.subtle.sign(
|
return window.crypto.subtle.sign(algorithm, cryptoKey, plaintext);
|
||||||
{ name: 'HMAC', hash: 'SHA-256' },
|
|
||||||
cryptoKey,
|
|
||||||
data
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _encrypt_aes256_CBC_PKCSPadding(key, iv, data) {
|
async function _encrypt_aes256_CBC_PKCSPadding(key, iv, plaintext) {
|
||||||
|
const algorithm = {
|
||||||
|
name: 'AES-CBC',
|
||||||
|
iv,
|
||||||
|
};
|
||||||
const extractable = false;
|
const extractable = false;
|
||||||
|
|
||||||
const cryptoKey = await window.crypto.subtle.importKey(
|
const cryptoKey = await window.crypto.subtle.importKey(
|
||||||
'raw',
|
'raw',
|
||||||
key,
|
key,
|
||||||
{ name: 'AES-CBC' },
|
algorithm,
|
||||||
extractable,
|
extractable,
|
||||||
['encrypt']
|
['encrypt']
|
||||||
);
|
);
|
||||||
|
|
||||||
return window.crypto.subtle.encrypt({ name: 'AES-CBC', iv }, cryptoKey, data);
|
return window.crypto.subtle.encrypt(algorithm, cryptoKey, plaintext);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _decrypt_aes256_CBC_PKCSPadding(key, iv, data) {
|
async function _decrypt_aes256_CBC_PKCSPadding(key, iv, plaintext) {
|
||||||
|
const algorithm = {
|
||||||
|
name: 'AES-CBC',
|
||||||
|
iv,
|
||||||
|
};
|
||||||
const extractable = false;
|
const extractable = false;
|
||||||
|
|
||||||
const cryptoKey = await window.crypto.subtle.importKey(
|
const cryptoKey = await window.crypto.subtle.importKey(
|
||||||
'raw',
|
'raw',
|
||||||
key,
|
key,
|
||||||
{ name: 'AES-CBC' },
|
algorithm,
|
||||||
extractable,
|
extractable,
|
||||||
['decrypt']
|
['decrypt']
|
||||||
);
|
);
|
||||||
|
return window.crypto.subtle.decrypt(algorithm, cryptoKey, plaintext);
|
||||||
return window.crypto.subtle.decrypt({ name: 'AES-CBC', iv }, cryptoKey, data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function _getRandomBytes(n) {
|
async function encryptAesCtr(key, plaintext, counter) {
|
||||||
|
const extractable = false;
|
||||||
|
const algorithm = {
|
||||||
|
name: 'AES-CTR',
|
||||||
|
counter: new Uint8Array(counter),
|
||||||
|
length: 128,
|
||||||
|
};
|
||||||
|
|
||||||
|
const cryptoKey = await crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
key,
|
||||||
|
algorithm,
|
||||||
|
extractable,
|
||||||
|
['encrypt']
|
||||||
|
);
|
||||||
|
|
||||||
|
const ciphertext = await crypto.subtle.encrypt(
|
||||||
|
algorithm,
|
||||||
|
cryptoKey,
|
||||||
|
plaintext
|
||||||
|
);
|
||||||
|
|
||||||
|
return ciphertext;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function decryptAesCtr(key, ciphertext, counter) {
|
||||||
|
const extractable = false;
|
||||||
|
const algorithm = {
|
||||||
|
name: 'AES-CTR',
|
||||||
|
counter: new Uint8Array(counter),
|
||||||
|
length: 128,
|
||||||
|
};
|
||||||
|
|
||||||
|
const cryptoKey = await crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
key,
|
||||||
|
algorithm,
|
||||||
|
extractable,
|
||||||
|
['decrypt']
|
||||||
|
);
|
||||||
|
const plaintext = await crypto.subtle.decrypt(
|
||||||
|
algorithm,
|
||||||
|
cryptoKey,
|
||||||
|
ciphertext
|
||||||
|
);
|
||||||
|
return plaintext;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _encrypt_aes_gcm(key, iv, plaintext) {
|
||||||
|
const algorithm = {
|
||||||
|
name: 'AES-GCM',
|
||||||
|
iv,
|
||||||
|
};
|
||||||
|
const extractable = false;
|
||||||
|
|
||||||
|
const cryptoKey = await crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
key,
|
||||||
|
algorithm,
|
||||||
|
extractable,
|
||||||
|
['encrypt']
|
||||||
|
);
|
||||||
|
return crypto.subtle.encrypt(algorithm, cryptoKey, plaintext);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility
|
||||||
|
|
||||||
|
function getRandomBytes(n) {
|
||||||
const bytes = new Uint8Array(n);
|
const bytes = new Uint8Array(n);
|
||||||
window.crypto.getRandomValues(bytes);
|
window.crypto.getRandomValues(bytes);
|
||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _getZeros(n) {
|
function getZeroes(n) {
|
||||||
const result = new Uint8Array(n);
|
const result = new Uint8Array(n);
|
||||||
|
|
||||||
const value = 0;
|
const value = 0;
|
||||||
|
@ -133,17 +255,43 @@ function _getZeros(n) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _getFirstBytes(data, n) {
|
function highBitsToInt(byte) {
|
||||||
const source = new Uint8Array(data);
|
return (byte & 0xff) >> 4;
|
||||||
return source.subarray(0, n);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function _getBytes(data, start, n) {
|
function intsToByteHighAndLow(highValue, lowValue) {
|
||||||
const source = new Uint8Array(data);
|
return ((highValue << 4) | lowValue) & 0xff;
|
||||||
return source.subarray(start, start + n);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function _concatData(elements) {
|
function trimBytes(buffer, length) {
|
||||||
|
return _getFirstBytes(buffer, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
function arrayBufferToBase64(arrayBuffer) {
|
||||||
|
return dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64');
|
||||||
|
}
|
||||||
|
function base64ToArrayBuffer(base64string) {
|
||||||
|
return dcodeIO.ByteBuffer.wrap(base64string, 'base64').toArrayBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromEncodedBinaryToArrayBuffer(key) {
|
||||||
|
return dcodeIO.ByteBuffer.wrap(key, 'binary').toArrayBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
function bytesFromString(string) {
|
||||||
|
return dcodeIO.ByteBuffer.wrap(string, 'utf8').toArrayBuffer();
|
||||||
|
}
|
||||||
|
function stringFromBytes(buffer) {
|
||||||
|
return dcodeIO.ByteBuffer.wrap(buffer).toString('utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getViewOfArrayBuffer(buffer, start, finish) {
|
||||||
|
const source = new Uint8Array(buffer);
|
||||||
|
const result = source.slice(start, finish);
|
||||||
|
return result.buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
function concatenateBytes(...elements) {
|
||||||
const length = elements.reduce(
|
const length = elements.reduce(
|
||||||
(total, element) => total + element.byteLength,
|
(total, element) => total + element.byteLength,
|
||||||
0
|
0
|
||||||
|
@ -161,5 +309,45 @@ function _concatData(elements) {
|
||||||
throw new Error('problem concatenating!');
|
throw new Error('problem concatenating!');
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result.buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitBytes(buffer, ...lengths) {
|
||||||
|
const total = lengths.reduce((acc, length) => acc + length, 0);
|
||||||
|
|
||||||
|
if (total !== buffer.byteLength) {
|
||||||
|
throw new Error(
|
||||||
|
`Requested lengths total ${total} does not match source total ${
|
||||||
|
buffer.byteLength
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = new Uint8Array(buffer);
|
||||||
|
const results = [];
|
||||||
|
let position = 0;
|
||||||
|
|
||||||
|
for (let i = 0, max = lengths.length; i < max; i += 1) {
|
||||||
|
const length = lengths[i];
|
||||||
|
const result = new Uint8Array(length);
|
||||||
|
const section = source.slice(position, position + length);
|
||||||
|
result.set(section);
|
||||||
|
position += result.byteLength;
|
||||||
|
|
||||||
|
results.push(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal-only
|
||||||
|
|
||||||
|
function _getFirstBytes(data, n) {
|
||||||
|
const source = new Uint8Array(data);
|
||||||
|
return source.subarray(0, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getBytes(data, start, n) {
|
||||||
|
const source = new Uint8Array(data);
|
||||||
|
return source.subarray(start, start + n);
|
||||||
}
|
}
|
||||||
|
|
13
js/modules/metadata/CiphertextMessage.js
Normal file
13
js/modules/metadata/CiphertextMessage.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
module.exports = {
|
||||||
|
CURRENT_VERSION: 3,
|
||||||
|
|
||||||
|
// This matches Envelope.Type.CIPHERTEXT
|
||||||
|
WHISPER_TYPE: 1,
|
||||||
|
// This matches Envelope.Type.PREKEY_BUNDLE
|
||||||
|
PREKEY_TYPE: 3,
|
||||||
|
|
||||||
|
SENDERKEY_TYPE: 4,
|
||||||
|
SENDERKEY_DISTRIBUTION_TYPE: 5,
|
||||||
|
|
||||||
|
ENCRYPTED_MESSAGE_OVERHEAD: 53,
|
||||||
|
};
|
586
js/modules/metadata/SecretSessionCipher.js
Normal file
586
js/modules/metadata/SecretSessionCipher.js
Normal file
|
@ -0,0 +1,586 @@
|
||||||
|
/* global libsignal, textsecure */
|
||||||
|
|
||||||
|
/* eslint-disable no-bitwise */
|
||||||
|
|
||||||
|
const CiphertextMessage = require('./CiphertextMessage');
|
||||||
|
const {
|
||||||
|
bytesFromString,
|
||||||
|
concatenateBytes,
|
||||||
|
constantTimeEqual,
|
||||||
|
decryptAesCtr,
|
||||||
|
encryptAesCtr,
|
||||||
|
fromEncodedBinaryToArrayBuffer,
|
||||||
|
getViewOfArrayBuffer,
|
||||||
|
getZeroes,
|
||||||
|
highBitsToInt,
|
||||||
|
hmacSha256,
|
||||||
|
intsToByteHighAndLow,
|
||||||
|
splitBytes,
|
||||||
|
trimBytes,
|
||||||
|
} = require('../crypto');
|
||||||
|
|
||||||
|
const REVOKED_CERTIFICATES = [];
|
||||||
|
|
||||||
|
function SecretSessionCipher(storage) {
|
||||||
|
this.storage = storage;
|
||||||
|
|
||||||
|
// We do this on construction because libsignal won't be available when this file loads
|
||||||
|
const { SessionCipher } = libsignal;
|
||||||
|
this.SessionCipher = SessionCipher;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CIPHERTEXT_VERSION = 1;
|
||||||
|
const UNIDENTIFIED_DELIVERY_PREFIX = 'UnidentifiedDelivery';
|
||||||
|
|
||||||
|
// public CertificateValidator(ECPublicKey trustRoot)
|
||||||
|
function createCertificateValidator(trustRoot) {
|
||||||
|
return {
|
||||||
|
// public void validate(SenderCertificate certificate, long validationTime)
|
||||||
|
async validate(certificate, validationTime) {
|
||||||
|
const serverCertificate = certificate.signer;
|
||||||
|
|
||||||
|
await libsignal.Curve.async.verifySignature(
|
||||||
|
trustRoot,
|
||||||
|
serverCertificate.certificate,
|
||||||
|
serverCertificate.signature
|
||||||
|
);
|
||||||
|
|
||||||
|
const serverCertId = serverCertificate.certificate.id;
|
||||||
|
if (REVOKED_CERTIFICATES.includes(serverCertId)) {
|
||||||
|
throw new Error(
|
||||||
|
`Server certificate id ${serverCertId} has been revoked`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await libsignal.Curve.async.verifySignature(
|
||||||
|
serverCertificate.key,
|
||||||
|
certificate.certificate,
|
||||||
|
certificate.signature
|
||||||
|
);
|
||||||
|
|
||||||
|
if (validationTime > certificate.expires) {
|
||||||
|
throw new Error('Certificate is expired');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function _decodePoint(serialized, offset = 0) {
|
||||||
|
const view =
|
||||||
|
offset > 0
|
||||||
|
? getViewOfArrayBuffer(serialized, offset, serialized.byteLength)
|
||||||
|
: serialized;
|
||||||
|
|
||||||
|
return libsignal.Curve.validatePubKeyFormat(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
// public ServerCertificate(byte[] serialized)
|
||||||
|
function _createServerCertificateFromBuffer(serialized) {
|
||||||
|
const wrapper = textsecure.protobuf.ServerCertificate.decode(serialized);
|
||||||
|
|
||||||
|
if (!wrapper.certificate || !wrapper.signature) {
|
||||||
|
throw new Error('Missing fields');
|
||||||
|
}
|
||||||
|
|
||||||
|
const certificate = textsecure.protobuf.ServerCertificate.Certificate.decode(
|
||||||
|
wrapper.certificate.toArrayBuffer()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!certificate.id || !certificate.key) {
|
||||||
|
throw new Error('Missing fields');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: certificate.id,
|
||||||
|
key: certificate.key.toArrayBuffer(),
|
||||||
|
serialized,
|
||||||
|
certificate: wrapper.certificate.toArrayBuffer(),
|
||||||
|
|
||||||
|
signature: wrapper.signature.toArrayBuffer(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// public SenderCertificate(byte[] serialized)
|
||||||
|
function _createSenderCertificateFromBuffer(serialized) {
|
||||||
|
const wrapper = textsecure.protobuf.SenderCertificate.decode(serialized);
|
||||||
|
|
||||||
|
if (!wrapper.signature || !wrapper.certificate) {
|
||||||
|
throw new Error('Missing fields');
|
||||||
|
}
|
||||||
|
|
||||||
|
const certificate = textsecure.protobuf.SenderCertificate.Certificate.decode(
|
||||||
|
wrapper.certificate.toArrayBuffer()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!certificate.signer ||
|
||||||
|
!certificate.identityKey ||
|
||||||
|
!certificate.senderDevice ||
|
||||||
|
!certificate.expires ||
|
||||||
|
!certificate.sender
|
||||||
|
) {
|
||||||
|
throw new Error('Missing fields');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
sender: certificate.sender,
|
||||||
|
senderDevice: certificate.senderDevice,
|
||||||
|
expires: certificate.expires.toNumber(),
|
||||||
|
identityKey: certificate.identityKey.toArrayBuffer(),
|
||||||
|
signer: _createServerCertificateFromBuffer(
|
||||||
|
certificate.signer.toArrayBuffer()
|
||||||
|
),
|
||||||
|
|
||||||
|
certificate: wrapper.certificate.toArrayBuffer(),
|
||||||
|
signature: wrapper.signature.toArrayBuffer(),
|
||||||
|
|
||||||
|
serialized,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// public UnidentifiedSenderMessage(byte[] serialized)
|
||||||
|
function _createUnidentifiedSenderMessageFromBuffer(serialized) {
|
||||||
|
const version = highBitsToInt(serialized[0]);
|
||||||
|
|
||||||
|
if (version > CIPHERTEXT_VERSION) {
|
||||||
|
throw new Error(`Unknown version: ${this.version}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const view = getViewOfArrayBuffer(serialized, 1, serialized.byteLength);
|
||||||
|
const unidentifiedSenderMessage = textsecure.protobuf.UnidentifiedSenderMessage.decode(
|
||||||
|
view
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!unidentifiedSenderMessage.ephemeralPublic ||
|
||||||
|
!unidentifiedSenderMessage.encryptedStatic ||
|
||||||
|
!unidentifiedSenderMessage.encryptedMessage
|
||||||
|
) {
|
||||||
|
throw new Error('Missing fields');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
version,
|
||||||
|
|
||||||
|
ephemeralPublic: unidentifiedSenderMessage.ephemeralPublic.toArrayBuffer(),
|
||||||
|
encryptedStatic: unidentifiedSenderMessage.encryptedStatic.toArrayBuffer(),
|
||||||
|
encryptedMessage: unidentifiedSenderMessage.encryptedMessage.toArrayBuffer(),
|
||||||
|
|
||||||
|
serialized,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// public UnidentifiedSenderMessage(
|
||||||
|
// ECPublicKey ephemeral, byte[] encryptedStatic, byte[] encryptedMessage) {
|
||||||
|
function _createUnidentifiedSenderMessage(
|
||||||
|
ephemeralPublic,
|
||||||
|
encryptedStatic,
|
||||||
|
encryptedMessage
|
||||||
|
) {
|
||||||
|
const versionBytes = new Uint8Array([
|
||||||
|
intsToByteHighAndLow(CIPHERTEXT_VERSION, CIPHERTEXT_VERSION),
|
||||||
|
]);
|
||||||
|
const unidentifiedSenderMessage = new textsecure.protobuf.UnidentifiedSenderMessage();
|
||||||
|
|
||||||
|
unidentifiedSenderMessage.encryptedMessage = encryptedMessage;
|
||||||
|
unidentifiedSenderMessage.encryptedStatic = encryptedStatic;
|
||||||
|
unidentifiedSenderMessage.ephemeralPublic = ephemeralPublic;
|
||||||
|
|
||||||
|
const messageBytes = unidentifiedSenderMessage.encode().toArrayBuffer();
|
||||||
|
|
||||||
|
return {
|
||||||
|
version: CIPHERTEXT_VERSION,
|
||||||
|
|
||||||
|
ephemeralPublic,
|
||||||
|
encryptedStatic,
|
||||||
|
encryptedMessage,
|
||||||
|
|
||||||
|
serialized: concatenateBytes(versionBytes, messageBytes),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// public UnidentifiedSenderMessageContent(byte[] serialized)
|
||||||
|
function _createUnidentifiedSenderMessageContentFromBuffer(serialized) {
|
||||||
|
const TypeEnum = textsecure.protobuf.UnidentifiedSenderMessage.Message.Type;
|
||||||
|
|
||||||
|
const message = textsecure.protobuf.UnidentifiedSenderMessage.Message.decode(
|
||||||
|
serialized
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!message.type || !message.senderCertificate || !message.content) {
|
||||||
|
throw new Error('Missing fields');
|
||||||
|
}
|
||||||
|
|
||||||
|
let type;
|
||||||
|
switch (message.type) {
|
||||||
|
case TypeEnum.MESSAGE:
|
||||||
|
type = CiphertextMessage.WHISPER_TYPE;
|
||||||
|
break;
|
||||||
|
case TypeEnum.PREKEY_MESSAGE:
|
||||||
|
type = CiphertextMessage.PREKEY_TYPE;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown type: ${message.type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
senderCertificate: _createSenderCertificateFromBuffer(
|
||||||
|
message.senderCertificate.toArrayBuffer()
|
||||||
|
),
|
||||||
|
content: message.content.toArrayBuffer(),
|
||||||
|
|
||||||
|
serialized,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// private int getProtoType(int type)
|
||||||
|
function _getProtoMessageType(type) {
|
||||||
|
const TypeEnum = textsecure.protobuf.UnidentifiedSenderMessage.Message.Type;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case CiphertextMessage.WHISPER_TYPE:
|
||||||
|
return TypeEnum.MESSAGE;
|
||||||
|
case CiphertextMessage.PREKEY_TYPE:
|
||||||
|
return TypeEnum.PREKEY_MESSAGE;
|
||||||
|
default:
|
||||||
|
throw new Error(`_getProtoMessageType: type '${type}' does not exist`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// public UnidentifiedSenderMessageContent(
|
||||||
|
// int type, SenderCertificate senderCertificate, byte[] content)
|
||||||
|
function _createUnidentifiedSenderMessageContent(
|
||||||
|
type,
|
||||||
|
senderCertificate,
|
||||||
|
content
|
||||||
|
) {
|
||||||
|
const innerMessage = new textsecure.protobuf.UnidentifiedSenderMessage.Message();
|
||||||
|
innerMessage.type = _getProtoMessageType(type);
|
||||||
|
innerMessage.senderCertificate = textsecure.protobuf.SenderCertificate.decode(
|
||||||
|
senderCertificate.serialized
|
||||||
|
);
|
||||||
|
innerMessage.content = content;
|
||||||
|
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
senderCertificate,
|
||||||
|
content,
|
||||||
|
|
||||||
|
serialized: innerMessage.encode().toArrayBuffer(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
SecretSessionCipher.prototype = {
|
||||||
|
// public byte[] encrypt(
|
||||||
|
// SignalProtocolAddress destinationAddress,
|
||||||
|
// SenderCertificate senderCertificate,
|
||||||
|
// byte[] paddedPlaintext
|
||||||
|
// )
|
||||||
|
async encrypt(destinationAddress, senderCertificate, paddedPlaintext) {
|
||||||
|
// Capture this.xxx variables to replicate Java's implicit this syntax
|
||||||
|
const { SessionCipher } = this;
|
||||||
|
const signalProtocolStore = this.storage;
|
||||||
|
const _calculateEphemeralKeys = this._calculateEphemeralKeys.bind(this);
|
||||||
|
const _encryptWithSecretKeys = this._encryptWithSecretKeys.bind(this);
|
||||||
|
const _calculateStaticKeys = this._calculateStaticKeys.bind(this);
|
||||||
|
|
||||||
|
const sessionCipher = new SessionCipher(
|
||||||
|
signalProtocolStore,
|
||||||
|
destinationAddress
|
||||||
|
);
|
||||||
|
const sessionRecord = await sessionCipher.getRecord(
|
||||||
|
destinationAddress.toString()
|
||||||
|
);
|
||||||
|
const openSession = sessionRecord.getOpenSession();
|
||||||
|
if (!openSession) {
|
||||||
|
throw new Error('No active session');
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = await sessionCipher.encrypt(paddedPlaintext);
|
||||||
|
const ourIdentity = await signalProtocolStore.getIdentityKeyPair();
|
||||||
|
const theirIdentity = fromEncodedBinaryToArrayBuffer(
|
||||||
|
openSession.indexInfo.remoteIdentityKey
|
||||||
|
);
|
||||||
|
|
||||||
|
const ephemeral = await libsignal.Curve.async.generateKeyPair();
|
||||||
|
const ephemeralSalt = concatenateBytes(
|
||||||
|
bytesFromString(UNIDENTIFIED_DELIVERY_PREFIX),
|
||||||
|
theirIdentity,
|
||||||
|
ephemeral.pubKey
|
||||||
|
);
|
||||||
|
const ephemeralKeys = await _calculateEphemeralKeys(
|
||||||
|
theirIdentity,
|
||||||
|
ephemeral.privKey,
|
||||||
|
ephemeralSalt
|
||||||
|
);
|
||||||
|
const staticKeyCiphertext = await _encryptWithSecretKeys(
|
||||||
|
ephemeralKeys.cipherKey,
|
||||||
|
ephemeralKeys.macKey,
|
||||||
|
ourIdentity.pubKey
|
||||||
|
);
|
||||||
|
|
||||||
|
const staticSalt = concatenateBytes(
|
||||||
|
ephemeralKeys.chainKey,
|
||||||
|
staticKeyCiphertext
|
||||||
|
);
|
||||||
|
const staticKeys = await _calculateStaticKeys(
|
||||||
|
theirIdentity,
|
||||||
|
ourIdentity.privKey,
|
||||||
|
staticSalt
|
||||||
|
);
|
||||||
|
const content = _createUnidentifiedSenderMessageContent(
|
||||||
|
message.type,
|
||||||
|
senderCertificate,
|
||||||
|
fromEncodedBinaryToArrayBuffer(message.body)
|
||||||
|
);
|
||||||
|
const messageBytes = await _encryptWithSecretKeys(
|
||||||
|
staticKeys.cipherKey,
|
||||||
|
staticKeys.macKey,
|
||||||
|
content.serialized
|
||||||
|
);
|
||||||
|
|
||||||
|
const unidentifiedSenderMessage = _createUnidentifiedSenderMessage(
|
||||||
|
ephemeral.pubKey,
|
||||||
|
staticKeyCiphertext,
|
||||||
|
messageBytes
|
||||||
|
);
|
||||||
|
|
||||||
|
return unidentifiedSenderMessage.serialized;
|
||||||
|
},
|
||||||
|
|
||||||
|
// public Pair<SignalProtocolAddress, byte[]> decrypt(
|
||||||
|
// CertificateValidator validator, byte[] ciphertext, long timestamp)
|
||||||
|
async decrypt(validator, ciphertext, timestamp, me) {
|
||||||
|
// Capture this.xxx variables to replicate Java's implicit this syntax
|
||||||
|
const signalProtocolStore = this.storage;
|
||||||
|
const _calculateEphemeralKeys = this._calculateEphemeralKeys.bind(this);
|
||||||
|
const _calculateStaticKeys = this._calculateStaticKeys.bind(this);
|
||||||
|
const _decryptWithUnidentifiedSenderMessage = this._decryptWithUnidentifiedSenderMessage.bind(
|
||||||
|
this
|
||||||
|
);
|
||||||
|
const _decryptWithSecretKeys = this._decryptWithSecretKeys.bind(this);
|
||||||
|
|
||||||
|
const ourIdentity = await signalProtocolStore.getIdentityKeyPair();
|
||||||
|
const wrapper = _createUnidentifiedSenderMessageFromBuffer(ciphertext);
|
||||||
|
const ephemeralSalt = concatenateBytes(
|
||||||
|
bytesFromString(UNIDENTIFIED_DELIVERY_PREFIX),
|
||||||
|
ourIdentity.pubKey,
|
||||||
|
wrapper.ephemeralPublic
|
||||||
|
);
|
||||||
|
const ephemeralKeys = await _calculateEphemeralKeys(
|
||||||
|
wrapper.ephemeralPublic,
|
||||||
|
ourIdentity.privKey,
|
||||||
|
ephemeralSalt
|
||||||
|
);
|
||||||
|
const staticKeyBytes = await _decryptWithSecretKeys(
|
||||||
|
ephemeralKeys.cipherKey,
|
||||||
|
ephemeralKeys.macKey,
|
||||||
|
wrapper.encryptedStatic
|
||||||
|
);
|
||||||
|
|
||||||
|
const staticKey = _decodePoint(staticKeyBytes, 0);
|
||||||
|
const staticSalt = concatenateBytes(
|
||||||
|
ephemeralKeys.chainKey,
|
||||||
|
wrapper.encryptedStatic
|
||||||
|
);
|
||||||
|
const staticKeys = await _calculateStaticKeys(
|
||||||
|
staticKey,
|
||||||
|
ourIdentity.privKey,
|
||||||
|
staticSalt
|
||||||
|
);
|
||||||
|
const messageBytes = await _decryptWithSecretKeys(
|
||||||
|
staticKeys.cipherKey,
|
||||||
|
staticKeys.macKey,
|
||||||
|
wrapper.encryptedMessage
|
||||||
|
);
|
||||||
|
|
||||||
|
const content = _createUnidentifiedSenderMessageContentFromBuffer(
|
||||||
|
messageBytes
|
||||||
|
);
|
||||||
|
|
||||||
|
await validator.validate(content.senderCertificate, timestamp);
|
||||||
|
if (
|
||||||
|
!constantTimeEqual(content.senderCertificate.identityKey, staticKeyBytes)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Sender's certificate key does not match key used in message"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { sender, senderDevice } = content.senderCertificate;
|
||||||
|
const { number, deviceId } = me || {};
|
||||||
|
if (sender === number && senderDevice === deviceId) {
|
||||||
|
return {
|
||||||
|
isMe: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const address = new libsignal.SignalProtocolAddress(sender, senderDevice);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
sender: address,
|
||||||
|
content: await _decryptWithUnidentifiedSenderMessage(content),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
error.sender = address;
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// public int getSessionVersion(SignalProtocolAddress remoteAddress) {
|
||||||
|
getSessionVersion(remoteAddress) {
|
||||||
|
const { SessionCipher } = this;
|
||||||
|
const signalProtocolStore = this.storage;
|
||||||
|
|
||||||
|
const cipher = new SessionCipher(signalProtocolStore, remoteAddress);
|
||||||
|
|
||||||
|
return cipher.getSessionVersion();
|
||||||
|
},
|
||||||
|
|
||||||
|
// public int getRemoteRegistrationId(SignalProtocolAddress remoteAddress) {
|
||||||
|
getRemoteRegistrationId(remoteAddress) {
|
||||||
|
const { SessionCipher } = this;
|
||||||
|
const signalProtocolStore = this.storage;
|
||||||
|
|
||||||
|
const cipher = new SessionCipher(signalProtocolStore, remoteAddress);
|
||||||
|
|
||||||
|
return cipher.getRemoteRegistrationId();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Used by outgoing_message.js
|
||||||
|
closeOpenSessionForDevice(remoteAddress) {
|
||||||
|
const { SessionCipher } = this;
|
||||||
|
const signalProtocolStore = this.storage;
|
||||||
|
|
||||||
|
const cipher = new SessionCipher(signalProtocolStore, remoteAddress);
|
||||||
|
|
||||||
|
return cipher.closeOpenSessionForDevice();
|
||||||
|
},
|
||||||
|
|
||||||
|
// private EphemeralKeys calculateEphemeralKeys(
|
||||||
|
// ECPublicKey ephemeralPublic, ECPrivateKey ephemeralPrivate, byte[] salt)
|
||||||
|
async _calculateEphemeralKeys(ephemeralPublic, ephemeralPrivate, salt) {
|
||||||
|
const ephemeralSecret = await libsignal.Curve.async.calculateAgreement(
|
||||||
|
ephemeralPublic,
|
||||||
|
ephemeralPrivate
|
||||||
|
);
|
||||||
|
const ephemeralDerivedParts = await libsignal.HKDF.deriveSecrets(
|
||||||
|
ephemeralSecret,
|
||||||
|
salt,
|
||||||
|
new ArrayBuffer()
|
||||||
|
);
|
||||||
|
|
||||||
|
// private EphemeralKeys(byte[] chainKey, byte[] cipherKey, byte[] macKey)
|
||||||
|
return {
|
||||||
|
chainKey: ephemeralDerivedParts[0],
|
||||||
|
cipherKey: ephemeralDerivedParts[1],
|
||||||
|
macKey: ephemeralDerivedParts[2],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
// private StaticKeys calculateStaticKeys(
|
||||||
|
// ECPublicKey staticPublic, ECPrivateKey staticPrivate, byte[] salt)
|
||||||
|
async _calculateStaticKeys(staticPublic, staticPrivate, salt) {
|
||||||
|
const staticSecret = await libsignal.Curve.async.calculateAgreement(
|
||||||
|
staticPublic,
|
||||||
|
staticPrivate
|
||||||
|
);
|
||||||
|
const staticDerivedParts = await libsignal.HKDF.deriveSecrets(
|
||||||
|
staticSecret,
|
||||||
|
salt,
|
||||||
|
new ArrayBuffer()
|
||||||
|
);
|
||||||
|
|
||||||
|
// private StaticKeys(byte[] cipherKey, byte[] macKey)
|
||||||
|
return {
|
||||||
|
cipherKey: staticDerivedParts[1],
|
||||||
|
macKey: staticDerivedParts[2],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
// private byte[] decrypt(UnidentifiedSenderMessageContent message)
|
||||||
|
_decryptWithUnidentifiedSenderMessage(message) {
|
||||||
|
const { SessionCipher } = this;
|
||||||
|
const signalProtocolStore = this.storage;
|
||||||
|
|
||||||
|
const sender = new libsignal.SignalProtocolAddress(
|
||||||
|
message.senderCertificate.sender,
|
||||||
|
message.senderCertificate.senderDevice
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (message.type) {
|
||||||
|
case CiphertextMessage.WHISPER_TYPE:
|
||||||
|
return new SessionCipher(
|
||||||
|
signalProtocolStore,
|
||||||
|
sender
|
||||||
|
).decryptWhisperMessage(message.content);
|
||||||
|
case CiphertextMessage.PREKEY_TYPE:
|
||||||
|
return new SessionCipher(
|
||||||
|
signalProtocolStore,
|
||||||
|
sender
|
||||||
|
).decryptPreKeyWhisperMessage(message.content);
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown type: ${message.type}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// private byte[] encrypt(
|
||||||
|
// SecretKeySpec cipherKey, SecretKeySpec macKey, byte[] plaintext)
|
||||||
|
async _encryptWithSecretKeys(cipherKey, macKey, plaintext) {
|
||||||
|
// Cipher const cipher = Cipher.getInstance('AES/CTR/NoPadding');
|
||||||
|
// cipher.init(Cipher.ENCRYPT_MODE, cipherKey, new IvParameterSpec(new byte[16]));
|
||||||
|
|
||||||
|
// Mac const mac = Mac.getInstance('HmacSHA256');
|
||||||
|
// mac.init(macKey);
|
||||||
|
|
||||||
|
// byte[] const ciphertext = cipher.doFinal(plaintext);
|
||||||
|
const ciphertext = await encryptAesCtr(cipherKey, plaintext, getZeroes(16));
|
||||||
|
|
||||||
|
// byte[] const ourFullMac = mac.doFinal(ciphertext);
|
||||||
|
const ourFullMac = await hmacSha256(macKey, ciphertext);
|
||||||
|
const ourMac = trimBytes(ourFullMac, 10);
|
||||||
|
|
||||||
|
return concatenateBytes(ciphertext, ourMac);
|
||||||
|
},
|
||||||
|
|
||||||
|
// private byte[] decrypt(
|
||||||
|
// SecretKeySpec cipherKey, SecretKeySpec macKey, byte[] ciphertext)
|
||||||
|
async _decryptWithSecretKeys(cipherKey, macKey, ciphertext) {
|
||||||
|
if (ciphertext.byteLength < 10) {
|
||||||
|
throw new Error('Ciphertext not long enough for MAC!');
|
||||||
|
}
|
||||||
|
|
||||||
|
const ciphertextParts = splitBytes(
|
||||||
|
ciphertext,
|
||||||
|
ciphertext.byteLength - 10,
|
||||||
|
10
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mac const mac = Mac.getInstance('HmacSHA256');
|
||||||
|
// mac.init(macKey);
|
||||||
|
|
||||||
|
// byte[] const digest = mac.doFinal(ciphertextParts[0]);
|
||||||
|
const digest = await hmacSha256(macKey, ciphertextParts[0]);
|
||||||
|
const ourMac = trimBytes(digest, 10);
|
||||||
|
const theirMac = ciphertextParts[1];
|
||||||
|
|
||||||
|
if (!constantTimeEqual(ourMac, theirMac)) {
|
||||||
|
throw new Error('Bad mac!');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cipher const cipher = Cipher.getInstance('AES/CTR/NoPadding');
|
||||||
|
// cipher.init(Cipher.DECRYPT_MODE, cipherKey, new IvParameterSpec(new byte[16]));
|
||||||
|
|
||||||
|
// return cipher.doFinal(ciphertextParts[0]);
|
||||||
|
return decryptAesCtr(cipherKey, ciphertextParts[0], getZeroes(16));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
SecretSessionCipher,
|
||||||
|
createCertificateValidator,
|
||||||
|
_createServerCertificateFromBuffer,
|
||||||
|
_createSenderCertificateFromBuffer,
|
||||||
|
};
|
120
js/modules/refresh_sender_certificate.js
Normal file
120
js/modules/refresh_sender_certificate.js
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
/* global window, setTimeout, clearTimeout, textsecure, WebAPI, ConversationController */
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
initialize,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ONE_DAY = 24 * 60 * 60 * 1000; // one day
|
||||||
|
const MINIMUM_TIME_LEFT = 2 * 60 * 60 * 1000; // two hours
|
||||||
|
|
||||||
|
let initialized = false;
|
||||||
|
let timeout = null;
|
||||||
|
let scheduledTime = null;
|
||||||
|
|
||||||
|
// We need to refresh our own profile regularly to account for newly-added devices which
|
||||||
|
// do not support unidentified delivery.
|
||||||
|
function refreshOurProfile() {
|
||||||
|
const ourNumber = textsecure.storage.user.getNumber();
|
||||||
|
const conversation = ConversationController.getOrCreate(ourNumber, 'private');
|
||||||
|
conversation.getProfiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
function initialize({ events, storage, navigator, logger }) {
|
||||||
|
if (initialized) {
|
||||||
|
logger.warn('refreshSenderCertificate: already initialized!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
initialized = true;
|
||||||
|
|
||||||
|
runWhenOnline();
|
||||||
|
|
||||||
|
events.on('timetravel', () => {
|
||||||
|
if (initialized) {
|
||||||
|
scheduleNextRotation();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function scheduleNextRotation() {
|
||||||
|
const now = Date.now();
|
||||||
|
const certificate = storage.get('senderCertificate');
|
||||||
|
if (!certificate) {
|
||||||
|
setTimeoutForNextRun(now);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The useful information in a SenderCertificate is all serialized, so we
|
||||||
|
// need to do another layer of decoding.
|
||||||
|
const decoded = textsecure.protobuf.SenderCertificate.Certificate.decode(
|
||||||
|
certificate.certificate
|
||||||
|
);
|
||||||
|
const expires = decoded.expires.toNumber();
|
||||||
|
|
||||||
|
const time = Math.min(now + ONE_DAY, expires - MINIMUM_TIME_LEFT);
|
||||||
|
|
||||||
|
setTimeoutForNextRun(time);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
logger.info('refreshSenderCertificate: Getting new certificate...');
|
||||||
|
try {
|
||||||
|
const username = storage.get('number_id');
|
||||||
|
const password = storage.get('password');
|
||||||
|
const server = WebAPI.connect({ username, password });
|
||||||
|
|
||||||
|
const { certificate } = await server.getSenderCertificate();
|
||||||
|
const arrayBuffer = window.Signal.Crypto.base64ToArrayBuffer(certificate);
|
||||||
|
const decoded = textsecure.protobuf.SenderCertificate.decode(arrayBuffer);
|
||||||
|
|
||||||
|
decoded.certificate = decoded.certificate.toArrayBuffer();
|
||||||
|
decoded.signature = decoded.signature.toArrayBuffer();
|
||||||
|
decoded.serialized = arrayBuffer;
|
||||||
|
|
||||||
|
storage.put('senderCertificate', decoded);
|
||||||
|
scheduleNextRotation();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
'refreshSenderCertificate: Get failed. Trying again in two minutes...',
|
||||||
|
error && error.stack ? error.stack : error
|
||||||
|
);
|
||||||
|
setTimeout(runWhenOnline, 2 * 60 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshOurProfile();
|
||||||
|
}
|
||||||
|
|
||||||
|
function runWhenOnline() {
|
||||||
|
if (navigator.onLine) {
|
||||||
|
run();
|
||||||
|
} else {
|
||||||
|
logger.info(
|
||||||
|
'refreshSenderCertificate: Offline. Will update certificate when online...'
|
||||||
|
);
|
||||||
|
const listener = () => {
|
||||||
|
logger.info(
|
||||||
|
'refreshSenderCertificate: Online. Now updating certificate...'
|
||||||
|
);
|
||||||
|
window.removeEventListener('online', listener);
|
||||||
|
run();
|
||||||
|
};
|
||||||
|
window.addEventListener('online', listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTimeoutForNextRun(time = Date.now()) {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
if (scheduledTime !== time || !timeout) {
|
||||||
|
logger.info(
|
||||||
|
'Next sender certificate refresh scheduled for',
|
||||||
|
new Date(time).toISOString()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduledTime = time;
|
||||||
|
const waitTime = Math.max(0, time - now);
|
||||||
|
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(runWhenOnline, waitTime);
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,6 +11,8 @@ const Settings = require('./settings');
|
||||||
const Startup = require('./startup');
|
const Startup = require('./startup');
|
||||||
const Util = require('../../ts/util');
|
const Util = require('../../ts/util');
|
||||||
const { migrateToSQL } = require('./migrate_to_sql');
|
const { migrateToSQL } = require('./migrate_to_sql');
|
||||||
|
const Metadata = require('./metadata/SecretSessionCipher');
|
||||||
|
const RefreshSenderCertificate = require('./refresh_sender_certificate');
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
const {
|
const {
|
||||||
|
@ -216,6 +218,7 @@ exports.setup = (options = {}) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
Metadata,
|
||||||
Backbone,
|
Backbone,
|
||||||
Components,
|
Components,
|
||||||
Crypto,
|
Crypto,
|
||||||
|
@ -225,6 +228,7 @@ exports.setup = (options = {}) => {
|
||||||
Migrations,
|
Migrations,
|
||||||
Notifications,
|
Notifications,
|
||||||
OS,
|
OS,
|
||||||
|
RefreshSenderCertificate,
|
||||||
Settings,
|
Settings,
|
||||||
Startup,
|
Startup,
|
||||||
Types,
|
Types,
|
||||||
|
|
|
@ -4,20 +4,28 @@ const Errors = require('./types/errors');
|
||||||
const Settings = require('./settings');
|
const Settings = require('./settings');
|
||||||
|
|
||||||
exports.syncReadReceiptConfiguration = async ({
|
exports.syncReadReceiptConfiguration = async ({
|
||||||
|
ourNumber,
|
||||||
deviceId,
|
deviceId,
|
||||||
sendRequestConfigurationSyncMessage,
|
sendRequestConfigurationSyncMessage,
|
||||||
storage,
|
storage,
|
||||||
|
prepareForSend,
|
||||||
}) => {
|
}) => {
|
||||||
if (!is.string(deviceId)) {
|
if (!is.string(deviceId)) {
|
||||||
throw new TypeError("'deviceId' is required");
|
throw new TypeError('deviceId is required');
|
||||||
|
}
|
||||||
|
if (!is.function(sendRequestConfigurationSyncMessage)) {
|
||||||
|
throw new TypeError('sendRequestConfigurationSyncMessage is required');
|
||||||
|
}
|
||||||
|
if (!is.function(prepareForSend)) {
|
||||||
|
throw new TypeError('prepareForSend is required');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!is.function(sendRequestConfigurationSyncMessage)) {
|
if (!is.string(ourNumber)) {
|
||||||
throw new TypeError("'sendRequestConfigurationSyncMessage' is required");
|
throw new TypeError('ourNumber is required');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!is.object(storage)) {
|
if (!is.object(storage)) {
|
||||||
throw new TypeError("'storage' is required");
|
throw new TypeError('storage is required');
|
||||||
}
|
}
|
||||||
|
|
||||||
const isPrimaryDevice = deviceId === '1';
|
const isPrimaryDevice = deviceId === '1';
|
||||||
|
@ -38,7 +46,8 @@ exports.syncReadReceiptConfiguration = async ({
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await sendRequestConfigurationSyncMessage();
|
const { wrap, sendOptions } = prepareForSend(ourNumber);
|
||||||
|
await wrap(sendRequestConfigurationSyncMessage(sendOptions));
|
||||||
storage.put(settingName, true);
|
storage.put(settingName, true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,21 +1,14 @@
|
||||||
/* global dcodeIO, crypto */
|
/* global crypto */
|
||||||
|
|
||||||
const { isFunction, isNumber } = require('lodash');
|
const { isFunction, isNumber } = require('lodash');
|
||||||
const { createLastMessageUpdate } = require('../../../ts/types/Conversation');
|
const { createLastMessageUpdate } = require('../../../ts/types/Conversation');
|
||||||
|
const { arrayBufferToBase64, base64ToArrayBuffer } = require('../crypto');
|
||||||
|
|
||||||
async function computeHash(arraybuffer) {
|
async function computeHash(arraybuffer) {
|
||||||
const hash = await crypto.subtle.digest({ name: 'SHA-512' }, arraybuffer);
|
const hash = await crypto.subtle.digest({ name: 'SHA-512' }, arraybuffer);
|
||||||
return arrayBufferToBase64(hash);
|
return arrayBufferToBase64(hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
function arrayBufferToBase64(arraybuffer) {
|
|
||||||
return dcodeIO.ByteBuffer.wrap(arraybuffer).toString('base64');
|
|
||||||
}
|
|
||||||
|
|
||||||
function base64ToArrayBuffer(base64) {
|
|
||||||
return dcodeIO.ByteBuffer.wrap(base64, 'base64').toArrayBuffer();
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildAvatarUpdater({ field }) {
|
function buildAvatarUpdater({ field }) {
|
||||||
return async (conversation, data, options = {}) => {
|
return async (conversation, data, options = {}) => {
|
||||||
if (!conversation) {
|
if (!conversation) {
|
||||||
|
|
|
@ -80,6 +80,8 @@ function _ensureStringed(thing) {
|
||||||
return res;
|
return res;
|
||||||
} else if (thing === null) {
|
} else if (thing === null) {
|
||||||
return null;
|
return null;
|
||||||
|
} else if (thing === undefined) {
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
throw new Error(`unsure of how to jsonify object of type ${typeof thing}`);
|
throw new Error(`unsure of how to jsonify object of type ${typeof thing}`);
|
||||||
}
|
}
|
||||||
|
@ -160,7 +162,9 @@ function _createSocket(url, { certificateAuthority, proxyUrl }) {
|
||||||
function _promiseAjax(providedUrl, options) {
|
function _promiseAjax(providedUrl, options) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const url = providedUrl || `${options.host}/${options.path}`;
|
const url = providedUrl || `${options.host}/${options.path}`;
|
||||||
log.info(options.type, url);
|
log.info(
|
||||||
|
`${options.type} ${url}${options.unauthenticated ? ' (unauth)' : ''}`
|
||||||
|
);
|
||||||
const timeout =
|
const timeout =
|
||||||
typeof options.timeout !== 'undefined' ? options.timeout : 10000;
|
typeof options.timeout !== 'undefined' ? options.timeout : 10000;
|
||||||
|
|
||||||
|
@ -188,15 +192,26 @@ function _promiseAjax(providedUrl, options) {
|
||||||
fetchOptions.headers['Content-Length'] = contentLength;
|
fetchOptions.headers['Content-Length'] = contentLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.user && options.password) {
|
const { accessKey, unauthenticated } = options;
|
||||||
|
if (unauthenticated) {
|
||||||
|
if (!accessKey) {
|
||||||
|
throw new Error(
|
||||||
|
'_promiseAjax: mode is aunathenticated, but accessKey was not provided'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Access key is already a Base64 string
|
||||||
|
fetchOptions.headers['Unidentified-Access-Key'] = accessKey;
|
||||||
|
} else if (options.user && options.password) {
|
||||||
const user = _getString(options.user);
|
const user = _getString(options.user);
|
||||||
const password = _getString(options.password);
|
const password = _getString(options.password);
|
||||||
const auth = _btoa(`${user}:${password}`);
|
const auth = _btoa(`${user}:${password}`);
|
||||||
fetchOptions.headers.Authorization = `Basic ${auth}`;
|
fetchOptions.headers.Authorization = `Basic ${auth}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.contentType) {
|
if (options.contentType) {
|
||||||
fetchOptions.headers['Content-Type'] = options.contentType;
|
fetchOptions.headers['Content-Type'] = options.contentType;
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch(url, fetchOptions)
|
fetch(url, fetchOptions)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
let resultPromise;
|
let resultPromise;
|
||||||
|
@ -292,12 +307,14 @@ function HTTPError(message, providedCode, response, stack) {
|
||||||
|
|
||||||
const URL_CALLS = {
|
const URL_CALLS = {
|
||||||
accounts: 'v1/accounts',
|
accounts: 'v1/accounts',
|
||||||
|
attachment: 'v1/attachments',
|
||||||
|
deliveryCert: 'v1/certificate/delivery',
|
||||||
|
supportUnauthenticatedDelivery: 'v1/devices/unauthenticated_delivery',
|
||||||
devices: 'v1/devices',
|
devices: 'v1/devices',
|
||||||
keys: 'v2/keys',
|
keys: 'v2/keys',
|
||||||
signed: 'v2/keys/signed',
|
|
||||||
messages: 'v1/messages',
|
messages: 'v1/messages',
|
||||||
attachment: 'v1/attachments',
|
|
||||||
profile: 'v1/profile',
|
profile: 'v1/profile',
|
||||||
|
signed: 'v2/keys/signed',
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@ -335,16 +352,21 @@ function initialize({ url, cdnUrl, certificateAuthority, proxyUrl }) {
|
||||||
getAttachment,
|
getAttachment,
|
||||||
getAvatar,
|
getAvatar,
|
||||||
getDevices,
|
getDevices,
|
||||||
|
getSenderCertificate,
|
||||||
|
registerSupportForUnauthenticatedDelivery,
|
||||||
getKeysForNumber,
|
getKeysForNumber,
|
||||||
|
getKeysForNumberUnauth,
|
||||||
getMessageSocket,
|
getMessageSocket,
|
||||||
getMyKeys,
|
getMyKeys,
|
||||||
getProfile,
|
getProfile,
|
||||||
|
getProfileUnauth,
|
||||||
getProvisioningSocket,
|
getProvisioningSocket,
|
||||||
putAttachment,
|
putAttachment,
|
||||||
registerKeys,
|
registerKeys,
|
||||||
requestVerificationSMS,
|
requestVerificationSMS,
|
||||||
requestVerificationVoice,
|
requestVerificationVoice,
|
||||||
sendMessages,
|
sendMessages,
|
||||||
|
sendMessagesUnauth,
|
||||||
setSignedPreKey,
|
setSignedPreKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -366,6 +388,8 @@ function initialize({ url, cdnUrl, certificateAuthority, proxyUrl }) {
|
||||||
type: param.httpType,
|
type: param.httpType,
|
||||||
user: username,
|
user: username,
|
||||||
validateResponse: param.validateResponse,
|
validateResponse: param.validateResponse,
|
||||||
|
unauthenticated: param.unauthenticated,
|
||||||
|
accessKey: param.accessKey,
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
const { code } = e;
|
const { code } = e;
|
||||||
if (code === 200) {
|
if (code === 200) {
|
||||||
|
@ -405,6 +429,23 @@ function initialize({ url, cdnUrl, certificateAuthority, proxyUrl }) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSenderCertificate() {
|
||||||
|
return _ajax({
|
||||||
|
call: 'deliveryCert',
|
||||||
|
httpType: 'GET',
|
||||||
|
responseType: 'json',
|
||||||
|
schema: { certificate: 'string' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerSupportForUnauthenticatedDelivery() {
|
||||||
|
return _ajax({
|
||||||
|
call: 'supportUnauthenticatedDelivery',
|
||||||
|
httpType: 'PUT',
|
||||||
|
responseType: 'json',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getProfile(number) {
|
function getProfile(number) {
|
||||||
return _ajax({
|
return _ajax({
|
||||||
call: 'profile',
|
call: 'profile',
|
||||||
|
@ -413,6 +454,16 @@ function initialize({ url, cdnUrl, certificateAuthority, proxyUrl }) {
|
||||||
responseType: 'json',
|
responseType: 'json',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
function getProfileUnauth(number, { accessKey } = {}) {
|
||||||
|
return _ajax({
|
||||||
|
call: 'profile',
|
||||||
|
httpType: 'GET',
|
||||||
|
urlParameters: `/${number}`,
|
||||||
|
responseType: 'json',
|
||||||
|
unauthenticated: true,
|
||||||
|
accessKey,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getAvatar(path) {
|
function getAvatar(path) {
|
||||||
// Using _outerAJAX, since it's not hardcoded to the Signal Server. Unlike our
|
// Using _outerAJAX, since it's not hardcoded to the Signal Server. Unlike our
|
||||||
|
@ -449,13 +500,19 @@ function initialize({ url, cdnUrl, certificateAuthority, proxyUrl }) {
|
||||||
newPassword,
|
newPassword,
|
||||||
signalingKey,
|
signalingKey,
|
||||||
registrationId,
|
registrationId,
|
||||||
deviceName
|
deviceName,
|
||||||
|
options = {}
|
||||||
) {
|
) {
|
||||||
|
const { accessKey } = options;
|
||||||
const jsonData = {
|
const jsonData = {
|
||||||
signalingKey: _btoa(_getString(signalingKey)),
|
signalingKey: _btoa(_getString(signalingKey)),
|
||||||
supportsSms: false,
|
supportsSms: false,
|
||||||
fetchesMessages: true,
|
fetchesMessages: true,
|
||||||
registrationId,
|
registrationId,
|
||||||
|
unidentifiedAccessKey: accessKey
|
||||||
|
? _btoa(_getString(accessKey))
|
||||||
|
: undefined,
|
||||||
|
unrestrictedUnidentifiedAccess: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let call;
|
let call;
|
||||||
|
@ -552,6 +609,43 @@ function initialize({ url, cdnUrl, certificateAuthority, proxyUrl }) {
|
||||||
}).then(res => res.count);
|
}).then(res => res.count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleKeys(res) {
|
||||||
|
if (!Array.isArray(res.devices)) {
|
||||||
|
throw new Error('Invalid response');
|
||||||
|
}
|
||||||
|
res.identityKey = _base64ToBytes(res.identityKey);
|
||||||
|
res.devices.forEach(device => {
|
||||||
|
if (
|
||||||
|
!_validateResponse(device, { signedPreKey: 'object' }) ||
|
||||||
|
!_validateResponse(device.signedPreKey, {
|
||||||
|
publicKey: 'string',
|
||||||
|
signature: 'string',
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
throw new Error('Invalid signedPreKey');
|
||||||
|
}
|
||||||
|
if (device.preKey) {
|
||||||
|
if (
|
||||||
|
!_validateResponse(device, { preKey: 'object' }) ||
|
||||||
|
!_validateResponse(device.preKey, { publicKey: 'string' })
|
||||||
|
) {
|
||||||
|
throw new Error('Invalid preKey');
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
device.preKey.publicKey = _base64ToBytes(device.preKey.publicKey);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
device.signedPreKey.publicKey = _base64ToBytes(
|
||||||
|
device.signedPreKey.publicKey
|
||||||
|
);
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
device.signedPreKey.signature = _base64ToBytes(
|
||||||
|
device.signedPreKey.signature
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
function getKeysForNumber(number, deviceId = '*') {
|
function getKeysForNumber(number, deviceId = '*') {
|
||||||
return _ajax({
|
return _ajax({
|
||||||
call: 'keys',
|
call: 'keys',
|
||||||
|
@ -559,41 +653,46 @@ function initialize({ url, cdnUrl, certificateAuthority, proxyUrl }) {
|
||||||
urlParameters: `/${number}/${deviceId}`,
|
urlParameters: `/${number}/${deviceId}`,
|
||||||
responseType: 'json',
|
responseType: 'json',
|
||||||
validateResponse: { identityKey: 'string', devices: 'object' },
|
validateResponse: { identityKey: 'string', devices: 'object' },
|
||||||
}).then(res => {
|
}).then(handleKeys);
|
||||||
if (res.devices.constructor !== Array) {
|
}
|
||||||
throw new Error('Invalid response');
|
|
||||||
}
|
function getKeysForNumberUnauth(
|
||||||
res.identityKey = _base64ToBytes(res.identityKey);
|
number,
|
||||||
res.devices.forEach(device => {
|
deviceId = '*',
|
||||||
if (
|
{ accessKey } = {}
|
||||||
!_validateResponse(device, { signedPreKey: 'object' }) ||
|
) {
|
||||||
!_validateResponse(device.signedPreKey, {
|
return _ajax({
|
||||||
publicKey: 'string',
|
call: 'keys',
|
||||||
signature: 'string',
|
httpType: 'GET',
|
||||||
})
|
urlParameters: `/${number}/${deviceId}`,
|
||||||
) {
|
responseType: 'json',
|
||||||
throw new Error('Invalid signedPreKey');
|
validateResponse: { identityKey: 'string', devices: 'object' },
|
||||||
}
|
unauthenticated: true,
|
||||||
if (device.preKey) {
|
accessKey,
|
||||||
if (
|
}).then(handleKeys);
|
||||||
!_validateResponse(device, { preKey: 'object' }) ||
|
}
|
||||||
!_validateResponse(device.preKey, { publicKey: 'string' })
|
|
||||||
) {
|
function sendMessagesUnauth(
|
||||||
throw new Error('Invalid preKey');
|
destination,
|
||||||
}
|
messageArray,
|
||||||
// eslint-disable-next-line no-param-reassign
|
timestamp,
|
||||||
device.preKey.publicKey = _base64ToBytes(device.preKey.publicKey);
|
silent,
|
||||||
}
|
{ accessKey } = {}
|
||||||
// eslint-disable-next-line no-param-reassign
|
) {
|
||||||
device.signedPreKey.publicKey = _base64ToBytes(
|
const jsonData = { messages: messageArray, timestamp };
|
||||||
device.signedPreKey.publicKey
|
|
||||||
);
|
if (silent) {
|
||||||
// eslint-disable-next-line no-param-reassign
|
jsonData.silent = true;
|
||||||
device.signedPreKey.signature = _base64ToBytes(
|
}
|
||||||
device.signedPreKey.signature
|
|
||||||
);
|
return _ajax({
|
||||||
});
|
call: 'messages',
|
||||||
return res;
|
httpType: 'PUT',
|
||||||
|
urlParameters: `/${destination}`,
|
||||||
|
jsonData,
|
||||||
|
responseType: 'json',
|
||||||
|
unauthenticated: true,
|
||||||
|
accessKey,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,8 @@
|
||||||
btoa,
|
btoa,
|
||||||
getString,
|
getString,
|
||||||
libphonenumber,
|
libphonenumber,
|
||||||
Event
|
Event,
|
||||||
|
ConversationController
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* eslint-disable more/no-then */
|
/* eslint-disable more/no-then */
|
||||||
|
@ -52,19 +53,29 @@
|
||||||
const confirmKeys = this.confirmKeys.bind(this);
|
const confirmKeys = this.confirmKeys.bind(this);
|
||||||
const registrationDone = this.registrationDone.bind(this);
|
const registrationDone = this.registrationDone.bind(this);
|
||||||
return this.queueTask(() =>
|
return this.queueTask(() =>
|
||||||
libsignal.KeyHelper.generateIdentityKeyPair().then(identityKeyPair => {
|
libsignal.KeyHelper.generateIdentityKeyPair().then(
|
||||||
const profileKey = textsecure.crypto.getRandomBytes(32);
|
async identityKeyPair => {
|
||||||
return createAccount(
|
const profileKey = textsecure.crypto.getRandomBytes(32);
|
||||||
number,
|
const accessKey = await window.Signal.Crypto.deriveAccessKey(
|
||||||
verificationCode,
|
profileKey
|
||||||
identityKeyPair,
|
);
|
||||||
profileKey
|
|
||||||
)
|
return createAccount(
|
||||||
.then(clearSessionsAndPreKeys)
|
number,
|
||||||
.then(generateKeys)
|
verificationCode,
|
||||||
.then(keys => registerKeys(keys).then(() => confirmKeys(keys)))
|
identityKeyPair,
|
||||||
.then(registrationDone);
|
profileKey,
|
||||||
})
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
{ accessKey }
|
||||||
|
)
|
||||||
|
.then(clearSessionsAndPreKeys)
|
||||||
|
.then(generateKeys)
|
||||||
|
.then(keys => registerKeys(keys).then(() => confirmKeys(keys)))
|
||||||
|
.then(() => registrationDone(number));
|
||||||
|
}
|
||||||
|
)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
registerSecondDevice(setProvisioningUrl, confirmNumber, progressCallback) {
|
registerSecondDevice(setProvisioningUrl, confirmNumber, progressCallback) {
|
||||||
|
@ -147,7 +158,9 @@
|
||||||
confirmKeys(keys)
|
confirmKeys(keys)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.then(registrationDone);
|
.then(() =>
|
||||||
|
registrationDone(provisionMessage.number)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -185,8 +198,6 @@
|
||||||
const store = textsecure.storage.protocol;
|
const store = textsecure.storage.protocol;
|
||||||
const { server, cleanSignedPreKeys } = this;
|
const { server, cleanSignedPreKeys } = this;
|
||||||
|
|
||||||
// TODO: harden this against missing identity key? Otherwise, we get
|
|
||||||
// retries every five seconds.
|
|
||||||
return store
|
return store
|
||||||
.getIdentityKeyPair()
|
.getIdentityKeyPair()
|
||||||
.then(
|
.then(
|
||||||
|
@ -196,6 +207,8 @@
|
||||||
signedKeyId
|
signedKeyId
|
||||||
),
|
),
|
||||||
() => {
|
() => {
|
||||||
|
// We swallow any error here, because we don't want to get into
|
||||||
|
// a loop of repeated retries.
|
||||||
window.log.error(
|
window.log.error(
|
||||||
'Failed to get identity key. Canceling key rotation.'
|
'Failed to get identity key. Canceling key rotation.'
|
||||||
);
|
);
|
||||||
|
@ -329,8 +342,10 @@
|
||||||
profileKey,
|
profileKey,
|
||||||
deviceName,
|
deviceName,
|
||||||
userAgent,
|
userAgent,
|
||||||
readReceipts
|
readReceipts,
|
||||||
|
options = {}
|
||||||
) {
|
) {
|
||||||
|
const { accessKey } = options;
|
||||||
const signalingKey = libsignal.crypto.getRandomBytes(32 + 20);
|
const signalingKey = libsignal.crypto.getRandomBytes(32 + 20);
|
||||||
let password = btoa(getString(libsignal.crypto.getRandomBytes(16)));
|
let password = btoa(getString(libsignal.crypto.getRandomBytes(16)));
|
||||||
password = password.substring(0, password.length - 2);
|
password = password.substring(0, password.length - 2);
|
||||||
|
@ -345,7 +360,8 @@
|
||||||
password,
|
password,
|
||||||
signalingKey,
|
signalingKey,
|
||||||
registrationId,
|
registrationId,
|
||||||
deviceName
|
deviceName,
|
||||||
|
{ accessKey }
|
||||||
)
|
)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (previousNumber && previousNumber !== number) {
|
if (previousNumber && previousNumber !== number) {
|
||||||
|
@ -499,8 +515,12 @@
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
registrationDone() {
|
async registrationDone(number) {
|
||||||
window.log.info('registration done');
|
window.log.info('registration done');
|
||||||
|
|
||||||
|
// Ensure that we always have a conversation for ourself
|
||||||
|
await ConversationController.getOrCreateAndWait(number, 'private');
|
||||||
|
|
||||||
this.dispatchEvent(new Event('registration'));
|
this.dispatchEvent(new Event('registration'));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -22848,7 +22848,7 @@ function _memset(ptr, value, num) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
while ((ptr|0) < (stop4|0)) {
|
while ((ptr|0) < (stop4|0)) {
|
||||||
HEAP32[ptr>>2]=value4;
|
HEAP32[((ptr)>>2)]=value4;
|
||||||
ptr = (ptr+4)|0;
|
ptr = (ptr+4)|0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22904,7 +22904,7 @@ function _memcpy(dest, src, num) {
|
||||||
num = (num-1)|0;
|
num = (num-1)|0;
|
||||||
}
|
}
|
||||||
while ((num|0) >= 4) {
|
while ((num|0) >= 4) {
|
||||||
HEAP32[dest>>2]=((HEAP32[src>>2])|0);
|
HEAP32[((dest)>>2)]=((HEAP32[((src)>>2)])|0);
|
||||||
dest = (dest+4)|0;
|
dest = (dest+4)|0;
|
||||||
src = (src+4)|0;
|
src = (src+4)|0;
|
||||||
num = (num-4)|0;
|
num = (num-4)|0;
|
||||||
|
@ -35093,7 +35093,6 @@ Curve25519Worker.prototype = {
|
||||||
if (pubKey.byteLength == 33) {
|
if (pubKey.byteLength == 33) {
|
||||||
return pubKey.slice(1);
|
return pubKey.slice(1);
|
||||||
} else {
|
} else {
|
||||||
console.error("WARNING: Expected pubkey of length 33, please report the ST and client that generated the pubkey");
|
|
||||||
return pubKey;
|
return pubKey;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35179,7 +35178,10 @@ Curve25519Worker.prototype = {
|
||||||
},
|
},
|
||||||
calculateSignature: function(privKey, message) {
|
calculateSignature: function(privKey, message) {
|
||||||
return curve.Ed25519Sign(privKey, message);
|
return curve.Ed25519Sign(privKey, message);
|
||||||
}
|
},
|
||||||
|
validatePubKeyFormat: function(buffer) {
|
||||||
|
return validatePubKeyFormat(buffer);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35272,10 +35274,6 @@ 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) {
|
||||||
if (salt.byteLength != 32) {
|
|
||||||
throw new Error("Got salt of incorrect length");
|
|
||||||
}
|
|
||||||
|
|
||||||
return Internal.crypto.HKDF(input, salt, util.toArrayBuffer(info));
|
return Internal.crypto.HKDF(input, salt, util.toArrayBuffer(info));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -35460,7 +35458,7 @@ Internal.protoText = function() {
|
||||||
/* vim: ts=4:sw=4 */
|
/* vim: ts=4:sw=4 */
|
||||||
var Internal = Internal || {};
|
var Internal = Internal || {};
|
||||||
|
|
||||||
Internal.protobuf = function() {
|
Internal.protobuf = (function() {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
function loadProtoBufs(filename) {
|
function loadProtoBufs(filename) {
|
||||||
|
@ -35473,7 +35471,7 @@ Internal.protobuf = function() {
|
||||||
WhisperMessage : protocolMessages.WhisperMessage,
|
WhisperMessage : protocolMessages.WhisperMessage,
|
||||||
PreKeyWhisperMessage : protocolMessages.PreKeyWhisperMessage
|
PreKeyWhisperMessage : protocolMessages.PreKeyWhisperMessage
|
||||||
};
|
};
|
||||||
}();
|
})();
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* vim: ts=4:sw=4
|
* vim: ts=4:sw=4
|
||||||
|
@ -35888,6 +35886,7 @@ SessionBuilder.prototype = {
|
||||||
if (message.preKeyId && !preKeyPair) {
|
if (message.preKeyId && !preKeyPair) {
|
||||||
console.log('Invalid prekey id', message.preKeyId);
|
console.log('Invalid prekey id', message.preKeyId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.initSession(false, preKeyPair, signedPreKeyPair,
|
return this.initSession(false, preKeyPair, signedPreKeyPair,
|
||||||
message.identityKey.toArrayBuffer(),
|
message.identityKey.toArrayBuffer(),
|
||||||
message.baseKey.toArrayBuffer(), undefined, message.registrationId
|
message.baseKey.toArrayBuffer(), undefined, message.registrationId
|
||||||
|
@ -36028,6 +36027,7 @@ SessionCipher.prototype = {
|
||||||
return Internal.SessionRecord.deserialize(serialized);
|
return Internal.SessionRecord.deserialize(serialized);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
// encoding is an optional parameter - wrap() will only translate if one is provided
|
||||||
encrypt: function(buffer, encoding) {
|
encrypt: function(buffer, encoding) {
|
||||||
buffer = dcodeIO.ByteBuffer.wrap(buffer, encoding).toArrayBuffer();
|
buffer = dcodeIO.ByteBuffer.wrap(buffer, encoding).toArrayBuffer();
|
||||||
return Internal.SessionLock.queueJobForNumber(this.remoteAddress.toString(), function() {
|
return Internal.SessionLock.queueJobForNumber(this.remoteAddress.toString(), function() {
|
||||||
|
@ -36370,6 +36370,20 @@ SessionCipher.prototype = {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
getSessionVersion: function() {
|
||||||
|
return Internal.SessionLock.queueJobForNumber(this.remoteAddress.toString(), function() {
|
||||||
|
return this.getRecord(this.remoteAddress.toString()).then(function(record) {
|
||||||
|
if (record === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
var openSession = record.getOpenSession();
|
||||||
|
if (openSession === undefined || openSession.indexInfo === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return openSession.indexInfo.baseKeyType;
|
||||||
|
});
|
||||||
|
}.bind(this));
|
||||||
|
},
|
||||||
getRemoteRegistrationId: function() {
|
getRemoteRegistrationId: function() {
|
||||||
return Internal.SessionLock.queueJobForNumber(this.remoteAddress.toString(), function() {
|
return Internal.SessionLock.queueJobForNumber(this.remoteAddress.toString(), function() {
|
||||||
return this.getRecord(this.remoteAddress.toString()).then(function(record) {
|
return this.getRecord(this.remoteAddress.toString()).then(function(record) {
|
||||||
|
@ -36428,6 +36442,7 @@ libsignal.SessionCipher = function(storage, remoteAddress) {
|
||||||
|
|
||||||
// returns a Promise that resolves to a ciphertext object
|
// returns a Promise that resolves to a ciphertext object
|
||||||
this.encrypt = cipher.encrypt.bind(cipher);
|
this.encrypt = cipher.encrypt.bind(cipher);
|
||||||
|
this.getRecord = cipher.getRecord.bind(cipher);
|
||||||
|
|
||||||
// returns a Promise that inits a session if necessary and resolves
|
// returns a Promise that inits a session if necessary and resolves
|
||||||
// to a decrypted plaintext array buffer
|
// to a decrypted plaintext array buffer
|
||||||
|
|
|
@ -123,6 +123,13 @@ function MessageReceiver(username, password, signalingKey, options = {}) {
|
||||||
this.password = password;
|
this.password = password;
|
||||||
this.server = WebAPI.connect({ username, password });
|
this.server = WebAPI.connect({ username, password });
|
||||||
|
|
||||||
|
if (!options.serverTrustRoot) {
|
||||||
|
throw new Error('Server trust root is required!');
|
||||||
|
}
|
||||||
|
this.serverTrustRoot = window.Signal.Crypto.base64ToArrayBuffer(
|
||||||
|
options.serverTrustRoot
|
||||||
|
);
|
||||||
|
|
||||||
const address = libsignal.SignalProtocolAddress.fromString(username);
|
const address = libsignal.SignalProtocolAddress.fromString(username);
|
||||||
this.number = address.getName();
|
this.number = address.getName();
|
||||||
this.deviceId = address.getDeviceId();
|
this.deviceId = address.getDeviceId();
|
||||||
|
@ -279,6 +286,8 @@ MessageReceiver.prototype.extend({
|
||||||
return request.respond(200, 'OK');
|
return request.respond(200, 'OK');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
envelope.id = envelope.serverGuid || window.getGuid();
|
||||||
|
|
||||||
return this.addToCache(envelope, plaintext).then(
|
return this.addToCache(envelope, plaintext).then(
|
||||||
async () => {
|
async () => {
|
||||||
request.respond(200, 'OK');
|
request.respond(200, 'OK');
|
||||||
|
@ -437,9 +446,13 @@ MessageReceiver.prototype.extend({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getEnvelopeId(envelope) {
|
getEnvelopeId(envelope) {
|
||||||
return `${envelope.source}.${
|
if (envelope.source) {
|
||||||
envelope.sourceDevice
|
return `${envelope.source}.${
|
||||||
} ${envelope.timestamp.toNumber()}`;
|
envelope.sourceDevice
|
||||||
|
} ${envelope.timestamp.toNumber()} (${envelope.id})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return envelope.id;
|
||||||
},
|
},
|
||||||
async getAllFromCache() {
|
async getAllFromCache() {
|
||||||
window.log.info('getAllFromCache');
|
window.log.info('getAllFromCache');
|
||||||
|
@ -482,7 +495,7 @@ MessageReceiver.prototype.extend({
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
async addToCache(envelope, plaintext) {
|
async addToCache(envelope, plaintext) {
|
||||||
const id = this.getEnvelopeId(envelope);
|
const { id } = envelope;
|
||||||
const data = {
|
const data = {
|
||||||
id,
|
id,
|
||||||
version: 2,
|
version: 2,
|
||||||
|
@ -493,7 +506,7 @@ MessageReceiver.prototype.extend({
|
||||||
return textsecure.storage.unprocessed.add(data);
|
return textsecure.storage.unprocessed.add(data);
|
||||||
},
|
},
|
||||||
async updateCache(envelope, plaintext) {
|
async updateCache(envelope, plaintext) {
|
||||||
const id = this.getEnvelopeId(envelope);
|
const { id } = envelope;
|
||||||
const item = await textsecure.storage.unprocessed.get(id);
|
const item = await textsecure.storage.unprocessed.get(id);
|
||||||
if (!item) {
|
if (!item) {
|
||||||
window.log.error(
|
window.log.error(
|
||||||
|
@ -517,11 +530,11 @@ MessageReceiver.prototype.extend({
|
||||||
return textsecure.storage.unprocessed.save(item.attributes);
|
return textsecure.storage.unprocessed.save(item.attributes);
|
||||||
},
|
},
|
||||||
removeFromCache(envelope) {
|
removeFromCache(envelope) {
|
||||||
const id = this.getEnvelopeId(envelope);
|
const { id } = envelope;
|
||||||
return textsecure.storage.unprocessed.remove(id);
|
return textsecure.storage.unprocessed.remove(id);
|
||||||
},
|
},
|
||||||
queueDecryptedEnvelope(envelope, plaintext) {
|
queueDecryptedEnvelope(envelope, plaintext) {
|
||||||
const id = this.getEnvelopeId(envelope);
|
const { id } = envelope;
|
||||||
window.log.info('queueing decrypted envelope', id);
|
window.log.info('queueing decrypted envelope', id);
|
||||||
|
|
||||||
const task = this.handleDecryptedEnvelope.bind(this, envelope, plaintext);
|
const task = this.handleDecryptedEnvelope.bind(this, envelope, plaintext);
|
||||||
|
@ -626,6 +639,8 @@ MessageReceiver.prototype.extend({
|
||||||
return plaintext;
|
return plaintext;
|
||||||
},
|
},
|
||||||
decrypt(envelope, ciphertext) {
|
decrypt(envelope, ciphertext) {
|
||||||
|
const { serverTrustRoot } = this;
|
||||||
|
|
||||||
let promise;
|
let promise;
|
||||||
const address = new libsignal.SignalProtocolAddress(
|
const address = new libsignal.SignalProtocolAddress(
|
||||||
envelope.source,
|
envelope.source,
|
||||||
|
@ -646,6 +661,14 @@ MessageReceiver.prototype.extend({
|
||||||
address,
|
address,
|
||||||
options
|
options
|
||||||
);
|
);
|
||||||
|
const secretSessionCipher = new window.Signal.Metadata.SecretSessionCipher(
|
||||||
|
textsecure.storage.protocol
|
||||||
|
);
|
||||||
|
|
||||||
|
const me = {
|
||||||
|
number: ourNumber,
|
||||||
|
deviceId: parseInt(textsecure.storage.user.getDeviceId(), 10),
|
||||||
|
};
|
||||||
|
|
||||||
switch (envelope.type) {
|
switch (envelope.type) {
|
||||||
case textsecure.protobuf.Envelope.Type.CIPHERTEXT:
|
case textsecure.protobuf.Envelope.Type.CIPHERTEXT:
|
||||||
|
@ -662,13 +685,76 @@ MessageReceiver.prototype.extend({
|
||||||
address
|
address
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER:
|
||||||
|
window.log.info('received unidentified sender message');
|
||||||
|
promise = secretSessionCipher
|
||||||
|
.decrypt(
|
||||||
|
window.Signal.Metadata.createCertificateValidator(serverTrustRoot),
|
||||||
|
ciphertext.toArrayBuffer(),
|
||||||
|
Math.min(
|
||||||
|
envelope.serverTimestamp
|
||||||
|
? envelope.serverTimestamp.toNumber()
|
||||||
|
: Date.now(),
|
||||||
|
Date.now()
|
||||||
|
),
|
||||||
|
me
|
||||||
|
)
|
||||||
|
.then(
|
||||||
|
result => {
|
||||||
|
const { isMe, sender, content } = result;
|
||||||
|
|
||||||
|
// We need to drop incoming messages from ourself since server can't
|
||||||
|
// do it for us
|
||||||
|
if (isMe) {
|
||||||
|
return { isMe: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Here we take this sender information and attach it back to the envelope
|
||||||
|
// to make the rest of the app work properly.
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
envelope.source = sender.getName();
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
envelope.sourceDevice = sender.getDeviceId();
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
envelope.unidentifiedDeliveryReceived = true;
|
||||||
|
|
||||||
|
// Return just the content because that matches the signature of the other
|
||||||
|
// decrypt methods used above.
|
||||||
|
return this.unpad(content);
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
const { sender } = error || {};
|
||||||
|
|
||||||
|
if (sender) {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
envelope.source = sender.getName();
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
envelope.sourceDevice = sender.getDeviceId();
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
envelope.unidentifiedDeliveryReceived = true;
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.removeFromCache().then(() => {
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
promise = Promise.reject(new Error('Unknown message type'));
|
promise = Promise.reject(new Error('Unknown message type'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return promise
|
return promise
|
||||||
.then(plaintext =>
|
.then(plaintext => {
|
||||||
this.updateCache(envelope, plaintext).then(
|
const { isMe } = plaintext || {};
|
||||||
|
if (isMe) {
|
||||||
|
return this.removeFromCache(envelope);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.updateCache(envelope, plaintext).then(
|
||||||
() => plaintext,
|
() => plaintext,
|
||||||
error => {
|
error => {
|
||||||
window.log.error(
|
window.log.error(
|
||||||
|
@ -677,8 +763,8 @@ MessageReceiver.prototype.extend({
|
||||||
);
|
);
|
||||||
return plaintext;
|
return plaintext;
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
)
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
let errorToThrow = error;
|
let errorToThrow = error;
|
||||||
|
|
||||||
|
@ -720,13 +806,14 @@ MessageReceiver.prototype.extend({
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleSentMessage(
|
handleSentMessage(envelope, sentContainer, msg) {
|
||||||
envelope,
|
const {
|
||||||
destination,
|
destination,
|
||||||
timestamp,
|
timestamp,
|
||||||
msg,
|
expirationStartTimestamp,
|
||||||
expirationStartTimestamp
|
unidentifiedStatus,
|
||||||
) {
|
} = sentContainer;
|
||||||
|
|
||||||
let p = Promise.resolve();
|
let p = Promise.resolve();
|
||||||
// eslint-disable-next-line no-bitwise
|
// eslint-disable-next-line no-bitwise
|
||||||
if (msg.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) {
|
if (msg.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) {
|
||||||
|
@ -757,6 +844,7 @@ MessageReceiver.prototype.extend({
|
||||||
destination,
|
destination,
|
||||||
timestamp: timestamp.toNumber(),
|
timestamp: timestamp.toNumber(),
|
||||||
device: envelope.sourceDevice,
|
device: envelope.sourceDevice,
|
||||||
|
unidentifiedStatus,
|
||||||
message,
|
message,
|
||||||
};
|
};
|
||||||
if (expirationStartTimestamp) {
|
if (expirationStartTimestamp) {
|
||||||
|
@ -799,6 +887,7 @@ MessageReceiver.prototype.extend({
|
||||||
sourceDevice: envelope.sourceDevice,
|
sourceDevice: envelope.sourceDevice,
|
||||||
timestamp: envelope.timestamp.toNumber(),
|
timestamp: envelope.timestamp.toNumber(),
|
||||||
receivedAt: envelope.receivedAt,
|
receivedAt: envelope.receivedAt,
|
||||||
|
unidentifiedDeliveryReceived: envelope.unidentifiedDeliveryReceived,
|
||||||
message,
|
message,
|
||||||
};
|
};
|
||||||
return this.dispatchAndWait(ev);
|
return this.dispatchAndWait(ev);
|
||||||
|
@ -806,18 +895,26 @@ MessageReceiver.prototype.extend({
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
handleLegacyMessage(envelope) {
|
handleLegacyMessage(envelope) {
|
||||||
return this.decrypt(envelope, envelope.legacyMessage).then(plaintext =>
|
return this.decrypt(envelope, envelope.legacyMessage).then(plaintext => {
|
||||||
this.innerHandleLegacyMessage(envelope, plaintext)
|
if (!plaintext) {
|
||||||
);
|
window.log.warn('handleLegacyMessage: plaintext was falsey');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.innerHandleLegacyMessage(envelope, plaintext);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
innerHandleLegacyMessage(envelope, plaintext) {
|
innerHandleLegacyMessage(envelope, plaintext) {
|
||||||
const message = textsecure.protobuf.DataMessage.decode(plaintext);
|
const message = textsecure.protobuf.DataMessage.decode(plaintext);
|
||||||
return this.handleDataMessage(envelope, message);
|
return this.handleDataMessage(envelope, message);
|
||||||
},
|
},
|
||||||
handleContentMessage(envelope) {
|
handleContentMessage(envelope) {
|
||||||
return this.decrypt(envelope, envelope.content).then(plaintext =>
|
return this.decrypt(envelope, envelope.content).then(plaintext => {
|
||||||
this.innerHandleContentMessage(envelope, plaintext)
|
if (!plaintext) {
|
||||||
);
|
window.log.warn('handleContentMessage: plaintext was falsey');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.innerHandleContentMessage(envelope, plaintext);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
innerHandleContentMessage(envelope, plaintext) {
|
innerHandleContentMessage(envelope, plaintext) {
|
||||||
const content = textsecure.protobuf.Content.decode(plaintext);
|
const content = textsecure.protobuf.Content.decode(plaintext);
|
||||||
|
@ -895,13 +992,7 @@ MessageReceiver.prototype.extend({
|
||||||
'from',
|
'from',
|
||||||
this.getEnvelopeId(envelope)
|
this.getEnvelopeId(envelope)
|
||||||
);
|
);
|
||||||
return this.handleSentMessage(
|
return this.handleSentMessage(envelope, sentMessage, sentMessage.message);
|
||||||
envelope,
|
|
||||||
sentMessage.destination,
|
|
||||||
sentMessage.timestamp,
|
|
||||||
sentMessage.message,
|
|
||||||
sentMessage.expirationStartTimestamp
|
|
||||||
);
|
|
||||||
} else if (syncMessage.contacts) {
|
} else if (syncMessage.contacts) {
|
||||||
return this.handleContacts(envelope, syncMessage.contacts);
|
return this.handleContacts(envelope, syncMessage.contacts);
|
||||||
} else if (syncMessage.groups) {
|
} else if (syncMessage.groups) {
|
||||||
|
@ -922,11 +1013,10 @@ MessageReceiver.prototype.extend({
|
||||||
throw new Error('Got empty SyncMessage');
|
throw new Error('Got empty SyncMessage');
|
||||||
},
|
},
|
||||||
handleConfiguration(envelope, configuration) {
|
handleConfiguration(envelope, configuration) {
|
||||||
|
window.log.info('got configuration sync message');
|
||||||
const ev = new Event('configuration');
|
const ev = new Event('configuration');
|
||||||
ev.confirm = this.removeFromCache.bind(this, envelope);
|
ev.confirm = this.removeFromCache.bind(this, envelope);
|
||||||
ev.configuration = {
|
ev.configuration = configuration;
|
||||||
readReceipts: configuration.readReceipts,
|
|
||||||
};
|
|
||||||
return this.dispatchAndWait(ev);
|
return this.dispatchAndWait(ev);
|
||||||
},
|
},
|
||||||
handleVerified(envelope, verified) {
|
handleVerified(envelope, verified) {
|
||||||
|
@ -1075,102 +1165,6 @@ MessageReceiver.prototype.extend({
|
||||||
.then(decryptAttachment)
|
.then(decryptAttachment)
|
||||||
.then(updateAttachment);
|
.then(updateAttachment);
|
||||||
},
|
},
|
||||||
validateRetryContentMessage(content) {
|
|
||||||
// Today this is only called for incoming identity key errors, so it can't be a sync
|
|
||||||
// message.
|
|
||||||
if (content.syncMessage) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We want at least one field set, but not more than one
|
|
||||||
let count = 0;
|
|
||||||
count += content.dataMessage ? 1 : 0;
|
|
||||||
count += content.callMessage ? 1 : 0;
|
|
||||||
count += content.nullMessage ? 1 : 0;
|
|
||||||
if (count !== 1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// It's most likely that dataMessage will be populated, so we look at it in detail
|
|
||||||
const data = content.dataMessage;
|
|
||||||
if (
|
|
||||||
data &&
|
|
||||||
!data.attachments.length &&
|
|
||||||
!data.body &&
|
|
||||||
!data.expireTimer &&
|
|
||||||
!data.flags &&
|
|
||||||
!data.group
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
tryMessageAgain(from, ciphertext, message) {
|
|
||||||
const address = libsignal.SignalProtocolAddress.fromString(from);
|
|
||||||
const sentAt = message.sent_at || Date.now();
|
|
||||||
const receivedAt = message.received_at || Date.now();
|
|
||||||
|
|
||||||
const ourNumber = textsecure.storage.user.getNumber();
|
|
||||||
const number = address.getName();
|
|
||||||
const device = address.getDeviceId();
|
|
||||||
const options = {};
|
|
||||||
|
|
||||||
// No limit on message keys if we're communicating with our other devices
|
|
||||||
if (ourNumber === number) {
|
|
||||||
options.messageKeysLimit = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sessionCipher = new libsignal.SessionCipher(
|
|
||||||
textsecure.storage.protocol,
|
|
||||||
address,
|
|
||||||
options
|
|
||||||
);
|
|
||||||
window.log.info('retrying prekey whisper message');
|
|
||||||
return this.decryptPreKeyWhisperMessage(
|
|
||||||
ciphertext,
|
|
||||||
sessionCipher,
|
|
||||||
address
|
|
||||||
).then(plaintext => {
|
|
||||||
const envelope = {
|
|
||||||
source: number,
|
|
||||||
sourceDevice: device,
|
|
||||||
receivedAt,
|
|
||||||
timestamp: {
|
|
||||||
toNumber() {
|
|
||||||
return sentAt;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Before June, all incoming messages were still DataMessage:
|
|
||||||
// - iOS: Michael Kirk says that they were sending Legacy messages until June
|
|
||||||
// - Desktop: https://github.com/signalapp/Signal-Desktop/commit/e8548879db405d9bcd78b82a456ad8d655592c0f
|
|
||||||
// - Android: https://github.com/signalapp/libsignal-service-java/commit/61a75d023fba950ff9b4c75a249d1a3408e12958
|
|
||||||
//
|
|
||||||
// var d = new Date('2017-06-01T07:00:00.000Z');
|
|
||||||
// d.getTime();
|
|
||||||
const startOfJune = 1496300400000;
|
|
||||||
if (sentAt < startOfJune) {
|
|
||||||
return this.innerHandleLegacyMessage(envelope, plaintext);
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is ugly. But we don't know what kind of proto we need to decode...
|
|
||||||
try {
|
|
||||||
// Simply decoding as a Content message may throw
|
|
||||||
const content = textsecure.protobuf.Content.decode(plaintext);
|
|
||||||
|
|
||||||
// But it might also result in an invalid object, so we try to detect that
|
|
||||||
if (this.validateRetryContentMessage(content)) {
|
|
||||||
return this.innerHandleContentMessage(envelope, plaintext);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
return this.innerHandleLegacyMessage(envelope, plaintext);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.innerHandleLegacyMessage(envelope, plaintext);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
async handleEndSession(number) {
|
async handleEndSession(number) {
|
||||||
window.log.info('got end session');
|
window.log.info('got end session');
|
||||||
const deviceIds = await textsecure.storage.protocol.getDeviceIds(number);
|
const deviceIds = await textsecure.storage.protocol.getDeviceIds(number);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* global textsecure, libsignal, window, btoa */
|
/* global textsecure, libsignal, window, btoa, _ */
|
||||||
|
|
||||||
/* eslint-disable more/no-then */
|
/* eslint-disable more/no-then */
|
||||||
|
|
||||||
|
@ -8,7 +8,8 @@ function OutgoingMessage(
|
||||||
numbers,
|
numbers,
|
||||||
message,
|
message,
|
||||||
silent,
|
silent,
|
||||||
callback
|
callback,
|
||||||
|
options = {}
|
||||||
) {
|
) {
|
||||||
if (message instanceof textsecure.protobuf.DataMessage) {
|
if (message instanceof textsecure.protobuf.DataMessage) {
|
||||||
const content = new textsecure.protobuf.Content();
|
const content = new textsecure.protobuf.Content();
|
||||||
|
@ -26,6 +27,12 @@ function OutgoingMessage(
|
||||||
this.numbersCompleted = 0;
|
this.numbersCompleted = 0;
|
||||||
this.errors = [];
|
this.errors = [];
|
||||||
this.successfulNumbers = [];
|
this.successfulNumbers = [];
|
||||||
|
this.failoverNumbers = [];
|
||||||
|
this.unidentifiedDeliveries = [];
|
||||||
|
|
||||||
|
const { numberInfo, senderCertificate } = options;
|
||||||
|
this.numberInfo = numberInfo;
|
||||||
|
this.senderCertificate = senderCertificate;
|
||||||
}
|
}
|
||||||
|
|
||||||
OutgoingMessage.prototype = {
|
OutgoingMessage.prototype = {
|
||||||
|
@ -35,7 +42,9 @@ OutgoingMessage.prototype = {
|
||||||
if (this.numbersCompleted >= this.numbers.length) {
|
if (this.numbersCompleted >= this.numbers.length) {
|
||||||
this.callback({
|
this.callback({
|
||||||
successfulNumbers: this.successfulNumbers,
|
successfulNumbers: this.successfulNumbers,
|
||||||
|
failoverNumbers: this.failoverNumbers,
|
||||||
errors: this.errors,
|
errors: this.errors,
|
||||||
|
unidentifiedDeliveries: this.unidentifiedDeliveries,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -57,7 +66,7 @@ OutgoingMessage.prototype = {
|
||||||
this.errors[this.errors.length] = error;
|
this.errors[this.errors.length] = error;
|
||||||
this.numberCompleted();
|
this.numberCompleted();
|
||||||
},
|
},
|
||||||
reloadDevicesAndSend(number, recurse) {
|
reloadDevicesAndSend(number, recurse, failover) {
|
||||||
return () =>
|
return () =>
|
||||||
textsecure.storage.protocol.getDeviceIds(number).then(deviceIds => {
|
textsecure.storage.protocol.getDeviceIds(number).then(deviceIds => {
|
||||||
if (deviceIds.length === 0) {
|
if (deviceIds.length === 0) {
|
||||||
|
@ -67,7 +76,7 @@ OutgoingMessage.prototype = {
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return this.doSendMessage(number, deviceIds, recurse);
|
return this.doSendMessage(number, deviceIds, recurse, failover);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -109,51 +118,108 @@ OutgoingMessage.prototype = {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { numberInfo } = this;
|
||||||
|
const info = numberInfo && numberInfo[number] ? numberInfo[number] : {};
|
||||||
|
const { accessKey } = info || {};
|
||||||
|
|
||||||
if (updateDevices === undefined) {
|
if (updateDevices === undefined) {
|
||||||
return this.server.getKeysForNumber(number).then(handleResult);
|
if (accessKey) {
|
||||||
}
|
return this.server
|
||||||
let promise = Promise.resolve();
|
.getKeysForNumberUnauth(number, '*', { accessKey })
|
||||||
updateDevices.forEach(device => {
|
.catch(error => {
|
||||||
promise = promise.then(() =>
|
if (error.code === 401 || error.code === 403) {
|
||||||
this.server
|
if (this.failoverNumbers.indexOf(number) === -1) {
|
||||||
.getKeysForNumber(number, device)
|
this.failoverNumbers.push(number);
|
||||||
.then(handleResult)
|
|
||||||
.catch(e => {
|
|
||||||
if (e.name === 'HTTPError' && e.code === 404) {
|
|
||||||
if (device !== 1) {
|
|
||||||
return this.removeDeviceIdsForNumber(number, [device]);
|
|
||||||
}
|
}
|
||||||
throw new textsecure.UnregisteredUserError(number, e);
|
return this.server.getKeysForNumber(number, '*');
|
||||||
} else {
|
|
||||||
throw e;
|
|
||||||
}
|
}
|
||||||
|
throw error;
|
||||||
})
|
})
|
||||||
);
|
.then(handleResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.server.getKeysForNumber(number, '*').then(handleResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
let promise = Promise.resolve();
|
||||||
|
updateDevices.forEach(deviceId => {
|
||||||
|
promise = promise.then(() => {
|
||||||
|
let innerPromise;
|
||||||
|
|
||||||
|
if (accessKey) {
|
||||||
|
innerPromise = this.server
|
||||||
|
.getKeysForNumberUnauth(number, deviceId, { accessKey })
|
||||||
|
.then(handleResult)
|
||||||
|
.catch(error => {
|
||||||
|
if (error.code === 401 || error.code === 403) {
|
||||||
|
if (this.failoverNumbers.indexOf(number) === -1) {
|
||||||
|
this.failoverNumbers.push(number);
|
||||||
|
}
|
||||||
|
return this.server
|
||||||
|
.getKeysForNumber(number, deviceId)
|
||||||
|
.then(handleResult);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
innerPromise = this.server
|
||||||
|
.getKeysForNumber(number, deviceId)
|
||||||
|
.then(handleResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
return innerPromise.catch(e => {
|
||||||
|
if (e.name === 'HTTPError' && e.code === 404) {
|
||||||
|
if (deviceId !== 1) {
|
||||||
|
return this.removeDeviceIdsForNumber(number, [deviceId]);
|
||||||
|
}
|
||||||
|
throw new textsecure.UnregisteredUserError(number, e);
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return promise;
|
return promise;
|
||||||
},
|
},
|
||||||
|
|
||||||
transmitMessage(number, jsonData, timestamp) {
|
transmitMessage(number, jsonData, timestamp, { accessKey } = {}) {
|
||||||
return this.server
|
let promise;
|
||||||
.sendMessages(number, jsonData, timestamp, this.silent)
|
|
||||||
.catch(e => {
|
if (accessKey) {
|
||||||
if (e.name === 'HTTPError' && (e.code !== 409 && e.code !== 410)) {
|
promise = this.server.sendMessagesUnauth(
|
||||||
// 409 and 410 should bubble and be handled by doSendMessage
|
number,
|
||||||
// 404 should throw UnregisteredUserError
|
jsonData,
|
||||||
// all other network errors can be retried later.
|
timestamp,
|
||||||
if (e.code === 404) {
|
this.silent,
|
||||||
throw new textsecure.UnregisteredUserError(number, e);
|
{ accessKey }
|
||||||
}
|
);
|
||||||
throw new textsecure.SendMessageNetworkError(
|
} else {
|
||||||
number,
|
promise = this.server.sendMessages(
|
||||||
jsonData,
|
number,
|
||||||
e,
|
jsonData,
|
||||||
timestamp
|
timestamp,
|
||||||
);
|
this.silent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise.catch(e => {
|
||||||
|
if (e.name === 'HTTPError' && (e.code !== 409 && e.code !== 410)) {
|
||||||
|
// 409 and 410 should bubble and be handled by doSendMessage
|
||||||
|
// 404 should throw UnregisteredUserError
|
||||||
|
// all other network errors can be retried later.
|
||||||
|
if (e.code === 404) {
|
||||||
|
throw new textsecure.UnregisteredUserError(number, e);
|
||||||
}
|
}
|
||||||
throw e;
|
throw new textsecure.SendMessageNetworkError(
|
||||||
});
|
number,
|
||||||
|
jsonData,
|
||||||
|
e,
|
||||||
|
timestamp
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getPaddedMessageLength(messageLength) {
|
getPaddedMessageLength(messageLength) {
|
||||||
|
@ -179,15 +245,42 @@ OutgoingMessage.prototype = {
|
||||||
return this.plaintext;
|
return this.plaintext;
|
||||||
},
|
},
|
||||||
|
|
||||||
doSendMessage(number, deviceIds, recurse) {
|
doSendMessage(number, deviceIds, recurse, failover) {
|
||||||
const ciphers = {};
|
const ciphers = {};
|
||||||
const plaintext = this.getPlaintext();
|
const plaintext = this.getPlaintext();
|
||||||
|
|
||||||
|
const { numberInfo, senderCertificate } = this;
|
||||||
|
const info = numberInfo && numberInfo[number] ? numberInfo[number] : {};
|
||||||
|
const { accessKey } = info || {};
|
||||||
|
|
||||||
|
if (accessKey && !senderCertificate) {
|
||||||
|
return Promise.reject(
|
||||||
|
new Error(
|
||||||
|
'OutgoingMessage.doSendMessage: accessKey was provided, but senderCertificate was not'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If failover is true, we don't send an unidentified sender message
|
||||||
|
const sealedSender = Boolean(!failover && accessKey && senderCertificate);
|
||||||
|
|
||||||
|
// We don't send to ourselves if unless sealedSender is enabled
|
||||||
|
const ourNumber = textsecure.storage.user.getNumber();
|
||||||
|
const ourDeviceId = textsecure.storage.user.getDeviceId();
|
||||||
|
if (number === ourNumber && !sealedSender) {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
deviceIds = _.reject(
|
||||||
|
deviceIds,
|
||||||
|
deviceId =>
|
||||||
|
// because we store our own device ID as a string at least sometimes
|
||||||
|
deviceId === ourDeviceId || deviceId === parseInt(ourDeviceId, 10)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
deviceIds.map(deviceId => {
|
deviceIds.map(async deviceId => {
|
||||||
const address = new libsignal.SignalProtocolAddress(number, deviceId);
|
const address = new libsignal.SignalProtocolAddress(number, deviceId);
|
||||||
|
|
||||||
const ourNumber = textsecure.storage.user.getNumber();
|
|
||||||
const options = {};
|
const options = {};
|
||||||
|
|
||||||
// No limit on message keys if we're communicating with our other devices
|
// No limit on message keys if we're communicating with our other devices
|
||||||
|
@ -195,26 +288,80 @@ OutgoingMessage.prototype = {
|
||||||
options.messageKeysLimit = false;
|
options.messageKeysLimit = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If failover is true, we don't send an unidentified sender message
|
||||||
|
if (sealedSender) {
|
||||||
|
const secretSessionCipher = new window.Signal.Metadata.SecretSessionCipher(
|
||||||
|
textsecure.storage.protocol
|
||||||
|
);
|
||||||
|
ciphers[address.getDeviceId()] = secretSessionCipher;
|
||||||
|
|
||||||
|
const ciphertext = await secretSessionCipher.encrypt(
|
||||||
|
address,
|
||||||
|
senderCertificate,
|
||||||
|
plaintext
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
type: textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER,
|
||||||
|
destinationDeviceId: address.getDeviceId(),
|
||||||
|
destinationRegistrationId: await secretSessionCipher.getRemoteRegistrationId(
|
||||||
|
address
|
||||||
|
),
|
||||||
|
content: window.Signal.Crypto.arrayBufferToBase64(ciphertext),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const sessionCipher = new libsignal.SessionCipher(
|
const sessionCipher = new libsignal.SessionCipher(
|
||||||
textsecure.storage.protocol,
|
textsecure.storage.protocol,
|
||||||
address,
|
address,
|
||||||
options
|
options
|
||||||
);
|
);
|
||||||
ciphers[address.getDeviceId()] = sessionCipher;
|
ciphers[address.getDeviceId()] = sessionCipher;
|
||||||
return sessionCipher.encrypt(plaintext).then(ciphertext => ({
|
|
||||||
|
const ciphertext = await sessionCipher.encrypt(plaintext);
|
||||||
|
return {
|
||||||
type: ciphertext.type,
|
type: ciphertext.type,
|
||||||
destinationDeviceId: address.getDeviceId(),
|
destinationDeviceId: address.getDeviceId(),
|
||||||
destinationRegistrationId: ciphertext.registrationId,
|
destinationRegistrationId: ciphertext.registrationId,
|
||||||
content: btoa(ciphertext.body),
|
content: btoa(ciphertext.body),
|
||||||
}));
|
};
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.then(jsonData =>
|
.then(jsonData => {
|
||||||
this.transmitMessage(number, jsonData, this.timestamp).then(() => {
|
if (sealedSender) {
|
||||||
this.successfulNumbers[this.successfulNumbers.length] = number;
|
return this.transmitMessage(number, jsonData, this.timestamp, {
|
||||||
this.numberCompleted();
|
accessKey,
|
||||||
})
|
}).then(
|
||||||
)
|
() => {
|
||||||
|
this.unidentifiedDeliveries.push(number);
|
||||||
|
this.successfulNumbers.push(number);
|
||||||
|
this.numberCompleted();
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
if (error.code === 401 || error.code === 403) {
|
||||||
|
if (this.failoverNumbers.indexOf(number) === -1) {
|
||||||
|
this.failoverNumbers.push(number);
|
||||||
|
}
|
||||||
|
if (info) {
|
||||||
|
info.accessKey = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set final parameter to true to ensure we don't hit this codepath a
|
||||||
|
// second time.
|
||||||
|
return this.doSendMessage(number, deviceIds, recurse, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.transmitMessage(number, jsonData, this.timestamp).then(
|
||||||
|
() => {
|
||||||
|
this.successfulNumbers.push(number);
|
||||||
|
this.numberCompleted();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
if (
|
if (
|
||||||
error instanceof Error &&
|
error instanceof Error &&
|
||||||
|
@ -237,7 +384,9 @@ OutgoingMessage.prototype = {
|
||||||
} else {
|
} else {
|
||||||
p = Promise.all(
|
p = Promise.all(
|
||||||
error.response.staleDevices.map(deviceId =>
|
error.response.staleDevices.map(deviceId =>
|
||||||
ciphers[deviceId].closeOpenSessionForDevice()
|
ciphers[deviceId].closeOpenSessionForDevice(
|
||||||
|
new libsignal.SignalProtocolAddress(number, deviceId)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -248,7 +397,9 @@ OutgoingMessage.prototype = {
|
||||||
? error.response.staleDevices
|
? error.response.staleDevices
|
||||||
: error.response.missingDevices;
|
: error.response.missingDevices;
|
||||||
return this.getKeysForNumber(number, resetDevices).then(
|
return this.getKeysForNumber(number, resetDevices).then(
|
||||||
this.reloadDevicesAndSend(number, error.code === 409)
|
// For now, we we won't retry unidentified delivery if we get here; new
|
||||||
|
// devices could have been added which don't support it.
|
||||||
|
this.reloadDevicesAndSend(number, error.code === 409, true)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
} else if (error.message === 'Identity key changed') {
|
} else if (error.message === 'Identity key changed') {
|
||||||
|
@ -305,28 +456,28 @@ OutgoingMessage.prototype = {
|
||||||
return promise;
|
return promise;
|
||||||
},
|
},
|
||||||
|
|
||||||
sendToNumber(number) {
|
async sendToNumber(number) {
|
||||||
return this.getStaleDeviceIdsForNumber(number).then(updateDevices =>
|
try {
|
||||||
this.getKeysForNumber(number, updateDevices)
|
const updateDevices = await this.getStaleDeviceIdsForNumber(number);
|
||||||
.then(this.reloadDevicesAndSend(number, true))
|
await this.getKeysForNumber(number, updateDevices);
|
||||||
.catch(error => {
|
await this.reloadDevicesAndSend(number, true)();
|
||||||
if (error.message === 'Identity key changed') {
|
} catch (error) {
|
||||||
// eslint-disable-next-line no-param-reassign
|
if (error.message === 'Identity key changed') {
|
||||||
error = new textsecure.OutgoingIdentityKeyError(
|
// eslint-disable-next-line no-param-reassign
|
||||||
number,
|
const newError = new textsecure.OutgoingIdentityKeyError(
|
||||||
error.originalMessage,
|
number,
|
||||||
error.timestamp,
|
error.originalMessage,
|
||||||
error.identityKey
|
error.timestamp,
|
||||||
);
|
error.identityKey
|
||||||
this.registerError(number, 'Identity key changed', error);
|
);
|
||||||
} else {
|
this.registerError(number, 'Identity key changed', newError);
|
||||||
this.registerError(
|
} else {
|
||||||
number,
|
this.registerError(
|
||||||
`Failed to retrieve new device keys for number ${number}`,
|
number,
|
||||||
error
|
`Failed to retrieve new device keys for number ${number}`,
|
||||||
);
|
error
|
||||||
}
|
);
|
||||||
})
|
}
|
||||||
);
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -35,4 +35,7 @@
|
||||||
loadProtoBufs('SignalService.proto');
|
loadProtoBufs('SignalService.proto');
|
||||||
loadProtoBufs('SubProtocol.proto');
|
loadProtoBufs('SubProtocol.proto');
|
||||||
loadProtoBufs('DeviceMessages.proto');
|
loadProtoBufs('DeviceMessages.proto');
|
||||||
|
|
||||||
|
// Metadata-specific protos
|
||||||
|
loadProtoBufs('UnidentifiedDelivery.proto');
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -190,71 +190,6 @@ MessageSender.prototype = {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
retransmitMessage(number, jsonData, timestamp) {
|
|
||||||
const outgoing = new OutgoingMessage(this.server);
|
|
||||||
return outgoing.transmitMessage(number, jsonData, timestamp);
|
|
||||||
},
|
|
||||||
|
|
||||||
validateRetryContentMessage(content) {
|
|
||||||
// We want at least one field set, but not more than one
|
|
||||||
let count = 0;
|
|
||||||
count += content.syncMessage ? 1 : 0;
|
|
||||||
count += content.dataMessage ? 1 : 0;
|
|
||||||
count += content.callMessage ? 1 : 0;
|
|
||||||
count += content.nullMessage ? 1 : 0;
|
|
||||||
if (count !== 1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// It's most likely that dataMessage will be populated, so we look at it in detail
|
|
||||||
const data = content.dataMessage;
|
|
||||||
if (
|
|
||||||
data &&
|
|
||||||
!data.attachments.length &&
|
|
||||||
!data.body &&
|
|
||||||
!data.expireTimer &&
|
|
||||||
!data.flags &&
|
|
||||||
!data.group
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
getRetryProto(message, timestamp) {
|
|
||||||
// If message was sent before v0.41.3 was released on Aug 7, then it was most
|
|
||||||
// certainly a DataMessage
|
|
||||||
//
|
|
||||||
// var d = new Date('2017-08-07T07:00:00.000Z');
|
|
||||||
// d.getTime();
|
|
||||||
const august7 = 1502089200000;
|
|
||||||
if (timestamp < august7) {
|
|
||||||
return textsecure.protobuf.DataMessage.decode(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is ugly. But we don't know what kind of proto we need to decode...
|
|
||||||
try {
|
|
||||||
// Simply decoding as a Content message may throw
|
|
||||||
const proto = textsecure.protobuf.Content.decode(message);
|
|
||||||
|
|
||||||
// But it might also result in an invalid object, so we try to detect that
|
|
||||||
if (this.validateRetryContentMessage(proto)) {
|
|
||||||
return proto;
|
|
||||||
}
|
|
||||||
|
|
||||||
return textsecure.protobuf.DataMessage.decode(message);
|
|
||||||
} catch (e) {
|
|
||||||
// If this call throws, something has really gone wrong, we'll fail to send
|
|
||||||
return textsecure.protobuf.DataMessage.decode(message);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
tryMessageAgain(number, encodedMessage, timestamp) {
|
|
||||||
const proto = this.getRetryProto(encodedMessage, timestamp);
|
|
||||||
return this.sendIndividualProto(number, proto, timestamp);
|
|
||||||
},
|
|
||||||
|
|
||||||
queueJobForNumber(number, runJob) {
|
queueJobForNumber(number, runJob) {
|
||||||
const taskWithTimeout = textsecure.createTaskWithTimeout(
|
const taskWithTimeout = textsecure.createTaskWithTimeout(
|
||||||
runJob,
|
runJob,
|
||||||
|
@ -321,8 +256,10 @@ MessageSender.prototype = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
sendMessage(attrs) {
|
sendMessage(attrs, options) {
|
||||||
const message = new Message(attrs);
|
const message = new Message(attrs);
|
||||||
|
const silent = false;
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
this.uploadAttachments(message),
|
this.uploadAttachments(message),
|
||||||
this.uploadThumbnails(message),
|
this.uploadThumbnails(message),
|
||||||
|
@ -340,12 +277,21 @@ MessageSender.prototype = {
|
||||||
} else {
|
} else {
|
||||||
resolve(res);
|
resolve(res);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
silent,
|
||||||
|
options
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
sendMessageProto(timestamp, numbers, message, callback, silent) {
|
sendMessageProto(
|
||||||
|
timestamp,
|
||||||
|
numbers,
|
||||||
|
message,
|
||||||
|
callback,
|
||||||
|
silent,
|
||||||
|
options = {}
|
||||||
|
) {
|
||||||
const rejections = textsecure.storage.get('signedKeyRotationRejected', 0);
|
const rejections = textsecure.storage.get('signedKeyRotationRejected', 0);
|
||||||
if (rejections > 5) {
|
if (rejections > 5) {
|
||||||
throw new textsecure.SignedPreKeyRotationError(
|
throw new textsecure.SignedPreKeyRotationError(
|
||||||
|
@ -361,7 +307,8 @@ MessageSender.prototype = {
|
||||||
numbers,
|
numbers,
|
||||||
message,
|
message,
|
||||||
silent,
|
silent,
|
||||||
callback
|
callback,
|
||||||
|
options
|
||||||
);
|
);
|
||||||
|
|
||||||
numbers.forEach(number => {
|
numbers.forEach(number => {
|
||||||
|
@ -369,20 +316,7 @@ MessageSender.prototype = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
retrySendMessageProto(numbers, encodedMessage, timestamp) {
|
sendIndividualProto(number, proto, timestamp, silent, options = {}) {
|
||||||
const proto = textsecure.protobuf.DataMessage.decode(encodedMessage);
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.sendMessageProto(timestamp, numbers, proto, res => {
|
|
||||||
if (res.errors.length > 0) {
|
|
||||||
reject(res);
|
|
||||||
} else {
|
|
||||||
resolve(res);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
sendIndividualProto(number, proto, timestamp, silent) {
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const callback = res => {
|
const callback = res => {
|
||||||
if (res.errors.length > 0) {
|
if (res.errors.length > 0) {
|
||||||
|
@ -391,7 +325,14 @@ MessageSender.prototype = {
|
||||||
resolve(res);
|
resolve(res);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.sendMessageProto(timestamp, [number], proto, callback, silent);
|
this.sendMessageProto(
|
||||||
|
timestamp,
|
||||||
|
[number],
|
||||||
|
proto,
|
||||||
|
callback,
|
||||||
|
silent,
|
||||||
|
options
|
||||||
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -412,7 +353,10 @@ MessageSender.prototype = {
|
||||||
encodedDataMessage,
|
encodedDataMessage,
|
||||||
timestamp,
|
timestamp,
|
||||||
destination,
|
destination,
|
||||||
expirationStartTimestamp
|
expirationStartTimestamp,
|
||||||
|
sentTo = [],
|
||||||
|
unidentifiedDeliveries = [],
|
||||||
|
options
|
||||||
) {
|
) {
|
||||||
const myNumber = textsecure.storage.user.getNumber();
|
const myNumber = textsecure.storage.user.getNumber();
|
||||||
const myDevice = textsecure.storage.user.getDeviceId();
|
const myDevice = textsecure.storage.user.getDeviceId();
|
||||||
|
@ -432,6 +376,27 @@ MessageSender.prototype = {
|
||||||
if (expirationStartTimestamp) {
|
if (expirationStartTimestamp) {
|
||||||
sentMessage.expirationStartTimestamp = expirationStartTimestamp;
|
sentMessage.expirationStartTimestamp = expirationStartTimestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const unidentifiedLookup = unidentifiedDeliveries.reduce(
|
||||||
|
(accumulator, item) => {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
accumulator[item] = true;
|
||||||
|
return accumulator;
|
||||||
|
},
|
||||||
|
Object.create(null)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Though this field has 'unidenified' in the name, it should have entries for each
|
||||||
|
// number we sent to.
|
||||||
|
if (sentTo && sentTo.length) {
|
||||||
|
sentMessage.unidentifiedStatus = sentTo.map(number => {
|
||||||
|
const status = new textsecure.protobuf.SyncMessage.Sent.UnidentifiedDeliveryStatus();
|
||||||
|
status.destination = number;
|
||||||
|
status.unidentified = Boolean(unidentifiedLookup[number]);
|
||||||
|
return status;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const syncMessage = this.createSyncMessage();
|
const syncMessage = this.createSyncMessage();
|
||||||
syncMessage.sent = sentMessage;
|
syncMessage.sent = sentMessage;
|
||||||
const contentMessage = new textsecure.protobuf.Content();
|
const contentMessage = new textsecure.protobuf.Content();
|
||||||
|
@ -442,18 +407,24 @@ MessageSender.prototype = {
|
||||||
myNumber,
|
myNumber,
|
||||||
contentMessage,
|
contentMessage,
|
||||||
Date.now(),
|
Date.now(),
|
||||||
silent
|
silent,
|
||||||
|
options
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
getProfile(number) {
|
async getProfile(number, { accessKey } = {}) {
|
||||||
|
if (accessKey) {
|
||||||
|
return this.server.getProfileUnauth(number, { accessKey });
|
||||||
|
}
|
||||||
|
|
||||||
return this.server.getProfile(number);
|
return this.server.getProfile(number);
|
||||||
},
|
},
|
||||||
|
|
||||||
getAvatar(path) {
|
getAvatar(path) {
|
||||||
return this.server.getAvatar(path);
|
return this.server.getAvatar(path);
|
||||||
},
|
},
|
||||||
|
|
||||||
sendRequestConfigurationSyncMessage() {
|
sendRequestConfigurationSyncMessage(options) {
|
||||||
const myNumber = textsecure.storage.user.getNumber();
|
const myNumber = textsecure.storage.user.getNumber();
|
||||||
const myDevice = textsecure.storage.user.getDeviceId();
|
const myDevice = textsecure.storage.user.getDeviceId();
|
||||||
if (myDevice !== 1 && myDevice !== '1') {
|
if (myDevice !== 1 && myDevice !== '1') {
|
||||||
|
@ -469,13 +440,14 @@ MessageSender.prototype = {
|
||||||
myNumber,
|
myNumber,
|
||||||
contentMessage,
|
contentMessage,
|
||||||
Date.now(),
|
Date.now(),
|
||||||
silent
|
silent,
|
||||||
|
options
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
sendRequestGroupSyncMessage() {
|
sendRequestGroupSyncMessage(options) {
|
||||||
const myNumber = textsecure.storage.user.getNumber();
|
const myNumber = textsecure.storage.user.getNumber();
|
||||||
const myDevice = textsecure.storage.user.getDeviceId();
|
const myDevice = textsecure.storage.user.getDeviceId();
|
||||||
if (myDevice !== 1 && myDevice !== '1') {
|
if (myDevice !== 1 && myDevice !== '1') {
|
||||||
|
@ -491,14 +463,15 @@ MessageSender.prototype = {
|
||||||
myNumber,
|
myNumber,
|
||||||
contentMessage,
|
contentMessage,
|
||||||
Date.now(),
|
Date.now(),
|
||||||
silent
|
silent,
|
||||||
|
options
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
|
|
||||||
sendRequestContactSyncMessage() {
|
sendRequestContactSyncMessage(options) {
|
||||||
const myNumber = textsecure.storage.user.getNumber();
|
const myNumber = textsecure.storage.user.getNumber();
|
||||||
const myDevice = textsecure.storage.user.getDeviceId();
|
const myDevice = textsecure.storage.user.getDeviceId();
|
||||||
if (myDevice !== 1 && myDevice !== '1') {
|
if (myDevice !== 1 && myDevice !== '1') {
|
||||||
|
@ -514,13 +487,37 @@ MessageSender.prototype = {
|
||||||
myNumber,
|
myNumber,
|
||||||
contentMessage,
|
contentMessage,
|
||||||
Date.now(),
|
Date.now(),
|
||||||
silent
|
silent,
|
||||||
|
options
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
sendReadReceipts(sender, timestamps) {
|
sendDeliveryReceipt(recipientId, timestamp, options) {
|
||||||
|
const myNumber = textsecure.storage.user.getNumber();
|
||||||
|
const myDevice = textsecure.storage.user.getDeviceId();
|
||||||
|
if (myNumber === recipientId && (myDevice === 1 || myDevice === '1')) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
const receiptMessage = new textsecure.protobuf.ReceiptMessage();
|
||||||
|
receiptMessage.type = textsecure.protobuf.ReceiptMessage.Type.DELIVERY;
|
||||||
|
receiptMessage.timestamp = [timestamp];
|
||||||
|
|
||||||
|
const contentMessage = new textsecure.protobuf.Content();
|
||||||
|
contentMessage.receiptMessage = receiptMessage;
|
||||||
|
|
||||||
|
const silent = true;
|
||||||
|
return this.sendIndividualProto(
|
||||||
|
recipientId,
|
||||||
|
contentMessage,
|
||||||
|
Date.now(),
|
||||||
|
silent,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
},
|
||||||
|
sendReadReceipts(sender, timestamps, options) {
|
||||||
const receiptMessage = new textsecure.protobuf.ReceiptMessage();
|
const receiptMessage = new textsecure.protobuf.ReceiptMessage();
|
||||||
receiptMessage.type = textsecure.protobuf.ReceiptMessage.Type.READ;
|
receiptMessage.type = textsecure.protobuf.ReceiptMessage.Type.READ;
|
||||||
receiptMessage.timestamp = timestamps;
|
receiptMessage.timestamp = timestamps;
|
||||||
|
@ -529,9 +526,15 @@ MessageSender.prototype = {
|
||||||
contentMessage.receiptMessage = receiptMessage;
|
contentMessage.receiptMessage = receiptMessage;
|
||||||
|
|
||||||
const silent = true;
|
const silent = true;
|
||||||
return this.sendIndividualProto(sender, contentMessage, Date.now(), silent);
|
return this.sendIndividualProto(
|
||||||
|
sender,
|
||||||
|
contentMessage,
|
||||||
|
Date.now(),
|
||||||
|
silent,
|
||||||
|
options
|
||||||
|
);
|
||||||
},
|
},
|
||||||
syncReadMessages(reads) {
|
syncReadMessages(reads, options) {
|
||||||
const myNumber = textsecure.storage.user.getNumber();
|
const myNumber = textsecure.storage.user.getNumber();
|
||||||
const myDevice = textsecure.storage.user.getDeviceId();
|
const myDevice = textsecure.storage.user.getDeviceId();
|
||||||
if (myDevice !== 1 && myDevice !== '1') {
|
if (myDevice !== 1 && myDevice !== '1') {
|
||||||
|
@ -551,13 +554,14 @@ MessageSender.prototype = {
|
||||||
myNumber,
|
myNumber,
|
||||||
contentMessage,
|
contentMessage,
|
||||||
Date.now(),
|
Date.now(),
|
||||||
silent
|
silent,
|
||||||
|
options
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
syncVerification(destination, state, identityKey) {
|
syncVerification(destination, state, identityKey, options) {
|
||||||
const myNumber = textsecure.storage.user.getNumber();
|
const myNumber = textsecure.storage.user.getNumber();
|
||||||
const myDevice = textsecure.storage.user.getDeviceId();
|
const myDevice = textsecure.storage.user.getDeviceId();
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
@ -580,7 +584,14 @@ MessageSender.prototype = {
|
||||||
contentMessage.nullMessage = nullMessage;
|
contentMessage.nullMessage = nullMessage;
|
||||||
|
|
||||||
// We want the NullMessage to look like a normal outgoing message; not silent
|
// We want the NullMessage to look like a normal outgoing message; not silent
|
||||||
const promise = this.sendIndividualProto(destination, contentMessage, now);
|
const silent = false;
|
||||||
|
const promise = this.sendIndividualProto(
|
||||||
|
destination,
|
||||||
|
contentMessage,
|
||||||
|
now,
|
||||||
|
silent,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
return promise.then(() => {
|
return promise.then(() => {
|
||||||
const verified = new textsecure.protobuf.Verified();
|
const verified = new textsecure.protobuf.Verified();
|
||||||
|
@ -595,12 +606,18 @@ MessageSender.prototype = {
|
||||||
const secondMessage = new textsecure.protobuf.Content();
|
const secondMessage = new textsecure.protobuf.Content();
|
||||||
secondMessage.syncMessage = syncMessage;
|
secondMessage.syncMessage = syncMessage;
|
||||||
|
|
||||||
const silent = true;
|
const innerSilent = true;
|
||||||
return this.sendIndividualProto(myNumber, secondMessage, now, silent);
|
return this.sendIndividualProto(
|
||||||
|
myNumber,
|
||||||
|
secondMessage,
|
||||||
|
now,
|
||||||
|
innerSilent,
|
||||||
|
options
|
||||||
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
sendGroupProto(providedNumbers, proto, timestamp = Date.now()) {
|
sendGroupProto(providedNumbers, proto, timestamp = Date.now(), options = {}) {
|
||||||
const me = textsecure.storage.user.getNumber();
|
const me = textsecure.storage.user.getNumber();
|
||||||
const numbers = providedNumbers.filter(number => number !== me);
|
const numbers = providedNumbers.filter(number => number !== me);
|
||||||
if (numbers.length === 0) {
|
if (numbers.length === 0) {
|
||||||
|
@ -618,7 +635,14 @@ MessageSender.prototype = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.sendMessageProto(timestamp, numbers, proto, callback, silent);
|
this.sendMessageProto(
|
||||||
|
timestamp,
|
||||||
|
numbers,
|
||||||
|
proto,
|
||||||
|
callback,
|
||||||
|
silent,
|
||||||
|
options
|
||||||
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -629,22 +653,27 @@ MessageSender.prototype = {
|
||||||
quote,
|
quote,
|
||||||
timestamp,
|
timestamp,
|
||||||
expireTimer,
|
expireTimer,
|
||||||
profileKey
|
profileKey,
|
||||||
|
options
|
||||||
) {
|
) {
|
||||||
return this.sendMessage({
|
return this.sendMessage(
|
||||||
recipients: [number],
|
{
|
||||||
body: messageText,
|
recipients: [number],
|
||||||
timestamp,
|
body: messageText,
|
||||||
attachments,
|
timestamp,
|
||||||
quote,
|
attachments,
|
||||||
needsSync: true,
|
quote,
|
||||||
expireTimer,
|
needsSync: true,
|
||||||
profileKey,
|
expireTimer,
|
||||||
});
|
profileKey,
|
||||||
|
},
|
||||||
|
options
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
resetSession(number, timestamp) {
|
resetSession(number, timestamp, options) {
|
||||||
window.log.info('resetting secure session');
|
window.log.info('resetting secure session');
|
||||||
|
const silent = false;
|
||||||
const proto = new textsecure.protobuf.DataMessage();
|
const proto = new textsecure.protobuf.DataMessage();
|
||||||
proto.body = 'TERMINATE';
|
proto.body = 'TERMINATE';
|
||||||
proto.flags = textsecure.protobuf.DataMessage.Flags.END_SESSION;
|
proto.flags = textsecure.protobuf.DataMessage.Flags.END_SESSION;
|
||||||
|
@ -677,9 +706,13 @@ MessageSender.prototype = {
|
||||||
window.log.info(
|
window.log.info(
|
||||||
'finished closing local sessions, now sending to contact'
|
'finished closing local sessions, now sending to contact'
|
||||||
);
|
);
|
||||||
return this.sendIndividualProto(number, proto, timestamp).catch(
|
return this.sendIndividualProto(
|
||||||
logError('resetSession/sendToContact error:')
|
number,
|
||||||
);
|
proto,
|
||||||
|
timestamp,
|
||||||
|
silent,
|
||||||
|
options
|
||||||
|
).catch(logError('resetSession/sendToContact error:'));
|
||||||
})
|
})
|
||||||
.then(() =>
|
.then(() =>
|
||||||
deleteAllSessions(number).catch(
|
deleteAllSessions(number).catch(
|
||||||
|
@ -688,9 +721,15 @@ MessageSender.prototype = {
|
||||||
);
|
);
|
||||||
|
|
||||||
const buffer = proto.toArrayBuffer();
|
const buffer = proto.toArrayBuffer();
|
||||||
const sendSync = this.sendSyncMessage(buffer, timestamp, number).catch(
|
const sendSync = this.sendSyncMessage(
|
||||||
logError('resetSession/sendSync error:')
|
buffer,
|
||||||
);
|
timestamp,
|
||||||
|
number,
|
||||||
|
null,
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
options
|
||||||
|
).catch(logError('resetSession/sendSync error:'));
|
||||||
|
|
||||||
return Promise.all([sendToContact, sendSync]);
|
return Promise.all([sendToContact, sendSync]);
|
||||||
},
|
},
|
||||||
|
@ -702,7 +741,8 @@ MessageSender.prototype = {
|
||||||
quote,
|
quote,
|
||||||
timestamp,
|
timestamp,
|
||||||
expireTimer,
|
expireTimer,
|
||||||
profileKey
|
profileKey,
|
||||||
|
options
|
||||||
) {
|
) {
|
||||||
return textsecure.storage.groups.getNumbers(groupId).then(targetNumbers => {
|
return textsecure.storage.groups.getNumbers(groupId).then(targetNumbers => {
|
||||||
if (targetNumbers === undefined) {
|
if (targetNumbers === undefined) {
|
||||||
|
@ -715,24 +755,27 @@ MessageSender.prototype = {
|
||||||
return Promise.reject(new Error('No other members in the group'));
|
return Promise.reject(new Error('No other members in the group'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.sendMessage({
|
return this.sendMessage(
|
||||||
recipients: numbers,
|
{
|
||||||
body: messageText,
|
recipients: numbers,
|
||||||
timestamp,
|
body: messageText,
|
||||||
attachments,
|
timestamp,
|
||||||
quote,
|
attachments,
|
||||||
needsSync: true,
|
quote,
|
||||||
expireTimer,
|
needsSync: true,
|
||||||
profileKey,
|
expireTimer,
|
||||||
group: {
|
profileKey,
|
||||||
id: groupId,
|
group: {
|
||||||
type: textsecure.protobuf.GroupContext.Type.DELIVER,
|
id: groupId,
|
||||||
|
type: textsecure.protobuf.GroupContext.Type.DELIVER,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
options
|
||||||
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
createGroup(targetNumbers, name, avatar) {
|
createGroup(targetNumbers, name, avatar, options) {
|
||||||
const proto = new textsecure.protobuf.DataMessage();
|
const proto = new textsecure.protobuf.DataMessage();
|
||||||
proto.group = new textsecure.protobuf.GroupContext();
|
proto.group = new textsecure.protobuf.GroupContext();
|
||||||
|
|
||||||
|
@ -748,12 +791,14 @@ MessageSender.prototype = {
|
||||||
|
|
||||||
return this.makeAttachmentPointer(avatar).then(attachment => {
|
return this.makeAttachmentPointer(avatar).then(attachment => {
|
||||||
proto.group.avatar = attachment;
|
proto.group.avatar = attachment;
|
||||||
return this.sendGroupProto(numbers, proto).then(() => proto.group.id);
|
return this.sendGroupProto(numbers, proto, Date.now(), options).then(
|
||||||
|
() => proto.group.id
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
updateGroup(groupId, name, avatar, targetNumbers) {
|
updateGroup(groupId, name, avatar, targetNumbers, options) {
|
||||||
const proto = new textsecure.protobuf.DataMessage();
|
const proto = new textsecure.protobuf.DataMessage();
|
||||||
proto.group = new textsecure.protobuf.GroupContext();
|
proto.group = new textsecure.protobuf.GroupContext();
|
||||||
|
|
||||||
|
@ -771,12 +816,14 @@ MessageSender.prototype = {
|
||||||
|
|
||||||
return this.makeAttachmentPointer(avatar).then(attachment => {
|
return this.makeAttachmentPointer(avatar).then(attachment => {
|
||||||
proto.group.avatar = attachment;
|
proto.group.avatar = attachment;
|
||||||
return this.sendGroupProto(numbers, proto).then(() => proto.group.id);
|
return this.sendGroupProto(numbers, proto, Date.now(), options).then(
|
||||||
|
() => proto.group.id
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
addNumberToGroup(groupId, number) {
|
addNumberToGroup(groupId, number, options) {
|
||||||
const proto = new textsecure.protobuf.DataMessage();
|
const proto = new textsecure.protobuf.DataMessage();
|
||||||
proto.group = new textsecure.protobuf.GroupContext();
|
proto.group = new textsecure.protobuf.GroupContext();
|
||||||
proto.group.id = stringToArrayBuffer(groupId);
|
proto.group.id = stringToArrayBuffer(groupId);
|
||||||
|
@ -789,11 +836,11 @@ MessageSender.prototype = {
|
||||||
return Promise.reject(new Error('Unknown Group'));
|
return Promise.reject(new Error('Unknown Group'));
|
||||||
proto.group.members = numbers;
|
proto.group.members = numbers;
|
||||||
|
|
||||||
return this.sendGroupProto(numbers, proto);
|
return this.sendGroupProto(numbers, proto, Date.now(), options);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
setGroupName(groupId, name) {
|
setGroupName(groupId, name, options) {
|
||||||
const proto = new textsecure.protobuf.DataMessage();
|
const proto = new textsecure.protobuf.DataMessage();
|
||||||
proto.group = new textsecure.protobuf.GroupContext();
|
proto.group = new textsecure.protobuf.GroupContext();
|
||||||
proto.group.id = stringToArrayBuffer(groupId);
|
proto.group.id = stringToArrayBuffer(groupId);
|
||||||
|
@ -805,11 +852,11 @@ MessageSender.prototype = {
|
||||||
return Promise.reject(new Error('Unknown Group'));
|
return Promise.reject(new Error('Unknown Group'));
|
||||||
proto.group.members = numbers;
|
proto.group.members = numbers;
|
||||||
|
|
||||||
return this.sendGroupProto(numbers, proto);
|
return this.sendGroupProto(numbers, proto, Date.now(), options);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
setGroupAvatar(groupId, avatar) {
|
setGroupAvatar(groupId, avatar, options) {
|
||||||
const proto = new textsecure.protobuf.DataMessage();
|
const proto = new textsecure.protobuf.DataMessage();
|
||||||
proto.group = new textsecure.protobuf.GroupContext();
|
proto.group = new textsecure.protobuf.GroupContext();
|
||||||
proto.group.id = stringToArrayBuffer(groupId);
|
proto.group.id = stringToArrayBuffer(groupId);
|
||||||
|
@ -822,12 +869,12 @@ MessageSender.prototype = {
|
||||||
|
|
||||||
return this.makeAttachmentPointer(avatar).then(attachment => {
|
return this.makeAttachmentPointer(avatar).then(attachment => {
|
||||||
proto.group.avatar = attachment;
|
proto.group.avatar = attachment;
|
||||||
return this.sendGroupProto(numbers, proto);
|
return this.sendGroupProto(numbers, proto, Date.now(), options);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
leaveGroup(groupId) {
|
leaveGroup(groupId, options) {
|
||||||
const proto = new textsecure.protobuf.DataMessage();
|
const proto = new textsecure.protobuf.DataMessage();
|
||||||
proto.group = new textsecure.protobuf.GroupContext();
|
proto.group = new textsecure.protobuf.GroupContext();
|
||||||
proto.group.id = stringToArrayBuffer(groupId);
|
proto.group.id = stringToArrayBuffer(groupId);
|
||||||
|
@ -838,14 +885,15 @@ MessageSender.prototype = {
|
||||||
return Promise.reject(new Error('Unknown Group'));
|
return Promise.reject(new Error('Unknown Group'));
|
||||||
return textsecure.storage.groups
|
return textsecure.storage.groups
|
||||||
.deleteGroup(groupId)
|
.deleteGroup(groupId)
|
||||||
.then(() => this.sendGroupProto(numbers, proto));
|
.then(() => this.sendGroupProto(numbers, proto, Date.now(), options));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
sendExpirationTimerUpdateToGroup(
|
sendExpirationTimerUpdateToGroup(
|
||||||
groupId,
|
groupId,
|
||||||
expireTimer,
|
expireTimer,
|
||||||
timestamp,
|
timestamp,
|
||||||
profileKey
|
profileKey,
|
||||||
|
options
|
||||||
) {
|
) {
|
||||||
return textsecure.storage.groups.getNumbers(groupId).then(targetNumbers => {
|
return textsecure.storage.groups.getNumbers(groupId).then(targetNumbers => {
|
||||||
if (targetNumbers === undefined)
|
if (targetNumbers === undefined)
|
||||||
|
@ -856,34 +904,41 @@ MessageSender.prototype = {
|
||||||
if (numbers.length === 0) {
|
if (numbers.length === 0) {
|
||||||
return Promise.reject(new Error('No other members in the group'));
|
return Promise.reject(new Error('No other members in the group'));
|
||||||
}
|
}
|
||||||
return this.sendMessage({
|
return this.sendMessage(
|
||||||
recipients: numbers,
|
{
|
||||||
timestamp,
|
recipients: numbers,
|
||||||
needsSync: true,
|
timestamp,
|
||||||
expireTimer,
|
needsSync: true,
|
||||||
profileKey,
|
expireTimer,
|
||||||
flags: textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
|
profileKey,
|
||||||
group: {
|
flags: textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
|
||||||
id: groupId,
|
group: {
|
||||||
type: textsecure.protobuf.GroupContext.Type.DELIVER,
|
id: groupId,
|
||||||
|
type: textsecure.protobuf.GroupContext.Type.DELIVER,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
options
|
||||||
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
sendExpirationTimerUpdateToNumber(
|
sendExpirationTimerUpdateToNumber(
|
||||||
number,
|
number,
|
||||||
expireTimer,
|
expireTimer,
|
||||||
timestamp,
|
timestamp,
|
||||||
profileKey
|
profileKey,
|
||||||
|
options
|
||||||
) {
|
) {
|
||||||
return this.sendMessage({
|
return this.sendMessage(
|
||||||
recipients: [number],
|
{
|
||||||
timestamp,
|
recipients: [number],
|
||||||
needsSync: true,
|
timestamp,
|
||||||
expireTimer,
|
needsSync: true,
|
||||||
profileKey,
|
expireTimer,
|
||||||
flags: textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
|
profileKey,
|
||||||
});
|
flags: textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
|
||||||
|
},
|
||||||
|
options
|
||||||
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -927,6 +982,7 @@ textsecure.MessageSender = function MessageSenderWrapper(
|
||||||
this.getAvatar = sender.getAvatar.bind(sender);
|
this.getAvatar = sender.getAvatar.bind(sender);
|
||||||
this.syncReadMessages = sender.syncReadMessages.bind(sender);
|
this.syncReadMessages = sender.syncReadMessages.bind(sender);
|
||||||
this.syncVerification = sender.syncVerification.bind(sender);
|
this.syncVerification = sender.syncVerification.bind(sender);
|
||||||
|
this.sendDeliveryReceipt = sender.sendDeliveryReceipt.bind(sender);
|
||||||
this.sendReadReceipts = sender.sendReadReceipts.bind(sender);
|
this.sendReadReceipts = sender.sendReadReceipts.bind(sender);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* global Event, textsecure, window */
|
/* global Event, textsecure, window, ConversationController */
|
||||||
|
|
||||||
/* eslint-disable more/no-then */
|
/* eslint-disable more/no-then */
|
||||||
|
|
||||||
|
@ -23,12 +23,15 @@
|
||||||
this.ongroup = this.onGroupSyncComplete.bind(this);
|
this.ongroup = this.onGroupSyncComplete.bind(this);
|
||||||
receiver.addEventListener('groupsync', this.ongroup);
|
receiver.addEventListener('groupsync', this.ongroup);
|
||||||
|
|
||||||
|
const ourNumber = textsecure.storage.user.getNumber();
|
||||||
|
const { wrap, sendOptions } = ConversationController.prepareForSend(
|
||||||
|
ourNumber
|
||||||
|
);
|
||||||
window.log.info('SyncRequest created. Sending contact sync message...');
|
window.log.info('SyncRequest created. Sending contact sync message...');
|
||||||
sender
|
wrap(sender.sendRequestContactSyncMessage(sendOptions))
|
||||||
.sendRequestContactSyncMessage()
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
window.log.info('SyncRequest now sending group sync messsage...');
|
window.log.info('SyncRequest now sending group sync messsage...');
|
||||||
return sender.sendRequestGroupSyncMessage();
|
return wrap(sender.sendRequestGroupSyncMessage(sendOptions));
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
window.log.error(
|
window.log.error(
|
||||||
|
|
|
@ -90,8 +90,11 @@ describe('MessageReceiver', () => {
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
const messageReceiver = new textsecure.MessageReceiver(
|
const messageReceiver = new textsecure.MessageReceiver(
|
||||||
'ws://localhost:8080',
|
'username',
|
||||||
window
|
'password',
|
||||||
|
'signalingKey'
|
||||||
|
// 'ws://localhost:8080',
|
||||||
|
// window,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
1
main.js
1
main.js
|
@ -149,6 +149,7 @@ function prepareURL(pathSegments, moreKeys) {
|
||||||
appInstance: process.env.NODE_APP_INSTANCE,
|
appInstance: process.env.NODE_APP_INSTANCE,
|
||||||
proxyUrl: process.env.HTTPS_PROXY || process.env.https_proxy,
|
proxyUrl: process.env.HTTPS_PROXY || process.env.https_proxy,
|
||||||
importMode: importMode ? true : undefined, // for stringify()
|
importMode: importMode ? true : undefined, // for stringify()
|
||||||
|
serverTrustRoot: config.get('serverTrustRoot'),
|
||||||
...moreKeys,
|
...moreKeys,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -27,6 +27,7 @@ window.isImportMode = () => config.importMode;
|
||||||
window.getExpiration = () => config.buildExpiration;
|
window.getExpiration = () => config.buildExpiration;
|
||||||
window.getNodeVersion = () => config.node_version;
|
window.getNodeVersion = () => config.node_version;
|
||||||
window.getHostName = () => config.hostname;
|
window.getHostName = () => config.hostname;
|
||||||
|
window.getServerTrustRoot = () => config.serverTrustRoot;
|
||||||
|
|
||||||
window.isBeforeVersion = (toCheck, baseVersion) => {
|
window.isBeforeVersion = (toCheck, baseVersion) => {
|
||||||
try {
|
try {
|
||||||
|
@ -215,6 +216,7 @@ window.filesize = require('filesize');
|
||||||
window.libphonenumber = require('google-libphonenumber').PhoneNumberUtil.getInstance();
|
window.libphonenumber = require('google-libphonenumber').PhoneNumberUtil.getInstance();
|
||||||
window.libphonenumber.PhoneNumberFormat = require('google-libphonenumber').PhoneNumberFormat;
|
window.libphonenumber.PhoneNumberFormat = require('google-libphonenumber').PhoneNumberFormat;
|
||||||
window.loadImage = require('blueimp-load-image');
|
window.loadImage = require('blueimp-load-image');
|
||||||
|
window.getGuid = require('uuid/v4');
|
||||||
|
|
||||||
window.React = require('react');
|
window.React = require('react');
|
||||||
window.ReactDOM = require('react-dom');
|
window.ReactDOM = require('react-dom');
|
||||||
|
|
|
@ -6,20 +6,24 @@ option java_outer_classname = "SignalServiceProtos";
|
||||||
|
|
||||||
message Envelope {
|
message Envelope {
|
||||||
enum Type {
|
enum Type {
|
||||||
UNKNOWN = 0;
|
UNKNOWN = 0;
|
||||||
CIPHERTEXT = 1;
|
CIPHERTEXT = 1;
|
||||||
KEY_EXCHANGE = 2;
|
KEY_EXCHANGE = 2;
|
||||||
PREKEY_BUNDLE = 3;
|
PREKEY_BUNDLE = 3;
|
||||||
RECEIPT = 5;
|
RECEIPT = 5;
|
||||||
|
UNIDENTIFIED_SENDER = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
optional Type type = 1;
|
optional Type type = 1;
|
||||||
optional string source = 2;
|
optional string source = 2;
|
||||||
optional uint32 sourceDevice = 7;
|
optional uint32 sourceDevice = 7;
|
||||||
optional string relay = 3;
|
optional string relay = 3;
|
||||||
optional uint64 timestamp = 5;
|
optional uint64 timestamp = 5;
|
||||||
optional bytes legacyMessage = 6; // Contains an encrypted DataMessage
|
optional bytes legacyMessage = 6; // Contains an encrypted DataMessage
|
||||||
optional bytes content = 8; // Contains an encrypted Content
|
optional bytes content = 8; // Contains an encrypted Content
|
||||||
|
optional string serverGuid = 9;
|
||||||
|
optional uint64 serverTimestamp = 10;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message Content {
|
message Content {
|
||||||
|
@ -191,10 +195,16 @@ message Verified {
|
||||||
|
|
||||||
message SyncMessage {
|
message SyncMessage {
|
||||||
message Sent {
|
message Sent {
|
||||||
optional string destination = 1;
|
message UnidentifiedDeliveryStatus {
|
||||||
optional uint64 timestamp = 2;
|
optional string destination = 1;
|
||||||
optional DataMessage message = 3;
|
optional bool unidentified = 2;
|
||||||
optional uint64 expirationStartTimestamp = 4;
|
}
|
||||||
|
|
||||||
|
optional string destination = 1;
|
||||||
|
optional uint64 timestamp = 2;
|
||||||
|
optional DataMessage message = 3;
|
||||||
|
optional uint64 expirationStartTimestamp = 4;
|
||||||
|
repeated UnidentifiedDeliveryStatus unidentifiedStatus = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
message Contacts {
|
message Contacts {
|
||||||
|
@ -229,7 +239,8 @@ message SyncMessage {
|
||||||
}
|
}
|
||||||
|
|
||||||
message Configuration {
|
message Configuration {
|
||||||
optional bool readReceipts = 1;
|
optional bool readReceipts = 1;
|
||||||
|
optional bool unidentifiedDeliveryIndicators = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
optional Sent sent = 1;
|
optional Sent sent = 1;
|
||||||
|
|
45
protos/UnidentifiedDelivery.proto
Normal file
45
protos/UnidentifiedDelivery.proto
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package signalservice;
|
||||||
|
|
||||||
|
option java_package = "org.whispersystems.libsignal.protocol";
|
||||||
|
option java_outer_classname = "WhisperProtos";
|
||||||
|
|
||||||
|
message ServerCertificate {
|
||||||
|
message Certificate {
|
||||||
|
optional uint32 id = 1;
|
||||||
|
optional bytes key = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional bytes certificate = 1;
|
||||||
|
optional bytes signature = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SenderCertificate {
|
||||||
|
message Certificate {
|
||||||
|
optional string sender = 1;
|
||||||
|
optional uint32 senderDevice = 2;
|
||||||
|
optional fixed64 expires = 3;
|
||||||
|
optional bytes identityKey = 4;
|
||||||
|
optional ServerCertificate signer = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional bytes certificate = 1;
|
||||||
|
optional bytes signature = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UnidentifiedSenderMessage {
|
||||||
|
|
||||||
|
message Message {
|
||||||
|
enum Type {
|
||||||
|
PREKEY_MESSAGE = 1;
|
||||||
|
MESSAGE = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional Type type = 1;
|
||||||
|
optional SenderCertificate senderCertificate = 2;
|
||||||
|
optional bytes content = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional bytes ephemeralPublic = 1;
|
||||||
|
optional bytes encryptedStatic = 2;
|
||||||
|
optional bytes encryptedMessage = 3;
|
||||||
|
}
|
|
@ -1482,7 +1482,6 @@
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: 6px;
|
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1500,20 +1499,30 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-message-detail__contact__status-icon--sent {
|
.module-message-detail__contact__status-icon--sent {
|
||||||
@include color-svg('../images/check-circle-outline.svg', $color-light-35);
|
@include color-svg('../images/check-circle-outline.svg', $color-gray-60);
|
||||||
}
|
}
|
||||||
.module-message-detail__contact__status-icon--delivered {
|
.module-message-detail__contact__status-icon--delivered {
|
||||||
@include color-svg('../images/double-check.svg', $color-light-35);
|
@include color-svg('../images/double-check.svg', $color-gray-60);
|
||||||
width: 18px;
|
width: 18px;
|
||||||
}
|
}
|
||||||
.module-message-detail__contact__status-icon--read {
|
.module-message-detail__contact__status-icon--read {
|
||||||
@include color-svg('../images/read.svg', $color-light-35);
|
@include color-svg('../images/read.svg', $color-gray-60);
|
||||||
width: 18px;
|
width: 18px;
|
||||||
}
|
}
|
||||||
.module-message-detail__contact__status-icon--error {
|
.module-message-detail__contact__status-icon--error {
|
||||||
@include color-svg('../images/error.svg', $color-core-red);
|
@include color-svg('../images/error.svg', $color-core-red);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module-message-detail__contact__unidentified-delivery-icon {
|
||||||
|
margin-left: 6px;
|
||||||
|
margin-right: 10px;
|
||||||
|
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: inline-block;
|
||||||
|
@include color-svg('../images/unidentified-delivery.svg', $color-gray-60);
|
||||||
|
}
|
||||||
|
|
||||||
.module-message-detail__contact__error-buttons {
|
.module-message-detail__contact__error-buttons {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,104 +1,119 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
describe('Crypto', function() {
|
describe('Crypto', () => {
|
||||||
it('roundtrip symmetric encryption succeeds', async function() {
|
describe('accessKey/profileKey', () => {
|
||||||
var message = 'this is my message';
|
it('verification roundtrips', async () => {
|
||||||
var plaintext = new dcodeIO.ByteBuffer.wrap(
|
const profileKey = await Signal.Crypto.getRandomBytes(32);
|
||||||
message,
|
const accessKey = await Signal.Crypto.deriveAccessKey(profileKey);
|
||||||
'binary'
|
|
||||||
).toArrayBuffer();
|
|
||||||
var key = textsecure.crypto.getRandomBytes(32);
|
|
||||||
|
|
||||||
var encrypted = await Signal.Crypto.encryptSymmetric(key, plaintext);
|
const verifier = await Signal.Crypto.getAccessKeyVerifier(accessKey);
|
||||||
var decrypted = await Signal.Crypto.decryptSymmetric(key, encrypted);
|
|
||||||
|
|
||||||
var equal = Signal.Crypto.constantTimeEqual(plaintext, decrypted);
|
const correct = await Signal.Crypto.verifyAccessKey(accessKey, verifier);
|
||||||
if (!equal) {
|
|
||||||
throw new Error('The output and input did not match!');
|
assert.strictEqual(correct, true);
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('roundtrip fails if nonce is modified', async function() {
|
describe('symmetric encryption', () => {
|
||||||
var message = 'this is my message';
|
it('roundtrips', async () => {
|
||||||
var plaintext = new dcodeIO.ByteBuffer.wrap(
|
var message = 'this is my message';
|
||||||
message,
|
var plaintext = new dcodeIO.ByteBuffer.wrap(
|
||||||
'binary'
|
message,
|
||||||
).toArrayBuffer();
|
'binary'
|
||||||
var key = textsecure.crypto.getRandomBytes(32);
|
).toArrayBuffer();
|
||||||
|
var key = textsecure.crypto.getRandomBytes(32);
|
||||||
|
|
||||||
var encrypted = await Signal.Crypto.encryptSymmetric(key, plaintext);
|
var encrypted = await Signal.Crypto.encryptSymmetric(key, plaintext);
|
||||||
var uintArray = new Uint8Array(encrypted);
|
var decrypted = await Signal.Crypto.decryptSymmetric(key, encrypted);
|
||||||
uintArray[2] = 9;
|
|
||||||
|
|
||||||
try {
|
var equal = Signal.Crypto.constantTimeEqual(plaintext, decrypted);
|
||||||
var decrypted = await Signal.Crypto.decryptSymmetric(
|
if (!equal) {
|
||||||
key,
|
throw new Error('The output and input did not match!');
|
||||||
uintArray.buffer
|
}
|
||||||
);
|
});
|
||||||
} catch (error) {
|
|
||||||
assert.strictEqual(
|
|
||||||
error.message,
|
|
||||||
'decryptSymmetric: Failed to decrypt; MAC verification failed'
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('Expected error to be thrown');
|
it('roundtrip fails if nonce is modified', async () => {
|
||||||
});
|
var message = 'this is my message';
|
||||||
|
var plaintext = new dcodeIO.ByteBuffer.wrap(
|
||||||
|
message,
|
||||||
|
'binary'
|
||||||
|
).toArrayBuffer();
|
||||||
|
var key = textsecure.crypto.getRandomBytes(32);
|
||||||
|
|
||||||
it('fails if mac is modified', async function() {
|
var encrypted = await Signal.Crypto.encryptSymmetric(key, plaintext);
|
||||||
var message = 'this is my message';
|
var uintArray = new Uint8Array(encrypted);
|
||||||
var plaintext = new dcodeIO.ByteBuffer.wrap(
|
uintArray[2] = 9;
|
||||||
message,
|
|
||||||
'binary'
|
|
||||||
).toArrayBuffer();
|
|
||||||
var key = textsecure.crypto.getRandomBytes(32);
|
|
||||||
|
|
||||||
var encrypted = await Signal.Crypto.encryptSymmetric(key, plaintext);
|
try {
|
||||||
var uintArray = new Uint8Array(encrypted);
|
var decrypted = await Signal.Crypto.decryptSymmetric(
|
||||||
uintArray[uintArray.length - 3] = 9;
|
key,
|
||||||
|
uintArray.buffer
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
assert.strictEqual(
|
||||||
|
error.message,
|
||||||
|
'decryptSymmetric: Failed to decrypt; MAC verification failed'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
throw new Error('Expected error to be thrown');
|
||||||
var decrypted = await Signal.Crypto.decryptSymmetric(
|
});
|
||||||
key,
|
|
||||||
uintArray.buffer
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
assert.strictEqual(
|
|
||||||
error.message,
|
|
||||||
'decryptSymmetric: Failed to decrypt; MAC verification failed'
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('Expected error to be thrown');
|
it('roundtrip fails if mac is modified', async () => {
|
||||||
});
|
var message = 'this is my message';
|
||||||
|
var plaintext = new dcodeIO.ByteBuffer.wrap(
|
||||||
|
message,
|
||||||
|
'binary'
|
||||||
|
).toArrayBuffer();
|
||||||
|
var key = textsecure.crypto.getRandomBytes(32);
|
||||||
|
|
||||||
it('fails if encrypted contents are modified', async function() {
|
var encrypted = await Signal.Crypto.encryptSymmetric(key, plaintext);
|
||||||
var message = 'this is my message';
|
var uintArray = new Uint8Array(encrypted);
|
||||||
var plaintext = new dcodeIO.ByteBuffer.wrap(
|
uintArray[uintArray.length - 3] = 9;
|
||||||
message,
|
|
||||||
'binary'
|
|
||||||
).toArrayBuffer();
|
|
||||||
var key = textsecure.crypto.getRandomBytes(32);
|
|
||||||
|
|
||||||
var encrypted = await Signal.Crypto.encryptSymmetric(key, plaintext);
|
try {
|
||||||
var uintArray = new Uint8Array(encrypted);
|
var decrypted = await Signal.Crypto.decryptSymmetric(
|
||||||
uintArray[35] = 9;
|
key,
|
||||||
|
uintArray.buffer
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
assert.strictEqual(
|
||||||
|
error.message,
|
||||||
|
'decryptSymmetric: Failed to decrypt; MAC verification failed'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
throw new Error('Expected error to be thrown');
|
||||||
var decrypted = await Signal.Crypto.decryptSymmetric(
|
});
|
||||||
key,
|
|
||||||
uintArray.buffer
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
assert.strictEqual(
|
|
||||||
error.message,
|
|
||||||
'decryptSymmetric: Failed to decrypt; MAC verification failed'
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('Expected error to be thrown');
|
it('roundtrip fails if encrypted contents are modified', async () => {
|
||||||
|
var message = 'this is my message';
|
||||||
|
var plaintext = new dcodeIO.ByteBuffer.wrap(
|
||||||
|
message,
|
||||||
|
'binary'
|
||||||
|
).toArrayBuffer();
|
||||||
|
var key = textsecure.crypto.getRandomBytes(32);
|
||||||
|
|
||||||
|
var encrypted = await Signal.Crypto.encryptSymmetric(key, plaintext);
|
||||||
|
var uintArray = new Uint8Array(encrypted);
|
||||||
|
uintArray[35] = 9;
|
||||||
|
|
||||||
|
try {
|
||||||
|
var decrypted = await Signal.Crypto.decryptSymmetric(
|
||||||
|
key,
|
||||||
|
uintArray.buffer
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
assert.strictEqual(
|
||||||
|
error.message,
|
||||||
|
'decryptSymmetric: Failed to decrypt; MAC verification failed'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Expected error to be thrown');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -383,6 +383,8 @@
|
||||||
<script type='text/javascript' src='../js/views/banner_view.js' data-cover></script>
|
<script type='text/javascript' src='../js/views/banner_view.js' data-cover></script>
|
||||||
<script type='text/javascript' src='../js/views/clear_data_view.js'></script>
|
<script type='text/javascript' src='../js/views/clear_data_view.js'></script>
|
||||||
|
|
||||||
|
<script type="text/javascript" src="metadata/SecretSessionCipher_test.js"></script>
|
||||||
|
|
||||||
<script type="text/javascript" src="views/whisper_view_test.js"></script>
|
<script type="text/javascript" src="views/whisper_view_test.js"></script>
|
||||||
<script type="text/javascript" src="views/group_update_view_test.js"></script>
|
<script type="text/javascript" src="views/group_update_view_test.js"></script>
|
||||||
<script type="text/javascript" src="views/attachment_view_test.js"></script>
|
<script type="text/javascript" src="views/attachment_view_test.js"></script>
|
||||||
|
|
405
test/metadata/SecretSessionCipher_test.js
Normal file
405
test/metadata/SecretSessionCipher_test.js
Normal file
|
@ -0,0 +1,405 @@
|
||||||
|
/* global libsignal, textsecure */
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const {
|
||||||
|
SecretSessionCipher,
|
||||||
|
createCertificateValidator,
|
||||||
|
_createSenderCertificateFromBuffer,
|
||||||
|
_createServerCertificateFromBuffer,
|
||||||
|
} = window.Signal.Metadata;
|
||||||
|
const {
|
||||||
|
bytesFromString,
|
||||||
|
stringFromBytes,
|
||||||
|
arrayBufferToBase64,
|
||||||
|
} = window.Signal.Crypto;
|
||||||
|
|
||||||
|
function InMemorySignalProtocolStore() {
|
||||||
|
this.store = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toString(thing) {
|
||||||
|
if (typeof thing === 'string') {
|
||||||
|
return thing;
|
||||||
|
}
|
||||||
|
return arrayBufferToBase64(thing);
|
||||||
|
}
|
||||||
|
|
||||||
|
InMemorySignalProtocolStore.prototype = {
|
||||||
|
Direction: {
|
||||||
|
SENDING: 1,
|
||||||
|
RECEIVING: 2,
|
||||||
|
},
|
||||||
|
|
||||||
|
getIdentityKeyPair() {
|
||||||
|
return Promise.resolve(this.get('identityKey'));
|
||||||
|
},
|
||||||
|
getLocalRegistrationId() {
|
||||||
|
return Promise.resolve(this.get('registrationId'));
|
||||||
|
},
|
||||||
|
put(key, value) {
|
||||||
|
if (
|
||||||
|
key === undefined ||
|
||||||
|
value === undefined ||
|
||||||
|
key === null ||
|
||||||
|
value === null
|
||||||
|
)
|
||||||
|
throw new Error('Tried to store undefined/null');
|
||||||
|
this.store[key] = value;
|
||||||
|
},
|
||||||
|
get(key, defaultValue) {
|
||||||
|
if (key === null || key === undefined)
|
||||||
|
throw new Error('Tried to get value for undefined/null key');
|
||||||
|
if (key in this.store) {
|
||||||
|
return this.store[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue;
|
||||||
|
},
|
||||||
|
remove(key) {
|
||||||
|
if (key === null || key === undefined)
|
||||||
|
throw new Error('Tried to remove value for undefined/null key');
|
||||||
|
delete this.store[key];
|
||||||
|
},
|
||||||
|
|
||||||
|
isTrustedIdentity(identifier, identityKey) {
|
||||||
|
if (identifier === null || identifier === undefined) {
|
||||||
|
throw new Error('tried to check identity key for undefined/null key');
|
||||||
|
}
|
||||||
|
if (!(identityKey instanceof ArrayBuffer)) {
|
||||||
|
throw new Error('Expected identityKey to be an ArrayBuffer');
|
||||||
|
}
|
||||||
|
const trusted = this.get(`identityKey${identifier}`);
|
||||||
|
if (trusted === undefined) {
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
return Promise.resolve(toString(identityKey) === toString(trusted));
|
||||||
|
},
|
||||||
|
loadIdentityKey(identifier) {
|
||||||
|
if (identifier === null || identifier === undefined)
|
||||||
|
throw new Error('Tried to get identity key for undefined/null key');
|
||||||
|
return Promise.resolve(this.get(`identityKey${identifier}`));
|
||||||
|
},
|
||||||
|
saveIdentity(identifier, identityKey) {
|
||||||
|
if (identifier === null || identifier === undefined)
|
||||||
|
throw new Error('Tried to put identity key for undefined/null key');
|
||||||
|
|
||||||
|
const address = libsignal.SignalProtocolAddress.fromString(identifier);
|
||||||
|
|
||||||
|
const existing = this.get(`identityKey${address.getName()}`);
|
||||||
|
this.put(`identityKey${address.getName()}`, identityKey);
|
||||||
|
|
||||||
|
if (existing && toString(identityKey) !== toString(existing)) {
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(false);
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Returns a prekeypair object or undefined */
|
||||||
|
loadPreKey(keyId) {
|
||||||
|
let res = this.get(`25519KeypreKey${keyId}`);
|
||||||
|
if (res !== undefined) {
|
||||||
|
res = { pubKey: res.pubKey, privKey: res.privKey };
|
||||||
|
}
|
||||||
|
return Promise.resolve(res);
|
||||||
|
},
|
||||||
|
storePreKey(keyId, keyPair) {
|
||||||
|
return Promise.resolve(this.put(`25519KeypreKey${keyId}`, keyPair));
|
||||||
|
},
|
||||||
|
removePreKey(keyId) {
|
||||||
|
return Promise.resolve(this.remove(`25519KeypreKey${keyId}`));
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Returns a signed keypair object or undefined */
|
||||||
|
loadSignedPreKey(keyId) {
|
||||||
|
let res = this.get(`25519KeysignedKey${keyId}`);
|
||||||
|
if (res !== undefined) {
|
||||||
|
res = { pubKey: res.pubKey, privKey: res.privKey };
|
||||||
|
}
|
||||||
|
return Promise.resolve(res);
|
||||||
|
},
|
||||||
|
storeSignedPreKey(keyId, keyPair) {
|
||||||
|
return Promise.resolve(this.put(`25519KeysignedKey${keyId}`, keyPair));
|
||||||
|
},
|
||||||
|
removeSignedPreKey(keyId) {
|
||||||
|
return Promise.resolve(this.remove(`25519KeysignedKey${keyId}`));
|
||||||
|
},
|
||||||
|
|
||||||
|
loadSession(identifier) {
|
||||||
|
return Promise.resolve(this.get(`session${identifier}`));
|
||||||
|
},
|
||||||
|
storeSession(identifier, record) {
|
||||||
|
return Promise.resolve(this.put(`session${identifier}`, record));
|
||||||
|
},
|
||||||
|
removeSession(identifier) {
|
||||||
|
return Promise.resolve(this.remove(`session${identifier}`));
|
||||||
|
},
|
||||||
|
removeAllSessions(identifier) {
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
for (const id in this.store) {
|
||||||
|
if (id.startsWith(`session${identifier}`)) {
|
||||||
|
delete this.store[id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('SecretSessionCipher', () => {
|
||||||
|
it('successfully roundtrips', async () => {
|
||||||
|
const aliceStore = new InMemorySignalProtocolStore();
|
||||||
|
const bobStore = new InMemorySignalProtocolStore();
|
||||||
|
|
||||||
|
await _initializeSessions(aliceStore, bobStore);
|
||||||
|
|
||||||
|
const aliceIdentityKey = await aliceStore.getIdentityKeyPair();
|
||||||
|
|
||||||
|
const trustRoot = await libsignal.Curve.async.generateKeyPair();
|
||||||
|
const senderCertificate = await _createSenderCertificateFor(
|
||||||
|
trustRoot,
|
||||||
|
'+14151111111',
|
||||||
|
1,
|
||||||
|
aliceIdentityKey.pubKey,
|
||||||
|
31337
|
||||||
|
);
|
||||||
|
const aliceCipher = new SecretSessionCipher(aliceStore);
|
||||||
|
|
||||||
|
const ciphertext = await aliceCipher.encrypt(
|
||||||
|
new libsignal.SignalProtocolAddress('+14152222222', 1),
|
||||||
|
senderCertificate,
|
||||||
|
bytesFromString('smert za smert')
|
||||||
|
);
|
||||||
|
|
||||||
|
const bobCipher = new SecretSessionCipher(bobStore);
|
||||||
|
|
||||||
|
const decryptResult = await bobCipher.decrypt(
|
||||||
|
createCertificateValidator(trustRoot.pubKey),
|
||||||
|
ciphertext,
|
||||||
|
31335
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
stringFromBytes(decryptResult.content),
|
||||||
|
'smert za smert'
|
||||||
|
);
|
||||||
|
assert.strictEqual(decryptResult.sender.toString(), '+14151111111.1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails when untrusted', async () => {
|
||||||
|
const aliceStore = new InMemorySignalProtocolStore();
|
||||||
|
const bobStore = new InMemorySignalProtocolStore();
|
||||||
|
|
||||||
|
await _initializeSessions(aliceStore, bobStore);
|
||||||
|
|
||||||
|
const aliceIdentityKey = await aliceStore.getIdentityKeyPair();
|
||||||
|
|
||||||
|
const trustRoot = await libsignal.Curve.async.generateKeyPair();
|
||||||
|
const falseTrustRoot = await libsignal.Curve.async.generateKeyPair();
|
||||||
|
const senderCertificate = await _createSenderCertificateFor(
|
||||||
|
falseTrustRoot,
|
||||||
|
'+14151111111',
|
||||||
|
1,
|
||||||
|
aliceIdentityKey.pubKey,
|
||||||
|
31337
|
||||||
|
);
|
||||||
|
const aliceCipher = new SecretSessionCipher(aliceStore);
|
||||||
|
|
||||||
|
const ciphertext = await aliceCipher.encrypt(
|
||||||
|
new libsignal.SignalProtocolAddress('+14152222222', 1),
|
||||||
|
senderCertificate,
|
||||||
|
bytesFromString('и вот я')
|
||||||
|
);
|
||||||
|
|
||||||
|
const bobCipher = new SecretSessionCipher(bobStore);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await bobCipher.decrypt(
|
||||||
|
createCertificateValidator(trustRoot.pubKey),
|
||||||
|
ciphertext,
|
||||||
|
31335
|
||||||
|
);
|
||||||
|
throw new Error('It did not fail!');
|
||||||
|
} catch (error) {
|
||||||
|
assert.strictEqual(error.message, 'Invalid signature');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails when expired', async () => {
|
||||||
|
const aliceStore = new InMemorySignalProtocolStore();
|
||||||
|
const bobStore = new InMemorySignalProtocolStore();
|
||||||
|
|
||||||
|
await _initializeSessions(aliceStore, bobStore);
|
||||||
|
|
||||||
|
const aliceIdentityKey = await aliceStore.getIdentityKeyPair();
|
||||||
|
|
||||||
|
const trustRoot = await libsignal.Curve.async.generateKeyPair();
|
||||||
|
const senderCertificate = await _createSenderCertificateFor(
|
||||||
|
trustRoot,
|
||||||
|
'+14151111111',
|
||||||
|
1,
|
||||||
|
aliceIdentityKey.pubKey,
|
||||||
|
31337
|
||||||
|
);
|
||||||
|
const aliceCipher = new SecretSessionCipher(aliceStore);
|
||||||
|
|
||||||
|
const ciphertext = await aliceCipher.encrypt(
|
||||||
|
new libsignal.SignalProtocolAddress('+14152222222', 1),
|
||||||
|
senderCertificate,
|
||||||
|
bytesFromString('и вот я')
|
||||||
|
);
|
||||||
|
|
||||||
|
const bobCipher = new SecretSessionCipher(bobStore);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await bobCipher.decrypt(
|
||||||
|
createCertificateValidator(trustRoot.pubKey),
|
||||||
|
ciphertext,
|
||||||
|
31338
|
||||||
|
);
|
||||||
|
throw new Error('It did not fail!');
|
||||||
|
} catch (error) {
|
||||||
|
assert.strictEqual(error.message, 'Certificate is expired');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails when wrong identity', async () => {
|
||||||
|
const aliceStore = new InMemorySignalProtocolStore();
|
||||||
|
const bobStore = new InMemorySignalProtocolStore();
|
||||||
|
|
||||||
|
await _initializeSessions(aliceStore, bobStore);
|
||||||
|
|
||||||
|
const trustRoot = await libsignal.Curve.async.generateKeyPair();
|
||||||
|
const randomKeyPair = await libsignal.Curve.async.generateKeyPair();
|
||||||
|
const senderCertificate = await _createSenderCertificateFor(
|
||||||
|
trustRoot,
|
||||||
|
'+14151111111',
|
||||||
|
1,
|
||||||
|
randomKeyPair.pubKey,
|
||||||
|
31337
|
||||||
|
);
|
||||||
|
const aliceCipher = new SecretSessionCipher(aliceStore);
|
||||||
|
|
||||||
|
const ciphertext = await aliceCipher.encrypt(
|
||||||
|
new libsignal.SignalProtocolAddress('+14152222222', 1),
|
||||||
|
senderCertificate,
|
||||||
|
bytesFromString('smert za smert')
|
||||||
|
);
|
||||||
|
|
||||||
|
const bobCipher = new SecretSessionCipher(bobStore);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await bobCipher.decrypt(
|
||||||
|
createCertificateValidator(trustRoot.puKey),
|
||||||
|
ciphertext,
|
||||||
|
31335
|
||||||
|
);
|
||||||
|
throw new Error('It did not fail!');
|
||||||
|
} catch (error) {
|
||||||
|
assert.strictEqual(error.message, 'Invalid public key');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// private SenderCertificate _createCertificateFor(
|
||||||
|
// ECKeyPair trustRoot
|
||||||
|
// String sender
|
||||||
|
// int deviceId
|
||||||
|
// ECPublicKey identityKey
|
||||||
|
// long expires
|
||||||
|
// )
|
||||||
|
async function _createSenderCertificateFor(
|
||||||
|
trustRoot,
|
||||||
|
sender,
|
||||||
|
deviceId,
|
||||||
|
identityKey,
|
||||||
|
expires
|
||||||
|
) {
|
||||||
|
const serverKey = await libsignal.Curve.async.generateKeyPair();
|
||||||
|
|
||||||
|
const serverCertificateCertificateProto = new textsecure.protobuf.ServerCertificate.Certificate();
|
||||||
|
serverCertificateCertificateProto.id = 1;
|
||||||
|
serverCertificateCertificateProto.key = serverKey.pubKey;
|
||||||
|
const serverCertificateCertificateBytes = serverCertificateCertificateProto
|
||||||
|
.encode()
|
||||||
|
.toArrayBuffer();
|
||||||
|
|
||||||
|
const serverCertificateSignature = await libsignal.Curve.async.calculateSignature(
|
||||||
|
trustRoot.privKey,
|
||||||
|
serverCertificateCertificateBytes
|
||||||
|
);
|
||||||
|
|
||||||
|
const serverCertificateProto = new textsecure.protobuf.ServerCertificate();
|
||||||
|
serverCertificateProto.certificate = serverCertificateCertificateBytes;
|
||||||
|
serverCertificateProto.signature = serverCertificateSignature;
|
||||||
|
const serverCertificate = _createServerCertificateFromBuffer(
|
||||||
|
serverCertificateProto.encode().toArrayBuffer()
|
||||||
|
);
|
||||||
|
|
||||||
|
const senderCertificateCertificateProto = new textsecure.protobuf.SenderCertificate.Certificate();
|
||||||
|
senderCertificateCertificateProto.sender = sender;
|
||||||
|
senderCertificateCertificateProto.senderDevice = deviceId;
|
||||||
|
senderCertificateCertificateProto.identityKey = identityKey;
|
||||||
|
senderCertificateCertificateProto.expires = expires;
|
||||||
|
senderCertificateCertificateProto.signer = textsecure.protobuf.ServerCertificate.decode(
|
||||||
|
serverCertificate.serialized
|
||||||
|
);
|
||||||
|
const senderCertificateBytes = senderCertificateCertificateProto
|
||||||
|
.encode()
|
||||||
|
.toArrayBuffer();
|
||||||
|
|
||||||
|
const senderCertificateSignature = await libsignal.Curve.async.calculateSignature(
|
||||||
|
serverKey.privKey,
|
||||||
|
senderCertificateBytes
|
||||||
|
);
|
||||||
|
|
||||||
|
const senderCertificateProto = new textsecure.protobuf.SenderCertificate();
|
||||||
|
senderCertificateProto.certificate = senderCertificateBytes;
|
||||||
|
senderCertificateProto.signature = senderCertificateSignature;
|
||||||
|
return _createSenderCertificateFromBuffer(
|
||||||
|
senderCertificateProto.encode().toArrayBuffer()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// private void _initializeSessions(
|
||||||
|
// SignalProtocolStore aliceStore, SignalProtocolStore bobStore)
|
||||||
|
async function _initializeSessions(aliceStore, bobStore) {
|
||||||
|
const aliceAddress = new libsignal.SignalProtocolAddress('+14152222222', 1);
|
||||||
|
await aliceStore.put(
|
||||||
|
'identityKey',
|
||||||
|
await libsignal.Curve.generateKeyPair()
|
||||||
|
);
|
||||||
|
await bobStore.put('identityKey', await libsignal.Curve.generateKeyPair());
|
||||||
|
|
||||||
|
await aliceStore.put('registrationId', 57);
|
||||||
|
await bobStore.put('registrationId', 58);
|
||||||
|
|
||||||
|
const bobPreKey = await libsignal.Curve.async.generateKeyPair();
|
||||||
|
const bobIdentityKey = await bobStore.getIdentityKeyPair();
|
||||||
|
const bobSignedPreKey = await libsignal.KeyHelper.generateSignedPreKey(
|
||||||
|
bobIdentityKey,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
|
||||||
|
const bobBundle = {
|
||||||
|
identityKey: bobIdentityKey.pubKey,
|
||||||
|
registrationId: 1,
|
||||||
|
preKey: {
|
||||||
|
keyId: 1,
|
||||||
|
publicKey: bobPreKey.pubKey,
|
||||||
|
},
|
||||||
|
signedPreKey: {
|
||||||
|
keyId: 2,
|
||||||
|
publicKey: bobSignedPreKey.keyPair.pubKey,
|
||||||
|
signature: bobSignedPreKey.signature,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const aliceSessionBuilder = new libsignal.SessionBuilder(
|
||||||
|
aliceStore,
|
||||||
|
aliceAddress
|
||||||
|
);
|
||||||
|
await aliceSessionBuilder.processPreKey(bobBundle);
|
||||||
|
|
||||||
|
await bobStore.storeSignedPreKey(2, bobSignedPreKey.keyPair);
|
||||||
|
await bobStore.storePreKey(1, bobPreKey);
|
||||||
|
}
|
||||||
|
});
|
|
@ -12,6 +12,7 @@ describe('Startup', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should complete if user hasn’t previously synced', async () => {
|
it('should complete if user hasn’t previously synced', async () => {
|
||||||
|
const ourNumber = '+15551234567';
|
||||||
const deviceId = '2';
|
const deviceId = '2';
|
||||||
const sendRequestConfigurationSyncMessage = sandbox.spy();
|
const sendRequestConfigurationSyncMessage = sandbox.spy();
|
||||||
const storagePutSpy = sandbox.spy();
|
const storagePutSpy = sandbox.spy();
|
||||||
|
@ -25,15 +26,21 @@ describe('Startup', () => {
|
||||||
},
|
},
|
||||||
put: storagePutSpy,
|
put: storagePutSpy,
|
||||||
};
|
};
|
||||||
|
const prepareForSend = () => ({
|
||||||
|
wrap: promise => promise,
|
||||||
|
sendOptions: {},
|
||||||
|
});
|
||||||
|
|
||||||
const expected = {
|
const expected = {
|
||||||
status: 'complete',
|
status: 'complete',
|
||||||
};
|
};
|
||||||
|
|
||||||
const actual = await Startup.syncReadReceiptConfiguration({
|
const actual = await Startup.syncReadReceiptConfiguration({
|
||||||
|
ourNumber,
|
||||||
deviceId,
|
deviceId,
|
||||||
sendRequestConfigurationSyncMessage,
|
sendRequestConfigurationSyncMessage,
|
||||||
storage,
|
storage,
|
||||||
|
prepareForSend,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.deepEqual(actual, expected);
|
assert.deepEqual(actual, expected);
|
||||||
|
@ -43,9 +50,14 @@ describe('Startup', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be skipped if this is the primary device', async () => {
|
it('should be skipped if this is the primary device', async () => {
|
||||||
|
const ourNumber = '+15551234567';
|
||||||
const deviceId = '1';
|
const deviceId = '1';
|
||||||
const sendRequestConfigurationSyncMessage = () => {};
|
const sendRequestConfigurationSyncMessage = () => {};
|
||||||
const storage = {};
|
const storage = {};
|
||||||
|
const prepareForSend = () => ({
|
||||||
|
wrap: promise => promise,
|
||||||
|
sendOptions: {},
|
||||||
|
});
|
||||||
|
|
||||||
const expected = {
|
const expected = {
|
||||||
status: 'skipped',
|
status: 'skipped',
|
||||||
|
@ -53,15 +65,18 @@ describe('Startup', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const actual = await Startup.syncReadReceiptConfiguration({
|
const actual = await Startup.syncReadReceiptConfiguration({
|
||||||
|
ourNumber,
|
||||||
deviceId,
|
deviceId,
|
||||||
sendRequestConfigurationSyncMessage,
|
sendRequestConfigurationSyncMessage,
|
||||||
storage,
|
storage,
|
||||||
|
prepareForSend,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.deepEqual(actual, expected);
|
assert.deepEqual(actual, expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be skipped if user has previously synced', async () => {
|
it('should be skipped if user has previously synced', async () => {
|
||||||
|
const ourNumber = '+15551234567';
|
||||||
const deviceId = '2';
|
const deviceId = '2';
|
||||||
const sendRequestConfigurationSyncMessage = () => {};
|
const sendRequestConfigurationSyncMessage = () => {};
|
||||||
const storage = {
|
const storage = {
|
||||||
|
@ -73,6 +88,10 @@ describe('Startup', () => {
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
const prepareForSend = () => ({
|
||||||
|
wrap: promise => promise,
|
||||||
|
sendOptions: {},
|
||||||
|
});
|
||||||
|
|
||||||
const expected = {
|
const expected = {
|
||||||
status: 'skipped',
|
status: 'skipped',
|
||||||
|
@ -80,15 +99,18 @@ describe('Startup', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const actual = await Startup.syncReadReceiptConfiguration({
|
const actual = await Startup.syncReadReceiptConfiguration({
|
||||||
|
ourNumber,
|
||||||
deviceId,
|
deviceId,
|
||||||
sendRequestConfigurationSyncMessage,
|
sendRequestConfigurationSyncMessage,
|
||||||
storage,
|
storage,
|
||||||
|
prepareForSend,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.deepEqual(actual, expected);
|
assert.deepEqual(actual, expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return error if sending of sync request fails', async () => {
|
it('should return error if sending of sync request fails', async () => {
|
||||||
|
const ourNumber = '+15551234567';
|
||||||
const deviceId = '2';
|
const deviceId = '2';
|
||||||
|
|
||||||
const sendRequestConfigurationSyncMessage = sandbox.stub();
|
const sendRequestConfigurationSyncMessage = sandbox.stub();
|
||||||
|
@ -105,11 +127,17 @@ describe('Startup', () => {
|
||||||
},
|
},
|
||||||
put: storagePutSpy,
|
put: storagePutSpy,
|
||||||
};
|
};
|
||||||
|
const prepareForSend = () => ({
|
||||||
|
wrap: promise => promise,
|
||||||
|
sendOptions: {},
|
||||||
|
});
|
||||||
|
|
||||||
const actual = await Startup.syncReadReceiptConfiguration({
|
const actual = await Startup.syncReadReceiptConfiguration({
|
||||||
|
ourNumber,
|
||||||
deviceId,
|
deviceId,
|
||||||
sendRequestConfigurationSyncMessage,
|
sendRequestConfigurationSyncMessage,
|
||||||
storage,
|
storage,
|
||||||
|
prepareForSend,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(actual.status, 'error');
|
assert.equal(actual.status, 'error');
|
||||||
|
|
|
@ -126,3 +126,41 @@
|
||||||
i18n={util.i18n}
|
i18n={util.i18n}
|
||||||
/>
|
/>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Unidentified Delivery
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<MessageDetail
|
||||||
|
message={{
|
||||||
|
disableMenu: true,
|
||||||
|
direction: 'outgoing',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
conversationColor: 'pink',
|
||||||
|
text:
|
||||||
|
'Hello there from the new world! And this is multiple lines of text. Lines and lines and lines.',
|
||||||
|
status: 'read',
|
||||||
|
onDelete: () => console.log('onDelete'),
|
||||||
|
}}
|
||||||
|
contacts={[
|
||||||
|
{
|
||||||
|
phoneNumber: '(202) 555-1001',
|
||||||
|
avatarPath: util.gifObjectUrl,
|
||||||
|
status: 'read',
|
||||||
|
isUnidentifiedDelivery: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
phoneNumber: '(202) 555-1002',
|
||||||
|
avatarPath: util.pngObjectUrl,
|
||||||
|
status: 'delivered',
|
||||||
|
isUnidentifiedDelivery: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
phoneNumber: '(202) 555-1003',
|
||||||
|
color: 'teal',
|
||||||
|
status: 'read',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
sentAt={Date.now()}
|
||||||
|
i18n={util.i18n}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
|
@ -15,8 +15,10 @@ interface Contact {
|
||||||
avatarPath?: string;
|
avatarPath?: string;
|
||||||
color: string;
|
color: string;
|
||||||
isOutgoingKeyError: boolean;
|
isOutgoingKeyError: boolean;
|
||||||
|
isUnidentifiedDelivery: boolean;
|
||||||
|
|
||||||
errors?: Array<Error>;
|
errors?: Array<Error>;
|
||||||
|
|
||||||
onSendAnyway: () => void;
|
onSendAnyway: () => void;
|
||||||
onShowSafetyNumber: () => void;
|
onShowSafetyNumber: () => void;
|
||||||
}
|
}
|
||||||
|
@ -94,6 +96,9 @@ export class MessageDetail extends React.Component<Props> {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
|
const unidentifiedDeliveryComponent = contact.isUnidentifiedDelivery ? (
|
||||||
|
<div className="module-message-detail__contact__unidentified-delivery-icon" />
|
||||||
|
) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={contact.phoneNumber} className="module-message-detail__contact">
|
<div key={contact.phoneNumber} className="module-message-detail__contact">
|
||||||
|
@ -114,6 +119,7 @@ export class MessageDetail extends React.Component<Props> {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{errorComponent}
|
{errorComponent}
|
||||||
|
{unidentifiedDeliveryComponent}
|
||||||
{statusComponent}
|
{statusComponent}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -164,7 +164,7 @@
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/background.js",
|
"path": "js/background.js",
|
||||||
"line": " if ($('.dark-overlay').length) {",
|
"line": " if ($('.dark-overlay').length) {",
|
||||||
"lineNumber": 264,
|
"lineNumber": 265,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2018-09-19T21:59:32.770Z",
|
"updated": "2018-09-19T21:59:32.770Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"reasonDetail": "Protected from arbitrary input"
|
||||||
|
@ -173,7 +173,7 @@
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/background.js",
|
"path": "js/background.js",
|
||||||
"line": " $(document.body).prepend('<div class=\"dark-overlay\"></div>');",
|
"line": " $(document.body).prepend('<div class=\"dark-overlay\"></div>');",
|
||||||
"lineNumber": 267,
|
"lineNumber": 268,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2018-09-19T21:59:32.770Z",
|
"updated": "2018-09-19T21:59:32.770Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"reasonDetail": "Protected from arbitrary input"
|
||||||
|
@ -182,7 +182,7 @@
|
||||||
"rule": "jQuery-prepend(",
|
"rule": "jQuery-prepend(",
|
||||||
"path": "js/background.js",
|
"path": "js/background.js",
|
||||||
"line": " $(document.body).prepend('<div class=\"dark-overlay\"></div>');",
|
"line": " $(document.body).prepend('<div class=\"dark-overlay\"></div>');",
|
||||||
"lineNumber": 267,
|
"lineNumber": 268,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2018-09-19T18:13:29.628Z",
|
"updated": "2018-09-19T18:13:29.628Z",
|
||||||
"reasonDetail": "Hard-coded value"
|
"reasonDetail": "Hard-coded value"
|
||||||
|
@ -191,7 +191,7 @@
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/background.js",
|
"path": "js/background.js",
|
||||||
"line": " $('.dark-overlay').on('click', () => $('.dark-overlay').remove());",
|
"line": " $('.dark-overlay').on('click', () => $('.dark-overlay').remove());",
|
||||||
"lineNumber": 268,
|
"lineNumber": 269,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2018-09-19T21:59:32.770Z",
|
"updated": "2018-09-19T21:59:32.770Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"reasonDetail": "Protected from arbitrary input"
|
||||||
|
@ -200,7 +200,7 @@
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/background.js",
|
"path": "js/background.js",
|
||||||
"line": " removeDarkOverlay: () => $('.dark-overlay').remove(),",
|
"line": " removeDarkOverlay: () => $('.dark-overlay').remove(),",
|
||||||
"lineNumber": 270,
|
"lineNumber": 271,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2018-09-19T21:59:32.770Z",
|
"updated": "2018-09-19T21:59:32.770Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"reasonDetail": "Protected from arbitrary input"
|
||||||
|
@ -209,7 +209,7 @@
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/background.js",
|
"path": "js/background.js",
|
||||||
"line": " $('body').append(clearDataView.el);",
|
"line": " $('body').append(clearDataView.el);",
|
||||||
"lineNumber": 273,
|
"lineNumber": 274,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2018-09-19T21:59:32.770Z",
|
"updated": "2018-09-19T21:59:32.770Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"reasonDetail": "Protected from arbitrary input"
|
||||||
|
@ -218,7 +218,7 @@
|
||||||
"rule": "jQuery-append(",
|
"rule": "jQuery-append(",
|
||||||
"path": "js/background.js",
|
"path": "js/background.js",
|
||||||
"line": " $('body').append(clearDataView.el);",
|
"line": " $('body').append(clearDataView.el);",
|
||||||
"lineNumber": 273,
|
"lineNumber": 274,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2018-09-19T18:13:29.628Z",
|
"updated": "2018-09-19T18:13:29.628Z",
|
||||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||||
|
@ -227,7 +227,7 @@
|
||||||
"rule": "jQuery-load(",
|
"rule": "jQuery-load(",
|
||||||
"path": "js/background.js",
|
"path": "js/background.js",
|
||||||
"line": " await ConversationController.load();",
|
"line": " await ConversationController.load();",
|
||||||
"lineNumber": 508,
|
"lineNumber": 509,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-10-02T21:00:44.007Z"
|
"updated": "2018-10-02T21:00:44.007Z"
|
||||||
},
|
},
|
||||||
|
@ -235,24 +235,32 @@
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/background.js",
|
"path": "js/background.js",
|
||||||
"line": " el: $('body'),",
|
"line": " el: $('body'),",
|
||||||
"lineNumber": 562,
|
"lineNumber": 572,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2018-09-19T21:59:32.770Z",
|
"updated": "2018-10-16T23:47:48.006Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"reasonDetail": "Protected from arbitrary input"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "js/background.js",
|
"path": "js/background.js",
|
||||||
"line": " const profileKey = dcodeIO.ByteBuffer.wrap(details.profileKey).toString(",
|
"line": " wrap(",
|
||||||
"lineNumber": 884,
|
"lineNumber": 830,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-10-02T21:00:44.007Z"
|
"updated": "2018-10-18T22:23:00.485Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "js/background.js",
|
||||||
|
"line": " await wrap(",
|
||||||
|
"lineNumber": 1320,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2018-10-26T22:43:23.229Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-load(",
|
"rule": "jQuery-load(",
|
||||||
"path": "js/conversation_controller.js",
|
"path": "js/conversation_controller.js",
|
||||||
"line": " async load() {",
|
"line": " async load() {",
|
||||||
"lineNumber": 198,
|
"lineNumber": 208,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-10-02T21:00:44.007Z"
|
"updated": "2018-10-02T21:00:44.007Z"
|
||||||
},
|
},
|
||||||
|
@ -260,7 +268,7 @@
|
||||||
"rule": "jQuery-load(",
|
"rule": "jQuery-load(",
|
||||||
"path": "js/conversation_controller.js",
|
"path": "js/conversation_controller.js",
|
||||||
"line": " this._initialPromise = load();",
|
"line": " this._initialPromise = load();",
|
||||||
"lineNumber": 227,
|
"lineNumber": 237,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-10-02T21:00:44.007Z"
|
"updated": "2018-10-02T21:00:44.007Z"
|
||||||
},
|
},
|
||||||
|
@ -293,51 +301,59 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "js/models/conversations.js",
|
"path": "js/models/messages.js",
|
||||||
"line": " const identityKey = dcodeIO.ByteBuffer.wrap(",
|
"line": " this.send(wrap(promise));",
|
||||||
"lineNumber": 1097,
|
"lineNumber": 794,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-10-02T21:00:44.007Z"
|
"updated": "2018-10-05T23:12:28.961Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "js/models/conversations.js",
|
"path": "js/models/messages.js",
|
||||||
"line": " const keyBuffer = dcodeIO.ByteBuffer.wrap(key, 'base64').toArrayBuffer();",
|
"line": " return wrap(",
|
||||||
"lineNumber": 1157,
|
"lineNumber": 996,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-10-02T21:00:44.007Z"
|
"updated": "2018-10-05T23:12:28.961Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "js/models/conversations.js",
|
"path": "js/modules/crypto.js",
|
||||||
"line": " const name = dcodeIO.ByteBuffer.wrap(decrypted).toString('utf8');",
|
"line": " return dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64');",
|
||||||
"lineNumber": 1170,
|
"lineNumber": 271,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-10-02T21:00:44.007Z"
|
"updated": "2018-10-05T23:12:28.961Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "js/models/conversations.js",
|
"path": "js/modules/crypto.js",
|
||||||
"line": " const keyBuffer = dcodeIO.ByteBuffer.wrap(key, 'base64').toArrayBuffer();",
|
"line": " return dcodeIO.ByteBuffer.wrap(base64string, 'base64').toArrayBuffer();",
|
||||||
"lineNumber": 1185,
|
"lineNumber": 274,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-10-02T21:00:44.007Z"
|
"updated": "2018-10-05T23:12:28.961Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "js/modules/backup.js",
|
"path": "js/modules/crypto.js",
|
||||||
"line": " data: dcodeIO.ByteBuffer.wrap(val).toString('base64'),",
|
"line": " return dcodeIO.ByteBuffer.wrap(key, 'binary').toArrayBuffer();",
|
||||||
"lineNumber": 51,
|
"lineNumber": 278,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
"updated": "2018-10-05T23:12:28.961Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "js/modules/backup.js",
|
"path": "js/modules/crypto.js",
|
||||||
"line": " object[key] = dcodeIO.ByteBuffer.wrap(val.data, 'base64').toArrayBuffer();",
|
"line": " return dcodeIO.ByteBuffer.wrap(string, 'utf8').toArrayBuffer();",
|
||||||
"lineNumber": 73,
|
"lineNumber": 282,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
"updated": "2018-10-05T23:12:28.961Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "js/modules/crypto.js",
|
||||||
|
"line": " return dcodeIO.ByteBuffer.wrap(buffer).toString('utf8');",
|
||||||
|
"lineNumber": 285,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2018-10-05T23:12:28.961Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-append(",
|
"rule": "jQuery-append(",
|
||||||
|
@ -365,19 +381,11 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "js/modules/types/conversation.js",
|
"path": "js/modules/startup.js",
|
||||||
"line": " return dcodeIO.ByteBuffer.wrap(arraybuffer).toString('base64');",
|
"line": " await wrap(sendRequestConfigurationSyncMessage(sendOptions));",
|
||||||
"lineNumber": 12,
|
"lineNumber": 50,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-10-02T21:00:44.007Z"
|
"updated": "2018-10-05T23:12:28.961Z"
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "js/modules/types/conversation.js",
|
|
||||||
"line": " return dcodeIO.ByteBuffer.wrap(base64, 'base64').toArrayBuffer();",
|
|
||||||
"lineNumber": 16,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2018-10-02T21:00:44.007Z"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
|
@ -2291,7 +2299,7 @@
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "libtextsecure/message_receiver.js",
|
"path": "libtextsecure/message_receiver.js",
|
||||||
"line": " Promise.resolve(dcodeIO.ByteBuffer.wrap(string, 'binary').toArrayBuffer());",
|
"line": " Promise.resolve(dcodeIO.ByteBuffer.wrap(string, 'binary').toArrayBuffer());",
|
||||||
"lineNumber": 138,
|
"lineNumber": 145,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
"updated": "2018-09-19T18:13:29.628Z"
|
||||||
},
|
},
|
||||||
|
@ -2299,7 +2307,7 @@
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "libtextsecure/message_receiver.js",
|
"path": "libtextsecure/message_receiver.js",
|
||||||
"line": " Promise.resolve(dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('binary'));",
|
"line": " Promise.resolve(dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('binary'));",
|
||||||
"lineNumber": 140,
|
"lineNumber": 147,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
"updated": "2018-09-19T18:13:29.628Z"
|
||||||
},
|
},
|
||||||
|
@ -2307,7 +2315,7 @@
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "libtextsecure/message_receiver.js",
|
"path": "libtextsecure/message_receiver.js",
|
||||||
"line": " const buffer = dcodeIO.ByteBuffer.wrap(ciphertext);",
|
"line": " const buffer = dcodeIO.ByteBuffer.wrap(ciphertext);",
|
||||||
"lineNumber": 688,
|
"lineNumber": 774,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
"updated": "2018-09-19T18:13:29.628Z"
|
||||||
},
|
},
|
||||||
|
@ -2315,10 +2323,26 @@
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "libtextsecure/message_receiver.js",
|
"path": "libtextsecure/message_receiver.js",
|
||||||
"line": " const buffer = dcodeIO.ByteBuffer.wrap(ciphertext);",
|
"line": " const buffer = dcodeIO.ByteBuffer.wrap(ciphertext);",
|
||||||
"lineNumber": 713,
|
"lineNumber": 799,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
"updated": "2018-09-19T18:13:29.628Z"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "libtextsecure/sync_request.js",
|
||||||
|
"line": " wrap(sender.sendRequestContactSyncMessage(sendOptions))",
|
||||||
|
"lineNumber": 31,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2018-10-05T23:12:28.961Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "libtextsecure/sync_request.js",
|
||||||
|
"line": " return wrap(sender.sendRequestGroupSyncMessage(sendOptions));",
|
||||||
|
"lineNumber": 34,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2018-10-05T23:12:28.961Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"rule": "eval",
|
"rule": "eval",
|
||||||
"path": "node_modules/@protobufjs/inquire/index.js",
|
"path": "node_modules/@protobufjs/inquire/index.js",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue