rm-outgoing-message

pull/1247/head
Vincent 5 years ago
parent 1b21191c3c
commit 9bfccd408f

@ -90,7 +90,6 @@ module.exports = grunt => {
'libtextsecure/websocket-resources.js',
'libtextsecure/http-resources.js',
'libtextsecure/message_receiver.js',
'libtextsecure/outgoing_message.js',
'libtextsecure/sendmessage.js',
'libtextsecure/sync_request.js',
'libtextsecure/contacts_parser.js',

@ -411,16 +411,6 @@ SecretSessionCipher.prototype = {
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) {

@ -23,7 +23,6 @@
<script type="text/javascript" src="../../libtextsecure/protocol_wrapper.js" data-cover></script>
<script type="text/javascript" src="../../libtextsecure/protobufs.js" data-cover></script>
<script type="text/javascript" src="../../libtextsecure/stringview.js" data-cover></script>
<script type="text/javascript" src="../../libtextsecure/outgoing_message.js" data-cover></script>
<script type="text/javascript" src="../api.js" data-cover></script>
<script type="text/javascript" src="../crypto.js" data-cover></script>

@ -1,618 +0,0 @@
/* global
textsecure,
libsignal,
window,
libloki,
StringView,
lokiMessageAPI,
log
*/
/* eslint-disable more/no-then */
/* eslint-disable no-unreachable */
const NUM_SEND_CONNECTIONS = 3;
const getTTLForType = type => {
switch (type) {
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 'pairing-request':
return 2 * 60 * 1000; // 2 minutes for pairing requests
default:
return 24 * 60 * 60 * 1000; // 1 day default for any other message
}
};
function _getPaddedMessageLength(messageLength) {
const messageLengthWithTerminator = messageLength + 1;
let messagePartCount = Math.floor(messageLengthWithTerminator / 160);
if (messageLengthWithTerminator % 160 !== 0) {
messagePartCount += 1;
}
return messagePartCount * 160;
}
function _convertMessageToText(messageBuffer) {
const plaintext = new Uint8Array(
_getPaddedMessageLength(messageBuffer.byteLength + 1) - 1
);
plaintext.set(new Uint8Array(messageBuffer));
plaintext[messageBuffer.byteLength] = 0x80;
return plaintext;
}
function _getPlaintext(messageBuffer) {
return _convertMessageToText(messageBuffer);
}
function wrapInWebsocketMessage(outgoingObject, timestamp) {
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,
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;
}
function 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);
});
}
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,
isPublic,
isMediumGroup,
publicSendData,
autoSession,
} = options || {};
this.numberInfo = numberInfo;
this.isPublic = isPublic;
this.isMediumGroup = !!isMediumGroup;
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.autoSession = autoSession || 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(primaryPubKey, multiDevice = true) {
const ourNumber = textsecure.storage.user.getNumber();
if (!multiDevice) {
if (primaryPubKey === ourNumber) {
return Promise.resolve();
}
return this.doSendMessage(primaryPubKey, [primaryPubKey]);
}
return (
window.libsession.Protocols.MultiDeviceProtocol.getAllDevices(
primaryPubKey
)
// Don't send to ourselves
.then(devicesPubKeys =>
devicesPubKeys.filter(pubKey => pubKey.key !== ourNumber)
)
.then(devicesPubKeys => {
if (devicesPubKeys.length === 0) {
// No need to start the sending of message without a recipient
return Promise.resolve();
}
return this.doSendMessage(primaryPubKey, devicesPubKeys);
})
);
},
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 => {
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,
};
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) {
throw new textsecure.SendMessageNetworkError(number, '', e, timestamp);
} else if (e.name === 'TimedOutError') {
throw new textsecure.PoWError(number, e);
}
throw e;
}
},
async buildMessage(devicePubKey) {
const updatedDevices = await getStaleDeviceIdsForNumber(devicePubKey);
const keysFound = await this.getKeysForNumber(devicePubKey, updatedDevices);
// Check if we need to attach the preKeys
const enableFallBackEncryption = !keysFound;
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 = false;
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');
}
const messageBuffer = this.message.toArrayBuffer();
const logDetails = {
message: this.message,
};
const ourPubKey = textsecure.storage.user.getNumber();
const ourPrimaryPubkey = window.storage.get('primaryDevicePubKey');
const secondaryPubKeys =
(await window.libsession.Protocols.MultiDeviceProtocol.getSecondaryDevices(
ourPubKey
)) || [];
let aliasedPubkey = devicePubKey;
if (devicePubKey === ourPubKey) {
aliasedPubkey = 'OUR_PUBKEY'; // should not happen
} else if (devicePubKey === ourPrimaryPubkey) {
aliasedPubkey = 'OUR_PRIMARY_PUBKEY';
} else if (secondaryPubKeys.some(device => device.key === devicePubKey)) {
aliasedPubkey = 'OUR SECONDARY PUBKEY';
}
libloki.api.debug.logSessionMessageSending(
`Sending :${this.messageType} message to ${aliasedPubkey} details:`,
logDetails
);
const plaintext = _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(this.messageType);
const ourKey = textsecure.storage.user.getNumber();
return {
ttl,
ourKey,
sourceDevice: 1,
plaintext,
pubKey: devicePubKey,
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 innerCiphertext = await sessionCipher.encrypt(plaintext);
const secretSessionCipher = new window.Signal.Metadata.SecretSessionCipher(
textsecure.storage.protocol
);
const senderCert = new textsecure.protobuf.SenderCertificate();
senderCert.sender = ourKey;
senderCert.senderDevice = deviceId;
const ciphertext = await secretSessionCipher.encrypt(
address.getName(),
senderCert,
innerCiphertext
);
const type = textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER;
const content = window.Signal.Crypto.arrayBufferToBase64(ciphertext);
return {
type,
ttl,
ourKey,
sourceDevice,
content,
pubKey,
isSessionRequest,
};
},
// Send a message to a public group
async sendPublicMessage(number) {
await this.transmitMessage(
number,
this.message.dataMessage,
this.timestamp,
0 // ttl
);
this.successfulNumbers[this.successfulNumbers.length] = number;
this.numberCompleted();
},
async sendMediumGroupMessage(groupId) {
const ttl = getTTLForType(this.messageType);
const plaintext = this.message.toArrayBuffer();
const ourIdentity = textsecure.storage.user.getNumber();
const {
ciphertext,
keyIdx,
} = await window.SenderKeyAPI.encryptWithSenderKey(
plaintext,
groupId,
ourIdentity
);
if (!ciphertext) {
log.error('could not encrypt for medium group');
return;
}
const source = ourIdentity;
// We should include ciphertext idx in the message
const content = new textsecure.protobuf.MediumGroupCiphertext({
ciphertext,
source,
keyIdx,
});
// Encrypt for the group's identity key to hide source and key idx:
const {
ciphertext: ciphertextOuter,
ephemeralKey,
} = await libloki.crypto.encryptForPubkey(
groupId,
content.encode().toArrayBuffer()
);
const contentOuter = new textsecure.protobuf.MediumGroupContent({
ciphertext: ciphertextOuter,
ephemeralKey,
});
log.debug(
'Group ciphertext: ',
window.Signal.Crypto.arrayBufferToBase64(ciphertext)
);
const outgoingObject = {
type: textsecure.protobuf.Envelope.Type.MEDIUM_GROUP_CIPHERTEXT,
ttl,
ourKey: ourIdentity,
sourceDevice: 1,
content: contentOuter.encode().toArrayBuffer(),
};
// TODO: Rather than using sealed sender, we just generate a key pair, perform an ECDH against
// the group's public key and encrypt using the derived key
const socketMessage = wrapInWebsocketMessage(
outgoingObject,
this.timestamp
);
await this.transmitMessage(groupId, socketMessage, this.timestamp, ttl);
this.successfulNumbers[this.successfulNumbers.length] = groupId;
this.numberCompleted();
},
// Send a message to a private group member 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 } = outgoingObject;
try {
const socketMessage = wrapInWebsocketMessage(
outgoingObject,
this.timestamp
);
await this.transmitMessage(
destination,
socketMessage,
this.timestamp,
ttl
);
this.successfulNumbers.push(destination);
} catch (e) {
e.number = destination;
this.errors.push(e);
}
});
await Promise.all(promises);
this.numbersCompleted += this.successfulNumbers.length;
this.numberCompleted();
},
async buildAndEncrypt(devicePubKey) {
const clearMessage = await this.buildMessage(devicePubKey);
return this.encryptMessage(clearMessage);
},
// eslint-disable-next-line no-unused-vars
async doSendMessage(primaryPubKey, devicesPubKeys) {
if (this.isPublic) {
await this.sendPublicMessage(primaryPubKey);
return;
}
this.numbers = devicesPubKeys;
if (this.isMediumGroup) {
await this.sendMediumGroupMessage(primaryPubKey);
return;
}
const outgoingObjects = await Promise.all(
devicesPubKeys.map(pk => this.buildAndEncrypt(pk, primaryPubKey))
);
this.sendSessionMessage(outgoingObjects);
},
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, multiDevice = true) {
return this.reloadDevicesAndSend(number, multiDevice).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
);
}
});
},
};
window.textsecure = window.textsecure || {};
window.textsecure.OutgoingMessage = OutgoingMessage;
Loading…
Cancel
Save