Integrate libaxolotl async storage changes

* Session records are now opaque strings, so treat them that way:
  - no more cross checking identity key and session records
  - Move hasOpenSession to axolotl wrapper
  - Remote registration ids must be fetched async'ly via protocol wrapper
* Implement async AxolotlStore using textsecure.storage
* Add some db stores and move prekeys and signed keys to indexeddb
* Add storage tests
* Rename identityKey storage key from libaxolotl25519KeyidentityKey to
  simply identityKey, since it's no longer hardcoded in libaxolotl
* Rework registration and key-generation, keeping logic in libtextsecure
  and rendering in options.js.
* Remove key_worker since workers are handled at the libaxolotl level
  now
This commit is contained in:
lilia 2015-04-01 13:08:09 -07:00
parent 8304aa903a
commit 96eafc7750
20 changed files with 1014 additions and 40445 deletions

View file

@ -61,16 +61,6 @@ module.exports = function(grunt) {
],
dest: 'js/libtextsecure.js',
},
key_worker: {
options: {
banner: 'var window = this;\n',
},
src: [
'js/libtextsecure.js',
'libtextsecure/key_worker.js'
],
dest: 'js/key_worker.js'
},
libtextsecuretest: {
src: [
'components/mock-socket/dist/mock-socket.js',
@ -148,7 +138,7 @@ module.exports = function(grunt) {
},
jscs: {
all: {
src: ['js/**/*.js', '!js/libtextsecure.js', '!js/key_worker.js', '!js/components.js', 'test/**/*.js']
src: ['js/**/*.js', '!js/libtextsecure.js', '!js/components.js', 'test/**/*.js']
}
},
watch: {
@ -164,10 +154,6 @@ module.exports = function(grunt) {
files: ['./libtextsecure/*.js', './libtextsecure/storage/*.js'],
tasks: ['concat:libtextsecure']
},
key_worker: {
files: ['<%= concat.key_worker.src %>'],
tasks: ['concat:key_worker']
},
dist: {
files: ['<%= dist.src %>'],
tasks: ['copy']

View file

@ -230,6 +230,7 @@
</script>
<script type="text/javascript" src="js/components.js"></script>
<script type="text/javascript" src="js/database.js"></script>
<script type="text/javascript" src="js/axolotl_store.js"></script>
<script type="text/javascript" src="js/libtextsecure.js"></script>
<script type="text/javascript" src="js/notifications.js"></script>

183
js/axolotl_store.js Normal file
View file

@ -0,0 +1,183 @@
/* 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 <http://www.gnu.org/licenses/>.
*/
;(function() {
'use strict';
function isStringable(thing) {
return (thing === Object(thing) &&
(thing.__proto__ == StaticArrayBufferProto ||
thing.__proto__ == StaticUint8ArrayProto ||
thing.__proto__ == StaticByteBufferProto));
}
function convertToArrayBuffer(thing) {
if (thing === undefined)
return undefined;
if (thing === Object(thing)) {
if (thing.__proto__ == StaticArrayBufferProto)
return thing;
//TODO: Several more cases here...
}
if (thing instanceof Array) {
// Assuming Uint16Array from curve25519
//TODO: Move to convertToArrayBuffer
var res = new ArrayBuffer(thing.length * 2);
var uint = new Uint16Array(res);
for (var i = 0; i < thing.length; i++)
uint[i] = thing[i];
return res;
}
var str;
if (isStringable(thing))
str = stringObject(thing);
else if (typeof thing == "string")
str = thing;
else
throw new Error("Tried to convert a non-stringable thing of type " + typeof thing + " to an array buffer");
var res = new ArrayBuffer(str.length);
var uint = new Uint8Array(res);
for (var i = 0; i < str.length; i++)
uint[i] = str.charCodeAt(i);
return res;
}
var Model = Backbone.Model.extend({ database: Whisper.Database });
var PreKey = Model.extend({ storeName: 'preKeys' });
var SignedPreKey = Model.extend({ storeName: 'signedPreKeys' });
function AxolotlStore() {}
AxolotlStore.prototype = {
constructor: AxolotlStore,
get: function(key,defaultValue) {
return textsecure.storage.get(key, defaultValue);
},
put: function(key, value) {
textsecure.storage.put(key, value);
},
remove: function(key) {
textsecure.storage.remove(key);
},
getMyIdentityKey: function() {
var res = textsecure.storage.get('identityKey');
if (res === undefined)
return undefined;
return {
pubKey: convertToArrayBuffer(res.pubKey),
privKey: convertToArrayBuffer(res.privKey)
};
},
getMyRegistrationId: function() {
return textsecure.storage.get('registrationId');
},
getIdentityKey: function(identifier) {
if (identifier === null || identifier === undefined)
throw new Error("Tried to get identity key for undefined/null key");
return Promise.resolve(convertToArrayBuffer(textsecure.storage.devices.getIdentityKeyForNumber(textsecure.utils.unencodeNumber(identifier)[0])));
},
putIdentityKey: function(identifier, identityKey) {
if (identifier === null || identifier === undefined)
throw new Error("Tried to put identity key for undefined/null key");
return Promise.resolve(textsecure.storage.devices.checkSaveIdentityKeyForNumber(textsecure.utils.unencodeNumber(identifier)[0], identityKey));
},
/* Returns a prekeypair object or undefined */
getPreKey: function(keyId) {
var prekey = new PreKey({id: keyId});
return new Promise(function(resolve) {
prekey.fetch().then(function() {
resolve({
pubKey: prekey.attributes.publicKey,
privKey: prekey.attributes.privateKey
});
}).fail(resolve);
});
},
putPreKey: function(keyId, keyPair) {
var prekey = new PreKey({
id : keyId,
publicKey : keyPair.pubKey,
privateKey : keyPair.privKey
});
return new Promise(function(resolve) {
prekey.save().always(function() {
resolve();
});
});
},
removePreKey: function(keyId) {
var prekey = new PreKey({id: keyId});
return new Promise(function(resolve) {
prekey.destroy().then(function() {
resolve();
});
});
},
/* Returns a signed keypair object or undefined */
getSignedPreKey: function(keyId) {
var prekey = new SignedPreKey({id: keyId});
return new Promise(function(resolve) {
prekey.fetch().then(function() {
resolve({
pubKey: prekey.attributes.publicKey,
privKey: prekey.attributes.privateKey
});
}).fail(resolve);
});
},
putSignedPreKey: function(keyId, keyPair) {
var prekey = new SignedPreKey({
id : keyId,
publicKey : keyPair.pubKey,
privateKey : keyPair.privKey
});
return new Promise(function(resolve) {
prekey.save().always(function() {
resolve();
});
});
},
removeSignedPreKey: function(keyId) {
var prekey = new SignedPreKey({id: keyId});
return new Promise(function(resolve) {
prekey.destroy().then(function() {
resolve();
});
});
},
getSession: function(identifier) {
if (identifier === null || identifier === undefined)
throw new Error("Tried to get session for undefined/null key");
return new Promise(function(resolve) {
resolve(textsecure.storage.sessions.getSessionsForNumber(identifier));
});
},
putSession: function(identifier, record) {
if (identifier === null || identifier === undefined)
throw new Error("Tried to put session for undefined/null key");
return new Promise(function(resolve) {
resolve(textsecure.storage.sessions.putSessionsForDevice(identifier, record));
});
}
};
window.AxolotlStore = AxolotlStore;
})();

View file

@ -33,6 +33,10 @@
conversations.createIndex("inbox", "active_at", { unique: false });
conversations.createIndex("group", "members", { unique: false, multiEntry: true });
conversations.createIndex("type", "type", { unique: false });
var preKeys = transaction.db.createObjectStore("preKeys");
var signedPreKeys = transaction.db.createObjectStore("signedPreKeys");
next();
}
}

File diff suppressed because one or more lines are too long

View file

@ -37771,47 +37771,12 @@ axolotlInternal.RecipientRecord = function() {
}();
})();
'use strict';
;(function() {
var axolotlInstance = axolotl.protocol({
getMyRegistrationId: function() {
return textsecure.storage.get("registrationId");
},
put: function(key, value) {
return textsecure.storage.put("libaxolotl" + key, value);
},
get: function(key, defaultValue) {
return textsecure.storage.get("libaxolotl" + key, defaultValue);
},
remove: function(key) {
return textsecure.storage.remove("libaxolotl" + 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.sessions.getSessionsForNumber(identifier);
},
put: function(identifier, record) {
return textsecure.storage.sessions.putSessionsForDevice(identifier, record);
}
}
},
function(keys) {
return textsecure.api.registerKeys(keys).catch(function(e) {
//TODO: Notify the user somehow?
console.error(e);
});
});
'use strict';
window.textsecure = window.textsecure || {};
window.textsecure.storage = window.textsecure.storage || {};
textsecure.storage.axolotl = new AxolotlStore();
var axolotlInstance = axolotl.protocol(textsecure.storage.axolotl);
var decodeMessageContents = function(res) {
var finalMessage = textsecure.protobuf.PushMessageContent.decode(res[0]);
@ -37826,7 +37791,7 @@ axolotlInternal.RecipientRecord = function() {
var handlePreKeyWhisperMessage = function(from, message) {
try {
return textsecure.protocol_wrapper.handlePreKeyWhisperMessage(from, message);
return axolotlInstance.handlePreKeyWhisperMessage(from, message);
} catch(e) {
if (e.message === 'Unknown identity key') {
// create an error that the UI will pick up and ask the
@ -37845,7 +37810,7 @@ axolotlInternal.RecipientRecord = function() {
return Promise.resolve(textsecure.protobuf.PushMessageContent.decode(proto.message));
case textsecure.protobuf.IncomingPushMessageSignal.Type.CIPHERTEXT:
var from = proto.source + "." + (proto.sourceDevice == null ? 0 : proto.sourceDevice);
return textsecure.protocol_wrapper.decryptWhisperMessage(from, getString(proto.message)).then(decodeMessageContents);
return axolotlInstance.decryptWhisperMessage(from, getString(proto.message)).then(decodeMessageContents);
case textsecure.protobuf.IncomingPushMessageSignal.Type.PREKEY_BUNDLE:
if (proto.message.readUint8() != ((3 << 4) | 3))
throw new Error("Bad version byte");
@ -37860,27 +37825,34 @@ axolotlInternal.RecipientRecord = function() {
closeOpenSessionForDevice: function(encodedNumber) {
return axolotlInstance.closeOpenSessionForDevice(encodedNumber)
},
decryptWhisperMessage: function(encodedNumber, messageBytes, session) {
return axolotlInstance.decryptWhisperMessage(encodedNumber, messageBytes, session);
},
handlePreKeyWhisperMessage: function(from, encodedMessage) {
return axolotlInstance.handlePreKeyWhisperMessage(from, encodedMessage);
},
encryptMessageFor: function(deviceObject, pushMessageContent) {
return axolotlInstance.encryptMessageFor(deviceObject, pushMessageContent);
},
generateKeys: function() {
return axolotlInstance.generateKeys();
generateKeys: function(count, progressCallback) {
if (textsecure.worker_path) {
axolotlInstance.startWorker(textsecure.worker_path);
}
return generateKeys(count, progressCallback).then(function(result) {
axolotlInstance.stopWorker();
return result;
});
},
createIdentityKeyRecvSocket: function() {
return axolotlInstance.createIdentityKeyRecvSocket();
},
hasOpenSession: function(encodedNumber) {
return axolotlInstance.hasOpenSession(encodedNumber);
},
getRegistrationId: function(encodedNumber) {
return axolotlInstance.getRegistrationId(encodedNumber);
}
};
var tryMessageAgain = function(from, encodedMessage) {
return textsecure.protocol_wrapper.handlePreKeyWhisperMessage(from, encodedMessage).then(decodeMessageContents);
return axolotlInstance.handlePreKeyWhisperMessage(from, encodedMessage).then(decodeMessageContents);
}
textsecure.replay.registerFunction(tryMessageAgain, textsecure.replay.Type.INIT_SESSION);
})();
/* vim: ts=4:sw=4:expandtab
@ -38136,46 +38108,30 @@ axolotlInternal.RecipientRecord = function() {
if (sessions[deviceId] === undefined)
return undefined;
var record = new axolotl.sessions.RecipientRecord();
record.deserialize(sessions[deviceId]);
if (getString(textsecure.storage.devices.getIdentityKeyForNumber(number)) !== getString(record.identityKey))
throw new Error("Got mismatched identity key on device object load");
return record;
return sessions[deviceId];
},
putSessionsForDevice: function(encodedNumber, record) {
var number = textsecure.utils.unencodeNumber(encodedNumber)[0];
var deviceId = textsecure.utils.unencodeNumber(encodedNumber)[1];
textsecure.storage.devices.checkSaveIdentityKeyForNumber(number, record.identityKey);
var sessions = textsecure.storage.get("sessions" + number);
if (sessions === undefined)
sessions = {};
sessions[deviceId] = record.serialize();
sessions[deviceId] = record;
textsecure.storage.put("sessions" + number, sessions);
var device = textsecure.storage.devices.getDeviceObject(encodedNumber);
if (device === undefined) {
var identityKey = textsecure.storage.devices.getIdentityKeyForNumber(number);
device = { encodedNumber: encodedNumber,
//TODO: Remove this duplication
identityKey: record.identityKey
identityKey: identityKey
};
}
if (getString(device.identityKey) !== getString(record.identityKey)) {
console.error("Got device object with key inconsistent after checkSaveIdentityKeyForNumber returned!");
throw new Error("Tried to put session for device with changed identity key");
}
return textsecure.storage.devices.saveDeviceObject(device);
},
haveOpenSessionForDevice: function(encodedNumber) {
var sessions = textsecure.storage.sessions.getSessionsForNumber(encodedNumber);
if (sessions === undefined || !sessions.haveOpenSession())
return false;
return true;
},
// Use textsecure.storage.devices.removeIdentityKeyForNumber (which calls this) instead
_removeIdentityKeyForNumber: function(number) {
textsecure.storage.remove("sessions" + number);
@ -38820,6 +38776,8 @@ window.textsecure.utils = function() {
for (var key in thing)
res[key] = ensureStringed(thing[key]);
return res;
} else if (thing === null) {
return null;
}
throw new Error("unsure of how to jsonify object of type " + typeof thing);
@ -38953,9 +38911,11 @@ textsecure.processDecrypted = function(decrypted, source) {
return Promise.all(promises).then(function() {
return decrypted;
});
}
};
function createAccount(number, verificationCode, identityKeyPair, single_device) {
textsecure.storage.put('identityKey', identityKeyPair);
window.textsecure.registerSingleDevice = function(number, verificationCode, stepDone) {
var signalingKey = textsecure.crypto.getRandomBytes(32 + 20);
textsecure.storage.put('signaling_key', signalingKey);
@ -38963,39 +38923,95 @@ window.textsecure.registerSingleDevice = function(number, verificationCode, step
password = password.substring(0, password.length - 2);
textsecure.storage.put("password", password);
var registrationId = new Uint16Array(textsecure.crypto.getRandomBytes(2))[0];
registrationId = registrationId & 0x3fff;
var registrationId = axolotl.util.generateRegistrationId();
textsecure.storage.put("registrationId", registrationId);
return textsecure.api.confirmCode(number, verificationCode, password, signalingKey, registrationId, true).then(function() {
textsecure.storage.user.setNumberAndDeviceId(number, 1);
return textsecure.api.confirmCode(
number, verificationCode, password, signalingKey, registrationId, single_device
).then(function(response) {
textsecure.storage.user.setNumberAndDeviceId(number, response.deviceId || 1);
textsecure.storage.put("regionCode", libphonenumber.util.getRegionCodeForNumber(number));
stepDone(1);
return textsecure.protocol_wrapper.generateKeys().then(function(keys) {
stepDone(2);
return textsecure.api.registerKeys(keys).then(function() {
stepDone(3);
});
});
return textsecure.protocol_wrapper.generateKeys().then(textsecure.registration.done);
});
}
window.textsecure.registerSecondDevice = function(provisionMessage) {
var signalingKey = textsecure.crypto.getRandomBytes(32 + 20);
textsecure.storage.put('signaling_key', signalingKey);
function generateKeys(count, progressCallback) {
if (count === undefined) {
throw TypeError('generateKeys: count is undefined');
}
if (typeof progressCallback !== 'function') {
progressCallback = undefined;
}
var store = textsecure.storage.axolotl;
var identityKey = store.getMyIdentityKey();
var result = { preKeys: [], identityKey: identityKey.pubKey };
var promises = [];
var password = btoa(getString(textsecure.crypto.getRandomBytes(16)));
password = password.substring(0, password.length - 2);
textsecure.storage.put("password", password);
var startId = textsecure.storage.get('maxPreKeyId', 1);
var signedKeyId = textsecure.storage.get('signedKeyId', 1);
var registrationId = new Uint16Array(textsecure.crypto.getRandomBytes(2))[0];
registrationId = registrationId & 0x3fff;
textsecure.storage.put("registrationId", registrationId);
for (var keyId = startId; keyId < startId+count; ++keyId) {
promises.push(
axolotl.util.generatePreKey(keyId).then(function(res) {
store.putPreKey(res.keyId, res.keyPair);
result.preKeys.push({
keyId : res.keyId,
publicKey : res.keyPair.pubKey
});
if (progressCallback) { progressCallback(); }
})
);
}
promises.push(
axolotl.util.generateSignedPreKey(identityKey, signedKeyId).then(function(res) {
store.putSignedPreKey(res.keyId, res.keyPair);
result.signedPreKey = {
keyId : res.keyId,
publicKey : res.keyPair.pubKey,
signature : res.signature
};
})
);
return textsecure.api.confirmCode(provisionMessage.number, provisionMessage.provisioningCode, password, signalingKey, registrationId, false).then(function(result) {
textsecure.storage.user.setNumberAndDeviceId(provisionMessage.number, result.deviceId);
textsecure.storage.put("regionCode", libphonenumber.util.getRegionCodeForNumber(provisionMessage.number));
store.removeSignedPreKey(signedKeyId - 2);
textsecure.storage.put('maxPreKeyId', startId + count);
textsecure.storage.put('signedKeyId', signedKeyId + 1);
return Promise.all(promises).then(function() {
return result;
});
};
window.textsecure.registerSecondDevice = function(setProvisioningUrl, confirmNumber, progressCallback) {
return textsecure.protocol_wrapper.createIdentityKeyRecvSocket().then(function(cryptoInfo) {
return new Promise(function(resolve) {
new WebSocketResource(textsecure.api.getTempWebsocket(), function(request) {
if (request.path == "/v1/address" && request.verb == "PUT") {
var proto = textsecure.protobuf.ProvisioningUuid.decode(request.body);
setProvisioningUrl([
'tsdevice:/?uuid=', proto.uuid, '&pub_key=',
encodeURIComponent(btoa(getString(cryptoInfo.pubKey)))
].join(''));
request.respond(200, 'OK');
} else if (request.path == "/v1/message" && request.verb == "PUT") {
var envelope = textsecure.protobuf.ProvisionEnvelope.decode(request.body, 'binary');
request.respond(200, 'OK');
resolve(cryptoInfo.decryptAndHandleDeviceInit(envelope).then(function(provisionMessage) {
return confirmNumber(provisionMessage.number).then(function() {
return createAccount(
provisionMessage.number,
provisionMessage.provisioningCode,
provisionMessage.identityKeyPair,
false
);
});
}));
} else {
console.log('Unknown websocket message', request.path);
}
});
});
});
};
@ -39584,22 +39600,21 @@ window.textsecure.messaging = function() {
return new Promise(function() { throw new Error("Mismatched relays for number " + number); });
}
var registrationId = deviceObjectList[i].registrationId;
if (registrationId === undefined) // ie this isnt a first-send-keyful deviceObject
registrationId = textsecure.storage.sessions.getSessionsForNumber(deviceObjectList[i].encodedNumber).registrationId;
return textsecure.protocol_wrapper.encryptMessageFor(deviceObjectList[i], message).then(function(encryptedMsg) {
textsecure.storage.devices.removeTempKeysFromDevice(deviceObjectList[i].encodedNumber);
return textsecure.protocol_wrapper.getRegistrationId(deviceObjectList[i].encodedNumber).then(function(registrationId) {
textsecure.storage.devices.removeTempKeysFromDevice(deviceObjectList[i].encodedNumber);
jsonData[i] = {
type: encryptedMsg.type,
destinationDeviceId: textsecure.utils.unencodeNumber(deviceObjectList[i].encodedNumber)[1],
destinationRegistrationId: registrationId,
body: encryptedMsg.body,
timestamp: timestamp
};
jsonData[i] = {
type: encryptedMsg.type,
destinationDeviceId: textsecure.utils.unencodeNumber(deviceObjectList[i].encodedNumber)[1],
destinationRegistrationId: registrationId,
body: encryptedMsg.body,
timestamp: timestamp
};
if (deviceObjectList[i].relay !== undefined)
jsonData[i].relay = deviceObjectList[i].relay;
if (deviceObjectList[i].relay !== undefined)
jsonData[i].relay = deviceObjectList[i].relay;
});
});
}
@ -39615,37 +39630,37 @@ window.textsecure.messaging = function() {
groupId = getString(groupId);
var doUpdate = false;
for (var i in devicesForNumber) {
var registrationId = deviceObjectList[i].registrationId;
if (registrationId === undefined) // ie this isnt a first-send-keyful deviceObject
registrationId = textsecure.storage.sessions.getSessionsForNumber(deviceObjectList[i].encodedNumber).registrationId;
if (textsecure.storage.groups.needUpdateByDeviceRegistrationId(groupId, number, devicesForNumber[i].encodedNumber, registrationId))
doUpdate = true;
}
if (!doUpdate)
return Promise.resolve(true);
var group = textsecure.storage.groups.getGroup(groupId);
var numberIndex = group.numbers.indexOf(number);
if (numberIndex < 0) // This is potentially a multi-message rare racing-AJAX race
return Promise.reject("Tried to refresh group to non-member");
var proto = new textsecure.protobuf.PushMessageContent();
proto.group = new textsecure.protobuf.PushMessageContent.GroupContext();
proto.group.id = toArrayBuffer(group.id);
proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE;
proto.group.members = group.numbers;
proto.group.name = group.name === undefined ? null : group.name;
if (group.avatar !== undefined) {
return makeAttachmentPointer(group.avatar).then(function(attachment) {
proto.group.avatar = attachment;
return sendMessageToDevices(Date.now(), number, devicesForNumber, proto);
Promise.all(devicesForNumber.map(function(device) {
return textsecure.protocol_wrapper.getRegistrationId(device.encodedNumber).then(function(registrationId) {
if (textsecure.storage.groups.needUpdateByDeviceRegistrationId(groupId, number, devicesForNumber[i].encodedNumber, registrationId))
doUpdate = true;
});
} else {
return sendMessageToDevices(Date.now(), number, devicesForNumber, proto);
}
})).then(function() {
if (!doUpdate)
return Promise.resolve(true);
var group = textsecure.storage.groups.getGroup(groupId);
var numberIndex = group.numbers.indexOf(number);
if (numberIndex < 0) // This is potentially a multi-message rare racing-AJAX race
return Promise.reject("Tried to refresh group to non-member");
var proto = new textsecure.protobuf.PushMessageContent();
proto.group = new textsecure.protobuf.PushMessageContent.GroupContext();
proto.group.id = toArrayBuffer(group.id);
proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE;
proto.group.members = group.numbers;
proto.group.name = group.name === undefined ? null : group.name;
if (group.avatar !== undefined) {
return makeAttachmentPointer(group.avatar).then(function(attachment) {
proto.group.avatar = attachment;
return sendMessageToDevices(Date.now(), number, devicesForNumber, proto);
});
} else {
return sendMessageToDevices(Date.now(), number, devicesForNumber, proto);
}
});
}
var tryMessageAgain = function(number, encodedMessage, timestamp) {
@ -39730,7 +39745,7 @@ window.textsecure.messaging = function() {
var promises = [];
for (var j in devicesForNumber)
if (!textsecure.storage.sessions.haveOpenSessionForDevice(devicesForNumber[j].encodedNumber))
if (!textsecure.protocol_wrapper.hasOpenSession(devicesForNumber[j].encodedNumber))
promises[promises.length] = getKeysForNumber(number, [parseInt(textsecure.utils.unencodeNumber(devicesForNumber[j].encodedNumber)[1])]);
Promise.all(promises).then(function() {

View file

@ -35,78 +35,54 @@
}
}
function setProvisioningUrl(url) {
$('#status').text('');
new QRCode($('#qr')[0]).makeCode(url);
}
function confirmNumber(number) {
return new Promise(function(resolve, reject) {
$('#qr').hide();
$('.confirmation-dialog .number').text(number);
$('.confirmation-dialog .cancel').click(function(e) {
localStorage.clear();
reject();
});
$('.confirmation-dialog .ok').click(function(e) {
e.stopPropagation();
$('.confirmation-dialog').hide();
$('.progress-dialog').show();
$('.progress-dialog .status').text('Registering new device...');
resolve();
});
$('.modal-container').show();
});
}
var counter = 0;
function incrementCounter() {
$('.progress-dialog .bar').css('width', (++counter * 100 / 100) + '%');
}
$('.modal-container .cancel').click(function() {
$('.modal-container').hide();
});
$(function() {
if (textsecure.registration.isDone()) {
$('#complete-number').text(textsecure.storage.user.getNumber());
var bg = extension.windows.getBackground();
if (bg.textsecure.registration.isDone()) {
$('#complete-number').text(bg.textsecure.storage.user.getNumber());
$('#setup-complete').show().addClass('in');
initOptions();
} else {
$('#init-setup').show().addClass('in');
$('#status').text("Connecting...");
textsecure.protocol_wrapper.createIdentityKeyRecvSocket().then(function(cryptoInfo) {
var qrCode = new QRCode(document.getElementById('qr'));
var socket = textsecure.api.getTempWebsocket();
new WebSocketResource(socket, function(request) {
if (request.path == "/v1/address" && request.verb == "PUT") {
var proto = textsecure.protobuf.ProvisioningUuid.decode(request.body);
var url = [ 'tsdevice:/', '?uuid=', proto.uuid, '&pub_key=',
encodeURIComponent(btoa(String.fromCharCode.apply(null, new Uint8Array(cryptoInfo.pubKey)))) ].join('');
$('#status').text('');
qrCode.makeCode(url);
request.respond(200, 'OK');
} else if (request.path == "/v1/message" && request.verb == "PUT") {
var envelope = textsecure.protobuf.ProvisionEnvelope.decode(request.body, 'binary');
cryptoInfo.decryptAndHandleDeviceInit(envelope).then(function(provisionMessage) {
$('.confirmation-dialog .number').text(provisionMessage.number);
$('.confirmation-dialog .cancel').click(function(e) {
localStorage.clear();
});
$('.confirmation-dialog .ok').click(function(e) {
e.stopPropagation();
$('.confirmation-dialog').hide();
$('.progress-dialog').show();
$('.progress-dialog .status').text('Registering new device...');
window.textsecure.registerSecondDevice(provisionMessage).then(function() {
$('.progress-dialog .status').text('Generating keys...');
var counter = 0;
var myWorker = new Worker('/js/key_worker.js');
myWorker.postMessage({
maxPreKeyId: textsecure.storage.get("maxPreKeyId", 0),
signedKeyId: textsecure.storage.get("signedKeyId", 0),
libaxolotl25519KeyidentityKey: textsecure.storage.get("libaxolotl25519KeyidentityKey"),
});
myWorker.onmessage = function(e) {
switch(e.data.method) {
case 'set':
textsecure.storage.put(e.data.key, e.data.value);
counter = counter + 1;
$('.progress-dialog .bar').css('width', (counter * 100 / 105) + '%');
break;
case 'remove':
textsecure.storage.remove(e.data.key);
break;
case 'done':
$('.progress-dialog .status').text('Uploading keys...');
textsecure.api.registerKeys(e.data.keys).then(function() {
textsecure.registration.done();
$('.modal-container').hide();
$('#init-setup').hide();
$('#setup-complete').show().addClass('in');
initOptions();
});
}
};
});
});
$('.modal-container').show();
});
} else
console.log(request.path);
});
bg.textsecure.registerSecondDevice(setProvisioningUrl, confirmNumber, incrementCounter).then(function() {
$('.modal-container').hide();
$('#init-setup').hide();
$('#setup-complete').show().addClass('in');
initOptions();
});
}
});

View file

@ -16,6 +16,7 @@
;(function() {
'use strict';
var bg = extension.windows.getBackground();
function log(s) {
console.log(s);
@ -36,7 +37,7 @@
var phoneView = new Whisper.PhoneInputView({el: $('#phone-number-input')});
phoneView.$el.find('input.number').intlTelInput();
var number = textsecure.storage.user.getNumber();
var number = bg.textsecure.storage.user.getNumber();
if (number) {
$('input.number').val(number);
}
@ -60,7 +61,7 @@
$('#error').hide();
var number = phoneView.validateNumber();
if (number) {
textsecure.api.requestVerificationVoice(number).catch(displayError);
bg.textsecure.api.requestVerificationVoice(number).catch(displayError);
$('#step2').addClass('in').fadeIn();
} else {
$('#number-container').addClass('invalid');
@ -71,7 +72,7 @@
$('#error').hide();
var number = phoneView.validateNumber();
if (number) {
textsecure.api.requestVerificationSMS(number).catch(displayError);
bg.textsecure.api.requestVerificationSMS(number).catch(displayError);
$('#step2').addClass('in').fadeIn();
} else {
$('#number-container').addClass('invalid');
@ -80,40 +81,14 @@
$('#form').submit(function(e) {
e.preventDefault();
log('registering');
var number = phoneView.validateNumber();
var verificationCode = $('#code').val().replace(/\D+/g, "");
var signalingKey = textsecure.crypto.getRandomBytes(32 + 20);
var password = btoa(String.fromCharCode.apply(null, new Uint8Array(textsecure.crypto.getRandomBytes(16))));
password = password.substring(0, password.length - 2);
var registrationId = new Uint16Array(textsecure.crypto.getRandomBytes(2))[0];
registrationId = registrationId & 0x3fff;
log('clearing data');
localStorage.clear();
localStorage.setItem('first_install_ran', 1);
textsecure.storage.put('registrationId', registrationId);
textsecure.storage.put('signaling_key', signalingKey);
textsecure.storage.put('password', password);
textsecure.storage.user.setNumberAndDeviceId(number, 1);
textsecure.storage.put('regionCode', libphonenumber.util.getRegionCodeForNumber(number));
log('verifying code');
return textsecure.api.confirmCode(
number, verificationCode, password, signalingKey, registrationId, true
).then(function() {
log('generating keys');
return textsecure.protocol_wrapper.generateKeys().then(function(keys) {
log('uploading keys');
return textsecure.api.registerKeys(keys).then(function() {
textsecure.registration.done();
log('done');
chrome.runtime.reload();
});
});
bg.textsecure.registerSingleDevice(number, verificationCode).then(function() {
extension.navigator.tabs.create("options.html");
window.close();
}).catch(function(e) {
log(e);
});

View file

@ -1,44 +1,9 @@
'use strict';
;(function() {
var axolotlInstance = axolotl.protocol({
getMyRegistrationId: function() {
return textsecure.storage.get("registrationId");
},
put: function(key, value) {
return textsecure.storage.put("libaxolotl" + key, value);
},
get: function(key, defaultValue) {
return textsecure.storage.get("libaxolotl" + key, defaultValue);
},
remove: function(key) {
return textsecure.storage.remove("libaxolotl" + 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.sessions.getSessionsForNumber(identifier);
},
put: function(identifier, record) {
return textsecure.storage.sessions.putSessionsForDevice(identifier, record);
}
}
},
function(keys) {
return textsecure.api.registerKeys(keys).catch(function(e) {
//TODO: Notify the user somehow?
console.error(e);
});
});
'use strict';
window.textsecure = window.textsecure || {};
window.textsecure.storage = window.textsecure.storage || {};
textsecure.storage.axolotl = new AxolotlStore();
var axolotlInstance = axolotl.protocol(textsecure.storage.axolotl);
var decodeMessageContents = function(res) {
var finalMessage = textsecure.protobuf.PushMessageContent.decode(res[0]);
@ -53,7 +18,7 @@
var handlePreKeyWhisperMessage = function(from, message) {
try {
return textsecure.protocol_wrapper.handlePreKeyWhisperMessage(from, message);
return axolotlInstance.handlePreKeyWhisperMessage(from, message);
} catch(e) {
if (e.message === 'Unknown identity key') {
// create an error that the UI will pick up and ask the
@ -72,7 +37,7 @@
return Promise.resolve(textsecure.protobuf.PushMessageContent.decode(proto.message));
case textsecure.protobuf.IncomingPushMessageSignal.Type.CIPHERTEXT:
var from = proto.source + "." + (proto.sourceDevice == null ? 0 : proto.sourceDevice);
return textsecure.protocol_wrapper.decryptWhisperMessage(from, getString(proto.message)).then(decodeMessageContents);
return axolotlInstance.decryptWhisperMessage(from, getString(proto.message)).then(decodeMessageContents);
case textsecure.protobuf.IncomingPushMessageSignal.Type.PREKEY_BUNDLE:
if (proto.message.readUint8() != ((3 << 4) | 3))
throw new Error("Bad version byte");
@ -87,25 +52,32 @@
closeOpenSessionForDevice: function(encodedNumber) {
return axolotlInstance.closeOpenSessionForDevice(encodedNumber)
},
decryptWhisperMessage: function(encodedNumber, messageBytes, session) {
return axolotlInstance.decryptWhisperMessage(encodedNumber, messageBytes, session);
},
handlePreKeyWhisperMessage: function(from, encodedMessage) {
return axolotlInstance.handlePreKeyWhisperMessage(from, encodedMessage);
},
encryptMessageFor: function(deviceObject, pushMessageContent) {
return axolotlInstance.encryptMessageFor(deviceObject, pushMessageContent);
},
generateKeys: function() {
return axolotlInstance.generateKeys();
generateKeys: function(count, progressCallback) {
if (textsecure.worker_path) {
axolotlInstance.startWorker(textsecure.worker_path);
}
return generateKeys(count, progressCallback).then(function(result) {
axolotlInstance.stopWorker();
return result;
});
},
createIdentityKeyRecvSocket: function() {
return axolotlInstance.createIdentityKeyRecvSocket();
},
hasOpenSession: function(encodedNumber) {
return axolotlInstance.hasOpenSession(encodedNumber);
},
getRegistrationId: function(encodedNumber) {
return axolotlInstance.getRegistrationId(encodedNumber);
}
};
var tryMessageAgain = function(from, encodedMessage) {
return textsecure.protocol_wrapper.handlePreKeyWhisperMessage(from, encodedMessage).then(decodeMessageContents);
return axolotlInstance.handlePreKeyWhisperMessage(from, encodedMessage).then(decodeMessageContents);
}
textsecure.replay.registerFunction(tryMessageAgain, textsecure.replay.Type.INIT_SESSION);
})();

View file

@ -111,6 +111,8 @@ window.textsecure.utils = function() {
for (var key in thing)
res[key] = ensureStringed(thing[key]);
return res;
} else if (thing === null) {
return null;
}
throw new Error("unsure of how to jsonify object of type " + typeof thing);
@ -244,9 +246,11 @@ textsecure.processDecrypted = function(decrypted, source) {
return Promise.all(promises).then(function() {
return decrypted;
});
}
};
function createAccount(number, verificationCode, identityKeyPair, single_device) {
textsecure.storage.put('identityKey', identityKeyPair);
window.textsecure.registerSingleDevice = function(number, verificationCode, stepDone) {
var signalingKey = textsecure.crypto.getRandomBytes(32 + 20);
textsecure.storage.put('signaling_key', signalingKey);
@ -254,38 +258,94 @@ window.textsecure.registerSingleDevice = function(number, verificationCode, step
password = password.substring(0, password.length - 2);
textsecure.storage.put("password", password);
var registrationId = new Uint16Array(textsecure.crypto.getRandomBytes(2))[0];
registrationId = registrationId & 0x3fff;
var registrationId = axolotl.util.generateRegistrationId();
textsecure.storage.put("registrationId", registrationId);
return textsecure.api.confirmCode(number, verificationCode, password, signalingKey, registrationId, true).then(function() {
textsecure.storage.user.setNumberAndDeviceId(number, 1);
return textsecure.api.confirmCode(
number, verificationCode, password, signalingKey, registrationId, single_device
).then(function(response) {
textsecure.storage.user.setNumberAndDeviceId(number, response.deviceId || 1);
textsecure.storage.put("regionCode", libphonenumber.util.getRegionCodeForNumber(number));
stepDone(1);
return textsecure.protocol_wrapper.generateKeys().then(function(keys) {
stepDone(2);
return textsecure.api.registerKeys(keys).then(function() {
stepDone(3);
return textsecure.protocol_wrapper.generateKeys().then(textsecure.registration.done);
});
}
function generateKeys(count, progressCallback) {
if (count === undefined) {
throw TypeError('generateKeys: count is undefined');
}
if (typeof progressCallback !== 'function') {
progressCallback = undefined;
}
var store = textsecure.storage.axolotl;
var identityKey = store.getMyIdentityKey();
var result = { preKeys: [], identityKey: identityKey.pubKey };
var promises = [];
var startId = textsecure.storage.get('maxPreKeyId', 1);
var signedKeyId = textsecure.storage.get('signedKeyId', 1);
for (var keyId = startId; keyId < startId+count; ++keyId) {
promises.push(
axolotl.util.generatePreKey(keyId).then(function(res) {
store.putPreKey(res.keyId, res.keyPair);
result.preKeys.push({
keyId : res.keyId,
publicKey : res.keyPair.pubKey
});
if (progressCallback) { progressCallback(); }
})
);
}
promises.push(
axolotl.util.generateSignedPreKey(identityKey, signedKeyId).then(function(res) {
store.putSignedPreKey(res.keyId, res.keyPair);
result.signedPreKey = {
keyId : res.keyId,
publicKey : res.keyPair.pubKey,
signature : res.signature
};
})
);
store.removeSignedPreKey(signedKeyId - 2);
textsecure.storage.put('maxPreKeyId', startId + count);
textsecure.storage.put('signedKeyId', signedKeyId + 1);
return Promise.all(promises).then(function() {
return result;
});
};
window.textsecure.registerSecondDevice = function(setProvisioningUrl, confirmNumber, progressCallback) {
return textsecure.protocol_wrapper.createIdentityKeyRecvSocket().then(function(cryptoInfo) {
return new Promise(function(resolve) {
new WebSocketResource(textsecure.api.getTempWebsocket(), function(request) {
if (request.path == "/v1/address" && request.verb == "PUT") {
var proto = textsecure.protobuf.ProvisioningUuid.decode(request.body);
setProvisioningUrl([
'tsdevice:/?uuid=', proto.uuid, '&pub_key=',
encodeURIComponent(btoa(getString(cryptoInfo.pubKey)))
].join(''));
request.respond(200, 'OK');
} else if (request.path == "/v1/message" && request.verb == "PUT") {
var envelope = textsecure.protobuf.ProvisionEnvelope.decode(request.body, 'binary');
request.respond(200, 'OK');
resolve(cryptoInfo.decryptAndHandleDeviceInit(envelope).then(function(provisionMessage) {
return confirmNumber(provisionMessage.number).then(function() {
return createAccount(
provisionMessage.number,
provisionMessage.provisioningCode,
provisionMessage.identityKeyPair,
false
);
});
}));
} else {
console.log('Unknown websocket message', request.path);
}
});
});
});
}
window.textsecure.registerSecondDevice = function(provisionMessage) {
var signalingKey = textsecure.crypto.getRandomBytes(32 + 20);
textsecure.storage.put('signaling_key', signalingKey);
var password = btoa(getString(textsecure.crypto.getRandomBytes(16)));
password = password.substring(0, password.length - 2);
textsecure.storage.put("password", password);
var registrationId = new Uint16Array(textsecure.crypto.getRandomBytes(2))[0];
registrationId = registrationId & 0x3fff;
textsecure.storage.put("registrationId", registrationId);
return textsecure.api.confirmCode(provisionMessage.number, provisionMessage.provisioningCode, password, signalingKey, registrationId, false).then(function(result) {
textsecure.storage.user.setNumberAndDeviceId(provisionMessage.number, result.deviceId);
textsecure.storage.put("regionCode", libphonenumber.util.getRegionCodeForNumber(provisionMessage.number));
});
};

View file

@ -67,22 +67,21 @@ window.textsecure.messaging = function() {
return new Promise(function() { throw new Error("Mismatched relays for number " + number); });
}
var registrationId = deviceObjectList[i].registrationId;
if (registrationId === undefined) // ie this isnt a first-send-keyful deviceObject
registrationId = textsecure.storage.sessions.getSessionsForNumber(deviceObjectList[i].encodedNumber).registrationId;
return textsecure.protocol_wrapper.encryptMessageFor(deviceObjectList[i], message).then(function(encryptedMsg) {
textsecure.storage.devices.removeTempKeysFromDevice(deviceObjectList[i].encodedNumber);
return textsecure.protocol_wrapper.getRegistrationId(deviceObjectList[i].encodedNumber).then(function(registrationId) {
textsecure.storage.devices.removeTempKeysFromDevice(deviceObjectList[i].encodedNumber);
jsonData[i] = {
type: encryptedMsg.type,
destinationDeviceId: textsecure.utils.unencodeNumber(deviceObjectList[i].encodedNumber)[1],
destinationRegistrationId: registrationId,
body: encryptedMsg.body,
timestamp: timestamp
};
jsonData[i] = {
type: encryptedMsg.type,
destinationDeviceId: textsecure.utils.unencodeNumber(deviceObjectList[i].encodedNumber)[1],
destinationRegistrationId: registrationId,
body: encryptedMsg.body,
timestamp: timestamp
};
if (deviceObjectList[i].relay !== undefined)
jsonData[i].relay = deviceObjectList[i].relay;
if (deviceObjectList[i].relay !== undefined)
jsonData[i].relay = deviceObjectList[i].relay;
});
});
}
@ -98,37 +97,37 @@ window.textsecure.messaging = function() {
groupId = getString(groupId);
var doUpdate = false;
for (var i in devicesForNumber) {
var registrationId = deviceObjectList[i].registrationId;
if (registrationId === undefined) // ie this isnt a first-send-keyful deviceObject
registrationId = textsecure.storage.sessions.getSessionsForNumber(deviceObjectList[i].encodedNumber).registrationId;
if (textsecure.storage.groups.needUpdateByDeviceRegistrationId(groupId, number, devicesForNumber[i].encodedNumber, registrationId))
doUpdate = true;
}
if (!doUpdate)
return Promise.resolve(true);
var group = textsecure.storage.groups.getGroup(groupId);
var numberIndex = group.numbers.indexOf(number);
if (numberIndex < 0) // This is potentially a multi-message rare racing-AJAX race
return Promise.reject("Tried to refresh group to non-member");
var proto = new textsecure.protobuf.PushMessageContent();
proto.group = new textsecure.protobuf.PushMessageContent.GroupContext();
proto.group.id = toArrayBuffer(group.id);
proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE;
proto.group.members = group.numbers;
proto.group.name = group.name === undefined ? null : group.name;
if (group.avatar !== undefined) {
return makeAttachmentPointer(group.avatar).then(function(attachment) {
proto.group.avatar = attachment;
return sendMessageToDevices(Date.now(), number, devicesForNumber, proto);
Promise.all(devicesForNumber.map(function(device) {
return textsecure.protocol_wrapper.getRegistrationId(device.encodedNumber).then(function(registrationId) {
if (textsecure.storage.groups.needUpdateByDeviceRegistrationId(groupId, number, devicesForNumber[i].encodedNumber, registrationId))
doUpdate = true;
});
} else {
return sendMessageToDevices(Date.now(), number, devicesForNumber, proto);
}
})).then(function() {
if (!doUpdate)
return Promise.resolve(true);
var group = textsecure.storage.groups.getGroup(groupId);
var numberIndex = group.numbers.indexOf(number);
if (numberIndex < 0) // This is potentially a multi-message rare racing-AJAX race
return Promise.reject("Tried to refresh group to non-member");
var proto = new textsecure.protobuf.PushMessageContent();
proto.group = new textsecure.protobuf.PushMessageContent.GroupContext();
proto.group.id = toArrayBuffer(group.id);
proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE;
proto.group.members = group.numbers;
proto.group.name = group.name === undefined ? null : group.name;
if (group.avatar !== undefined) {
return makeAttachmentPointer(group.avatar).then(function(attachment) {
proto.group.avatar = attachment;
return sendMessageToDevices(Date.now(), number, devicesForNumber, proto);
});
} else {
return sendMessageToDevices(Date.now(), number, devicesForNumber, proto);
}
});
}
var tryMessageAgain = function(number, encodedMessage, timestamp) {
@ -213,7 +212,7 @@ window.textsecure.messaging = function() {
var promises = [];
for (var j in devicesForNumber)
if (!textsecure.storage.sessions.haveOpenSessionForDevice(devicesForNumber[j].encodedNumber))
if (!textsecure.protocol_wrapper.hasOpenSession(devicesForNumber[j].encodedNumber))
promises[promises.length] = getKeysForNumber(number, [parseInt(textsecure.utils.unencodeNumber(devicesForNumber[j].encodedNumber)[1])]);
Promise.all(promises).then(function() {

View file

@ -34,46 +34,30 @@
if (sessions[deviceId] === undefined)
return undefined;
var record = new axolotl.sessions.RecipientRecord();
record.deserialize(sessions[deviceId]);
if (getString(textsecure.storage.devices.getIdentityKeyForNumber(number)) !== getString(record.identityKey))
throw new Error("Got mismatched identity key on device object load");
return record;
return sessions[deviceId];
},
putSessionsForDevice: function(encodedNumber, record) {
var number = textsecure.utils.unencodeNumber(encodedNumber)[0];
var deviceId = textsecure.utils.unencodeNumber(encodedNumber)[1];
textsecure.storage.devices.checkSaveIdentityKeyForNumber(number, record.identityKey);
var sessions = textsecure.storage.get("sessions" + number);
if (sessions === undefined)
sessions = {};
sessions[deviceId] = record.serialize();
sessions[deviceId] = record;
textsecure.storage.put("sessions" + number, sessions);
var device = textsecure.storage.devices.getDeviceObject(encodedNumber);
if (device === undefined) {
var identityKey = textsecure.storage.devices.getIdentityKeyForNumber(number);
device = { encodedNumber: encodedNumber,
//TODO: Remove this duplication
identityKey: record.identityKey
identityKey: identityKey
};
}
if (getString(device.identityKey) !== getString(record.identityKey)) {
console.error("Got device object with key inconsistent after checkSaveIdentityKeyForNumber returned!");
throw new Error("Tried to put session for device with changed identity key");
}
return textsecure.storage.devices.saveDeviceObject(device);
},
haveOpenSessionForDevice: function(encodedNumber) {
var sessions = textsecure.storage.sessions.getSessionsForNumber(encodedNumber);
if (sessions === undefined || !sessions.haveOpenSession())
return false;
return true;
},
// Use textsecure.storage.devices.removeIdentityKeyForNumber (which calls this) instead
_removeIdentityKeyForNumber: function(number) {
textsecure.storage.remove("sessions" + number);

View file

@ -0,0 +1,181 @@
/* 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 <http://www.gnu.org/licenses/>.
*/
'use strict';
describe("Key generation", function() {
var count = 10;
this.timeout(count*1000);
function validateStoredKeyPair(keyPair) {
/* Ensure the keypair matches the format used internally by libaxolotl */
assert.isObject(keyPair, 'Stored keyPair is not an object');
assert.instanceOf(keyPair.pubKey, ArrayBuffer);
assert.instanceOf(keyPair.privKey, ArrayBuffer);
assert.strictEqual(keyPair.pubKey.byteLength, 33);
assert.strictEqual(new Uint8Array(keyPair.pubKey)[0], 5);
assert.strictEqual(keyPair.privKey.byteLength, 32);
}
function itStoresPreKey(keyId) {
it('prekey ' + keyId + ' is valid', function(done) {
return textsecure.storage.axolotl.getPreKey(keyId).then(function(keyPair) {
validateStoredKeyPair(keyPair);
}).then(done,done);
});
}
function itStoresSignedPreKey(keyId) {
it('signed prekey ' + keyId + ' is valid', function(done) {
return textsecure.storage.axolotl.getSignedPreKey(keyId).then(function(keyPair) {
validateStoredKeyPair(keyPair);
}).then(done,done);
});
}
function validateResultKey(resultKey) {
return textsecure.storage.axolotl.getPreKey(resultKey.keyId).then(function(keyPair) {
assertEqualArrayBuffers(resultKey.publicKey, keyPair.pubKey);
});
}
function validateResultSignedKey(resultSignedKey) {
return textsecure.storage.axolotl.getSignedPreKey(resultSignedKey.keyId).then(function(keyPair) {
assertEqualArrayBuffers(resultSignedKey.publicKey, keyPair.pubKey);
});
}
before(function(done) {
localStorage.clear();
axolotl.util.generateIdentityKeyPair().then(function(keyPair) {
return textsecure.storage.axolotl.put('identityKey', keyPair);
}).then(done, done);
});
describe('the first time', function() {
var result;
/* result should have this format
* {
* preKeys: [ { keyId, publicKey }, ... ],
* signedPreKey: { keyId, publicKey, signature },
* identityKey: <ArrayBuffer>
* }
*/
before(function(done) {
generateKeys(count).then(function(res) {
result = res;
}).then(done,done);
});
for (var i = 1; i <= count; i++) {
itStoresPreKey(i);
}
itStoresSignedPreKey(1);
it('result contains ' + count + ' preKeys', function() {
assert.isArray(result.preKeys);
assert.lengthOf(result.preKeys, count);
for (var i = 0; i < count; i++) {
assert.isObject(result.preKeys[i]);
}
});
it('result contains the correct keyIds', function() {
for (var i = 0; i < count; i++) {
assert.strictEqual(result.preKeys[i].keyId, i+1);
}
});
it('result contains the correct public keys', function(done) {
Promise.all(result.preKeys.map(validateResultKey)).then(function() {
done();
}).catch(done);
});
it('returns a signed prekey', function(done) {
assert.strictEqual(result.signedPreKey.keyId, 1);
assert.instanceOf(result.signedPreKey.signature, ArrayBuffer);
validateResultSignedKey(result.signedPreKey).then(done,done);
});
});
describe('the second time', function() {
var result;
before(function(done) {
generateKeys(count).then(function(res) {
result = res;
}).then(done,done);
});
for (var i = 1; i <= 2*count; i++) {
itStoresPreKey(i);
}
itStoresSignedPreKey(1);
itStoresSignedPreKey(2);
it('result contains ' + count + ' preKeys', function() {
assert.isArray(result.preKeys);
assert.lengthOf(result.preKeys, count);
for (var i = 0; i < count; i++) {
assert.isObject(result.preKeys[i]);
}
});
it('result contains the correct keyIds', function() {
for (var i = 1; i <= count; i++) {
assert.strictEqual(result.preKeys[i-1].keyId, i+count);
}
});
it('result contains the correct public keys', function(done) {
Promise.all(result.preKeys.map(validateResultKey)).then(function() {
done();
}).catch(done);
});
it('returns a signed prekey', function(done) {
assert.strictEqual(result.signedPreKey.keyId, 2);
assert.instanceOf(result.signedPreKey.signature, ArrayBuffer);
validateResultSignedKey(result.signedPreKey).then(done,done);
});
});
describe('the third time', function() {
var result;
before(function(done) {
generateKeys(count).then(function(res) {
result = res;
}).then(done,done);
});
for (var i = 1; i <= 3*count; i++) {
itStoresPreKey(i);
}
itStoresSignedPreKey(2);
itStoresSignedPreKey(3);
it('result contains ' + count + ' preKeys', function() {
assert.isArray(result.preKeys);
assert.lengthOf(result.preKeys, count);
for (var i = 0; i < count; i++) {
assert.isObject(result.preKeys[i]);
}
});
it('result contains the correct keyIds', function() {
for (var i = 1; i <= count; i++) {
assert.strictEqual(result.preKeys[i-1].keyId, i+2*count);
}
});
it('result contains the correct public keys', function(done) {
Promise.all(result.preKeys.map(validateResultKey)).then(function() {
done();
}).catch(done);
});
it('result contains a signed prekey', function(done) {
assert.strictEqual(result.signedPreKey.keyId, 3);
assert.instanceOf(result.signedPreKey.signature, ArrayBuffer);
validateResultSignedKey(result.signedPreKey).then(done,done);
});
it('deletes signed key 1', function() {
textsecure.storage.axolotl.getSignedPreKey(1).then(function(keyPair) {
assert.isUndefined(keyPair);
});
});
});
});

View file

@ -0,0 +1,93 @@
function AxolotlStore() {
this.store = {};
}
AxolotlStore.prototype = {
getMyIdentityKey: function() {
return this.get('identityKey');
},
getMyRegistrationId: function() {
return this.get('registrationId');
},
put: function(key, value) {
if (key === undefined || value === undefined || key === null || value === null)
throw new Error("Tried to store undefined/null");
this.store[key] = value;
},
get: function(key, defaultValue) {
if (key === null || key === undefined)
throw new Error("Tried to get value for undefined/null key");
if (key in this.store) {
return this.store[key];
} else {
return defaultValue;
}
},
remove: function(key) {
if (key === null || key === undefined)
throw new Error("Tried to remove value for undefined/null key");
delete this.store[key];
},
getIdentityKey: function(identifier) {
if (identifier === null || identifier === undefined)
throw new Error("Tried to get identity key for undefined/null key");
return new Promise(function(resolve) {
resolve(this.get('identityKey' + identifier));
}.bind(this));
},
putIdentityKey: function(identifier, identityKey) {
if (identifier === null || identifier === undefined)
throw new Error("Tried to put identity key for undefined/null key");
return new Promise(function(resolve) {
resolve(this.put('identityKey' + identifier, identityKey));
}.bind(this));
},
/* Returns a prekeypair object or undefined */
getPreKey: function(keyId) {
return new Promise(function(resolve) {
var res = this.get('25519KeypreKey' + keyId);
resolve(res);
}.bind(this));
},
putPreKey: function(keyId, keyPair) {
return new Promise(function(resolve) {
resolve(this.put('25519KeypreKey' + keyId, keyPair));
}.bind(this));
},
removePreKey: function(keyId) {
return new Promise(function(resolve) {
resolve(this.remove('25519KeypreKey' + keyId));
}.bind(this));
},
/* Returns a signed keypair object or undefined */
getSignedPreKey: function(keyId) {
return new Promise(function(resolve) {
var res = this.get('25519KeysignedKey' + keyId);
resolve(res);
}.bind(this));
},
putSignedPreKey: function(keyId, keyPair) {
return new Promise(function(resolve) {
resolve(this.put('25519KeysignedKey' + keyId, keyPair));
}.bind(this));
},
removeSignedPreKey: function(keyId) {
return new Promise(function(resolve) {
resolve(this.remove('25519KeysignedKey' + keyId));
}.bind(this));
},
getSession: function(identifier) {
return new Promise(function(resolve) {
resolve(this.get('session' + identifier));
}.bind(this));
},
putSession: function(identifier, record) {
return new Promise(function(resolve) {
resolve(this.put('session' + identifier, record));
}.bind(this));
}
};

View file

@ -28,9 +28,10 @@
<script type="text/javascript" src="test.js"></script>
<script type="text/javascript" src="blanket_mocha.js"></script>
<script type="text/javascript" src="in_memory_axolotl_store.js"></script>
<script type="text/javascript" src="../components.js"></script>
<script type="text/javascript" src="../crypto.js"></script>
<script type="text/javascript" src="../protobufs.js" data-cover></script>
<script type="text/javascript" src="../errors.js" data-cover></script>
<script type="text/javascript" src="../storage.js" data-cover></script>
@ -49,6 +50,8 @@
<script type="text/javascript" src="helpers_test.js"></script>
<script type="text/javascript" src="websocket-resources_test.js"></script>
<script type="text/javascript" src="protocol_test.js"></script>
<script type="text/javascript" src="storage_test.js"></script>
<script type="text/javascript" src="generate_keys_test.js"></script>
<script type="text/javascript" src="websocket_test.js"></script>
</body>
</html>

View file

@ -0,0 +1,93 @@
/* 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 <http://www.gnu.org/licenses/>.
*/
'use strict';
describe("AxolotlStore", function() {
before(function() { localStorage.clear(); });
var store = textsecure.storage.axolotl;
var identifier = '+5558675309';
var identityKey = {
pubKey: textsecure.crypto.getRandomBytes(33),
privKey: textsecure.crypto.getRandomBytes(32),
};
var testKey = {
pubKey: textsecure.crypto.getRandomBytes(33),
privKey: textsecure.crypto.getRandomBytes(32),
};
it('retrieves my registration id', function() {
store.put('registrationId', 1337);
var reg = store.getMyRegistrationId();
assert.strictEqual(reg, 1337);
});
it('retrieves my identity key', function() {
store.put('identityKey', identityKey);
var key = store.getMyIdentityKey();
assertEqualArrayBuffers(key.pubKey, identityKey.pubKey);
assertEqualArrayBuffers(key.privKey, identityKey.privKey);
});
it('stores identity keys', function(done) {
store.putIdentityKey(identifier, testKey.pubKey).then(function() {
return store.getIdentityKey(identifier).then(function(key) {
assertEqualArrayBuffers(key, testKey.pubKey);
});
}).then(done,done);
});
it('stores prekeys', function(done) {
store.putPreKey(1, testKey).then(function() {
return store.getPreKey(1).then(function(key) {
assertEqualArrayBuffers(key.pubKey, testKey.pubKey);
assertEqualArrayBuffers(key.privKey, testKey.privKey);
});
}).then(done,done);
});
it('deletes prekeys', function(done) {
before(function(done) {
store.putPreKey(2, testKey).then(done);
});
store.removePreKey(2, testKey).then(function() {
return store.getPreKey(2).then(function(key) {
assert.isUndefined(key);
});
}).then(done,done);
});
it('stores signed prekeys', function(done) {
store.putSignedPreKey(3, testKey).then(function() {
return store.getSignedPreKey(3).then(function(key) {
assertEqualArrayBuffers(key.pubKey, testKey.pubKey);
assertEqualArrayBuffers(key.privKey, testKey.privKey);
});
}).then(done,done);
});
it('deletes signed prekeys', function(done) {
before(function(done) {
store.putSignedPreKey(4, testKey).then(done);
});
store.removeSignedPreKey(4, testKey).then(function() {
return store.getSignedPreKey(4).then(function(key) {
assert.isUndefined(key);
});
}).then(done,done);
});
it('stores sessions', function(done) {
var testRecord = "an opaque string";
store.putSession(identifier + '.1', testRecord).then(function() {
return store.getSession(identifier + '.1').then(function(record) {
assert.deepEqual(record, testRecord);
});
}).then(done,done);
});
});

View file

@ -116,7 +116,6 @@
</div>
<script type="text/javascript" src="js/components.js"></script>
<script type="text/javascript" src="js/database.js"></script>
<script type="text/javascript" src="js/libtextsecure.js"></script>
<script type="text/javascript" src="js/notifications.js"></script>
<script type="text/javascript" src="js/libphonenumber-util.js"></script>

View file

@ -64,7 +64,6 @@
</script>
<script type="text/javascript" src="js/components.js"></script>
<script type="text/javascript" src="js/database.js"></script>
<script type="text/javascript" src="js/libtextsecure.js"></script>
<script type="text/javascript" src="js/libphonenumber-util.js"></script>
<script type="text/javascript" src="js/models/messages.js"></script>

View file

@ -117,6 +117,7 @@
<script type="text/javascript" src="../js/components.js"></script>
<script type="text/javascript" src="../js/database.js"></script>
<script type="text/javascript" src="../js/axolotl_store.js"></script>
<script type="text/javascript" src="../js/libtextsecure.js"></script>
<script type="text/javascript" src="../js/libphonenumber-util.js"></script>
@ -143,5 +144,6 @@
<script type="text/javascript" src="views/message_list_view_test.js"></script>
<script type="text/javascript" src="models/conversations_test.js"></script>
<script type="text/javascript" src="models/messages_test.js"></script>
<script type="text/javascript" src="storage_test.js"></script>
</body>
</html>

93
test/storage_test.js Normal file
View file

@ -0,0 +1,93 @@
/* 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 <http://www.gnu.org/licenses/>.
*/
'use strict';
describe("AxolotlStore", function() {
before(function() { localStorage.clear(); });
var store = textsecure.storage.axolotl;
var identifier = '+5558675309';
var identityKey = {
pubKey: textsecure.crypto.getRandomBytes(33),
privKey: textsecure.crypto.getRandomBytes(32),
};
var testKey = {
pubKey: textsecure.crypto.getRandomBytes(33),
privKey: textsecure.crypto.getRandomBytes(32),
};
it('retrieves my registration id', function() {
store.put('registrationId', 1337);
var reg = store.getMyRegistrationId();
assert.strictEqual(reg, 1337);
});
it('retrieves my identity key', function() {
store.put('identityKey', identityKey);
var key = store.getMyIdentityKey();
assertEqualArrayBuffers(key.pubKey, identityKey.pubKey);
assertEqualArrayBuffers(key.privKey, identityKey.privKey);
});
it('stores identity keys', function(done) {
store.putIdentityKey(identifier, testKey.pubKey).then(function() {
return store.getIdentityKey(identifier).then(function(key) {
assertEqualArrayBuffers(key, testKey.pubKey);
});
}).then(done,done);
});
it('stores prekeys', function(done) {
store.putPreKey(1, testKey).then(function() {
return store.getPreKey(1).then(function(key) {
assertEqualArrayBuffers(key.pubKey, testKey.pubKey);
assertEqualArrayBuffers(key.privKey, testKey.privKey);
});
}).then(done,done);
});
it('deletes prekeys', function(done) {
before(function(done) {
store.putPreKey(2, testKey).then(done);
});
store.removePreKey(2, testKey).then(function() {
return store.getPreKey(2).then(function(key) {
assert.isUndefined(key);
});
}).then(done,done);
});
it('stores signed prekeys', function(done) {
store.putSignedPreKey(3, testKey).then(function() {
return store.getSignedPreKey(3).then(function(key) {
assertEqualArrayBuffers(key.pubKey, testKey.pubKey);
assertEqualArrayBuffers(key.privKey, testKey.privKey);
});
}).then(done,done);
});
it('deletes signed prekeys', function(done) {
before(function(done) {
store.putSignedPreKey(4, testKey).then(done);
});
store.removeSignedPreKey(4, testKey).then(function() {
return store.getSignedPreKey(4).then(function(key) {
assert.isUndefined(key);
});
}).then(done,done);
});
it('stores sessions', function(done) {
var testRecord = "an opaque string";
store.putSession(identifier + '.1', testRecord).then(function() {
return store.getSession(identifier + '.1').then(function(record) {
assert.deepEqual(record, testRecord);
});
}).then(done,done);
});
});