Don't promote archived sessions, more logging on error

This commit is contained in:
Scott Nonnenberg 2020-12-09 14:05:11 -08:00 committed by GitHub
parent 1098e59f87
commit 5369950c1d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 161 additions and 48 deletions

View File

@ -345,6 +345,7 @@
try { try {
const id = await normalizeEncodedAddress(encodedAddress); const id = await normalizeEncodedAddress(encodedAddress);
window.log.info('loadSession', { encodedAddress, id });
const session = this.sessions[id]; const session = this.sessions[id];
if (session) { if (session) {
@ -365,6 +366,7 @@
try { try {
const id = await normalizeEncodedAddress(encodedAddress); const id = await normalizeEncodedAddress(encodedAddress);
window.log.info('storeSession', { encodedAddress, id });
const data = { const data = {
id, id,
@ -393,18 +395,34 @@
const sessions = allSessions.filter( const sessions = allSessions.filter(
session => session.conversationId === id session => session.conversationId === id
); );
const openSessions = await Promise.all(
sessions.map(async session => {
const sessionCipher = new libsignal.SessionCipher(
textsecure.storage.protocol,
session.id
);
return _.pluck(sessions, 'deviceId'); const hasOpenSession = await sessionCipher.hasOpenSession();
} catch (e) { if (hasOpenSession) {
return session;
}
return undefined;
})
);
return openSessions.filter(Boolean).map(item => item.deviceId);
} catch (error) {
window.log.error( window.log.error(
`could not get device ids for identifier ${identifier}` `could not get device ids for identifier ${identifier}`,
error && error.stack ? error.stack : error
); );
} }
return []; return [];
}, },
async removeSession(encodedAddress) { async removeSession(encodedAddress) {
window.log.info('deleting session for ', encodedAddress); window.log.info('removeSession: deleting session for', encodedAddress);
try { try {
const id = await normalizeEncodedAddress(encodedAddress); const id = await normalizeEncodedAddress(encodedAddress);
delete this.sessions[id]; delete this.sessions[id];
@ -418,6 +436,8 @@
throw new Error('Tried to remove sessions for undefined/null number'); throw new Error('Tried to remove sessions for undefined/null number');
} }
window.log.info('removeAllSessions: deleting sessions for', identifier);
const id = ConversationController.getConversationId(identifier); const id = ConversationController.getConversationId(identifier);
const allSessions = Object.values(this.sessions); const allSessions = Object.values(this.sessions);
@ -432,6 +452,11 @@
await window.Signal.Data.removeSessionsByConversation(identifier); await window.Signal.Data.removeSessionsByConversation(identifier);
}, },
async archiveSiblingSessions(identifier) { async archiveSiblingSessions(identifier) {
window.log.info(
'archiveSiblingSessions: archiving sibling sessions for',
identifier
);
const address = libsignal.SignalProtocolAddress.fromString(identifier); const address = libsignal.SignalProtocolAddress.fromString(identifier);
const deviceIds = await this.getDeviceIds(address.getName()); const deviceIds = await this.getDeviceIds(address.getName());
@ -443,7 +468,10 @@
address.getName(), address.getName(),
deviceId deviceId
); );
window.log.info('closing session for', sibling.toString()); window.log.info(
'archiveSiblingSessions: closing session for',
sibling.toString()
);
const sessionCipher = new libsignal.SessionCipher( const sessionCipher = new libsignal.SessionCipher(
textsecure.storage.protocol, textsecure.storage.protocol,
sibling sibling
@ -453,6 +481,11 @@
); );
}, },
async archiveAllSessions(identifier) { async archiveAllSessions(identifier) {
window.log.info(
'archiveAllSessions: archiving all sessions for',
identifier
);
const deviceIds = await this.getDeviceIds(identifier); const deviceIds = await this.getDeviceIds(identifier);
await Promise.all( await Promise.all(
@ -461,7 +494,10 @@
identifier, identifier,
deviceId deviceId
); );
window.log.info('closing session for', address.toString()); window.log.info(
'archiveAllSessions: closing session for',
address.toString()
);
const sessionCipher = new libsignal.SessionCipher( const sessionCipher = new libsignal.SessionCipher(
textsecure.storage.protocol, textsecure.storage.protocol,
address address

View File

@ -24840,7 +24840,14 @@ SessionCipher.prototype = {
if (sessionList.length === 0) { if (sessionList.length === 0) {
var error = errors[0]; var error = errors[0];
if (!error) { if (!error) {
error = new Error('decryptWithSessionList: list is empty, but no errors in array'); error = new Error('decryptWithSessionList: list is empty, but no errors in array');
}
if (errors.length > 1) {
errors.forEach((item, index) => {
var stackString = error && error.stack ? error.stack : error;
var extraString = error && error.extra ? JSON.stringify(error.extra) : '';
console.error(`decryptWithSessionList: Error at index ${index}: ${extraString} ${stackString}`);
});
} }
return Promise.reject(error); return Promise.reject(error);
} }
@ -24865,11 +24872,17 @@ SessionCipher.prototype = {
if (!record) { if (!record) {
throw new Error("No record for device " + address); throw new Error("No record for device " + address);
} }
// Only used for printing out debug information when errors happen
var messageProto = buffer.slice(1, buffer.byteLength - 8);
var message = Internal.protobuf.WhisperMessage.decode(messageProto);
var byEphemeralKey = record.getSessionByRemoteEphemeralKey(util.toString(message.ephemeralKey));
var errors = []; var errors = [];
return this.decryptWithSessionList(buffer, record.getSessions(), errors).then(function(result) { return this.decryptWithSessionList(buffer, record.getSessions(), errors).then(function(result) {
return this.getRecord(address).then(function(record) { return this.getRecord(address).then(function(record) {
var openSession = record.getOpenSession(); var openSession = record.getOpenSession();
if (!openSession || result.session.indexInfo.baseKey !== openSession.indexInfo.baseKey) { if (!openSession) {
record.archiveCurrentState(); record.archiveCurrentState();
record.promoteState(result.session); record.promoteState(result.session);
} }
@ -24889,7 +24902,36 @@ SessionCipher.prototype = {
}); });
}.bind(this)); }.bind(this));
}.bind(this)); }.bind(this));
}.bind(this)); }.bind(this)).catch(function(error) {
try {
error.extra = error.extra || {};
error.extra.foundMatchingSession = Boolean(byEphemeralKey);
if (byEphemeralKey) {
var receivingChainInfo = {};
var entries = Object.entries(byEphemeralKey);
entries.forEach(([key, item]) => {
if (item && item.chainType === Internal.ChainType.RECEIVING && item.chainKey) {
var hexKey = dcodeIO.ByteBuffer.wrap(key, 'binary').toString('hex');
receivingChainInfo[hexKey] = {
counter: item.chainKey.counter,
key: item.chainKey.key ? dcodeIO.ByteBuffer.wrap(item.chainKey.key, 'binary').toString('hex') : null,
};
}
});
error.extra.receivingChainInfo = receivingChainInfo;
}
} catch (innerError) {
console.error(
'decryptWhisperMessage: Problem collecting extra information:',
innerError && innerError.stack ? innerError.stack : innerError
);
}
throw error;
});
}.bind(this)); }.bind(this));
}.bind(this)); }.bind(this));
}, },
@ -24946,7 +24988,12 @@ SessionCipher.prototype = {
var remoteEphemeralKey = message.ephemeralKey.toArrayBuffer(); var remoteEphemeralKey = message.ephemeralKey.toArrayBuffer();
if (session === undefined) { if (session === undefined) {
return Promise.reject(new Error("No session found to decrypt message from " + this.remoteAddress.toString())); var error = new Error('No session found to decrypt message from ' + this.remoteAddress.toString());
error.extra = {
messageCounter: message.counter,
ratchetKey: message.ephemeralKey.toString('hex'),
};
return Promise.reject(error);
} }
if (session.indexInfo.closed != -1) { if (session.indexInfo.closed != -1) {
console.log('decrypting message for closed session'); console.log('decrypting message for closed session');
@ -24961,7 +25008,7 @@ SessionCipher.prototype = {
return this.fillMessageKeys(chain, message.counter).then(function() { return this.fillMessageKeys(chain, message.counter).then(function() {
var messageKey = chain.messageKeys[message.counter]; var messageKey = chain.messageKeys[message.counter];
if (messageKey === undefined) { if (messageKey === undefined) {
var e = new Error("Message key not found. The counter was repeated or the key was not filled."); var e = new Error(`Message key not found. Counter ${message.counter} was repeated or the key was not filled.`);
e.name = 'MessageCounterError'; e.name = 'MessageCounterError';
throw e; throw e;
} }
@ -24979,24 +25026,19 @@ SessionCipher.prototype = {
macInput[33*2] = (3 << 4) | 3; macInput[33*2] = (3 << 4) | 3;
macInput.set(new Uint8Array(messageProto), 33*2 + 1); macInput.set(new Uint8Array(messageProto), 33*2 + 1);
return Internal.verifyMAC(macInput.buffer, keys[1], mac, 8).catch(function(error) { return Internal.verifyMAC(macInput.buffer, keys[1], mac, 8);
function logArrayBuffer(name, arrayBuffer) {
console.log('Bad MAC: ' + name + ' - truthy: ' + Boolean(arrayBuffer) +', length: ' + (arrayBuffer ? arrayBuffer.byteLength : 'NaN'));
}
logArrayBuffer('ourPubKey', ourPubKey);
logArrayBuffer('remoteIdentityKey', remoteIdentityKey);
logArrayBuffer('messageProto', messageProto);
logArrayBuffer('mac', mac);
throw error;
});
}.bind(this)).then(function() { }.bind(this)).then(function() {
return Internal.crypto.decrypt(keys[0], message.ciphertext.toArrayBuffer(), keys[2].slice(0, 16)); return Internal.crypto.decrypt(keys[0], message.ciphertext.toArrayBuffer(), keys[2].slice(0, 16));
}); });
}.bind(this)).then(function(plaintext) { }.bind(this)).then(function(plaintext) {
delete session.pendingPreKey; delete session.pendingPreKey;
return plaintext; return plaintext;
}).catch(function(error) {
error.extra = {
messageCounter: message.counter,
ratchetKey: message.ephemeralKey.toString('hex'),
};
throw error;
}); });
}, },
fillMessageKeys: function(chain, counter) { fillMessageKeys: function(chain, counter) {

View File

@ -975,17 +975,39 @@ describe('SignalProtocolStore', () => {
}); });
describe('getDeviceIds', () => { describe('getDeviceIds', () => {
it('returns deviceIds for a number', async () => { it('returns deviceIds for a number', async () => {
const testRecord = 'an opaque string'; const openRecord = JSON.stringify({
const devices = [1, 2, 3, 10].map(deviceId => { version: 'v1',
sessions: {
ephemeralKey: {
registrationId: 25,
indexInfo: {
closed: -1,
},
},
},
});
const openDevices = [1, 2, 3, 10].map(deviceId => {
return [number, deviceId].join('.'); return [number, deviceId].join('.');
}); });
await Promise.all( await Promise.all(
devices.map(async encodedNumber => { openDevices.map(async encodedNumber => {
await store.storeSession(encodedNumber, testRecord + encodedNumber); await store.storeSession(encodedNumber, openRecord);
}) })
); );
const closedRecord = JSON.stringify({
version: 'v1',
sessions: {
ephemeralKey: {
registrationId: 24,
indexInfo: {
closed: Date.now(),
},
},
},
});
await store.storeSession([number, 11].join('.'), closedRecord);
const deviceIds = await store.getDeviceIds(number); const deviceIds = await store.getDeviceIds(number);
assert.sameMembers(deviceIds, [1, 2, 3, 10]); assert.sameMembers(deviceIds, [1, 2, 3, 10]);
}); });

View File

@ -775,6 +775,7 @@ class MessageReceiverInner extends EventTarget {
return promise.catch(error => { return promise.catch(error => {
window.log.error( window.log.error(
`queueDecryptedEnvelope error handling envelope ${id}:`, `queueDecryptedEnvelope error handling envelope ${id}:`,
error && error.extra ? JSON.stringify(error.extra) : '',
error && error.stack ? error.stack : error error && error.stack ? error.stack : error
); );
}); });
@ -796,6 +797,7 @@ class MessageReceiverInner extends EventTarget {
'queueEnvelope error handling envelope', 'queueEnvelope error handling envelope',
this.getEnvelopeId(envelope), this.getEnvelopeId(envelope),
':', ':',
error && error.extra ? JSON.stringify(error.extra) : '',
error && error.stack ? error.stack : error, error && error.stack ? error.stack : error,
]; ];
if (error.warn) { if (error.warn) {
@ -2044,8 +2046,11 @@ class MessageReceiverInner extends EventTarget {
address address
); );
window.log.info('deleting sessions for', address.toString()); window.log.info(
return sessionCipher.deleteAllSessionsForDevice(); 'handleEndSession: closing sessions for',
address.toString()
);
return sessionCipher.closeOpenSessionForDevice();
}) })
); );
} }

