parent
817cf5ed03
commit
a7d78c0e9b
38 changed files with 2996 additions and 789 deletions
|
@ -6,7 +6,8 @@
|
|||
btoa,
|
||||
getString,
|
||||
libphonenumber,
|
||||
Event
|
||||
Event,
|
||||
ConversationController
|
||||
*/
|
||||
|
||||
/* eslint-disable more/no-then */
|
||||
|
@ -52,19 +53,29 @@
|
|||
const confirmKeys = this.confirmKeys.bind(this);
|
||||
const registrationDone = this.registrationDone.bind(this);
|
||||
return this.queueTask(() =>
|
||||
libsignal.KeyHelper.generateIdentityKeyPair().then(identityKeyPair => {
|
||||
const profileKey = textsecure.crypto.getRandomBytes(32);
|
||||
return createAccount(
|
||||
number,
|
||||
verificationCode,
|
||||
identityKeyPair,
|
||||
profileKey
|
||||
)
|
||||
.then(clearSessionsAndPreKeys)
|
||||
.then(generateKeys)
|
||||
.then(keys => registerKeys(keys).then(() => confirmKeys(keys)))
|
||||
.then(registrationDone);
|
||||
})
|
||||
libsignal.KeyHelper.generateIdentityKeyPair().then(
|
||||
async identityKeyPair => {
|
||||
const profileKey = textsecure.crypto.getRandomBytes(32);
|
||||
const accessKey = await window.Signal.Crypto.deriveAccessKey(
|
||||
profileKey
|
||||
);
|
||||
|
||||
return createAccount(
|
||||
number,
|
||||
verificationCode,
|
||||
identityKeyPair,
|
||||
profileKey,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
{ accessKey }
|
||||
)
|
||||
.then(clearSessionsAndPreKeys)
|
||||
.then(generateKeys)
|
||||
.then(keys => registerKeys(keys).then(() => confirmKeys(keys)))
|
||||
.then(() => registrationDone(number));
|
||||
}
|
||||
)
|
||||
);
|
||||
},
|
||||
registerSecondDevice(setProvisioningUrl, confirmNumber, progressCallback) {
|
||||
|
@ -147,7 +158,9 @@
|
|||
confirmKeys(keys)
|
||||
)
|
||||
)
|
||||
.then(registrationDone);
|
||||
.then(() =>
|
||||
registrationDone(provisionMessage.number)
|
||||
);
|
||||
}
|
||||
)
|
||||
)
|
||||
|
@ -185,8 +198,6 @@
|
|||
const store = textsecure.storage.protocol;
|
||||
const { server, cleanSignedPreKeys } = this;
|
||||
|
||||
// TODO: harden this against missing identity key? Otherwise, we get
|
||||
// retries every five seconds.
|
||||
return store
|
||||
.getIdentityKeyPair()
|
||||
.then(
|
||||
|
@ -196,6 +207,8 @@
|
|||
signedKeyId
|
||||
),
|
||||
() => {
|
||||
// We swallow any error here, because we don't want to get into
|
||||
// a loop of repeated retries.
|
||||
window.log.error(
|
||||
'Failed to get identity key. Canceling key rotation.'
|
||||
);
|
||||
|
@ -329,8 +342,10 @@
|
|||
profileKey,
|
||||
deviceName,
|
||||
userAgent,
|
||||
readReceipts
|
||||
readReceipts,
|
||||
options = {}
|
||||
) {
|
||||
const { accessKey } = options;
|
||||
const signalingKey = libsignal.crypto.getRandomBytes(32 + 20);
|
||||
let password = btoa(getString(libsignal.crypto.getRandomBytes(16)));
|
||||
password = password.substring(0, password.length - 2);
|
||||
|
@ -345,7 +360,8 @@
|
|||
password,
|
||||
signalingKey,
|
||||
registrationId,
|
||||
deviceName
|
||||
deviceName,
|
||||
{ accessKey }
|
||||
)
|
||||
.then(response => {
|
||||
if (previousNumber && previousNumber !== number) {
|
||||
|
@ -499,8 +515,12 @@
|
|||
);
|
||||
});
|
||||
},
|
||||
registrationDone() {
|
||||
async registrationDone(number) {
|
||||
window.log.info('registration done');
|
||||
|
||||
// Ensure that we always have a conversation for ourself
|
||||
await ConversationController.getOrCreateAndWait(number, 'private');
|
||||
|
||||
this.dispatchEvent(new Event('registration'));
|
||||
},
|
||||
});
|
||||
|
|
|
@ -22848,7 +22848,7 @@ function _memset(ptr, value, num) {
|
|||
}
|
||||
}
|
||||
while ((ptr|0) < (stop4|0)) {
|
||||
HEAP32[ptr>>2]=value4;
|
||||
HEAP32[((ptr)>>2)]=value4;
|
||||
ptr = (ptr+4)|0;
|
||||
}
|
||||
}
|
||||
|
@ -22904,7 +22904,7 @@ function _memcpy(dest, src, num) {
|
|||
num = (num-1)|0;
|
||||
}
|
||||
while ((num|0) >= 4) {
|
||||
HEAP32[dest>>2]=((HEAP32[src>>2])|0);
|
||||
HEAP32[((dest)>>2)]=((HEAP32[((src)>>2)])|0);
|
||||
dest = (dest+4)|0;
|
||||
src = (src+4)|0;
|
||||
num = (num-4)|0;
|
||||
|
@ -35093,7 +35093,6 @@ Curve25519Worker.prototype = {
|
|||
if (pubKey.byteLength == 33) {
|
||||
return pubKey.slice(1);
|
||||
} else {
|
||||
console.error("WARNING: Expected pubkey of length 33, please report the ST and client that generated the pubkey");
|
||||
return pubKey;
|
||||
}
|
||||
}
|
||||
|
@ -35179,7 +35178,10 @@ Curve25519Worker.prototype = {
|
|||
},
|
||||
calculateSignature: function(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
|
||||
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));
|
||||
};
|
||||
|
||||
|
@ -35460,7 +35458,7 @@ Internal.protoText = function() {
|
|||
/* vim: ts=4:sw=4 */
|
||||
var Internal = Internal || {};
|
||||
|
||||
Internal.protobuf = function() {
|
||||
Internal.protobuf = (function() {
|
||||
'use strict';
|
||||
|
||||
function loadProtoBufs(filename) {
|
||||
|
@ -35473,7 +35471,7 @@ Internal.protobuf = function() {
|
|||
WhisperMessage : protocolMessages.WhisperMessage,
|
||||
PreKeyWhisperMessage : protocolMessages.PreKeyWhisperMessage
|
||||
};
|
||||
}();
|
||||
})();
|
||||
|
||||
/*
|
||||
* vim: ts=4:sw=4
|
||||
|
@ -35888,6 +35886,7 @@ SessionBuilder.prototype = {
|
|||
if (message.preKeyId && !preKeyPair) {
|
||||
console.log('Invalid prekey id', message.preKeyId);
|
||||
}
|
||||
|
||||
return this.initSession(false, preKeyPair, signedPreKeyPair,
|
||||
message.identityKey.toArrayBuffer(),
|
||||
message.baseKey.toArrayBuffer(), undefined, message.registrationId
|
||||
|
@ -36028,6 +36027,7 @@ SessionCipher.prototype = {
|
|||
return Internal.SessionRecord.deserialize(serialized);
|
||||
});
|
||||
},
|
||||
// encoding is an optional parameter - wrap() will only translate if one is provided
|
||||
encrypt: function(buffer, encoding) {
|
||||
buffer = dcodeIO.ByteBuffer.wrap(buffer, encoding).toArrayBuffer();
|
||||
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() {
|
||||
return Internal.SessionLock.queueJobForNumber(this.remoteAddress.toString(), function() {
|
||||
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
|
||||
this.encrypt = cipher.encrypt.bind(cipher);
|
||||
this.getRecord = cipher.getRecord.bind(cipher);
|
||||
|
||||
// returns a Promise that inits a session if necessary and resolves
|
||||
// to a decrypted plaintext array buffer
|
||||
|
|
|
@ -123,6 +123,13 @@ function MessageReceiver(username, password, signalingKey, options = {}) {
|
|||
this.password = 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);
|
||||
this.number = address.getName();
|
||||
this.deviceId = address.getDeviceId();
|
||||
|
@ -279,6 +286,8 @@ MessageReceiver.prototype.extend({
|
|||
return request.respond(200, 'OK');
|
||||
}
|
||||
|
||||
envelope.id = envelope.serverGuid || window.getGuid();
|
||||
|
||||
return this.addToCache(envelope, plaintext).then(
|
||||
async () => {
|
||||
request.respond(200, 'OK');
|
||||
|
@ -437,9 +446,13 @@ MessageReceiver.prototype.extend({
|
|||
}
|
||||
},
|
||||
getEnvelopeId(envelope) {
|
||||
return `${envelope.source}.${
|
||||
envelope.sourceDevice
|
||||
} ${envelope.timestamp.toNumber()}`;
|
||||
if (envelope.source) {
|
||||
return `${envelope.source}.${
|
||||
envelope.sourceDevice
|
||||
} ${envelope.timestamp.toNumber()} (${envelope.id})`;
|
||||
}
|
||||
|
||||
return envelope.id;
|
||||
},
|
||||
async getAllFromCache() {
|
||||
window.log.info('getAllFromCache');
|
||||
|
@ -482,7 +495,7 @@ MessageReceiver.prototype.extend({
|
|||
);
|
||||
},
|
||||
async addToCache(envelope, plaintext) {
|
||||
const id = this.getEnvelopeId(envelope);
|
||||
const { id } = envelope;
|
||||
const data = {
|
||||
id,
|
||||
version: 2,
|
||||
|
@ -493,7 +506,7 @@ MessageReceiver.prototype.extend({
|
|||
return textsecure.storage.unprocessed.add(data);
|
||||
},
|
||||
async updateCache(envelope, plaintext) {
|
||||
const id = this.getEnvelopeId(envelope);
|
||||
const { id } = envelope;
|
||||
const item = await textsecure.storage.unprocessed.get(id);
|
||||
if (!item) {
|
||||
window.log.error(
|
||||
|
@ -517,11 +530,11 @@ MessageReceiver.prototype.extend({
|
|||
return textsecure.storage.unprocessed.save(item.attributes);
|
||||
},
|
||||
removeFromCache(envelope) {
|
||||
const id = this.getEnvelopeId(envelope);
|
||||
const { id } = envelope;
|
||||
return textsecure.storage.unprocessed.remove(id);
|
||||
},
|
||||
queueDecryptedEnvelope(envelope, plaintext) {
|
||||
const id = this.getEnvelopeId(envelope);
|
||||
const { id } = envelope;
|
||||
window.log.info('queueing decrypted envelope', id);
|
||||
|
||||
const task = this.handleDecryptedEnvelope.bind(this, envelope, plaintext);
|
||||
|
@ -626,6 +639,8 @@ MessageReceiver.prototype.extend({
|
|||
return plaintext;
|
||||
},
|
||||
decrypt(envelope, ciphertext) {
|
||||
const { serverTrustRoot } = this;
|
||||
|
||||
let promise;
|
||||
const address = new libsignal.SignalProtocolAddress(
|
||||
envelope.source,
|
||||
|
@ -646,6 +661,14 @@ MessageReceiver.prototype.extend({
|
|||
address,
|
||||
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) {
|
||||
case textsecure.protobuf.Envelope.Type.CIPHERTEXT:
|
||||
|
@ -662,13 +685,76 @@ MessageReceiver.prototype.extend({
|
|||
address
|
||||
);
|
||||
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:
|
||||
promise = Promise.reject(new Error('Unknown message type'));
|
||||
}
|
||||
|
||||
return promise
|
||||
.then(plaintext =>
|
||||
this.updateCache(envelope, plaintext).then(
|
||||
.then(plaintext => {
|
||||
const { isMe } = plaintext || {};
|
||||
if (isMe) {
|
||||
return this.removeFromCache(envelope);
|
||||
}
|
||||
|
||||
return this.updateCache(envelope, plaintext).then(
|
||||
() => plaintext,
|
||||
error => {
|
||||
window.log.error(
|
||||
|
@ -677,8 +763,8 @@ MessageReceiver.prototype.extend({
|
|||
);
|
||||
return plaintext;
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
})
|
||||
.catch(error => {
|
||||
let errorToThrow = error;
|
||||
|
||||
|
@ -720,13 +806,14 @@ MessageReceiver.prototype.extend({
|
|||
throw e;
|
||||
}
|
||||
},
|
||||
handleSentMessage(
|
||||
envelope,
|
||||
destination,
|
||||
timestamp,
|
||||
msg,
|
||||
expirationStartTimestamp
|
||||
) {
|
||||
handleSentMessage(envelope, sentContainer, msg) {
|
||||
const {
|
||||
destination,
|
||||
timestamp,
|
||||
expirationStartTimestamp,
|
||||
unidentifiedStatus,
|
||||
} = sentContainer;
|
||||
|
||||
let p = Promise.resolve();
|
||||
// eslint-disable-next-line no-bitwise
|
||||
if (msg.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) {
|
||||
|
@ -757,6 +844,7 @@ MessageReceiver.prototype.extend({
|
|||
destination,
|
||||
timestamp: timestamp.toNumber(),
|
||||
device: envelope.sourceDevice,
|
||||
unidentifiedStatus,
|
||||
message,
|
||||
};
|
||||
if (expirationStartTimestamp) {
|
||||
|
@ -799,6 +887,7 @@ MessageReceiver.prototype.extend({
|
|||
sourceDevice: envelope.sourceDevice,
|
||||
timestamp: envelope.timestamp.toNumber(),
|
||||
receivedAt: envelope.receivedAt,
|
||||
unidentifiedDeliveryReceived: envelope.unidentifiedDeliveryReceived,
|
||||
message,
|
||||
};
|
||||
return this.dispatchAndWait(ev);
|
||||
|
@ -806,18 +895,26 @@ MessageReceiver.prototype.extend({
|
|||
);
|
||||
},
|
||||
handleLegacyMessage(envelope) {
|
||||
return this.decrypt(envelope, envelope.legacyMessage).then(plaintext =>
|
||||
this.innerHandleLegacyMessage(envelope, plaintext)
|
||||
);
|
||||
return this.decrypt(envelope, envelope.legacyMessage).then(plaintext => {
|
||||
if (!plaintext) {
|
||||
window.log.warn('handleLegacyMessage: plaintext was falsey');
|
||||
return null;
|
||||
}
|
||||
return this.innerHandleLegacyMessage(envelope, plaintext);
|
||||
});
|
||||
},
|
||||
innerHandleLegacyMessage(envelope, plaintext) {
|
||||
const message = textsecure.protobuf.DataMessage.decode(plaintext);
|
||||
return this.handleDataMessage(envelope, message);
|
||||
},
|
||||
handleContentMessage(envelope) {
|
||||
return this.decrypt(envelope, envelope.content).then(plaintext =>
|
||||
this.innerHandleContentMessage(envelope, plaintext)
|
||||
);
|
||||
return this.decrypt(envelope, envelope.content).then(plaintext => {
|
||||
if (!plaintext) {
|
||||
window.log.warn('handleContentMessage: plaintext was falsey');
|
||||
return null;
|
||||
}
|
||||
return this.innerHandleContentMessage(envelope, plaintext);
|
||||
});
|
||||
},
|
||||
innerHandleContentMessage(envelope, plaintext) {
|
||||
const content = textsecure.protobuf.Content.decode(plaintext);
|
||||
|
@ -895,13 +992,7 @@ MessageReceiver.prototype.extend({
|
|||
'from',
|
||||
this.getEnvelopeId(envelope)
|
||||
);
|
||||
return this.handleSentMessage(
|
||||
envelope,
|
||||
sentMessage.destination,
|
||||
sentMessage.timestamp,
|
||||
sentMessage.message,
|
||||
sentMessage.expirationStartTimestamp
|
||||
);
|
||||
return this.handleSentMessage(envelope, sentMessage, sentMessage.message);
|
||||
} else if (syncMessage.contacts) {
|
||||
return this.handleContacts(envelope, syncMessage.contacts);
|
||||
} else if (syncMessage.groups) {
|
||||
|
@ -922,11 +1013,10 @@ MessageReceiver.prototype.extend({
|
|||
throw new Error('Got empty SyncMessage');
|
||||
},
|
||||
handleConfiguration(envelope, configuration) {
|
||||
window.log.info('got configuration sync message');
|
||||
const ev = new Event('configuration');
|
||||
ev.confirm = this.removeFromCache.bind(this, envelope);
|
||||
ev.configuration = {
|
||||
readReceipts: configuration.readReceipts,
|
||||
};
|
||||
ev.configuration = configuration;
|
||||
return this.dispatchAndWait(ev);
|
||||
},
|
||||
handleVerified(envelope, verified) {
|
||||
|
@ -1075,102 +1165,6 @@ MessageReceiver.prototype.extend({
|
|||
.then(decryptAttachment)
|
||||
.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) {
|
||||
window.log.info('got end session');
|
||||
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 */
|
||||
|
||||
|
@ -8,7 +8,8 @@ function OutgoingMessage(
|
|||
numbers,
|
||||
message,
|
||||
silent,
|
||||
callback
|
||||
callback,
|
||||
options = {}
|
||||
) {
|
||||
if (message instanceof textsecure.protobuf.DataMessage) {
|
||||
const content = new textsecure.protobuf.Content();
|
||||
|
@ -26,6 +27,12 @@ function OutgoingMessage(
|
|||
this.numbersCompleted = 0;
|
||||
this.errors = [];
|
||||
this.successfulNumbers = [];
|
||||
this.failoverNumbers = [];
|
||||
this.unidentifiedDeliveries = [];
|
||||
|
||||
const { numberInfo, senderCertificate } = options;
|
||||
this.numberInfo = numberInfo;
|
||||
this.senderCertificate = senderCertificate;
|
||||
}
|
||||
|
||||
OutgoingMessage.prototype = {
|
||||
|
@ -35,7 +42,9 @@ OutgoingMessage.prototype = {
|
|||
if (this.numbersCompleted >= this.numbers.length) {
|
||||
this.callback({
|
||||
successfulNumbers: this.successfulNumbers,
|
||||
failoverNumbers: this.failoverNumbers,
|
||||
errors: this.errors,
|
||||
unidentifiedDeliveries: this.unidentifiedDeliveries,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -57,7 +66,7 @@ OutgoingMessage.prototype = {
|
|||
this.errors[this.errors.length] = error;
|
||||
this.numberCompleted();
|
||||
},
|
||||
reloadDevicesAndSend(number, recurse) {
|
||||
reloadDevicesAndSend(number, recurse, failover) {
|
||||
return () =>
|
||||
textsecure.storage.protocol.getDeviceIds(number).then(deviceIds => {
|
||||
if (deviceIds.length === 0) {
|
||||
|
@ -67,7 +76,7 @@ OutgoingMessage.prototype = {
|
|||
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) {
|
||||
return this.server.getKeysForNumber(number).then(handleResult);
|
||||
}
|
||||
let promise = Promise.resolve();
|
||||
updateDevices.forEach(device => {
|
||||
promise = promise.then(() =>
|
||||
this.server
|
||||
.getKeysForNumber(number, device)
|
||||
.then(handleResult)
|
||||
.catch(e => {
|
||||
if (e.name === 'HTTPError' && e.code === 404) {
|
||||
if (device !== 1) {
|
||||
return this.removeDeviceIdsForNumber(number, [device]);
|
||||
if (accessKey) {
|
||||
return this.server
|
||||
.getKeysForNumberUnauth(number, '*', { accessKey })
|
||||
.catch(error => {
|
||||
if (error.code === 401 || error.code === 403) {
|
||||
if (this.failoverNumbers.indexOf(number) === -1) {
|
||||
this.failoverNumbers.push(number);
|
||||
}
|
||||
throw new textsecure.UnregisteredUserError(number, e);
|
||||
} else {
|
||||
throw e;
|
||||
return this.server.getKeysForNumber(number, '*');
|
||||
}
|
||||
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;
|
||||
},
|
||||
|
||||
transmitMessage(number, jsonData, timestamp) {
|
||||
return this.server
|
||||
.sendMessages(number, jsonData, timestamp, this.silent)
|
||||
.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 new textsecure.SendMessageNetworkError(
|
||||
number,
|
||||
jsonData,
|
||||
e,
|
||||
timestamp
|
||||
);
|
||||
transmitMessage(number, jsonData, timestamp, { accessKey } = {}) {
|
||||
let promise;
|
||||
|
||||
if (accessKey) {
|
||||
promise = this.server.sendMessagesUnauth(
|
||||
number,
|
||||
jsonData,
|
||||
timestamp,
|
||||
this.silent,
|
||||
{ accessKey }
|
||||
);
|
||||
} else {
|
||||
promise = this.server.sendMessages(
|
||||
number,
|
||||
jsonData,
|
||||
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) {
|
||||
|
@ -179,15 +245,42 @@ OutgoingMessage.prototype = {
|
|||
return this.plaintext;
|
||||
},
|
||||
|
||||
doSendMessage(number, deviceIds, recurse) {
|
||||
doSendMessage(number, deviceIds, recurse, failover) {
|
||||
const ciphers = {};
|
||||
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(
|
||||
deviceIds.map(deviceId => {
|
||||
deviceIds.map(async deviceId => {
|
||||
const address = new libsignal.SignalProtocolAddress(number, deviceId);
|
||||
|
||||
const ourNumber = textsecure.storage.user.getNumber();
|
||||
const options = {};
|
||||
|
||||
// No limit on message keys if we're communicating with our other devices
|
||||
|
@ -195,26 +288,80 @@ OutgoingMessage.prototype = {
|
|||
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(
|
||||
textsecure.storage.protocol,
|
||||
address,
|
||||
options
|
||||
);
|
||||
ciphers[address.getDeviceId()] = sessionCipher;
|
||||
return sessionCipher.encrypt(plaintext).then(ciphertext => ({
|
||||
|
||||
const ciphertext = await sessionCipher.encrypt(plaintext);
|
||||
return {
|
||||
type: ciphertext.type,
|
||||
destinationDeviceId: address.getDeviceId(),
|
||||
destinationRegistrationId: ciphertext.registrationId,
|
||||
content: btoa(ciphertext.body),
|
||||
}));
|
||||
};
|
||||
})
|
||||
)
|
||||
.then(jsonData =>
|
||||
this.transmitMessage(number, jsonData, this.timestamp).then(() => {
|
||||
this.successfulNumbers[this.successfulNumbers.length] = number;
|
||||
this.numberCompleted();
|
||||
})
|
||||
)
|
||||
.then(jsonData => {
|
||||
if (sealedSender) {
|
||||
return this.transmitMessage(number, jsonData, this.timestamp, {
|
||||
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 => {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
|
@ -237,7 +384,9 @@ OutgoingMessage.prototype = {
|
|||
} else {
|
||||
p = Promise.all(
|
||||
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.missingDevices;
|
||||
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') {
|
||||
|
@ -305,28 +456,28 @@ OutgoingMessage.prototype = {
|
|||
return promise;
|
||||
},
|
||||
|
||||
sendToNumber(number) {
|
||||
return this.getStaleDeviceIdsForNumber(number).then(updateDevices =>
|
||||
this.getKeysForNumber(number, updateDevices)
|
||||
.then(this.reloadDevicesAndSend(number, true))
|
||||
.catch(error => {
|
||||
if (error.message === 'Identity key changed') {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
error = new textsecure.OutgoingIdentityKeyError(
|
||||
number,
|
||||
error.originalMessage,
|
||||
error.timestamp,
|
||||
error.identityKey
|
||||
);
|
||||
this.registerError(number, 'Identity key changed', error);
|
||||
} else {
|
||||
this.registerError(
|
||||
number,
|
||||
`Failed to retrieve new device keys for number ${number}`,
|
||||
error
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
async sendToNumber(number) {
|
||||
try {
|
||||
const updateDevices = await this.getStaleDeviceIdsForNumber(number);
|
||||
await this.getKeysForNumber(number, updateDevices);
|
||||
await this.reloadDevicesAndSend(number, true)();
|
||||
} catch (error) {
|
||||
if (error.message === 'Identity key changed') {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
const newError = new textsecure.OutgoingIdentityKeyError(
|
||||
number,
|
||||
error.originalMessage,
|
||||
error.timestamp,
|
||||
error.identityKey
|
||||
);
|
||||
this.registerError(number, 'Identity key changed', newError);
|
||||
} else {
|
||||
this.registerError(
|
||||
number,
|
||||
`Failed to retrieve new device keys for number ${number}`,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -35,4 +35,7 @@
|
|||
loadProtoBufs('SignalService.proto');
|
||||
loadProtoBufs('SubProtocol.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) {
|
||||
const taskWithTimeout = textsecure.createTaskWithTimeout(
|
||||
runJob,
|
||||
|
@ -321,8 +256,10 @@ MessageSender.prototype = {
|
|||
});
|
||||
},
|
||||
|
||||
sendMessage(attrs) {
|
||||
sendMessage(attrs, options) {
|
||||
const message = new Message(attrs);
|
||||
const silent = false;
|
||||
|
||||
return Promise.all([
|
||||
this.uploadAttachments(message),
|
||||
this.uploadThumbnails(message),
|
||||
|
@ -340,12 +277,21 @@ MessageSender.prototype = {
|
|||
} else {
|
||||
resolve(res);
|
||||
}
|
||||
}
|
||||
},
|
||||
silent,
|
||||
options
|
||||
);
|
||||
})
|
||||
);
|
||||
},
|
||||
sendMessageProto(timestamp, numbers, message, callback, silent) {
|
||||
sendMessageProto(
|
||||
timestamp,
|
||||
numbers,
|
||||
message,
|
||||
callback,
|
||||
silent,
|
||||
options = {}
|
||||
) {
|
||||
const rejections = textsecure.storage.get('signedKeyRotationRejected', 0);
|
||||
if (rejections > 5) {
|
||||
throw new textsecure.SignedPreKeyRotationError(
|
||||
|
@ -361,7 +307,8 @@ MessageSender.prototype = {
|
|||
numbers,
|
||||
message,
|
||||
silent,
|
||||
callback
|
||||
callback,
|
||||
options
|
||||
);
|
||||
|
||||
numbers.forEach(number => {
|
||||
|
@ -369,20 +316,7 @@ MessageSender.prototype = {
|
|||
});
|
||||
},
|
||||
|
||||
retrySendMessageProto(numbers, encodedMessage, timestamp) {
|
||||
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) {
|
||||
sendIndividualProto(number, proto, timestamp, silent, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const callback = res => {
|
||||
if (res.errors.length > 0) {
|
||||
|
@ -391,7 +325,14 @@ MessageSender.prototype = {
|
|||
resolve(res);
|
||||
}
|
||||
};
|
||||
this.sendMessageProto(timestamp, [number], proto, callback, silent);
|
||||
this.sendMessageProto(
|
||||
timestamp,
|
||||
[number],
|
||||
proto,
|
||||
callback,
|
||||
silent,
|
||||
options
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -412,7 +353,10 @@ MessageSender.prototype = {
|
|||
encodedDataMessage,
|
||||
timestamp,
|
||||
destination,
|
||||
expirationStartTimestamp
|
||||
expirationStartTimestamp,
|
||||
sentTo = [],
|
||||
unidentifiedDeliveries = [],
|
||||
options
|
||||
) {
|
||||
const myNumber = textsecure.storage.user.getNumber();
|
||||
const myDevice = textsecure.storage.user.getDeviceId();
|
||||
|
@ -432,6 +376,27 @@ MessageSender.prototype = {
|
|||
if (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();
|
||||
syncMessage.sent = sentMessage;
|
||||
const contentMessage = new textsecure.protobuf.Content();
|
||||
|
@ -442,18 +407,24 @@ MessageSender.prototype = {
|
|||
myNumber,
|
||||
contentMessage,
|
||||
Date.now(),
|
||||
silent
|
||||
silent,
|
||||
options
|
||||
);
|
||||
},
|
||||
|
||||
getProfile(number) {
|
||||
async getProfile(number, { accessKey } = {}) {
|
||||
if (accessKey) {
|
||||
return this.server.getProfileUnauth(number, { accessKey });
|
||||
}
|
||||
|
||||
return this.server.getProfile(number);
|
||||
},
|
||||
|
||||
getAvatar(path) {
|
||||
return this.server.getAvatar(path);
|
||||
},
|
||||
|
||||
sendRequestConfigurationSyncMessage() {
|
||||
sendRequestConfigurationSyncMessage(options) {
|
||||
const myNumber = textsecure.storage.user.getNumber();
|
||||
const myDevice = textsecure.storage.user.getDeviceId();
|
||||
if (myDevice !== 1 && myDevice !== '1') {
|
||||
|
@ -469,13 +440,14 @@ MessageSender.prototype = {
|
|||
myNumber,
|
||||
contentMessage,
|
||||
Date.now(),
|
||||
silent
|
||||
silent,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
sendRequestGroupSyncMessage() {
|
||||
sendRequestGroupSyncMessage(options) {
|
||||
const myNumber = textsecure.storage.user.getNumber();
|
||||
const myDevice = textsecure.storage.user.getDeviceId();
|
||||
if (myDevice !== 1 && myDevice !== '1') {
|
||||
|
@ -491,14 +463,15 @@ MessageSender.prototype = {
|
|||
myNumber,
|
||||
contentMessage,
|
||||
Date.now(),
|
||||
silent
|
||||
silent,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
sendRequestContactSyncMessage() {
|
||||
sendRequestContactSyncMessage(options) {
|
||||
const myNumber = textsecure.storage.user.getNumber();
|
||||
const myDevice = textsecure.storage.user.getDeviceId();
|
||||
if (myDevice !== 1 && myDevice !== '1') {
|
||||
|
@ -514,13 +487,37 @@ MessageSender.prototype = {
|
|||
myNumber,
|
||||
contentMessage,
|
||||
Date.now(),
|
||||
silent
|
||||
silent,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
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();
|
||||
receiptMessage.type = textsecure.protobuf.ReceiptMessage.Type.READ;
|
||||
receiptMessage.timestamp = timestamps;
|
||||
|
@ -529,9 +526,15 @@ MessageSender.prototype = {
|
|||
contentMessage.receiptMessage = receiptMessage;
|
||||
|
||||
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 myDevice = textsecure.storage.user.getDeviceId();
|
||||
if (myDevice !== 1 && myDevice !== '1') {
|
||||
|
@ -551,13 +554,14 @@ MessageSender.prototype = {
|
|||
myNumber,
|
||||
contentMessage,
|
||||
Date.now(),
|
||||
silent
|
||||
silent,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
syncVerification(destination, state, identityKey) {
|
||||
syncVerification(destination, state, identityKey, options) {
|
||||
const myNumber = textsecure.storage.user.getNumber();
|
||||
const myDevice = textsecure.storage.user.getDeviceId();
|
||||
const now = Date.now();
|
||||
|
@ -580,7 +584,14 @@ MessageSender.prototype = {
|
|||
contentMessage.nullMessage = nullMessage;
|
||||
|
||||
// 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(() => {
|
||||
const verified = new textsecure.protobuf.Verified();
|
||||
|
@ -595,12 +606,18 @@ MessageSender.prototype = {
|
|||
const secondMessage = new textsecure.protobuf.Content();
|
||||
secondMessage.syncMessage = syncMessage;
|
||||
|
||||
const silent = true;
|
||||
return this.sendIndividualProto(myNumber, secondMessage, now, silent);
|
||||
const innerSilent = true;
|
||||
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 numbers = providedNumbers.filter(number => number !== me);
|
||||
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,
|
||||
timestamp,
|
||||
expireTimer,
|
||||
profileKey
|
||||
profileKey,
|
||||
options
|
||||
) {
|
||||
return this.sendMessage({
|
||||
recipients: [number],
|
||||
body: messageText,
|
||||
timestamp,
|
||||
attachments,
|
||||
quote,
|
||||
needsSync: true,
|
||||
expireTimer,
|
||||
profileKey,
|
||||
});
|
||||
return this.sendMessage(
|
||||
{
|
||||
recipients: [number],
|
||||
body: messageText,
|
||||
timestamp,
|
||||
attachments,
|
||||
quote,
|
||||
needsSync: true,
|
||||
expireTimer,
|
||||
profileKey,
|
||||
},
|
||||
options
|
||||
);
|
||||
},
|
||||
|
||||
resetSession(number, timestamp) {
|
||||
resetSession(number, timestamp, options) {
|
||||
window.log.info('resetting secure session');
|
||||
const silent = false;
|
||||
const proto = new textsecure.protobuf.DataMessage();
|
||||
proto.body = 'TERMINATE';
|
||||
proto.flags = textsecure.protobuf.DataMessage.Flags.END_SESSION;
|
||||
|
@ -677,9 +706,13 @@ MessageSender.prototype = {
|
|||
window.log.info(
|
||||
'finished closing local sessions, now sending to contact'
|
||||
);
|
||||
return this.sendIndividualProto(number, proto, timestamp).catch(
|
||||
logError('resetSession/sendToContact error:')
|
||||
);
|
||||
return this.sendIndividualProto(
|
||||
number,
|
||||
proto,
|
||||
timestamp,
|
||||
silent,
|
||||
options
|
||||
).catch(logError('resetSession/sendToContact error:'));
|
||||
})
|
||||
.then(() =>
|
||||
deleteAllSessions(number).catch(
|
||||
|
@ -688,9 +721,15 @@ MessageSender.prototype = {
|
|||
);
|
||||
|
||||
const buffer = proto.toArrayBuffer();
|
||||
const sendSync = this.sendSyncMessage(buffer, timestamp, number).catch(
|
||||
logError('resetSession/sendSync error:')
|
||||
);
|
||||
const sendSync = this.sendSyncMessage(
|
||||
buffer,
|
||||
timestamp,
|
||||
number,
|
||||
null,
|
||||
[],
|
||||
[],
|
||||
options
|
||||
).catch(logError('resetSession/sendSync error:'));
|
||||
|
||||
return Promise.all([sendToContact, sendSync]);
|
||||
},
|
||||
|
@ -702,7 +741,8 @@ MessageSender.prototype = {
|
|||
quote,
|
||||
timestamp,
|
||||
expireTimer,
|
||||
profileKey
|
||||
profileKey,
|
||||
options
|
||||
) {
|
||||
return textsecure.storage.groups.getNumbers(groupId).then(targetNumbers => {
|
||||
if (targetNumbers === undefined) {
|
||||
|
@ -715,24 +755,27 @@ MessageSender.prototype = {
|
|||
return Promise.reject(new Error('No other members in the group'));
|
||||
}
|
||||
|
||||
return this.sendMessage({
|
||||
recipients: numbers,
|
||||
body: messageText,
|
||||
timestamp,
|
||||
attachments,
|
||||
quote,
|
||||
needsSync: true,
|
||||
expireTimer,
|
||||
profileKey,
|
||||
group: {
|
||||
id: groupId,
|
||||
type: textsecure.protobuf.GroupContext.Type.DELIVER,
|
||||
return this.sendMessage(
|
||||
{
|
||||
recipients: numbers,
|
||||
body: messageText,
|
||||
timestamp,
|
||||
attachments,
|
||||
quote,
|
||||
needsSync: true,
|
||||
expireTimer,
|
||||
profileKey,
|
||||
group: {
|
||||
id: groupId,
|
||||
type: textsecure.protobuf.GroupContext.Type.DELIVER,
|
||||
},
|
||||
},
|
||||
});
|
||||
options
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
createGroup(targetNumbers, name, avatar) {
|
||||
createGroup(targetNumbers, name, avatar, options) {
|
||||
const proto = new textsecure.protobuf.DataMessage();
|
||||
proto.group = new textsecure.protobuf.GroupContext();
|
||||
|
||||
|
@ -748,12 +791,14 @@ MessageSender.prototype = {
|
|||
|
||||
return this.makeAttachmentPointer(avatar).then(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();
|
||||
proto.group = new textsecure.protobuf.GroupContext();
|
||||
|
||||
|
@ -771,12 +816,14 @@ MessageSender.prototype = {
|
|||
|
||||
return this.makeAttachmentPointer(avatar).then(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();
|
||||
proto.group = new textsecure.protobuf.GroupContext();
|
||||
proto.group.id = stringToArrayBuffer(groupId);
|
||||
|
@ -789,11 +836,11 @@ MessageSender.prototype = {
|
|||
return Promise.reject(new Error('Unknown Group'));
|
||||
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();
|
||||
proto.group = new textsecure.protobuf.GroupContext();
|
||||
proto.group.id = stringToArrayBuffer(groupId);
|
||||
|
@ -805,11 +852,11 @@ MessageSender.prototype = {
|
|||
return Promise.reject(new Error('Unknown Group'));
|
||||
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();
|
||||
proto.group = new textsecure.protobuf.GroupContext();
|
||||
proto.group.id = stringToArrayBuffer(groupId);
|
||||
|
@ -822,12 +869,12 @@ MessageSender.prototype = {
|
|||
|
||||
return this.makeAttachmentPointer(avatar).then(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();
|
||||
proto.group = new textsecure.protobuf.GroupContext();
|
||||
proto.group.id = stringToArrayBuffer(groupId);
|
||||
|
@ -838,14 +885,15 @@ MessageSender.prototype = {
|
|||
return Promise.reject(new Error('Unknown Group'));
|
||||
return textsecure.storage.groups
|
||||
.deleteGroup(groupId)
|
||||
.then(() => this.sendGroupProto(numbers, proto));
|
||||
.then(() => this.sendGroupProto(numbers, proto, Date.now(), options));
|
||||
});
|
||||
},
|
||||
sendExpirationTimerUpdateToGroup(
|
||||
groupId,
|
||||
expireTimer,
|
||||
timestamp,
|
||||
profileKey
|
||||
profileKey,
|
||||
options
|
||||
) {
|
||||
return textsecure.storage.groups.getNumbers(groupId).then(targetNumbers => {
|
||||
if (targetNumbers === undefined)
|
||||
|
@ -856,34 +904,41 @@ MessageSender.prototype = {
|
|||
if (numbers.length === 0) {
|
||||
return Promise.reject(new Error('No other members in the group'));
|
||||
}
|
||||
return this.sendMessage({
|
||||
recipients: numbers,
|
||||
timestamp,
|
||||
needsSync: true,
|
||||
expireTimer,
|
||||
profileKey,
|
||||
flags: textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
|
||||
group: {
|
||||
id: groupId,
|
||||
type: textsecure.protobuf.GroupContext.Type.DELIVER,
|
||||
return this.sendMessage(
|
||||
{
|
||||
recipients: numbers,
|
||||
timestamp,
|
||||
needsSync: true,
|
||||
expireTimer,
|
||||
profileKey,
|
||||
flags: textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
|
||||
group: {
|
||||
id: groupId,
|
||||
type: textsecure.protobuf.GroupContext.Type.DELIVER,
|
||||
},
|
||||
},
|
||||
});
|
||||
options
|
||||
);
|
||||
});
|
||||
},
|
||||
sendExpirationTimerUpdateToNumber(
|
||||
number,
|
||||
expireTimer,
|
||||
timestamp,
|
||||
profileKey
|
||||
profileKey,
|
||||
options
|
||||
) {
|
||||
return this.sendMessage({
|
||||
recipients: [number],
|
||||
timestamp,
|
||||
needsSync: true,
|
||||
expireTimer,
|
||||
profileKey,
|
||||
flags: textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
|
||||
});
|
||||
return this.sendMessage(
|
||||
{
|
||||
recipients: [number],
|
||||
timestamp,
|
||||
needsSync: true,
|
||||
expireTimer,
|
||||
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.syncReadMessages = sender.syncReadMessages.bind(sender);
|
||||
this.syncVerification = sender.syncVerification.bind(sender);
|
||||
this.sendDeliveryReceipt = sender.sendDeliveryReceipt.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 */
|
||||
|
||||
|
@ -23,12 +23,15 @@
|
|||
this.ongroup = this.onGroupSyncComplete.bind(this);
|
||||
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...');
|
||||
sender
|
||||
.sendRequestContactSyncMessage()
|
||||
wrap(sender.sendRequestContactSyncMessage(sendOptions))
|
||||
.then(() => {
|
||||
window.log.info('SyncRequest now sending group sync messsage...');
|
||||
return sender.sendRequestGroupSyncMessage();
|
||||
return wrap(sender.sendRequestGroupSyncMessage(sendOptions));
|
||||
})
|
||||
.catch(error => {
|
||||
window.log.error(
|
||||
|
|
|
@ -90,8 +90,11 @@ describe('MessageReceiver', () => {
|
|||
done();
|
||||
});
|
||||
const messageReceiver = new textsecure.MessageReceiver(
|
||||
'ws://localhost:8080',
|
||||
window
|
||||
'username',
|
||||
'password',
|
||||
'signalingKey'
|
||||
// 'ws://localhost:8080',
|
||||
// window,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue