diff --git a/js/libtextsecure.js b/js/libtextsecure.js index 22bfec48..e161510c 100644 --- a/js/libtextsecure.js +++ b/js/libtextsecure.js @@ -96,12 +96,34 @@ return textsecure.storage.removeEncrypted(key); }, + identityKeys: { + get: function(identifier) { + return textsecure.storage.devices.getIdentityKeyForNumber(textsecure.utils.unencodeNumber(identifier)[0]); + }, + put: function(identifier, identityKey) { + return textsecure.storage.devices.checkSaveIdentityKeyForNumber(textsecure.utils.unencodeNumber(identifier)[0], identityKey); + }, + }, + sessions: { get: function(identifier) { - return textsecure.storage.devices.getDeviceObject(identifier); + var device = textsecure.storage.devices.getDeviceObject(identifier, true); + if (device === undefined || device.sessions === undefined) + return undefined; + return device.sessions; }, - put: function(object) { - return textsecure.storage.devices.saveDeviceObject(object); + put: function(identifier, record) { + var device = textsecure.storage.devices.getDeviceObject(identifier); + if (device === undefined) { + device = { encodedNumber: identifier, + //TODO: Remove this duplication + identityKey: record.identityKey + }; + } + if (getString(device.identityKey) !== getString(record.identityKey)) + throw new Error("Tried to put session for device with changed identity key"); + device.sessions = record; + return textsecure.storage.devices.saveDeviceObject(device); } } }, @@ -23827,25 +23849,22 @@ window.axolotl.protocol = function() { } crypto_storage.saveSession = function(encodedNumber, session, registrationId) { - var device = axolotl.api.storage.sessions.get(encodedNumber); - if (device === undefined) - device = { sessions: {}, encodedNumber: encodedNumber }; + var record = axolotl.api.storage.sessions.get(encodedNumber); + if (record === undefined) { + if (registrationId === undefined) + throw new Error("Tried to save a session for an existing device that didn't exist"); + else + record = new axolotl.sessions.RecipientRecord(session.indexInfo.remoteIdentityKey, registrationId); + } - if (registrationId !== undefined) - device.registrationId = registrationId; + var sessions = record._sessions; - crypto_storage.saveSessionAndDevice(device, session); - } - - crypto_storage.saveSessionAndDevice = function(device, session) { - if (device.sessions === undefined) - device.sessions = {}; - var sessions = device.sessions; + if (record.identityKey === null) + record.identityKey = session.indexInfo.remoteIdentityKey; + if (getString(record.identityKey) !== getString(session.indexInfo.remoteIdentityKey)) + throw new Error("Identity key changed at session save time"); var doDeleteSession = false; - if (session.indexInfo.closed == -1 || device.identityKey === undefined) - device.identityKey = session.indexInfo.remoteIdentityKey; - if (session.indexInfo.closed != -1) { doDeleteSession = (session.indexInfo.closed < (new Date().getTime() - MESSAGE_LOST_THRESHOLD_MS)); @@ -23872,19 +23891,27 @@ window.axolotl.protocol = function() { for (var key in sessions) if (sessions[key].indexInfo.closed == -1) openSessionRemaining = true; - if (!openSessionRemaining) - try { - delete device['registrationId']; - } catch(_) {} + if (!openSessionRemaining) // Used as a flag to get new pre keys for the next session + record.registrationId = null; + else if (record.registrationId === null && registrationId !== undefined) + record.registrationId = registrationId; + else if (record.registrationId === null) + throw new Error("Had open sessions on a record that had no registrationId set"); - axolotl.api.storage.sessions.put(device); + var identityKey = axolotl.api.storage.identityKeys.get(encodedNumber); + if (identityKey === undefined) + axolotl.api.storage.identityKeys.put(encodedNumber, record.identityKey); + else if (getString(identityKey) !== getString(record.identityKey)) + throw new Error("Tried to change identity key at save time"); + + axolotl.api.storage.sessions.put(encodedNumber, record); } var getSessions = function(encodedNumber) { - var device = axolotl.api.storage.sessions.get(encodedNumber); - if (device === undefined || device.sessions === undefined) + var record = axolotl.api.storage.sessions.get(encodedNumber); + if (record === undefined) return undefined; - return device.sessions; + return record._sessions; } crypto_storage.getOpenSession = function(encodedNumber) { @@ -23922,17 +23949,21 @@ window.axolotl.protocol = function() { } crypto_storage.getSessionOrIdentityKeyByBaseKey = function(encodedNumber, baseKey) { - var sessions = getSessions(encodedNumber); - var device = axolotl.api.storage.sessions.get(encodedNumber); - if (device === undefined) - return undefined; + var record = axolotl.api.storage.sessions.get(encodedNumber); + if (record === undefined) { + var identityKey = axolotl.api.storage.identityKeys.get(encodedNumber); + if (identityKey === undefined) + return undefined; + return { indexInfo: { remoteIdentityKey: identityKey } }; + } + var sessions = record._sessions; - var preferredSession = device.sessions && device.sessions[getString(baseKey)]; + var preferredSession = record._sessions[getString(baseKey)]; if (preferredSession !== undefined) return preferredSession; - if (device.identityKey !== undefined) - return { indexInfo: { remoteIdentityKey: device.identityKey } }; + if (record.identityKey !== undefined) + return { indexInfo: { remoteIdentityKey: record.identityKey } }; throw new Error("Datastore inconsistency: device was stored without identity key"); } @@ -24298,6 +24329,7 @@ window.axolotl.protocol = function() { // return Promise(encoded [PreKey]WhisperMessage) self.encryptMessageFor = function(deviceObject, pushMessageContent) { var session = crypto_storage.getOpenSession(deviceObject.encodedNumber); + var hadSession = session !== undefined; var doEncryptPushMessageContent = function() { var msg = new axolotl.protobuf.WhisperMessage(); @@ -24332,17 +24364,9 @@ window.axolotl.protocol = function() { result.set(new Uint8Array(encodedMsg), 1); result.set(new Uint8Array(mac, 0, 8), encodedMsg.byteLength + 1); - try { - delete deviceObject['signedKey']; - delete deviceObject['signedKeyId']; - delete deviceObject['signedKeySignature']; - delete deviceObject['preKey']; - delete deviceObject['preKeyId']; - } catch(_) {} - removeOldChains(session); - crypto_storage.saveSessionAndDevice(deviceObject, session); + crypto_storage.saveSession(deviceObject.encodedNumber, session, !hadSession ? deviceObject.registrationId : undefined); return result; }); }); @@ -24517,6 +24541,60 @@ window.axolotl.protocol = function() { }; })(); +/* vim: ts=4:sw=4 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +;(function() { + +'use strict'; +window.axolotl = window.axolotl || {}; + +var RecipientRecord = function(identityKey, registrationId) { + this._sessions = {}; + this.identityKey = identityKey !== undefined ? getString(identityKey) : null; + this.registrationId = registrationId; + + if (this.registrationId === undefined || typeof this.registrationId !== "number") + this.registrationId = null; +}; + +RecipientRecord.prototype.serialize = function() { + return textsecure.utils.jsonThing({sessions: this._sessions, registrationId: this.registrationId, identityKey: this.identityKey}); +} + +RecipientRecord.prototype.deserialize = function(serialized) { + var data = JSON.parse(serialized); + this._sessions = data.sessions; + if (this._sessions === undefined || this._sessions === null || typeof this._sessions !== "object" || Array.isArray(this._sessions)) + throw new Error("Error deserializing RecipientRecord"); + this.identityKey = data.identityKey; + this.registrationId = data.registrationId; + if (this.identityKey === undefined || this.registrationId === undefined) + throw new Error("Error deserializing RecipientRecord"); +} + +RecipientRecord.prototype.haveOpenSession = function() { + return this.registrationId !== null; +} + +window.axolotl.sessions = { + RecipientRecord: RecipientRecord, +}; + +})(); + /* vim: ts=4:sw=4:expandtab * * This program is free software: you can redistribute it and/or modify @@ -24729,21 +24807,67 @@ window.axolotl.protocol = function() { return internalSaveDeviceObject(deviceObject, true); }, - getDeviceObjectsForNumber: function(number) { - var map = textsecure.storage.getEncrypted("devices" + number); - return map === undefined ? [] : map.devices; + removeTempKeysFromDevice: function(encodedNumber) { + var deviceObject = textsecure.storage.devices.getDeviceObject(encodedNumber); + try { + delete deviceObject['signedKey']; + delete deviceObject['signedKeyId']; + delete deviceObject['signedKeySignature']; + delete deviceObject['preKey']; + delete deviceObject['preKeyId']; + delete deviceObject['registrationId']; + } catch(_) {} + return internalSaveDeviceObject(deviceObject, false); }, - getDeviceObject: function(encodedNumber) { - var number = textsecure.utils.unencodeNumber(encodedNumber); - var devices = textsecure.storage.devices.getDeviceObjectsForNumber(number[0]); - if (devices === undefined) + getDeviceObjectsForNumber: function(number) { + var map = textsecure.storage.getEncrypted("devices" + number); + if (map === undefined) + return []; + for (key in map.devices) { + if (map.devices[key].sessions !== undefined) { + var record = new axolotl.sessions.RecipientRecord(); + record.deserialize(map.devices[key].sessions); + if (getString(map.identityKey) !== getString(record.identityKey)) + throw new Error("Got mismatched identity key on device object load"); + map.devices[key].sessions = record; + } + } + return map.devices; + }, + + getIdentityKeyForNumber: function(number) { + var map = textsecure.storage.getEncrypted("devices" + number); + return map === undefined ? undefined : map.identityKey; + }, + + checkSaveIdentityKeyForNumber: function(number, identityKey) { + var map = textsecure.storage.getEncrypted("devices" + number); + if (map === undefined) + textsecure.storage.putEncrypted("devices" + number, { devices: [], identityKey: identityKey}); + else if (getString(map.identityKey) !== getString(identityKey)) + throw new Error("Attempted to overwrite a different identity key"); + }, + + getDeviceObject: function(encodedNumber, returnIdentityKey) { + var number = textsecure.utils.unencodeNumber(encodedNumber)[0]; + var devices = textsecure.storage.devices.getDeviceObjectsForNumber(number); + if (devices.length == 0) { + if (returnIdentityKey) { + var identityKey = textsecure.storage.devices.getIdentityKeyForNumber(number); + if (identityKey !== undefined) + return {identityKey: identityKey}; + } return undefined; + } for (var i in devices) if (devices[i].encodedNumber == encodedNumber) return devices[i]; + if (returnIdentityKey) + return {identityKey: devices[0].identityKey}; + return undefined; }, @@ -24768,16 +24892,26 @@ window.axolotl.protocol = function() { if (devicesRemoved != deviceIdsToRemove.length) throw new Error("Tried to remove unknown device"); + + if (newDevices.length === 0) + textsecure.storage.removeEncrypted("devices" + number); + else { + map.devices = newDevices; + textsecure.storage.putEncrypted("devices" + number, map); + } } }; var internalSaveDeviceObject = function(deviceObject, onlyKeys) { - if (deviceObject.identityKey === undefined || deviceObject.encodedNumber === undefined || deviceObject.registrationId === undefined) + if (deviceObject.identityKey === undefined || deviceObject.encodedNumber === undefined) throw new Error("Tried to store invalid deviceObject"); var number = textsecure.utils.unencodeNumber(deviceObject.encodedNumber)[0]; var map = textsecure.storage.getEncrypted("devices" + number); + if (deviceObject.sessions !== undefined) + deviceObject.sessions = deviceObject.sessions.serialize() + if (map === undefined) map = { devices: [deviceObject], identityKey: deviceObject.identityKey }; else if (map.identityKey != getString(deviceObject.identityKey)) @@ -24793,6 +24927,7 @@ window.axolotl.protocol = function() { map.devices[i].preKeyId = deviceObject.preKeyId; map.devices[i].signedKey = deviceObject.signedKey; map.devices[i].signedKeyId = deviceObject.signedKeyId; + map.devices[i].signedKeySignature = deviceObject.signedKeySignature; map.devices[i].registrationId = deviceObject.registrationId; } updated = true; @@ -25930,6 +26065,7 @@ window.textsecure.messaging = function() { var self = {}; + //TODO: Dont hit disk for any of the key-fetching! function getKeysForNumber(number, updateDevices) { var handleResult = function(response) { for (var i in response.devices) { @@ -25978,10 +26114,13 @@ window.textsecure.messaging = function() { } return axolotl.protocol.encryptMessageFor(deviceObjectList[i], message).then(function(encryptedMsg) { + textsecure.storage.devices.removeTempKeysFromDevice(deviceObjectList[i].encodedNumber); + var registrationId = deviceObjectList[i].registrationId || deviceObjectList[i].sessions.registrationId; + jsonData[i] = { type: encryptedMsg.type, destinationDeviceId: textsecure.utils.unencodeNumber(deviceObjectList[i].encodedNumber)[1], - destinationRegistrationId: deviceObjectList[i].registrationId, + destinationRegistrationId: registrationId, body: encryptedMsg.body, timestamp: timestamp }; @@ -26004,7 +26143,8 @@ window.textsecure.messaging = function() { var doUpdate = false; for (var i in devicesForNumber) { - if (textsecure.storage.groups.needUpdateByDeviceRegistrationId(groupId, number, devicesForNumber[i].encodedNumber, devicesForNumber[i].registrationId)) + var registrationId = devicesForNumber[i].registrationId || devicesForNumber[i].sessions.registrationId; + if (textsecure.storage.groups.needUpdateByDeviceRegistrationId(groupId, number, devicesForNumber[i].encodedNumber, registrationId)) doUpdate = true; } if (!doUpdate) @@ -26036,6 +26176,7 @@ window.textsecure.messaging = function() { var tryMessageAgain = function(number, encodedMessage, message_id) { var message = new Whisper.MessageCollection().add({id: message_id}); message.fetch().then(function() { + //TODO: Encapsuate with the rest of textsecure.storage.devices textsecure.storage.removeEncrypted("devices" + number); var proto = textsecure.protobuf.PushMessageContent.decode(encodedMessage, 'binary'); sendMessageProto(message.get('sent_at'), [number], proto, function(res) { @@ -26120,7 +26261,7 @@ window.textsecure.messaging = function() { var promises = []; for (var j in devicesForNumber) - if (devicesForNumber[j].registrationId === undefined) + if (devicesForNumber[j].sessions === undefined || !devicesForNumber[j].sessions.haveOpenSession()) promises[promises.length] = getKeysForNumber(number, [parseInt(textsecure.utils.unencodeNumber(devicesForNumber[j].encodedNumber)[1])]); Promise.all(promises).then(function() { diff --git a/libtextsecure/libaxolotl_concat.js b/libtextsecure/libaxolotl_concat.js index 89525786..f6679051 100644 --- a/libtextsecure/libaxolotl_concat.js +++ b/libtextsecure/libaxolotl_concat.js @@ -23644,25 +23644,22 @@ window.axolotl.protocol = function() { } crypto_storage.saveSession = function(encodedNumber, session, registrationId) { - var device = axolotl.api.storage.sessions.get(encodedNumber); - if (device === undefined) - device = { sessions: {}, encodedNumber: encodedNumber }; + var record = axolotl.api.storage.sessions.get(encodedNumber); + if (record === undefined) { + if (registrationId === undefined) + throw new Error("Tried to save a session for an existing device that didn't exist"); + else + record = new axolotl.sessions.RecipientRecord(session.indexInfo.remoteIdentityKey, registrationId); + } - if (registrationId !== undefined) - device.registrationId = registrationId; + var sessions = record._sessions; - crypto_storage.saveSessionAndDevice(device, session); - } - - crypto_storage.saveSessionAndDevice = function(device, session) { - if (device.sessions === undefined) - device.sessions = {}; - var sessions = device.sessions; + if (record.identityKey === null) + record.identityKey = session.indexInfo.remoteIdentityKey; + if (getString(record.identityKey) !== getString(session.indexInfo.remoteIdentityKey)) + throw new Error("Identity key changed at session save time"); var doDeleteSession = false; - if (session.indexInfo.closed == -1 || device.identityKey === undefined) - device.identityKey = session.indexInfo.remoteIdentityKey; - if (session.indexInfo.closed != -1) { doDeleteSession = (session.indexInfo.closed < (new Date().getTime() - MESSAGE_LOST_THRESHOLD_MS)); @@ -23689,19 +23686,27 @@ window.axolotl.protocol = function() { for (var key in sessions) if (sessions[key].indexInfo.closed == -1) openSessionRemaining = true; - if (!openSessionRemaining) - try { - delete device['registrationId']; - } catch(_) {} + if (!openSessionRemaining) // Used as a flag to get new pre keys for the next session + record.registrationId = null; + else if (record.registrationId === null && registrationId !== undefined) + record.registrationId = registrationId; + else if (record.registrationId === null) + throw new Error("Had open sessions on a record that had no registrationId set"); - axolotl.api.storage.sessions.put(device); + var identityKey = axolotl.api.storage.identityKeys.get(encodedNumber); + if (identityKey === undefined) + axolotl.api.storage.identityKeys.put(encodedNumber, record.identityKey); + else if (getString(identityKey) !== getString(record.identityKey)) + throw new Error("Tried to change identity key at save time"); + + axolotl.api.storage.sessions.put(encodedNumber, record); } var getSessions = function(encodedNumber) { - var device = axolotl.api.storage.sessions.get(encodedNumber); - if (device === undefined || device.sessions === undefined) + var record = axolotl.api.storage.sessions.get(encodedNumber); + if (record === undefined) return undefined; - return device.sessions; + return record._sessions; } crypto_storage.getOpenSession = function(encodedNumber) { @@ -23739,17 +23744,21 @@ window.axolotl.protocol = function() { } crypto_storage.getSessionOrIdentityKeyByBaseKey = function(encodedNumber, baseKey) { - var sessions = getSessions(encodedNumber); - var device = axolotl.api.storage.sessions.get(encodedNumber); - if (device === undefined) - return undefined; + var record = axolotl.api.storage.sessions.get(encodedNumber); + if (record === undefined) { + var identityKey = axolotl.api.storage.identityKeys.get(encodedNumber); + if (identityKey === undefined) + return undefined; + return { indexInfo: { remoteIdentityKey: identityKey } }; + } + var sessions = record._sessions; - var preferredSession = device.sessions && device.sessions[getString(baseKey)]; + var preferredSession = record._sessions[getString(baseKey)]; if (preferredSession !== undefined) return preferredSession; - if (device.identityKey !== undefined) - return { indexInfo: { remoteIdentityKey: device.identityKey } }; + if (record.identityKey !== undefined) + return { indexInfo: { remoteIdentityKey: record.identityKey } }; throw new Error("Datastore inconsistency: device was stored without identity key"); } @@ -24115,6 +24124,7 @@ window.axolotl.protocol = function() { // return Promise(encoded [PreKey]WhisperMessage) self.encryptMessageFor = function(deviceObject, pushMessageContent) { var session = crypto_storage.getOpenSession(deviceObject.encodedNumber); + var hadSession = session !== undefined; var doEncryptPushMessageContent = function() { var msg = new axolotl.protobuf.WhisperMessage(); @@ -24149,17 +24159,9 @@ window.axolotl.protocol = function() { result.set(new Uint8Array(encodedMsg), 1); result.set(new Uint8Array(mac, 0, 8), encodedMsg.byteLength + 1); - try { - delete deviceObject['signedKey']; - delete deviceObject['signedKeyId']; - delete deviceObject['signedKeySignature']; - delete deviceObject['preKey']; - delete deviceObject['preKeyId']; - } catch(_) {} - removeOldChains(session); - crypto_storage.saveSessionAndDevice(deviceObject, session); + crypto_storage.saveSession(deviceObject.encodedNumber, session, !hadSession ? deviceObject.registrationId : undefined); return result; }); }); @@ -24333,3 +24335,57 @@ window.axolotl.protocol = function() { DeviceControl : deviceMessages.DeviceControl, }; })(); + +/* vim: ts=4:sw=4 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +;(function() { + +'use strict'; +window.axolotl = window.axolotl || {}; + +var RecipientRecord = function(identityKey, registrationId) { + this._sessions = {}; + this.identityKey = identityKey !== undefined ? getString(identityKey) : null; + this.registrationId = registrationId; + + if (this.registrationId === undefined || typeof this.registrationId !== "number") + this.registrationId = null; +}; + +RecipientRecord.prototype.serialize = function() { + return textsecure.utils.jsonThing({sessions: this._sessions, registrationId: this.registrationId, identityKey: this.identityKey}); +} + +RecipientRecord.prototype.deserialize = function(serialized) { + var data = JSON.parse(serialized); + this._sessions = data.sessions; + if (this._sessions === undefined || this._sessions === null || typeof this._sessions !== "object" || Array.isArray(this._sessions)) + throw new Error("Error deserializing RecipientRecord"); + this.identityKey = data.identityKey; + this.registrationId = data.registrationId; + if (this.identityKey === undefined || this.registrationId === undefined) + throw new Error("Error deserializing RecipientRecord"); +} + +RecipientRecord.prototype.haveOpenSession = function() { + return this.registrationId !== null; +} + +window.axolotl.sessions = { + RecipientRecord: RecipientRecord, +}; + +})();