View File

@ -589,14 +589,19 @@ export default class OutgoingMessage {
identifier: string, identifier: string,
deviceIdsToRemove: Array<number> deviceIdsToRemove: Array<number>
): Promise<void> { ): Promise<void> {
let promise = Promise.resolve(); await Promise.all(
for (const j in deviceIdsToRemove) { deviceIdsToRemove.map(async deviceId => {
promise = promise.then(async () => { const address = new window.libsignal.SignalProtocolAddress(
const encodedAddress = `${identifier}.${deviceIdsToRemove[j]}`; identifier,
return window.textsecure.storage.protocol.removeSession(encodedAddress); deviceId
}); );
} const sessionCipher = new window.libsignal.SessionCipher(
return promise; window.textsecure.storage.protocol,
address
);
await sessionCipher.closeOpenSessionForDevice();
})
);
} }
async sendToIdentifier(providedIdentifier: string): Promise<void> { async sendToIdentifier(providedIdentifier: string): Promise<void> {

View File

@ -1574,7 +1574,7 @@ export default class MessageSender {
): Promise< ): Promise<
CallbackResultType | void | Array<CallbackResultType | void | Array<void>> CallbackResultType | void | Array<CallbackResultType | void | Array<void>>
> { > {
window.log.info('resetting secure session'); window.log.info('resetSession: start');
const silent = false; const silent = false;
const proto = new window.textsecure.protobuf.DataMessage(); const proto = new window.textsecure.protobuf.DataMessage();
proto.body = 'TERMINATE'; proto.body = 'TERMINATE';
@ -1587,7 +1587,7 @@ export default class MessageSender {
window.log.error(prefix, error && error.stack ? error.stack : error); window.log.error(prefix, error && error.stack ? error.stack : error);
throw error; throw error;
}; };
const deleteAllSessions = async (targetIdentifier: string) => const closeAllSessions = async (targetIdentifier: string) =>
window.textsecure.storage.protocol window.textsecure.storage.protocol
.getDeviceIds(targetIdentifier) .getDeviceIds(targetIdentifier)
.then(async deviceIds => .then(async deviceIds =>
@ -1597,21 +1597,24 @@ export default class MessageSender {
targetIdentifier, targetIdentifier,
deviceId deviceId
); );
window.log.info('deleting sessions for', address.toString()); window.log.info(
'resetSession: closing sessions for',
address.toString()
);
const sessionCipher = new window.libsignal.SessionCipher( const sessionCipher = new window.libsignal.SessionCipher(
window.textsecure.storage.protocol, window.textsecure.storage.protocol,
address address
); );
return sessionCipher.deleteAllSessionsForDevice(); return sessionCipher.closeOpenSessionForDevice();
}) })
) )
); );
const sendToContactPromise = deleteAllSessions(identifier) const sendToContactPromise = closeAllSessions(identifier)
.catch(logError('resetSession/deleteAllSessions1 error:')) .catch(logError('resetSession/closeAllSessions1 error:'))
.then(async () => { .then(async () => {
window.log.info( window.log.info(
'finished closing local sessions, now sending to contact' 'resetSession: finished closing local sessions, now sending to contact'
); );
return this.sendIndividualProto( return this.sendIndividualProto(
identifier, identifier,
@ -1622,8 +1625,8 @@ export default class MessageSender {
).catch(logError('resetSession/sendToContact error:')); ).catch(logError('resetSession/sendToContact error:'));
}) })
.then(async () => .then(async () =>
deleteAllSessions(identifier).catch( closeAllSessions(identifier).catch(
logError('resetSession/deleteAllSessions2 error:') logError('resetSession/closeAllSessions2 error:')
) )
); );

View File

@ -326,7 +326,7 @@
"rule": "jQuery-load(", "rule": "jQuery-load(",
"path": "js/signal_protocol_store.js", "path": "js/signal_protocol_store.js",
"line": " await ConversationController.load();", "line": " await ConversationController.load();",
"lineNumber": 983, "lineNumber": 1019,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2020-06-12T14:20:09.936Z" "updated": "2020-06-12T14:20:09.936Z"
}, },