Merge pull request #62 from BeaudanBrown/prekeybundle-content

Big refactor, prekeybundle moved back to a content message
pull/65/head
sachaaaaa 6 years ago committed by GitHub
commit 78507ad40f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,5 +1,6 @@
/* global _: false */
/* global Backbone: false */
/* global BlockedNumberController: false */
/* global ConversationController: false */
/* global i18n: false */
/* global libsignal: false */
@ -42,14 +43,10 @@
const FriendStatusEnum = Object.freeze({
// New conversation, no messages sent or received
none: 0,
// Have received a friend request, waiting to accept/decline
pendingAction: 1,
// Have sent a friend request, waiting for response
pendingResponse: 2,
// Have sent and received prekeybundle, waiting for ciphertext confirming key exchange
pendingCipher: 3,
// Friend request not complete yet, input blocked
pending: 1,
// We did it!
friends: 4,
friends: 2,
});
const COLORS = [
@ -66,26 +63,6 @@
'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() {
@ -93,7 +70,6 @@
unreadCount: 0,
verified: textsecure.storage.protocol.VerifiedStatus.DEFAULT,
friendStatus: FriendStatusEnum.none,
keyExchangeCompleted: false,
unlockTimestamp: null, // Timestamp used for expiring friend requests.
};
},
@ -477,50 +453,17 @@
return contact.isVerified();
});
},
isKeyExchangeCompleted() {
if (!this.isPrivate()) {
return false;
// throw new Error('isKeyExchangeCompleted not implemented for groups');
}
if (this.isMe()) {
return true;
}
return this.get('friendStatus') === FriendStatusEnum.friends;
},
async setKeyExchangeCompleted() {
if (this.get('friendStatus') !== FriendStatusEnum.pendingCipher) return;
this.set({ friendStatus: FriendStatusEnum.friends });
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());
// 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);
},
getPreKeyBundleType() {
switch (this.get('friendStatus')) {
case FriendStatusEnum.none:
case FriendStatusEnum.pendingResponse:
return textsecure.protobuf.PreKeyBundleMessage.Type.FRIEND_REQUEST;
case FriendStatusEnum.pendingAction:
case FriendStatusEnum.pendingCipher:
return textsecure.protobuf.PreKeyBundleMessage.Type.FRIEND_REQUEST_ACCEPT;
default:
return textsecure.protobuf.PreKeyBundleMessage.Type.UNKNOWN;
}
return (incoming.length > 0 || successfulOutgoing.length > 0);
},
isFriend() {
return this.get('friendStatus') === FriendStatusEnum.pendingCipher ||
this.get('friendStatus') === FriendStatusEnum.friends;
return this.get('friendStatus') === FriendStatusEnum.friends;
},
// Update any pending friend requests for the current user
async updateFriendRequestUI() {
@ -554,7 +497,7 @@
// We have declined an incoming friend request
async onDeclineFriendRequest() {
// Should we change states for other states? (They should never happen)
if (this.get('friendStatus') === FriendStatusEnum.pendingAction) {
if (this.get('friendStatus') === FriendStatusEnum.pending) {
this.set({ friendStatus: FriendStatusEnum.none });
await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation,
@ -567,8 +510,8 @@
// We have accepted an incoming friend request
async onAcceptFriendRequest() {
// Should we change states for other states? (They should never happen)
if (this.get('friendStatus') === FriendStatusEnum.pendingAction) {
this.set({ friendStatus: FriendStatusEnum.pendingCipher });
if (this.get('friendStatus') === FriendStatusEnum.pending) {
this.set({ friendStatus: FriendStatusEnum.friends });
await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation,
});
@ -579,9 +522,8 @@
},
// Our outgoing friend request has been accepted
async onFriendRequestAccepted() {
// TODO: Think about how we want to handle other states
if (this.get('friendStatus') === FriendStatusEnum.pendingResponse) {
this.set({ friendStatus: FriendStatusEnum.pendingCipher });
if (this.get('friendStatus') === FriendStatusEnum.pending) {
this.set({ friendStatus: FriendStatusEnum.friends });
await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation,
});
@ -622,30 +564,12 @@
await this.updateFriendRequestUI();
},
async onFriendRequestReceived() {
switch (this.get('friendStatus')) {
case FriendStatusEnum.none:
this.set({ friendStatus: FriendStatusEnum.pendingAction });
await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation,
});
await this.updateFriendRequestUI();
return;
case FriendStatusEnum.pendingResponse:
this.set({ friendStatus: FriendStatusEnum.pendingCipher });
await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation,
});
await this.updateFriendRequestUI();
return;
case FriendStatusEnum.pendingAction:
case FriendStatusEnum.pendingCipher:
// No need to change state
return;
case FriendStatusEnum.friends:
// TODO: Handle this case (discuss with squad)
return;
default:
throw new TypeError(`Invalid friendStatus type: '${this.friendStatus}'`);
if (this.get('friendStatus') === FriendStatusEnum.none) {
this.set({ friendStatus: FriendStatusEnum.pending });
await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation,
});
await this.updateFriendRequestUI();
}
},
async onFriendRequestSent() {
@ -665,7 +589,7 @@
}
if (this.get('friendStatus') === FriendStatusEnum.none) {
this.set({ friendStatus: FriendStatusEnum.pendingResponse });
this.set({ friendStatus: FriendStatusEnum.pending });
await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation,
});
@ -1170,6 +1094,7 @@
);
const options = this.getSendOptions();
options.messageType = message.get('type');
// Add the message sending on another queue so that our UI doesn't get blocked
this.queueMessageSend(async () =>
@ -1295,12 +1220,10 @@
getSendOptions(options = {}) {
const senderCertificate = storage.get('senderCertificate');
const numberInfo = this.getNumberInfo(options);
const preKeyBundleType = this.getPreKeyBundleType();
return {
senderCertificate,
numberInfo,
preKeyBundleType,
};
},
@ -1371,8 +1294,8 @@
// Delete the old messages if it's pending
await Promise.all(
incoming
.filter(i => i.id !== message.id)
.map(request => this._removeMessage(request.id))
.filter(i => i.id !== message.id)
.map(request => this._removeMessage(request.id))
);
// If we have an outgoing friend request then

@ -164,7 +164,7 @@
color: this.model.getColor(),
avatarPath: this.model.getAvatarPath(),
isVerified: this.model.isVerified(),
isKeysPending: this.model.isKeyExchangeCompleted() === false,
isKeysPending: !this.model.isFriend(),
isMe: this.model.isMe(),
isBlocked: this.model.isBlocked(),
isGroup: !this.model.isPrivate(),

@ -25,7 +25,7 @@
ivAndCiphertext.set(new Uint8Array(iv));
ivAndCiphertext.set(new Uint8Array(ciphertext), iv.byteLength);
return {
type: textsecure.protobuf.Envelope.Type.FALLBACK_CIPHERTEXT,
type: textsecure.protobuf.Envelope.Type.FRIEND_REQUEST,
body: ivAndCiphertext,
registrationId: null,
};
@ -141,9 +141,7 @@
log.info('empty message sent successfully');
}
};
const options = {
preKeyBundleType: textsecure.protobuf.PreKeyBundleMessage.Type.FRIEND_REQUEST_ACCEPT,
};
const options = {};
// send an empty message. The logic in ougoing_message will attach the prekeys.
const outgoingMessage = new textsecure.OutgoingMessage(
null, // server

@ -631,9 +631,6 @@ MessageReceiver.prototype.extend({
return this.onDeliveryReceipt(envelope);
}
if (envelope.preKeyBundleMessage) {
return this.handlePreKeyBundleMessage(envelope);
}
if (envelope.content) {
return this.handleContentMessage(envelope);
}
@ -725,7 +722,7 @@ MessageReceiver.prototype.extend({
promise = sessionCipher.decryptWhisperMessage(ciphertext)
.then(this.unpad);
break;
case textsecure.protobuf.Envelope.Type.FALLBACK_CIPHERTEXT: {
case textsecure.protobuf.Envelope.Type.FRIEND_REQUEST: {
window.log.info('friend-request message from ', envelope.source);
promise = fallBackSessionCipher.decrypt(ciphertext.toArrayBuffer())
.then(this.unpad);
@ -984,45 +981,38 @@ MessageReceiver.prototype.extend({
let conversation;
try {
conversation = window.ConversationController.get(envelope.source);
conversation = await window.ConversationController.getOrCreateAndWait(envelope.source, 'private');
} catch (e) {
window.log.info('Error getting conversation: ', envelope.source);
}
if (
envelope.preKeyBundleMessage &&
envelope.preKeyBundleMessage.type ===
textsecure.protobuf.PreKeyBundleMessage.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
) {
// If we get a cipher and we're already friends
// then we set our key exchange to complete
if (conversation && conversation.isFriend()) {
await conversation.setKeyExchangeCompleted();
}
if (envelope.type === textsecure.protobuf.Envelope.Type.FRIEND_REQUEST) {
conversation.onFriendRequestReceived();
} else {
conversation.onFriendRequestAccepted();
}
if (content.syncMessage) {
if (content.preKeyBundleMessage) {
const preKeyBundleMessage =
this.decodePreKeyBundleMessage(content.preKeyBundleMessage);
await this.savePreKeyBundleMessage(
envelope.source,
preKeyBundleMessage
);
return this.handleDataMessage(envelope, content.dataMessage, 'friend-request');
}
if (content.syncMessage)
return this.handleSyncMessage(envelope, content.syncMessage);
} else if (content.dataMessage) {
if (content.dataMessage)
return this.handleDataMessage(envelope, content.dataMessage);
} else if (content.nullMessage) {
if (content.nullMessage)
return this.handleNullMessage(envelope, content.nullMessage);
} else if (content.callMessage) {
if (content.callMessage)
return this.handleCallMessage(envelope, content.callMessage);
} else if (content.receiptMessage) {
if (content.receiptMessage)
return this.handleReceiptMessage(envelope, content.receiptMessage);
}
this.removeFromCache(envelope);
if (envelope.preKeyBundleMessage) return null;
throw new Error('Unsupported content message');
this.removeFromCache(envelope);
return null;
},
handleCallMessage(envelope) {
window.log.info('call message from', this.getEnvelopeId(envelope));
@ -1240,43 +1230,6 @@ MessageReceiver.prototype.extend({
signature,
};
},
async handlePreKeyBundleMessage(envelope) {
const preKeyBundleMessage = await this.decryptPreKeyBundleMessage(envelope);
// eslint-disable-next-line no-param-reassign
envelope.preKeyBundleMessage = preKeyBundleMessage;
await this.savePreKeyBundleMessage(
envelope.source,
preKeyBundleMessage
);
const conversation = await window.ConversationController.getOrCreateAndWait(
envelope.source,
'private'
);
if (preKeyBundleMessage.type === textsecure.protobuf.PreKeyBundleMessage.Type.FRIEND_REQUEST) {
conversation.onFriendRequestReceived();
} else if (preKeyBundleMessage.type === textsecure.protobuf.PreKeyBundleMessage.Type.FRIEND_REQUEST_ACCEPT) {
conversation.onFriendRequestAccepted();
} else {
window.log.warn('Unknown PreKeyBundleMessage Type')
}
if (envelope.content)
return this.handleContentMessage(envelope);
return null;
},
async decryptPreKeyBundleMessage(envelope) {
if (!envelope.preKeyBundleMessage) return null;
const address = new libsignal.SignalProtocolAddress(envelope.source, envelope.sourceDevice);
const fallBackSessionCipher = new libloki.FallBackSessionCipher(
address
);
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);
return decodedBundle;
},
async savePreKeyBundleMessage(pubKey, preKeyBundleMessage) {
if (!preKeyBundleMessage) return null;

@ -43,11 +43,11 @@ function OutgoingMessage(
this.failoverNumbers = [];
this.unidentifiedDeliveries = [];
const { numberInfo, senderCertificate, preKeyBundleType } = options;
const { numberInfo, senderCertificate, messageType } = options;
this.numberInfo = numberInfo;
this.senderCertificate = senderCertificate;
this.preKeyBundleType =
preKeyBundleType || textsecure.protobuf.PreKeyBundleMessage.Type.UNKNOWN;
this.messageType =
messageType || 'outgoing';
}
OutgoingMessage.prototype = {
@ -221,16 +221,12 @@ OutgoingMessage.prototype = {
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
@ -248,7 +244,6 @@ OutgoingMessage.prototype = {
},
doSendMessage(number, deviceIds, recurse) {
const ciphers = {};
const plaintext = this.getPlaintext();
/* Disabled because i'm not sure how senderCertificate works :thinking:
const { numberInfo, senderCertificate } = this;
@ -288,25 +283,11 @@ OutgoingMessage.prototype = {
const fallBackEncryption = new libloki.FallBackSessionCipher(address);
// Check if we need to attach the preKeys
let preKeys = {};
if (this.attachPrekeys) {
let sessionCipher;
if (this.messageType === 'friend-request') {
// Encrypt them with the fallback
const preKeyBundleMessage = await libloki.getPreKeyBundleForNumber(number);
preKeyBundleMessage.type = this.preKeyBundleType;
const textBundle = this.convertMessageToText(preKeyBundleMessage);
const encryptedBundle = await fallBackEncryption.encrypt(textBundle);
preKeys = { preKeyBundleMessage: encryptedBundle.body };
this.message.preKeyBundleMessage = await libloki.getPreKeyBundleForNumber(number);
window.log.info('attaching prekeys to outgoing message');
}
// No limit on message keys if we're communicating with our other devices
if (ourKey === number) {
options.messageKeysLimit = false;
}
let sessionCipher;
if (this.fallBackEncryption) {
sessionCipher = fallBackEncryption;
} else {
sessionCipher = new libsignal.SessionCipher(
@ -315,6 +296,13 @@ OutgoingMessage.prototype = {
options
);
}
const plaintext = this.getPlaintext();
// No limit on message keys if we're communicating with our other devices
if (ourKey === number) {
options.messageKeysLimit = false;
}
ciphers[address.getDeviceId()] = sessionCipher;
// Encrypt our plain text
@ -334,7 +322,6 @@ OutgoingMessage.prototype = {
sourceDevice: 1,
destinationRegistrationId: ciphertext.registrationId,
content: ciphertext.body,
...preKeys,
};
})
)
@ -344,10 +331,7 @@ OutgoingMessage.prototype = {
const socketMessage = await this.wrapInWebsocketMessage(outgoingObject);
let ttl;
// TODO: Allow user to set ttl manually
if (
outgoingObject.type ===
textsecure.protobuf.Envelope.Type.FALLBACK_CIPHERTEXT
) {
if (outgoingObject.type === textsecure.protobuf.Envelope.Type.FRIEND_REQUEST) {
ttl = 4 * 24 * 60 * 60; // 4 days for friend request message
} else {
ttl = 24 * 60 * 60; // 1 day default for any other message
@ -465,17 +449,9 @@ OutgoingMessage.prototype = {
return this.getStaleDeviceIdsForNumber(number).then(updateDevices =>
this.getKeysForNumber(number, updateDevices)
.then(async keysFound => {
this.attachPrekeys = false;
if (!keysFound) {
log.info('Fallback encryption enabled');
this.fallBackEncryption = true;
this.attachPrekeys = true;
} else if (conversation) {
try {
this.attachPrekeys = !conversation.isKeyExchangeCompleted();
} catch (e) {
// do nothing
}
}
if (this.fallBackEncryption && conversation) {

@ -12,7 +12,7 @@ message Envelope {
PREKEY_BUNDLE = 3; //Used By Signal. DO NOT TOUCH! we don't use this at all.
RECEIPT = 5;
UNIDENTIFIED_SENDER = 6;
FALLBACK_CIPHERTEXT = 101; // contains prekeys + message and is using simple encryption
FRIEND_REQUEST = 101; // contains prekeys + message and is using simple encryption
}
optional Type type = 1;
@ -24,7 +24,6 @@ message Envelope {
optional bytes content = 8; // Contains an encrypted Content
optional string serverGuid = 9;
optional uint64 serverTimestamp = 10;
optional bytes preKeyBundleMessage = 101;
}
@ -34,16 +33,10 @@ message Content {
optional CallMessage callMessage = 3;
optional NullMessage nullMessage = 4;
optional ReceiptMessage receiptMessage = 5;
optional PreKeyBundleMessage preKeyBundleMessage = 101;
}
message PreKeyBundleMessage {
enum Type {
UNKNOWN = 0;
FRIEND_REQUEST = 1;
FRIEND_REQUEST_ACCEPT = 2;
RESET_SESSION = 3;
RESET_SESSION_ACK = 4;
}
optional bytes identityKey = 1;
optional uint32 deviceId = 2;
optional uint32 preKeyId = 3;
@ -51,7 +44,6 @@ message PreKeyBundleMessage {
optional bytes preKey = 5;
optional bytes signedKey = 6;
optional bytes signature = 7;
optional Type type = 8;
}
message CallMessage {

Loading…
Cancel
Save