Merge pull request #50 from Mikunj/fix/friend-request

Friend request fixes
pull/55/head
sachaaaaa 6 years ago committed by GitHub
commit dbdd52b4eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1590,6 +1590,10 @@
"message": "Friend request declined",
"description": "Shown in the conversation history when the user declines a friend request"
},
"friendRequestExpired": {
"message": "Friend request expired",
"description": "Shown in the conversation history when the users friend request expires"
},
"friendRequestNotificationTitle": {
"message": "Friend request",
"description": "Shown in a notification title when receiving a friend request"

@ -572,12 +572,6 @@
}
});
Whisper.events.on('showFriendRequest', friendRequest => {
if (appView) {
appView.showFriendRequest(friendRequest);
}
});
Whisper.events.on('calculatingPoW', ({ pubKey, timestamp }) => {
try {
const conversation = ConversationController.get(pubKey);
@ -1261,7 +1255,7 @@
async function initIncomingMessage(data, options = {}) {
const { isError } = options;
const message = new Whisper.Message({
let messageData = {
source: data.source,
sourceDevice: data.sourceDevice,
sent_at: data.timestamp,
@ -1270,7 +1264,19 @@
unidentifiedDeliveryReceived: data.unidentifiedDeliveryReceived,
type: 'incoming',
unread: 1,
});
preKeyBundle: data.preKeyBundle || null,
};
if (data.type === 'friend-request') {
messageData = {
...messageData,
type: 'friend-request',
friendStatus: 'pending',
direction: 'incoming',
}
}
const message = new Whisper.Message(messageData);
// If we don't return early here, we can get into infinite error loops. So, no
// delivery receipts for sealed sender errors.

@ -52,14 +52,35 @@
'blue_grey',
];
/**
* A few key things that need to be known in this is the difference
* between isFriend() and isKeyExchangeCompleted().
*
* `isFriend` returns whether we have accepted the other user as a friend.
* - This is explicilty stored as a state in the conversation
*
* `isKeyExchangeCompleted` return whether we know for certain
* that both of our preKeyBundles have been exchanged.
* - This will be set when we receive a valid CIPHER or
* PREKEY_BUNDLE message from the other user.
* * Valid meaning we can decypher the message using the preKeys provided
* or the keys we have stored.
*
* `isFriend` will determine whether we should send a FRIEND_REQUEST message.
*
* `isKeyExchangeCompleted` will determine whether we keep
* sending preKeyBundle to the other user.
*/
Whisper.Conversation = Backbone.Model.extend({
storeName: 'conversations',
defaults() {
return {
unreadCount: 0,
verified: textsecure.storage.protocol.VerifiedStatus.DEFAULT,
isFriend: false,
keyExchangeCompleted: false,
friendRequestStatus: { allowSending: true, unlockTimestamp: null },
unlockTimestamp: null, // Timestamp used for expiring friend requests.
};
},
@ -115,7 +136,7 @@
this.updateLastMessage
);
this.on('newmessage', this.updateLastMessage);
this.on('newmessage', this.onNewMessage);
this.on('change:profileKey', this.onChangeProfileKey);
// Listening for out-of-band data updates
@ -125,10 +146,6 @@
this.on('expiration-change', this.updateAndMerge);
this.on('expired', this.onExpired);
setTimeout(() => {
this.setFriendRequestTimer();
}, 0);
const sealedSender = this.get('sealedSender');
if (sealedSender === undefined) {
this.set({ sealedSender: SEALED_SENDER.UNKNOWN });
@ -141,6 +158,7 @@
this.unset('lastMessageStatus');
this.updateTextInputState();
this.setFriendRequestExpiryTimeout();
},
isMe() {
@ -209,6 +227,7 @@
await this.inProgressFetch;
removeMessage();
},
async onCalculatingPoW(pubKey, timestamp) {
if (this.id !== pubKey) return;
@ -222,7 +241,6 @@
if (setToExpire) model.setToExpire();
return model;
},
format() {
const { format } = PhoneNumber;
const regionCode = storage.get('regionCode');
@ -457,48 +475,35 @@
return this.get('keyExchangeCompleted') || false;
},
getFriendRequestStatus() {
return this.get('friendRequestStatus');
},
waitingForFriendRequestApproval() {
const friendRequestStatus = this.getFriendRequestStatus();
if (!friendRequestStatus) {
return false;
}
return !friendRequestStatus.allowSending;
},
setFriendRequestTimer() {
const friendRequestStatus = this.getFriendRequestStatus();
if (friendRequestStatus) {
if (!friendRequestStatus.allowSending) {
const delay = Math.max(
friendRequestStatus.unlockTimestamp - Date.now(),
0
);
setTimeout(() => {
this.onFriendRequestTimedOut();
}, delay);
}
}
},
async onFriendRequestAccepted({ updateUnread }) {
// Make sure we don't keep incrementing the unread count
const unreadCount = !updateUnread || this.isKeyExchangeCompleted()
? {}
: { unreadCount: this.get('unreadCount') + 1 };
this.set({
friendRequestStatus: null,
keyExchangeCompleted: true,
...unreadCount,
});
async setKeyExchangeCompleted(value) {
// Only update the value if it's different
if (this.get('keyExchangeCompleted') === value) return;
this.set({ keyExchangeCompleted: value });
await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation,
});
},
async waitingForFriendRequestApproval() {
// Check if we have an incoming friend request
// Or any successful outgoing ones
const incoming = await this.getPendingFriendRequests('incoming');
const outgoing = await this.getPendingFriendRequests('outgoing');
const successfulOutgoing = outgoing.filter(o => !o.hasErrors());
return (incoming.length > 0 || successfulOutgoing.length > 0);
},
isFriend() {
return this.get('isFriend');
},
// Update any pending friend requests for the current user
async updateFriendRequestUI() {
// Enable the text inputs early
this.updateTextInputState();
// We only update our friend requests if we have the user as a friend
if (!this.isFriend()) return;
// Update any pending outgoing messages
const pending = await this.getPendingFriendRequests('outgoing');
await Promise.all(
@ -513,50 +518,82 @@
})
);
// Update our local state
await this.updatePendingFriendRequests();
this.notifyFriendRequest(this.id, 'accepted')
// Send the notification if we had an outgoing friend request
if (pending.length > 0)
this.notifyFriendRequest(this.id, 'accepted')
},
async onFriendRequestTimedOut() {
this.updateTextInputState();
const friendRequestStatus = this.getFriendRequestStatus();
if (friendRequestStatus) {
friendRequestStatus.allowSending = true;
this.set({ friendRequestStatus });
async onFriendRequestAccepted() {
if (!this.isFriend()) {
this.set({ isFriend: true });
await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation,
});
}
await this.updateFriendRequestUI();
},
async onFriendRequestSent() {
// Don't bother setting the friend request if we have already exchanged keys
if (this.isKeyExchangeCompleted()) return;
async onFriendRequestTimeout() {
// Unset the timer
if (this.unlockTimer)
clearTimeout(this.unlockTimer);
const friendRequestLockDuration = 72; // hours
this.unlockTimer = null;
let friendRequestStatus = this.getFriendRequestStatus();
if (!friendRequestStatus) {
friendRequestStatus = {};
// Set the unlock timestamp to null
if (this.get('unlockTimestamp')) {
this.set({ unlockTimestamp: null });
await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation,
});
}
friendRequestStatus.allowSending = false;
const delayMs = 60 * 60 * 1000 * friendRequestLockDuration;
friendRequestStatus.unlockTimestamp = Date.now() + delayMs;
// Change any pending outgoing friend requests to expired
const outgoing = await this.getPendingFriendRequests('outgoing');
await Promise.all(
outgoing.map(async request => {
if (request.hasErrors()) return;
// Update the text input state
this.updateTextInputState();
request.set({ friendStatus: 'expired' });
await window.Signal.Data.saveMessage(request.attributes, {
Message: Whisper.Message,
});
this.trigger('updateMessage', request);
})
);
this.set({ friendRequestStatus });
// Update the UI
await this.updatePendingFriendRequests();
await this.updateFriendRequestUI();
},
async onFriendRequestSent() {
// Check if we need to set the friend request expiry
const unlockTimestamp = this.get('unlockTimestamp');
if (!this.isFriend() && !unlockTimestamp) {
// Expire the messages after 72 hours
const hourLockDuration = 72;
const ms = 60 * 60 * 1000 * hourLockDuration;
this.set({ unlockTimestamp: Date.now() + ms });
await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation,
});
await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation,
});
this.setFriendRequestExpiryTimeout();
}
setTimeout(() => {
this.onFriendRequestTimedOut();
}, delayMs);
this.updateFriendRequestUI();
},
setFriendRequestExpiryTimeout() {
const unlockTimestamp = this.get('unlockTimestamp');
if (unlockTimestamp && !this.unlockTimer) {
const delta = Math.max(unlockTimestamp - Date.now(), 0);
this.unlockTimer = setTimeout(() => {
this.onFriendRequestTimeout();
}, delta);
}
},
isUnverified() {
if (this.isPrivate()) {
@ -708,91 +745,6 @@
existing.trigger('destroy');
}
},
// This will add a message which will allow the user to reply to a friend request
async addFriendRequest(body, options = {}) {
const _options = {
friendStatus: 'pending',
direction: 'incoming',
preKeyBundle: null,
timestamp: null,
source: null,
sourceDevice: null,
received_at: null,
...options,
};
if (this.isMe()) {
window.log.info(
'refusing to send friend request to ourselves'
);
return;
}
const timestamp = _options.timestamp || this.get('timestamp') || Date.now();
window.log.info(
'adding friend request for',
this.ourNumber,
this.idForLogging(),
timestamp
);
this.lastMessageStatus = 'sending';
this.set({
active_at: Date.now(),
timestamp: Date.now(),
unreadCount: this.get('unreadCount') + 1,
});
await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation,
});
// If we need to add new incoming friend requests
// Then we need to make sure we remove any pending requests that we may have
// This is to ensure that one user cannot spam us with multiple friend requests
if (_options.direction === 'incoming') {
const requests = await this.getPendingFriendRequests('incoming');
// Delete the old message if it's pending
await Promise.all(requests.map(request => this._removeMessage(request.id)));
// Trigger an update if we removed messages
if (requests.length > 0)
this.trigger('change');
}
// Add the new message
// eslint-disable-next-line camelcase
const received_at = _options.received_at || Date.now();
const message = {
conversationId: this.id,
type: 'friend-request',
sent_at: timestamp,
received_at,
unread: 1,
from: this.id,
to: this.ourNumber,
friendStatus: _options.friendStatus,
direction: _options.direction,
body,
preKeyBundle: _options.preKeyBundle,
source: _options.source,
sourceDevice: _options.sourceDevice,
};
const id = await window.Signal.Data.saveMessage(message, {
Message: Whisper.Message,
});
const whisperMessage = new Whisper.Message({
...message,
id,
});
this.trigger('newmessage', whisperMessage);
this.notify(whisperMessage);
},
async addVerifiedChange(verifiedChangeId, verified, providedOptions) {
const options = providedOptions || {};
_.defaults(options, { local: true });
@ -1025,8 +977,8 @@
let messageWithSchema = null;
// If we have exchanged keys then let the user send the message normally
if (this.isKeyExchangeCompleted()) {
// If we are a friend then let the user send the message normally
if (this.isFriend()) {
messageWithSchema = await upgradeMessageSchema({
type: 'outgoing',
body,
@ -1156,15 +1108,10 @@
},
async updateTextInputState() {
// Check if we need to disable the text field
if (!this.isKeyExchangeCompleted()) {
// Check if we have an incoming friend request
// Or any successful outgoing ones
const incoming = await this.getPendingFriendRequests('incoming');
const outgoing = await this.getPendingFriendRequests('outgoing');
const successfulOutgoing = outgoing.filter(o => !o.hasErrors());
// Disable the input
if (incoming.length > 0 || successfulOutgoing.length > 0) {
if (!this.isFriend()) {
// Disable the input if we're waiting for friend request approval
const waiting = await this.waitingForFriendRequestApproval();
if (waiting) {
this.trigger('disable:input', true);
this.trigger('change:placeholder', 'disabled');
return;
@ -1327,6 +1274,38 @@
},
};
},
async onNewMessage(message) {
if (message.get('type') === 'friend-request' && message.get('direction') === 'incoming') {
// We need to make sure we remove any pending requests that we may have
// This is to ensure that one user cannot spam us with multiple friend requests.
const incoming = await this.getPendingFriendRequests('incoming');
// Delete the old messages if it's pending
await Promise.all(
incoming
.filter(i => i.id !== message.id)
.map(request => this._removeMessage(request.id))
);
// If we have an outgoing friend request then
// we auto accept the incoming friend request
const outgoing = await this.getPendingFriendRequests('outgoing');
if (outgoing.length > 0) {
const current = this.messageCollection.find(i => i.id === message.id);
if (current) {
await current.acceptFriendRequest();
} else {
window.log.debug('onNewMessage: Failed to find incoming friend request');
}
}
// Trigger an update if we removed or updated messages
if (outgoing.length > 0 || incoming.length > 0)
this.trigger('change');
}
return this.updateLastMessage();
},
async updateLastMessage() {
if (!this.id) {
return;

@ -297,34 +297,42 @@
// It doesn't need anything right now!
return {};
},
getPropsForFriendRequest() {
const friendStatus = this.get('friendStatus') || 'pending';
const direction = this.get('direction') || 'incoming';
async acceptFriendRequest() {
if (this.get('friendStatus') !== 'pending') return;
const conversation = this.getConversation();
const onAccept = async () => {
this.set({ friendStatus: 'accepted' });
await window.Signal.Data.saveMessage(this.attributes, {
Message: Whisper.Message,
});
this.set({ friendStatus: 'accepted' });
await window.Signal.Data.saveMessage(this.attributes, {
Message: Whisper.Message,
});
window.Whisper.events.trigger('friendRequestUpdated', {
pubKey: conversation.id,
...this.attributes,
});
};
window.Whisper.events.trigger('friendRequestUpdated', {
pubKey: conversation.id,
...this.attributes,
});
},
async declineFriendRequest() {
if (this.get('friendStatus') !== 'pending') return;
const conversation = this.getConversation();
const onDecline = async () => {
this.set({ friendStatus: 'declined' });
await window.Signal.Data.saveMessage(this.attributes, {
Message: Whisper.Message,
});
this.set({ friendStatus: 'declined' });
await window.Signal.Data.saveMessage(this.attributes, {
Message: Whisper.Message,
});
window.Whisper.events.trigger('friendRequestUpdated', {
pubKey: conversation.id,
...this.attributes,
});
};
window.Whisper.events.trigger('friendRequestUpdated', {
pubKey: conversation.id,
...this.attributes,
});
},
getPropsForFriendRequest() {
const friendStatus = this.get('friendStatus') || 'pending';
const direction = this.get('direction') || 'incoming';
const conversation = this.getConversation();
const onAccept = () => this.acceptFriendRequest();
const onDecline = () => this.declineFriendRequest()
const onDeleteConversation = async () => {
// Delete the whole conversation
@ -1240,6 +1248,7 @@
quote: dataMessage.quote,
schemaVersion: dataMessage.schemaVersion,
});
if (type === 'outgoing') {
const receipts = Whisper.DeliveryReceipts.forMessage(
conversation,
@ -1303,7 +1312,7 @@
);
}
}
if (type === 'incoming') {
if (type === 'incoming' || type === 'friend-request') {
const readSync = Whisper.ReadSyncs.forMessage(message);
if (readSync) {
if (

@ -178,15 +178,5 @@
});
}
},
async showFriendRequest({ pubKey, message, preKeyBundle, options }) {
const controller = window.ConversationController;
const conversation = await controller.getOrCreateAndWait(pubKey, 'private');
if (conversation) {
conversation.addFriendRequest(message, {
preKeyBundle: preKeyBundle || null,
...options,
});
}
},
});
})();

@ -70,16 +70,9 @@
},
template: $('#conversation').html(),
render_attributes() {
let sendMessagePlaceholder = 'sendMessageFriendRequest';
const sendDisabled = this.model.waitingForFriendRequestApproval();
if (sendDisabled) {
sendMessagePlaceholder = 'sendMessageDisabled';
} else if (this.model.getFriendRequestStatus() === null) {
sendMessagePlaceholder = 'sendMessage';
}
return {
'disable-inputs': sendDisabled,
'send-message': i18n(sendMessagePlaceholder),
'disable-inputs': false,
'send-message': i18n('sendMessage'),
'android-length-warning': i18n('androidMessageLengthWarning'),
};
},
@ -145,6 +138,8 @@
this.render();
this.model.updateTextInputState();
this.loadingScreen = new Whisper.ConversationLoadingScreen();
this.loadingScreen.render();
this.loadingScreen.$el.prependTo(this.$('.discussion-container'));
@ -249,6 +244,8 @@
this.$('.send-message').blur(this.unfocusBottomBar.bind(this));
this.$emojiPanelContainer = this.$('.emoji-panel-container');
this.model.updateFriendRequestUI();
},
events: {

@ -431,6 +431,7 @@ MessageReceiver.prototype.extend({
envelope.sourceDevice = envelope.sourceDevice || item.sourceDevice;
envelope.serverTimestamp =
envelope.serverTimestamp || item.serverTimestamp;
envelope.preKeyBundleMessage = envelope.preKeyBundleMessage || item.preKeyBundleMessage;
const { decrypted } = item;
if (decrypted) {
@ -447,6 +448,13 @@ MessageReceiver.prototype.extend({
payloadPlaintext
);
}
// Convert preKeys to array buffer
if (typeof envelope.preKeyBundleMessage === 'string') {
envelope.preKeyBundleMessage = await MessageReceiver.stringToArrayBuffer(
envelope.preKeyBundleMessage
);
}
this.queueDecryptedEnvelope(envelope, payloadPlaintext);
} else {
this.queueEnvelope(envelope);
@ -672,7 +680,7 @@ MessageReceiver.prototype.extend({
return plaintext;
},
decrypt(envelope, ciphertext) {
async decrypt(envelope, ciphertext) {
const { serverTrustRoot } = this;
let promise;
@ -699,6 +707,31 @@ MessageReceiver.prototype.extend({
textsecure.storage.protocol
);
const fallBackSessionCipher = new libloki.FallBackSessionCipher(
address
);
// Check if we have preKey bundles to decrypt
if (envelope.preKeyBundleMessage) {
const decryptedText = await fallBackSessionCipher.decrypt(envelope.preKeyBundleMessage.toArrayBuffer());
const unpadded = await this.unpad(decryptedText);
const decodedProto = textsecure.protobuf.PreKeyBundleMessage.decode(unpadded);
const decodedBundle = this.decodePreKeyBundleMessage(decodedProto);
// eslint-disable-next-line no-param-reassign
envelope.preKeyBundleMessage = decodedBundle;
// Save the preKey bundle if this is not a friend request.
// We don't automatically save on a friend request because
// we only want to save the preKeys when we click the accept button.
if (envelope.type !== textsecure.protobuf.Envelope.Type.FRIEND_REQUEST) {
await this.handlePreKeyBundleMessage(
envelope.source,
envelope.preKeyBundleMessage
);
}
}
const me = {
number: ourNumber,
deviceId: parseInt(textsecure.storage.user.getDeviceId(), 10),
@ -712,9 +745,6 @@ MessageReceiver.prototype.extend({
break;
case textsecure.protobuf.Envelope.Type.FRIEND_REQUEST: {
window.log.info('friend-request message from ', envelope.source);
const fallBackSessionCipher = new libloki.FallBackSessionCipher(
address
);
promise = fallBackSessionCipher.decrypt(ciphertext.toArrayBuffer())
.then(this.unpad);
break;
@ -894,7 +924,10 @@ MessageReceiver.prototype.extend({
})
);
},
handleDataMessage(envelope, msg) {
async handleFriendRequestMessage(envelope, msg) {
return this.handleDataMessage(envelope, msg, 'friend-request');
},
handleDataMessage(envelope, msg, type = 'data') {
window.log.info('data message from', this.getEnvelopeId(envelope));
let p = Promise.resolve();
// eslint-disable-next-line no-bitwise
@ -911,6 +944,13 @@ MessageReceiver.prototype.extend({
message.group.type === textsecure.protobuf.GroupContext.Type.QUIT
);
if (type === 'friend-request' && isMe) {
window.log.info(
'refusing to add a friend request to ourselves'
);
throw new Error('Cannot add a friend request for ourselves!')
}
if (groupId && isBlocked && !(isMe && isLeavingGroup)) {
window.log.warn(
`Message ${this.getEnvelopeId(
@ -923,12 +963,14 @@ MessageReceiver.prototype.extend({
const ev = new Event('message');
ev.confirm = this.removeFromCache.bind(this, envelope);
ev.data = {
type,
source: envelope.source,
sourceDevice: envelope.sourceDevice,
timestamp: envelope.timestamp.toNumber(),
receivedAt: envelope.receivedAt,
unidentifiedDeliveryReceived: envelope.unidentifiedDeliveryReceived,
message,
preKeyBundle: envelope.preKeyBundleMessage || null,
};
return this.dispatchAndWait(ev);
})
@ -956,19 +998,6 @@ MessageReceiver.prototype.extend({
return this.innerHandleContentMessage(envelope, plaintext);
});
},
promptUserToAcceptFriendRequest(envelope, message, preKeyBundleMessage) {
window.Whisper.events.trigger('showFriendRequest', {
pubKey: envelope.source,
message,
preKeyBundle: this.decodePreKeyBundleMessage(preKeyBundleMessage),
options: {
source: envelope.source,
sourceDevice: envelope.sourceDevice,
timestamp: envelope.timestamp.toNumber(),
receivedAt: envelope.receivedAt,
},
});
},
// A handler function for when a friend request is accepted or declined
async onFriendRequestUpdate(pubKey, message) {
if (!message || !message.direction || !message.friendStatus) return;
@ -977,14 +1006,12 @@ MessageReceiver.prototype.extend({
const conversation = window.ConversationController.get(pubKey);
if (conversation) {
// Update the conversation friend request indicator
conversation.updatePendingFriendRequests();
conversation.updateTextInputState();
await conversation.updatePendingFriendRequests();
await conversation.updateTextInputState();
}
// Send our own prekeys as a response
// If we accepted an incoming friend request then save the preKeyBundle
if (message.direction === 'incoming' && message.friendStatus === 'accepted') {
libloki.sendEmptyMessageWithPreKeys(pubKey);
// Register the preKeys used for communication
if (message.preKeyBundle) {
await this.handlePreKeyBundleMessage(
@ -993,60 +1020,41 @@ MessageReceiver.prototype.extend({
);
}
await conversation.onFriendRequestAccepted({ updateUnread: false });
// Accept the friend request
if (conversation) {
await conversation.onFriendRequestAccepted();
}
// Send a reply back
libloki.sendEmptyMessageWithPreKeys(pubKey);
}
window.log.info(`Friend request for ${pubKey} was ${message.friendStatus}`, message);
},
async innerHandleContentMessage(envelope, plaintext) {
const content = textsecure.protobuf.Content.decode(plaintext);
if (envelope.type === textsecure.protobuf.Envelope.Type.FRIEND_REQUEST) {
let conversation;
try {
conversation = window.ConversationController.get(envelope.source);
} catch (e) {
throw new Error('Error getting conversation for message.')
}
let conversation;
try {
conversation = window.ConversationController.get(envelope.source);
} catch (e) {
window.log.info('Error getting conversation: ', envelope.source);
}
// only prompt friend request if there is no conversation yet
if (!conversation) {
this.promptUserToAcceptFriendRequest(
envelope,
content.dataMessage.body,
content.preKeyBundleMessage
);
} else {
const keyExchangeComplete = conversation.isKeyExchangeCompleted();
// Check here if we received preKeys from the other user
// We are certain that other user accepted the friend request IF:
// - The message has a preKeyBundleMessage
// - We have an outgoing friend request that is pending
// The second check is crucial because it makes sure we don't save the preKeys of
// the incoming friend request (which is saved only when we press accept)
if (!keyExchangeComplete && content.preKeyBundleMessage) {
// Check for any outgoing friend requests
const pending = await conversation.getPendingFriendRequests('outgoing');
const successful = pending.filter(p => !p.hasErrors());
// Save the key only if we have an outgoing friend request
const savePreKey = (successful.length > 0);
// Save the pre key
if (savePreKey) {
await this.handlePreKeyBundleMessage(
envelope.source,
this.decodePreKeyBundleMessage(content.preKeyBundleMessage)
);
if (envelope.type === textsecure.protobuf.Envelope.Type.FRIEND_REQUEST) {
return this.handleFriendRequestMessage(envelope, content.dataMessage);
} else if (
envelope.type === textsecure.protobuf.Envelope.Type.CIPHERTEXT ||
// We also need to check for PREKEY_BUNDLE aswell if the session hasn't started.
// ref: libsignal-protocol.js:36120
envelope.type === textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE
) {
// We know for sure that keys are exchanged
if (conversation) {
await conversation.setKeyExchangeCompleted(true);
// Update the conversation
await conversation.onFriendRequestAccepted({ updateUnread: true });
}
}
// TODO: We should probably set this based on the PKB type
await conversation.onFriendRequestAccepted();
}
// Exit early since the friend request reply will be a regular empty message
return null;
}
if (content.syncMessage) {
@ -1060,6 +1068,8 @@ MessageReceiver.prototype.extend({
} else if (content.receiptMessage) {
return this.handleReceiptMessage(envelope, content.receiptMessage);
}
if (envelope.preKeyBundleMessage) return null;
throw new Error('Unsupported content message');
},
handleCallMessage(envelope) {
@ -1279,6 +1289,8 @@ MessageReceiver.prototype.extend({
};
},
async handlePreKeyBundleMessage(pubKey, preKeyBundleMessage) {
if (!preKeyBundleMessage) return null;
const {
preKeyId,
signedKeyId,

@ -202,25 +202,33 @@ OutgoingMessage.prototype = {
return messagePartCount * 160;
},
convertMessageToText(message) {
const messageBuffer = message.toArrayBuffer();
const plaintext = new Uint8Array(
this.getPaddedMessageLength(messageBuffer.byteLength + 1) - 1
);
plaintext.set(new Uint8Array(messageBuffer));
plaintext[messageBuffer.byteLength] = 0x80;
return plaintext;
},
getPlaintext() {
if (!this.plaintext) {
const messageBuffer = this.message.toArrayBuffer();
this.plaintext = new Uint8Array(
this.getPaddedMessageLength(messageBuffer.byteLength + 1) - 1
);
this.plaintext.set(new Uint8Array(messageBuffer));
this.plaintext[messageBuffer.byteLength] = 0x80;
this.plaintext = this.convertMessageToText(this.message);
}
return this.plaintext;
},
async wrapInWebsocketMessage(outgoingObject) {
const preKeyEnvelope = outgoingObject.preKeyBundleMessage ? {
preKeyBundleMessage: outgoingObject.preKeyBundleMessage,
} : {};
const messageEnvelope = new textsecure.protobuf.Envelope({
type: outgoingObject.type,
source: outgoingObject.ourKey,
sourceDevice: outgoingObject.sourceDevice,
timestamp: this.timestamp,
content: outgoingObject.content,
...preKeyEnvelope,
});
const requestMessage = new textsecure.protobuf.WebSocketRequestMessage({
id: new Uint8Array(libsignal.crypto.getRandomBytes(1))[0], // random ID for now
@ -275,6 +283,18 @@ OutgoingMessage.prototype = {
const address = new libsignal.SignalProtocolAddress(number, deviceId);
const ourKey = textsecure.storage.user.getNumber();
const options = {};
const fallBackEncryption = new libloki.FallBackSessionCipher(address);
// Check if we need to attach the preKeys
let preKeys = {};
if (this.attachPrekeys) {
// Encrypt them with the fallback
const preKeyBundleMessage = await libloki.getPreKeyBundleForNumber(number);
const textBundle = this.convertMessageToText(preKeyBundleMessage);
const encryptedBundle = await fallBackEncryption.encrypt(textBundle);
preKeys = { preKeyBundleMessage: encryptedBundle.body };
window.log.info('attaching prekeys to outgoing message');
}
// No limit on message keys if we're communicating with our other devices
if (ourKey === number) {
@ -283,7 +303,7 @@ OutgoingMessage.prototype = {
let sessionCipher;
if (this.fallBackEncryption) {
sessionCipher = new libloki.FallBackSessionCipher(address);
sessionCipher = fallBackEncryption;
} else {
sessionCipher = new libsignal.SessionCipher(
textsecure.storage.protocol,
@ -292,26 +312,26 @@ OutgoingMessage.prototype = {
);
}
ciphers[address.getDeviceId()] = sessionCipher;
return sessionCipher
.encrypt(plaintext)
.then(ciphertext => {
if (!this.fallBackEncryption)
// eslint-disable-next-line no-param-reassign
ciphertext.body = new Uint8Array(
dcodeIO.ByteBuffer.wrap(
ciphertext.body,
'binary'
).toArrayBuffer()
);
return ciphertext;
})
.then(ciphertext => ({
type: ciphertext.type,
ourKey,
sourceDevice: 1,
destinationRegistrationId: ciphertext.registrationId,
content: ciphertext.body,
}));
// Encrypt our plain text
const ciphertext = await sessionCipher.encrypt(plaintext);
if (!this.fallBackEncryption) {
// eslint-disable-next-line no-param-reassign
ciphertext.body = new Uint8Array(
dcodeIO.ByteBuffer.wrap(
ciphertext.body,
'binary'
).toArrayBuffer()
);
}
return {
type: ciphertext.type, // FallBackSessionCipher sets this to FRIEND_REQUEST
ourKey,
sourceDevice: 1,
destinationRegistrationId: ciphertext.registrationId,
content: ciphertext.body,
...preKeys,
};
})
)
.then(async outgoingObjects => {
@ -441,34 +461,27 @@ OutgoingMessage.prototype = {
return this.getStaleDeviceIdsForNumber(number).then(updateDevices =>
this.getKeysForNumber(number, updateDevices)
.then(async keysFound => {
let attachPrekeys = false;
this.attachPrekeys = false;
if (!keysFound) {
log.info('Fallback encryption enabled');
this.fallBackEncryption = true;
attachPrekeys = true;
this.attachPrekeys = true;
} else if (conversation) {
try {
attachPrekeys = !conversation.isKeyExchangeCompleted();
this.attachPrekeys = !conversation.isKeyExchangeCompleted();
} catch (e) {
// do nothing
}
}
if (this.fallBackEncryption && conversation) {
conversation.onFriendRequestSent();
}
if (attachPrekeys) {
log.info('attaching prekeys to outgoing message');
this.message.preKeyBundleMessage = await libloki.getPreKeyBundleForNumber(
number
);
await conversation.onFriendRequestSent();
}
})
.then(this.reloadDevicesAndSend(number, true))
.catch(error => {
if (this.fallBackEncryption && conversation) {
conversation.onFriendRequestTimedOut();
conversation.updateFriendRequestUI();
}
if (error.message === 'Identity key changed') {
// eslint-disable-next-line no-param-reassign

@ -9,7 +9,7 @@ message Envelope {
UNKNOWN = 0;
CIPHERTEXT = 1;
KEY_EXCHANGE = 2;
PREKEY_BUNDLE = 3;
PREKEY_BUNDLE = 3; //Used By Signal. DO NOT TOUCH! we don't use this at all.
RECEIPT = 5;
UNIDENTIFIED_SENDER = 6;
FRIEND_REQUEST = 101; // contains prekeys + message and is using simple encryption
@ -24,6 +24,7 @@ message Envelope {
optional bytes content = 8; // Contains an encrypted Content
optional string serverGuid = 9;
optional uint64 serverTimestamp = 10;
optional bytes preKeyBundleMessage = 101;
}
@ -33,7 +34,6 @@ message Content {
optional CallMessage callMessage = 3;
optional NullMessage nullMessage = 4;
optional ReceiptMessage receiptMessage = 5;
optional PreKeyBundleMessage preKeyBundleMessage = 6;
}
message PreKeyBundleMessage {

@ -8,7 +8,7 @@ interface Props {
text: string;
direction: 'incoming' | 'outgoing';
status: string;
friendStatus: 'pending' | 'accepted' | 'declined';
friendStatus: 'pending' | 'accepted' | 'declined' | 'expired';
i18n: Localizer;
isBlocked: boolean;
onAccept: () => void;
@ -25,11 +25,13 @@ export class FriendRequest extends React.Component<Props> {
switch (friendStatus) {
case 'pending':
return `friendRequestPending`;
return 'friendRequestPending';
case 'accepted':
return `friendRequestAccepted`;
return 'friendRequestAccepted';
case 'declined':
return `friendRequestDeclined`;
return 'friendRequestDeclined';
case 'expired':
return 'friendRequestExpired'
default:
throw new Error(`Invalid friend request status: ${friendStatus}`);
}
@ -48,7 +50,6 @@ export class FriendRequest extends React.Component<Props> {
<MessageBody text={text || ''} i18n={i18n} />
</div>
</div>
);
}
@ -143,7 +144,7 @@ export class FriendRequest extends React.Component<Props> {
public render() {
const { direction } = this.props;
return (
<div
className={classNames(
@ -159,7 +160,7 @@ export class FriendRequest extends React.Component<Props> {
'module-message-friend-request__container',
)}
>
<div
<div
className={classNames(
'module-message__text',
`module-message__text--${direction}`,

Loading…
Cancel
Save