checkout outgoing
parent
3de82cd292
commit
a708ce56ad
@ -1,694 +1,4 @@
|
|||||||
/* global
|
bgsgsdfg
|
||||||
textsecure,
|
sdfg
|
||||||
libsignal,
|
s
|
||||||
window,
|
d
|
||||||
ConversationController,
|
|
||||||
libloki,
|
|
||||||
StringView,
|
|
||||||
lokiMessageAPI,
|
|
||||||
i18n,
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* eslint-disable more/no-then */
|
|
||||||
/* eslint-disable no-unreachable */
|
|
||||||
const NUM_SEND_CONNECTIONS = 3;
|
|
||||||
|
|
||||||
const getTTLForType = type => {
|
|
||||||
switch (type) {
|
|
||||||
case 'friend-request':
|
|
||||||
return 4 * 24 * 60 * 60 * 1000; // 4 days for friend request message
|
|
||||||
case 'device-unpairing':
|
|
||||||
return 4 * 24 * 60 * 60 * 1000; // 4 days for device unpairing
|
|
||||||
case 'onlineBroadcast':
|
|
||||||
return 60 * 1000; // 1 minute for online broadcast message
|
|
||||||
case 'typing':
|
|
||||||
return 60 * 1000; // 1 minute for typing indicators
|
|
||||||
case 'pairing-request':
|
|
||||||
return 2 * 60 * 1000; // 2 minutes for pairing requests
|
|
||||||
default:
|
|
||||||
return (window.getMessageTTL() || 24) * 60 * 60 * 1000; // 1 day default for any other message
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function OutgoingMessage(
|
|
||||||
server,
|
|
||||||
timestamp,
|
|
||||||
numbers,
|
|
||||||
message,
|
|
||||||
silent,
|
|
||||||
callback,
|
|
||||||
options = {}
|
|
||||||
) {
|
|
||||||
if (message instanceof textsecure.protobuf.DataMessage) {
|
|
||||||
const content = new textsecure.protobuf.Content();
|
|
||||||
content.dataMessage = message;
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
message = content;
|
|
||||||
}
|
|
||||||
this.server = server;
|
|
||||||
this.timestamp = timestamp;
|
|
||||||
this.numbers = numbers;
|
|
||||||
this.message = message; // ContentMessage proto
|
|
||||||
this.callback = callback;
|
|
||||||
this.silent = silent;
|
|
||||||
|
|
||||||
this.numbersCompleted = 0;
|
|
||||||
this.errors = [];
|
|
||||||
this.successfulNumbers = [];
|
|
||||||
this.fallBackEncryption = false;
|
|
||||||
this.failoverNumbers = [];
|
|
||||||
this.unidentifiedDeliveries = [];
|
|
||||||
|
|
||||||
const {
|
|
||||||
numberInfo,
|
|
||||||
senderCertificate,
|
|
||||||
online,
|
|
||||||
messageType,
|
|
||||||
isPing,
|
|
||||||
isPublic,
|
|
||||||
publicSendData,
|
|
||||||
} =
|
|
||||||
options || {};
|
|
||||||
this.numberInfo = numberInfo;
|
|
||||||
this.isPublic = isPublic;
|
|
||||||
this.isGroup = !!(
|
|
||||||
this.message &&
|
|
||||||
this.message.dataMessage &&
|
|
||||||
this.message.dataMessage.group
|
|
||||||
);
|
|
||||||
this.publicSendData = publicSendData;
|
|
||||||
this.senderCertificate = senderCertificate;
|
|
||||||
this.online = online;
|
|
||||||
this.messageType = messageType || 'outgoing';
|
|
||||||
this.isPing = isPing || false;
|
|
||||||
}
|
|
||||||
|
|
||||||
OutgoingMessage.prototype = {
|
|
||||||
constructor: OutgoingMessage,
|
|
||||||
numberCompleted() {
|
|
||||||
this.numbersCompleted += 1;
|
|
||||||
if (this.numbersCompleted >= this.numbers.length) {
|
|
||||||
this.callback({
|
|
||||||
successfulNumbers: this.successfulNumbers,
|
|
||||||
failoverNumbers: this.failoverNumbers,
|
|
||||||
errors: this.errors,
|
|
||||||
unidentifiedDeliveries: this.unidentifiedDeliveries,
|
|
||||||
messageType: this.messageType,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
registerError(number, reason, error) {
|
|
||||||
if (!error || (error.name === 'HTTPError' && error.code !== 404)) {
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
error = new textsecure.OutgoingMessageError(
|
|
||||||
number,
|
|
||||||
this.message.toArrayBuffer(),
|
|
||||||
this.timestamp,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
error.number = number;
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
error.reason = reason;
|
|
||||||
this.errors[this.errors.length] = error;
|
|
||||||
this.numberCompleted();
|
|
||||||
},
|
|
||||||
reloadDevicesAndSend(number, recurse) {
|
|
||||||
const ourNumber = textsecure.storage.user.getNumber();
|
|
||||||
return () =>
|
|
||||||
libloki.storage
|
|
||||||
.getAllDevicePubKeysForPrimaryPubKey(number)
|
|
||||||
// Don't send to ourselves
|
|
||||||
.then(devicesPubKeys => {
|
|
||||||
console.log('[vince] devicesPubKeys should display for B1 and B2', devicesPubKeys);
|
|
||||||
console.log('[vince] devicesPubKeys:', devicesPubKeys);
|
|
||||||
|
|
||||||
return devicesPubKeys.filter(pubKey => pubKey !== ourNumber)
|
|
||||||
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then(devicesPubKeys => {
|
|
||||||
if (devicesPubKeys.length === 0) {
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
devicesPubKeys = [number];
|
|
||||||
}
|
|
||||||
return this.doSendMessage(number, devicesPubKeys, recurse);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
getKeysForNumber(number, updateDevices) {
|
|
||||||
const handleResult = response =>
|
|
||||||
Promise.all(
|
|
||||||
response.devices.map(device => {
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
device.identityKey = response.identityKey;
|
|
||||||
if (
|
|
||||||
updateDevices === undefined ||
|
|
||||||
updateDevices.indexOf(device.deviceId) > -1
|
|
||||||
) {
|
|
||||||
const address = new libsignal.SignalProtocolAddress(
|
|
||||||
number,
|
|
||||||
device.deviceId
|
|
||||||
);
|
|
||||||
const builder = new libsignal.SessionBuilder(
|
|
||||||
textsecure.storage.protocol,
|
|
||||||
address
|
|
||||||
);
|
|
||||||
if (device.registrationId === 0) {
|
|
||||||
window.log.info('device registrationId 0!');
|
|
||||||
}
|
|
||||||
return builder
|
|
||||||
.processPreKey(device)
|
|
||||||
.then(async () => {
|
|
||||||
// TODO: only remove the keys that were used above!
|
|
||||||
await libloki.storage.removeContactPreKeyBundle(number);
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
if (error.message === 'Identity key changed') {
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
error.timestamp = this.timestamp;
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
error.originalMessage = this.message.toArrayBuffer();
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
error.identityKey = device.identityKey;
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
let promise = Promise.resolve(true);
|
|
||||||
updateDevices.forEach(device => {
|
|
||||||
promise = promise.then(() =>
|
|
||||||
Promise.all([
|
|
||||||
textsecure.storage.protocol.loadContactPreKey(number),
|
|
||||||
textsecure.storage.protocol.loadContactSignedPreKey(number),
|
|
||||||
])
|
|
||||||
.then(keys => {
|
|
||||||
const [preKey, signedPreKey] = keys;
|
|
||||||
if (preKey === undefined || signedPreKey === undefined) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const identityKey = StringView.hexToArrayBuffer(number);
|
|
||||||
return handleResult({
|
|
||||||
identityKey,
|
|
||||||
devices: [
|
|
||||||
{ deviceId: device, preKey, signedPreKey, registrationId: 0 },
|
|
||||||
],
|
|
||||||
}).then(results => results.every(value => value === true));
|
|
||||||
})
|
|
||||||
.catch(e => {
|
|
||||||
if (e.name === 'HTTPError' && e.code === 404) {
|
|
||||||
if (device !== 1) {
|
|
||||||
return this.removeDeviceIdsForNumber(number, [device]);
|
|
||||||
}
|
|
||||||
throw new textsecure.UnregisteredUserError(number, e);
|
|
||||||
} else {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return promise;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Default ttl to 24 hours if no value provided
|
|
||||||
async transmitMessage(number, data, timestamp, ttl = 24 * 60 * 60 * 1000) {
|
|
||||||
const pubKey = number;
|
|
||||||
try {
|
|
||||||
// TODO: Make NUM_CONCURRENT_CONNECTIONS a global constant
|
|
||||||
const options = {
|
|
||||||
numConnections: NUM_SEND_CONNECTIONS,
|
|
||||||
isPing: this.isPing,
|
|
||||||
};
|
|
||||||
options.isPublic = this.isPublic;
|
|
||||||
if (this.isPublic) {
|
|
||||||
options.publicSendData = this.publicSendData;
|
|
||||||
}
|
|
||||||
await lokiMessageAPI.sendMessage(pubKey, data, timestamp, ttl, options);
|
|
||||||
} 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, '', e, timestamp);
|
|
||||||
} else if (e.name === 'TimedOutError') {
|
|
||||||
throw new textsecure.PoWError(number, e);
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getPaddedMessageLength(messageLength) {
|
|
||||||
const messageLengthWithTerminator = messageLength + 1;
|
|
||||||
let messagePartCount = Math.floor(messageLengthWithTerminator / 160);
|
|
||||||
|
|
||||||
if (messageLengthWithTerminator % 160 !== 0) {
|
|
||||||
messagePartCount += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return messagePartCount * 160;
|
|
||||||
},
|
|
||||||
convertMessageToText(messageBuffer) {
|
|
||||||
const plaintext = new Uint8Array(
|
|
||||||
this.getPaddedMessageLength(messageBuffer.byteLength + 1) - 1
|
|
||||||
);
|
|
||||||
plaintext.set(new Uint8Array(messageBuffer));
|
|
||||||
plaintext[messageBuffer.byteLength] = 0x80;
|
|
||||||
|
|
||||||
return plaintext;
|
|
||||||
},
|
|
||||||
getPlaintext(messageBuffer) {
|
|
||||||
return this.convertMessageToText(messageBuffer);
|
|
||||||
},
|
|
||||||
async wrapInWebsocketMessage(outgoingObject) {
|
|
||||||
const source =
|
|
||||||
outgoingObject.type ===
|
|
||||||
textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER
|
|
||||||
? null
|
|
||||||
: outgoingObject.ourKey;
|
|
||||||
|
|
||||||
const messageEnvelope = new textsecure.protobuf.Envelope({
|
|
||||||
type: outgoingObject.type,
|
|
||||||
source,
|
|
||||||
sourceDevice: outgoingObject.sourceDevice,
|
|
||||||
timestamp: this.timestamp,
|
|
||||||
content: outgoingObject.content,
|
|
||||||
});
|
|
||||||
const requestMessage = new textsecure.protobuf.WebSocketRequestMessage({
|
|
||||||
id: new Uint8Array(libsignal.crypto.getRandomBytes(1))[0], // random ID for now
|
|
||||||
verb: 'PUT',
|
|
||||||
path: '/api/v1/message',
|
|
||||||
body: messageEnvelope.encode().toArrayBuffer(),
|
|
||||||
});
|
|
||||||
const websocketMessage = new textsecure.protobuf.WebSocketMessage({
|
|
||||||
type: textsecure.protobuf.WebSocketMessage.Type.REQUEST,
|
|
||||||
request: requestMessage,
|
|
||||||
});
|
|
||||||
const bytes = new Uint8Array(websocketMessage.encode().toArrayBuffer());
|
|
||||||
return bytes;
|
|
||||||
},
|
|
||||||
doSendMessage(number, devicesPubKeys, recurse) {
|
|
||||||
const ciphers = {};
|
|
||||||
if (this.isPublic) {
|
|
||||||
return this.transmitMessage(
|
|
||||||
number,
|
|
||||||
this.message.dataMessage,
|
|
||||||
this.timestamp,
|
|
||||||
0 // ttl
|
|
||||||
)
|
|
||||||
.then(() => {
|
|
||||||
this.successfulNumbers[this.successfulNumbers.length] = number;
|
|
||||||
this.numberCompleted();
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.numbers = devicesPubKeys;
|
|
||||||
|
|
||||||
return Promise.all(
|
|
||||||
devicesPubKeys.map(async devicePubKey => {
|
|
||||||
|
|
||||||
// Session doesn't use the deviceId scheme, it's always 1.
|
|
||||||
// Instead, there are multiple device public keys.
|
|
||||||
const deviceId = 1;
|
|
||||||
const updatedDevices = await this.getStaleDeviceIdsForNumber(
|
|
||||||
devicePubKey
|
|
||||||
);
|
|
||||||
const keysFound = await this.getKeysForNumber(
|
|
||||||
devicePubKey,
|
|
||||||
updatedDevices
|
|
||||||
);
|
|
||||||
let enableFallBackEncryption = !keysFound;
|
|
||||||
|
|
||||||
const address = new libsignal.SignalProtocolAddress(
|
|
||||||
devicePubKey,
|
|
||||||
deviceId
|
|
||||||
);
|
|
||||||
const ourKey = textsecure.storage.user.getNumber();
|
|
||||||
const options = {};
|
|
||||||
|
|
||||||
let isMultiDeviceRequest = false;
|
|
||||||
let thisDeviceMessageType = this.messageType;
|
|
||||||
if (
|
|
||||||
thisDeviceMessageType !== 'pairing-request' &&
|
|
||||||
thisDeviceMessageType !== 'friend-request'
|
|
||||||
) {
|
|
||||||
let conversation;
|
|
||||||
try {
|
|
||||||
conversation = ConversationController.get(devicePubKey);
|
|
||||||
} catch (e) {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
if (conversation && !this.isGroup) {
|
|
||||||
const isOurDevice = await conversation.isOurDevice();
|
|
||||||
const isFriends =
|
|
||||||
conversation.isFriend() ||
|
|
||||||
conversation.hasReceivedFriendRequest();
|
|
||||||
// We should only send a friend request to our device if we don't have keys
|
|
||||||
const shouldSendAutomatedFR = isOurDevice ? !keysFound : !isFriends;
|
|
||||||
if (shouldSendAutomatedFR) {
|
|
||||||
// We want to send an automated friend request if:
|
|
||||||
// - We aren't already friends
|
|
||||||
// - We haven't received a friend request from this device
|
|
||||||
// - We haven't sent a friend request recently
|
|
||||||
if (conversation.friendRequestTimerIsExpired()) {
|
|
||||||
isMultiDeviceRequest = true;
|
|
||||||
thisDeviceMessageType = 'friend-request';
|
|
||||||
} else {
|
|
||||||
// Throttle automated friend requests
|
|
||||||
this.successfulNumbers.push(devicePubKey);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async buildMessage(devicePubKey) {
|
|
||||||
const updatedDevices = await this.getStaleDeviceIdsForNumber(devicePubKey);
|
|
||||||
const keysFound = await this.getKeysForNumber(devicePubKey, updatedDevices);
|
|
||||||
|
|
||||||
let isMultiDeviceRequest = false;
|
|
||||||
let thisDeviceMessageType = this.messageType;
|
|
||||||
if (
|
|
||||||
thisDeviceMessageType !== 'pairing-request' &&
|
|
||||||
thisDeviceMessageType !== 'friend-request'
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const conversation = ConversationController.get(devicePubKey);
|
|
||||||
if (conversation && !this.isGroup) {
|
|
||||||
const isOurDevice = await conversation.isOurDevice();
|
|
||||||
const isFriends =
|
|
||||||
conversation.isFriend() || conversation.hasReceivedFriendRequest();
|
|
||||||
// We should only send a friend request to our device if we don't have keys
|
|
||||||
const shouldSendAutomatedFR = isOurDevice ? !keysFound : !isFriends;
|
|
||||||
if (shouldSendAutomatedFR) {
|
|
||||||
// We want to send an automated friend request if:
|
|
||||||
// - We aren't already friends
|
|
||||||
// - We haven't received a friend request from this device
|
|
||||||
// - We haven't sent a friend request recently
|
|
||||||
if (conversation.friendRequestTimerIsExpired()) {
|
|
||||||
isMultiDeviceRequest = true;
|
|
||||||
thisDeviceMessageType = 'friend-request';
|
|
||||||
} else {
|
|
||||||
// Throttle automated friend requests
|
|
||||||
this.successfulNumbers.push(devicePubKey);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we're not friends with our own device then we should become friends
|
|
||||||
if (isOurDevice && keysFound && !isFriends) {
|
|
||||||
conversation.setFriendRequestStatus(
|
|
||||||
window.friends.friendRequestStatusEnum.friends
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we need to attach the preKeys
|
|
||||||
const enableFallBackEncryption =
|
|
||||||
!keysFound || thisDeviceMessageType === 'friend-request';
|
|
||||||
const flags = this.message.dataMessage
|
|
||||||
? this.message.dataMessage.get_flags()
|
|
||||||
: null;
|
|
||||||
// END_SESSION means Session reset message
|
|
||||||
const isEndSession =
|
|
||||||
flags === textsecure.protobuf.DataMessage.Flags.END_SESSION;
|
|
||||||
const isSessionRequest =
|
|
||||||
flags === textsecure.protobuf.DataMessage.Flags.SESSION_REQUEST;
|
|
||||||
|
|
||||||
if (enableFallBackEncryption || isEndSession) {
|
|
||||||
// Encrypt them with the fallback
|
|
||||||
const pkb = await libloki.storage.getPreKeyBundleForContact(devicePubKey);
|
|
||||||
this.message.preKeyBundleMessage = new textsecure.protobuf.PreKeyBundleMessage(
|
|
||||||
pkb
|
|
||||||
);
|
|
||||||
window.log.info('attaching prekeys to outgoing message');
|
|
||||||
}
|
|
||||||
|
|
||||||
let messageBuffer;
|
|
||||||
if (isMultiDeviceRequest) {
|
|
||||||
const tempMessage = new textsecure.protobuf.Content();
|
|
||||||
const tempDataMessage = new textsecure.protobuf.DataMessage();
|
|
||||||
tempDataMessage.body = i18n('secondaryDeviceDefaultFR');
|
|
||||||
if (this.message.dataMessage && this.message.dataMessage.profile) {
|
|
||||||
tempDataMessage.profile = this.message.dataMessage.profile;
|
|
||||||
}
|
|
||||||
tempMessage.preKeyBundleMessage = this.message.preKeyBundleMessage;
|
|
||||||
tempMessage.dataMessage = tempDataMessage;
|
|
||||||
messageBuffer = tempMessage.toArrayBuffer();
|
|
||||||
} else {
|
|
||||||
messageBuffer = this.message.toArrayBuffer();
|
|
||||||
}
|
|
||||||
|
|
||||||
const plaintext = this.getPlaintext(messageBuffer);
|
|
||||||
|
|
||||||
// No limit on message keys if we're communicating with our other devices
|
|
||||||
// FIXME options not used at all; if (ourPubkey === number) {
|
|
||||||
// options.messageKeysLimit = false;
|
|
||||||
// }
|
|
||||||
const ttl = getTTLForType(thisDeviceMessageType);
|
|
||||||
const ourKey = textsecure.storage.user.getNumber();
|
|
||||||
|
|
||||||
return {
|
|
||||||
ttl,
|
|
||||||
ourKey,
|
|
||||||
sourceDevice: 1,
|
|
||||||
plaintext,
|
|
||||||
pubKey: devicePubKey,
|
|
||||||
isFriendRequest: enableFallBackEncryption,
|
|
||||||
isSessionRequest,
|
|
||||||
enableFallBackEncryption,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
async encryptMessage(clearMessage) {
|
|
||||||
if (clearMessage === null) {
|
|
||||||
window.log.warn(
|
|
||||||
'clearMessage is null on encryptMessage... Returning null'
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const {
|
|
||||||
ttl,
|
|
||||||
ourKey,
|
|
||||||
sourceDevice,
|
|
||||||
plaintext,
|
|
||||||
pubKey,
|
|
||||||
isSessionRequest,
|
|
||||||
enableFallBackEncryption,
|
|
||||||
} = clearMessage;
|
|
||||||
// Session doesn't use the deviceId scheme, it's always 1.
|
|
||||||
// Instead, there are multiple device public keys.
|
|
||||||
const deviceId = 1;
|
|
||||||
|
|
||||||
const address = new libsignal.SignalProtocolAddress(pubKey, deviceId);
|
|
||||||
|
|
||||||
let sessionCipher;
|
|
||||||
|
|
||||||
if (enableFallBackEncryption) {
|
|
||||||
sessionCipher = new libloki.crypto.FallBackSessionCipher(address);
|
|
||||||
} else {
|
|
||||||
sessionCipher = new libsignal.SessionCipher(
|
|
||||||
textsecure.storage.protocol,
|
|
||||||
address
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const secretSessionCipher = new window.Signal.Metadata.SecretSessionCipher(
|
|
||||||
textsecure.storage.protocol
|
|
||||||
);
|
|
||||||
// ciphers[address.getDeviceId()] = secretSessionCipher;
|
|
||||||
|
|
||||||
const senderCert = new textsecure.protobuf.SenderCertificate();
|
|
||||||
|
|
||||||
senderCert.sender = ourKey;
|
|
||||||
senderCert.senderDevice = deviceId;
|
|
||||||
|
|
||||||
const ciphertext = await secretSessionCipher.encrypt(
|
|
||||||
address,
|
|
||||||
senderCert,
|
|
||||||
plaintext,
|
|
||||||
sessionCipher
|
|
||||||
);
|
|
||||||
const type = textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER;
|
|
||||||
const content = window.Signal.Crypto.arrayBufferToBase64(ciphertext);
|
|
||||||
|
|
||||||
return {
|
|
||||||
type, // FallBackSessionCipher sets this to FRIEND_REQUEST
|
|
||||||
ttl,
|
|
||||||
ourKey,
|
|
||||||
sourceDevice,
|
|
||||||
content,
|
|
||||||
pubKey,
|
|
||||||
isFriendRequest: enableFallBackEncryption,
|
|
||||||
isSessionRequest,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
// Send a message to a public group
|
|
||||||
sendPublicMessage(number) {
|
|
||||||
return this.transmitMessage(
|
|
||||||
number,
|
|
||||||
this.message.dataMessage,
|
|
||||||
this.timestamp,
|
|
||||||
0 // ttl
|
|
||||||
)
|
|
||||||
.then(() => {
|
|
||||||
this.successfulNumbers[this.successfulNumbers.length] = number;
|
|
||||||
this.numberCompleted();
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
// Send a message to a private group or a session chat (one to one)
|
|
||||||
async sendSessionMessage(outgoingObjects) {
|
|
||||||
// TODO: handle multiple devices/messages per transmit
|
|
||||||
const promises = outgoingObjects.map(async outgoingObject => {
|
|
||||||
if (!outgoingObject) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const {
|
|
||||||
pubKey: destination,
|
|
||||||
ttl,
|
|
||||||
isFriendRequest,
|
|
||||||
isSessionRequest,
|
|
||||||
} = outgoingObject;
|
|
||||||
try {
|
|
||||||
const socketMessage = await this.wrapInWebsocketMessage(outgoingObject);
|
|
||||||
await this.transmitMessage(
|
|
||||||
destination,
|
|
||||||
socketMessage,
|
|
||||||
this.timestamp,
|
|
||||||
ttl
|
|
||||||
);
|
|
||||||
if (!this.isGroup && isFriendRequest && !isSessionRequest) {
|
|
||||||
const conversation = ConversationController.get(destination);
|
|
||||||
if (conversation) {
|
|
||||||
// Redundant for primary device but marks secondary devices as pending
|
|
||||||
await conversation.onFriendRequestSent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.successfulNumbers.push(destination);
|
|
||||||
} catch (e) {
|
|
||||||
e.number = destination;
|
|
||||||
this.errors.push(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
await Promise.all(promises);
|
|
||||||
// TODO: the retrySend should only send to the devices
|
|
||||||
// for which the transmission failed.
|
|
||||||
|
|
||||||
// ensure numberCompleted() will execute the callback
|
|
||||||
this.numbersCompleted += this.errors.length + this.successfulNumbers.length;
|
|
||||||
// Absorb errors if message sent to at least 1 device
|
|
||||||
if (this.successfulNumbers.length > 0) {
|
|
||||||
this.errors = [];
|
|
||||||
}
|
|
||||||
this.numberCompleted();
|
|
||||||
},
|
|
||||||
async buildAndEncrypt(devicePubKey) {
|
|
||||||
const clearMessage = await this.buildMessage(devicePubKey);
|
|
||||||
return this.encryptMessage(clearMessage);
|
|
||||||
},
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
doSendMessage(number, devicesPubKeys, recurse) {
|
|
||||||
if (this.isPublic) {
|
|
||||||
return this.sendPublicMessage(number);
|
|
||||||
}
|
|
||||||
this.numbers = devicesPubKeys;
|
|
||||||
|
|
||||||
return Promise.all(
|
|
||||||
devicesPubKeys.map(devicePubKey => this.buildAndEncrypt(devicePubKey))
|
|
||||||
)
|
|
||||||
.then(outgoingObjects => this.sendSessionMessage(outgoingObjects))
|
|
||||||
.catch(error => {
|
|
||||||
// TODO(loki): handle http errors properly
|
|
||||||
// - retry later if 400
|
|
||||||
// - ignore if 409 (conflict) means the hash already exists
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
getStaleDeviceIdsForNumber(number) {
|
|
||||||
return textsecure.storage.protocol.getDeviceIds(number).then(deviceIds => {
|
|
||||||
if (deviceIds.length === 0) {
|
|
||||||
return [1];
|
|
||||||
}
|
|
||||||
const updateDevices = [];
|
|
||||||
return Promise.all(
|
|
||||||
deviceIds.map(deviceId => {
|
|
||||||
const address = new libsignal.SignalProtocolAddress(number, deviceId);
|
|
||||||
const sessionCipher = new libsignal.SessionCipher(
|
|
||||||
textsecure.storage.protocol,
|
|
||||||
address
|
|
||||||
);
|
|
||||||
return sessionCipher.hasOpenSession().then(hasSession => {
|
|
||||||
if (!hasSession) {
|
|
||||||
updateDevices.push(deviceId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
).then(() => updateDevices);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
removeDeviceIdsForNumber(number, deviceIdsToRemove) {
|
|
||||||
let promise = Promise.resolve();
|
|
||||||
// eslint-disable-next-line no-restricted-syntax, guard-for-in
|
|
||||||
for (const j in deviceIdsToRemove) {
|
|
||||||
promise = promise.then(() => {
|
|
||||||
const encodedNumber = `${number}.${deviceIdsToRemove[j]}`;
|
|
||||||
return textsecure.storage.protocol.removeSession(encodedNumber);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return promise;
|
|
||||||
},
|
|
||||||
|
|
||||||
sendToNumber(number) {
|
|
||||||
let conversation;
|
|
||||||
try {
|
|
||||||
conversation = ConversationController.get(number);
|
|
||||||
} catch (e) {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
return this.reloadDevicesAndSend(number, true)().catch(error => {
|
|
||||||
conversation.resetPendingSend();
|
|
||||||
|
|
||||||
console.log('[vince] error:', 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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
window.textsecure = window.textsecure || {};
|
|
||||||
window.textsecure.OutgoingMessage = OutgoingMessage;
|
|
Loading…
Reference in New Issue