Refine sealed sender logic

This commit is contained in:
Scott Nonnenberg 2018-10-31 16:58:14 -07:00
parent f11dd18536
commit e2e0e4c96b
3 changed files with 156 additions and 98 deletions

View file

@ -1121,9 +1121,11 @@
// Sent: // Sent:
async function handleMessageSentProfileUpdate({ async function handleMessageSentProfileUpdate({
data,
confirm, confirm,
messageDescriptor, messageDescriptor,
}) { }) {
// First set profileSharing = true for the conversation we sent to
const { id, type } = messageDescriptor; const { id, type } = messageDescriptor;
const conversation = await ConversationController.getOrCreateAndWait( const conversation = await ConversationController.getOrCreateAndWait(
id, id,
@ -1135,6 +1137,14 @@
Conversation: Whisper.Conversation, Conversation: Whisper.Conversation,
}); });
// Then we update our own profileKey if it's different from what we have
const ourNumber = textsecure.storage.user.getNumber();
const profileKey = data.message.profileKey.toString('base64');
const me = await ConversationController.getOrCreate(ourNumber, 'private');
// Will do the save for us if needed
await me.setProfileKey(profileKey);
return confirm(); return confirm();
} }

View file

@ -16,6 +16,13 @@
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
const SEALED_SENDER = {
UNKNOWN: 0,
ENABLED: 1,
DISABLED: 2,
UNRESTRICTED: 3,
};
const { Util } = window.Signal; const { Util } = window.Signal;
const { const {
Conversation, Conversation,
@ -113,6 +120,14 @@
this.on('read', this.updateAndMerge); this.on('read', this.updateAndMerge);
this.on('expiration-change', this.updateAndMerge); this.on('expiration-change', this.updateAndMerge);
this.on('expired', this.onExpired); this.on('expired', this.onExpired);
const sealedSender = this.get('sealedSender');
if (sealedSender === undefined) {
this.set({ sealedSender: SEALED_SENDER.UNKNOWN });
}
this.unset('unidentifiedDelivery');
this.unset('unidentifiedDeliveryUnrestricted');
this.unset('hasFetchedProfile');
}, },
isMe() { isMe() {
@ -783,38 +798,65 @@
return promise.then( return promise.then(
async result => { async result => {
// success // success
if ( if (result) {
result && await this.handleMessageSendResult(
result.failoverNumbers && result.failoverNumbers,
result.failoverNumbers.length result.unidentifiedDeliveries
) { );
await this.handleFailover(result.failoverNumbers);
} }
return result; return result;
}, },
async result => { async result => {
// failure // failure
if ( if (result) {
result && await this.handleMessageSendResult(
result.failoverNumbers && result.failoverNumbers,
result.failoverNumbers.length result.unidentifiedDeliveries
) { );
await this.handleFailover(result.failoverNumbers);
} }
throw result; throw result;
} }
); );
}, },
handleFailover(numberArray) { async handleMessageSendResult(failoverNumbers, unidentifiedDeliveries) {
return Promise.all( await Promise.all(
(numberArray || []).map(async number => { (failoverNumbers || []).map(async number => {
const conversation = ConversationController.get(number); const conversation = ConversationController.get(number);
if (conversation && conversation.get('unidentifiedDelivery')) {
window.log.info( if (
`Marking unidentifiedDelivery false for conversation ${conversation.idForLogging()}` conversation &&
conversation.get('sealedSender') !== SEALED_SENDER.DISABLED
) {
conversation.set({
sealedSender: SEALED_SENDER.DISABLED,
});
await window.Signal.Data.updateConversation(
conversation.id,
conversation.attributes,
{ Conversation: Whisper.Conversation }
); );
conversation.set({ unidentifiedDelivery: false }); }
})
);
await Promise.all(
(unidentifiedDeliveries || []).map(async number => {
const conversation = ConversationController.get(number);
if (
conversation &&
conversation.get('sealedSender') === SEALED_SENDER.UNKNOWN
) {
if (conversation.get('accessKey')) {
conversation.set({
sealedSender: SEALED_SENDER.ENABLED,
});
} else {
conversation.set({
sealedSender: SEALED_SENDER.UNRESTRICTED,
});
}
await window.Signal.Data.updateConversation( await window.Signal.Data.updateConversation(
conversation.id, conversation.id,
conversation.attributes, conversation.attributes,
@ -835,42 +877,54 @@
}; };
}, },
getNumberInfo() { getNumberInfo({ disableMeCheck } = {}) {
const UD = 'unidentifiedDelivery';
const UNRESTRICTED_UD = 'unidentifiedDeliveryUnrestricted';
// We don't want to enable unidentified delivery for send unless it is // We don't want to enable unidentified delivery for send unless it is
// also enabled for our own account. // also enabled for our own account.
const me = ConversationController.getOrCreate(this.ourNumber, 'private'); const me = ConversationController.getOrCreate(this.ourNumber, 'private');
if (!me.get(UD) && !me.get(UNRESTRICTED_UD)) { if (
!disableMeCheck &&
me.get('sealedSender') === SEALED_SENDER.DISABLED
) {
return null; return null;
} }
if (this.isPrivate()) { if (!this.isPrivate()) {
const accessKey = this.get('accessKey'); const infoArray = this.contactCollection.map(conversation =>
const unidentifiedDelivery = this.get(UD); conversation.getNumberInfo({ disableMeCheck })
const unrestricted = this.get(UNRESTRICTED_UD); );
return Object.assign({}, ...infoArray);
}
if (!unidentifiedDelivery && !unrestricted) { const accessKey = this.get('accessKey');
return null; const sealedSender = this.get('sealedSender');
}
// If we've never fetched user's profile, we default to what we have
if (sealedSender === SEALED_SENDER.UNKNOWN) {
return { return {
[this.id]: { [this.id]: {
accessKey: accessKey:
accessKey && !unrestricted accessKey ||
? accessKey window.Signal.Crypto.arrayBufferToBase64(
: window.Signal.Crypto.arrayBufferToBase64( window.Signal.Crypto.getRandomBytes(16)
window.Signal.Crypto.getRandomBytes(16) ),
),
}, },
}; };
} }
const infoArray = this.contactCollection.map(conversation => if (sealedSender === SEALED_SENDER.DISABLED) {
conversation.getNumberInfo() return null;
); }
return Object.assign({}, ...infoArray);
return {
[this.id]: {
accessKey:
accessKey && sealedSender === SEALED_SENDER.ENABLED
? accessKey
: window.Signal.Crypto.arrayBufferToBase64(
window.Signal.Crypto.getRandomBytes(16)
),
},
};
}, },
async updateLastMessage() { async updateLastMessage() {
@ -1237,37 +1291,19 @@
c.changed = {}; c.changed = {};
try { try {
if (c.get('profileKey') && !c.get('accessKey')) { await c.deriveAccessKeyIfNeeded();
const profileKeyBuffer = window.Signal.Crypto.base64ToArrayBuffer( const numberInfo = c.getNumberInfo({ disableMeCheck: true }) || {};
c.get('profileKey') const getInfo = numberInfo[c.id] || {};
);
const buffer = await window.Signal.Crypto.deriveAccessKey(
profileKeyBuffer
);
c.set({
accessKey: window.Signal.Crypto.arrayBufferToBase64(buffer),
});
}
const firstProfileFetch = !c.get('hasFetchedProfile');
const accessKey = c.get('accessKey');
let profile; let profile;
if (c.get('unidentifiedDelivery') || firstProfileFetch) { if (getInfo.accessKey) {
try { try {
profile = await textsecure.messaging.getProfile(id, { profile = await textsecure.messaging.getProfile(id, {
accessKey: accessKey: getInfo.accessKey,
accessKey ||
window.Signal.Crypto.arrayBufferToBase64(
window.window.Signal.Crypto.getRandomBytes(16)
),
}); });
} catch (error) { } catch (error) {
if (error.code === 401 || error.code === 403) { if (error.code === 401 || error.code === 403) {
c.set({ c.set({ sealedSender: SEALED_SENDER.DISABLED });
unidentifiedDelivery: false,
unidentifiedDeliveryUnrestricted: false,
});
profile = await textsecure.messaging.getProfile(id); profile = await textsecure.messaging.getProfile(id);
} else { } else {
throw error; throw error;
@ -1297,17 +1333,13 @@
await sessionCipher.closeOpenSessionForDevice(); await sessionCipher.closeOpenSessionForDevice();
} }
c.set({ const accessKey = c.get('accessKey');
hasFetchedProfile: true,
});
if ( if (
profile.unrestrictedUnidentifiedAccess && profile.unrestrictedUnidentifiedAccess &&
profile.unidentifiedAccess profile.unidentifiedAccess
) { ) {
c.set({ c.set({
unidentifiedDelivery: true, sealedSender: SEALED_SENDER.UNRESTRICTED,
unidentifiedDeliveryUnrestricted: true,
}); });
} else if (accessKey && profile.unidentifiedAccess) { } else if (accessKey && profile.unidentifiedAccess) {
const haveCorrectKey = await window.Signal.Crypto.verifyAccessKey( const haveCorrectKey = await window.Signal.Crypto.verifyAccessKey(
@ -1315,17 +1347,18 @@
window.Signal.Crypto.base64ToArrayBuffer(profile.unidentifiedAccess) window.Signal.Crypto.base64ToArrayBuffer(profile.unidentifiedAccess)
); );
window.log.info( if (haveCorrectKey) {
`Setting unidentifiedDelivery to ${haveCorrectKey} for conversation ${c.idForLogging()}` c.set({
); sealedSender: SEALED_SENDER.ENABLED,
c.set({ });
unidentifiedDelivery: haveCorrectKey, } else {
unidentifiedDeliveryUnrestricted: false, c.set({
}); sealedSender: SEALED_SENDER.DISABLED,
});
}
} else { } else {
c.set({ c.set({
unidentifiedDelivery: false, sealedSender: SEALED_SENDER.DISABLED,
unidentifiedDeliveryUnrestricted: false,
}); });
} }
@ -1406,17 +1439,13 @@
async setProfileKey(profileKey) { async setProfileKey(profileKey) {
// profileKey is a string so we can compare it directly // profileKey is a string so we can compare it directly
if (this.get('profileKey') !== profileKey) { if (this.get('profileKey') !== profileKey) {
const profileKeyBuffer = window.Signal.Crypto.base64ToArrayBuffer( this.set({
profileKey profileKey,
); accessKey: null,
const accessKeyBuffer = await window.Signal.Crypto.deriveAccessKey( sealedSender: SEALED_SENDER.UNKNOWN,
profileKeyBuffer });
);
const accessKey = window.Signal.Crypto.arrayBufferToBase64(
accessKeyBuffer
);
this.set({ profileKey, accessKey }); await this.deriveAccessKeyIfNeeded();
await window.Signal.Data.updateConversation(this.id, this.attributes, { await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation, Conversation: Whisper.Conversation,
@ -1424,6 +1453,27 @@
} }
}, },
async deriveAccessKeyIfNeeded() {
const profileKey = this.get('profileKey');
if (!profileKey) {
return;
}
if (this.get('accessKey')) {
return;
}
const profileKeyBuffer = window.Signal.Crypto.base64ToArrayBuffer(
profileKey
);
const accessKeyBuffer = await window.Signal.Crypto.deriveAccessKey(
profileKeyBuffer
);
const accessKey = window.Signal.Crypto.arrayBufferToBase64(
accessKeyBuffer
);
this.set({ accessKey });
},
async upgradeMessages(messages) { async upgradeMessages(messages) {
for (let max = messages.length, i = 0; i < max; i += 1) { for (let max = messages.length, i = 0; i < max; i += 1) {
const message = messages.at(i); const message = messages.at(i);

View file

@ -66,7 +66,7 @@ OutgoingMessage.prototype = {
this.errors[this.errors.length] = error; this.errors[this.errors.length] = error;
this.numberCompleted(); this.numberCompleted();
}, },
reloadDevicesAndSend(number, recurse, failover) { reloadDevicesAndSend(number, recurse) {
return () => return () =>
textsecure.storage.protocol.getDeviceIds(number).then(deviceIds => { textsecure.storage.protocol.getDeviceIds(number).then(deviceIds => {
if (deviceIds.length === 0) { if (deviceIds.length === 0) {
@ -76,7 +76,7 @@ OutgoingMessage.prototype = {
null null
); );
} }
return this.doSendMessage(number, deviceIds, recurse, failover); return this.doSendMessage(number, deviceIds, recurse);
}); });
}, },
@ -245,7 +245,7 @@ OutgoingMessage.prototype = {
return this.plaintext; return this.plaintext;
}, },
doSendMessage(number, deviceIds, recurse, failover) { doSendMessage(number, deviceIds, recurse) {
const ciphers = {}; const ciphers = {};
const plaintext = this.getPlaintext(); const plaintext = this.getPlaintext();
@ -261,8 +261,7 @@ OutgoingMessage.prototype = {
); );
} }
// If failover is true, we don't send an unidentified sender message const sealedSender = Boolean(accessKey && senderCertificate);
const sealedSender = Boolean(!failover && accessKey && senderCertificate);
// We don't send to ourselves if unless sealedSender is enabled // We don't send to ourselves if unless sealedSender is enabled
const ourNumber = textsecure.storage.user.getNumber(); const ourNumber = textsecure.storage.user.getNumber();
@ -288,7 +287,6 @@ OutgoingMessage.prototype = {
options.messageKeysLimit = false; options.messageKeysLimit = false;
} }
// If failover is true, we don't send an unidentified sender message
if (sealedSender) { if (sealedSender) {
const secretSessionCipher = new window.Signal.Metadata.SecretSessionCipher( const secretSessionCipher = new window.Signal.Metadata.SecretSessionCipher(
textsecure.storage.protocol textsecure.storage.protocol
@ -397,9 +395,9 @@ OutgoingMessage.prototype = {
? error.response.staleDevices ? error.response.staleDevices
: error.response.missingDevices; : error.response.missingDevices;
return this.getKeysForNumber(number, resetDevices).then( return this.getKeysForNumber(number, resetDevices).then(
// For now, we we won't retry unidentified delivery if we get here; new // We continue to retry as long as the error code was 409; the assumption is
// devices could have been added which don't support it. // that we'll request new device info and the next request will succeed.
this.reloadDevicesAndSend(number, error.code === 409, true) this.reloadDevicesAndSend(number, error.code === 409)
); );
}); });
} else if (error.message === 'Identity key changed') { } else if (error.message === 'Identity key changed') {