signal-desktop/js/signal_protocol_store.js

392 lines
15 KiB
JavaScript

/*
* vim: ts=4:sw=4:expandtab
*/
;(function() {
'use strict';
var StaticByteBufferProto = new dcodeIO.ByteBuffer().__proto__;
var StaticArrayBufferProto = new ArrayBuffer().__proto__;
var StaticUint8ArrayProto = new Uint8Array().__proto__;
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
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;
}
function equalArrayBuffers(ab1, ab2) {
if (!(ab1 instanceof ArrayBuffer && ab2 instanceof ArrayBuffer)) {
return false;
}
if (ab1.byteLength !== ab2.byteLength) {
return false;
}
var result = true;
var ta1 = new Uint8Array(ab1);
var ta2 = new Uint8Array(ab2);
for (var i = 0; i < ab1.byteLength; ++i) {
if (ta1[i] !== ta2[i]) { result = false; }
}
return result;
}
var Model = Backbone.Model.extend({ database: Whisper.Database });
var PreKey = Model.extend({ storeName: 'preKeys' });
var SignedPreKey = Model.extend({ storeName: 'signedPreKeys' });
var SignedPreKeyCollection = Backbone.Collection.extend({
storeName: 'signedPreKeys',
database: Whisper.Database,
model: SignedPreKey
});
var Session = Model.extend({ storeName: 'sessions' });
var SessionCollection = Backbone.Collection.extend({
storeName: 'sessions',
database: Whisper.Database,
model: Session,
fetchSessionsForNumber: function(number) {
return this.fetch({range: [number + '.1', number + '.' + ':']});
}
});
var IdentityKey = Model.extend({ storeName: 'identityKeys' });
var Group = Model.extend({ storeName: 'groups' });
var Item = Model.extend({ storeName: 'items' });
function SignalProtocolStore() {}
SignalProtocolStore.prototype = {
constructor: SignalProtocolStore,
getIdentityKeyPair: function() {
var item = new Item({id: 'identityKey'});
return new Promise(function(resolve) {
item.fetch().then(function() {
resolve(item.get('value'));
});
});
},
getLocalRegistrationId: function() {
var item = new Item({id: 'registrationId'});
return new Promise(function(resolve) {
item.fetch().then(function() {
resolve(item.get('value'));
});
});
},
/* Returns a prekeypair object or undefined */
loadPreKey: 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);
});
},
storePreKey: 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});
new Promise(function(resolve) {
getAccountManager().refreshPreKeys().then(resolve);
});
return new Promise(function(resolve) {
prekey.destroy().then(function() {
resolve();
});
});
},
/* Returns a signed keypair object or undefined */
loadSignedPreKey: function(keyId) {
var prekey = new SignedPreKey({id: keyId});
return new Promise(function(resolve) {
prekey.fetch().then(function() {
resolve({
pubKey : prekey.get('publicKey'),
privKey : prekey.get('privateKey'),
created_at : prekey.get('created_at'),
keyId : prekey.get('id')
});
}).fail(resolve);
});
},
loadSignedPreKeys: function() {
if (arguments.length > 0) {
return Promise.reject(new Error("loadSignedPreKeys takes no arguments"));
}
var signedPreKeys = new SignedPreKeyCollection();
return new Promise(function(resolve) {
signedPreKeys.fetch().then(function() {
resolve(signedPreKeys.map(function(prekey) {
return {
pubKey : prekey.get('publicKey'),
privKey : prekey.get('privateKey'),
created_at : prekey.get('created_at'),
keyId : prekey.get('id')
};
}));
});
});
},
storeSignedPreKey: function(keyId, keyPair) {
var prekey = new SignedPreKey({
id : keyId,
publicKey : keyPair.pubKey,
privateKey : keyPair.privKey,
created_at : Date.now()
});
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();
});
});
},
loadSession: function(encodedNumber) {
if (encodedNumber === null || encodedNumber === undefined) {
throw new Error("Tried to get session for undefined/null number");
}
return new Promise(function(resolve) {
var session = new Session({id: encodedNumber});
session.fetch().always(function() {
resolve(session.get('record'));
});
});
},
storeSession: function(encodedNumber, record) {
if (encodedNumber === null || encodedNumber === undefined) {
throw new Error("Tried to put session for undefined/null number");
}
return new Promise(function(resolve) {
var number = textsecure.utils.unencodeNumber(encodedNumber)[0];
var deviceId = parseInt(textsecure.utils.unencodeNumber(encodedNumber)[1]);
var session = new Session({id: encodedNumber});
session.fetch().always(function() {
session.save({
record: record,
deviceId: deviceId,
number: number
}).fail(function(e) {
console.log('Failed to save session', encodedNumber, e);
}).always(function() {
resolve();
});
});
});
},
getDeviceIds: function(number) {
if (number === null || number === undefined) {
throw new Error("Tried to get device ids for undefined/null number");
}
return new Promise(function(resolve) {
var sessions = new SessionCollection();
sessions.fetchSessionsForNumber(number).always(function() {
resolve(sessions.pluck('deviceId'));
});
});
},
removeSession: function(encodedNumber) {
return new Promise(function(resolve) {
var session = new Session({id: encodedNumber});
session.fetch().then(function() {
session.destroy().then(resolve);
});
});
},
removeAllSessions: function(number) {
if (number === null || number === undefined) {
throw new Error("Tried to remove sessions for undefined/null number");
}
return new Promise(function(resolve) {
var sessions = new SessionCollection();
sessions.fetchSessionsForNumber(number).always(function() {
var promises = [];
while (sessions.length > 0) {
promises.push(new Promise(function(res) {
sessions.pop().destroy().then(res);
}));
}
Promise.all(promises).then(resolve);
});
});
},
clearSessionStore: function() {
return new Promise(function(resolve) {
var sessions = new SessionCollection();
sessions.sync('delete', sessions, {}).always(resolve);
});
},
isTrustedIdentity: function(identifier, publicKey) {
if (identifier === null || identifier === undefined) {
throw new Error("Tried to get identity key for undefined/null key");
}
var number = textsecure.utils.unencodeNumber(identifier)[0];
return new Promise(function(resolve) {
var identityKey = new IdentityKey({id: number});
identityKey.fetch().always(function() {
var oldpublicKey = identityKey.get('publicKey');
if (!oldpublicKey || equalArrayBuffers(oldpublicKey, publicKey)) {
resolve(true);
} else if (!storage.get('safety-numbers-approval', true)) {
this.removeIdentityKey(identifier).then(function() {
this.saveIdentity(identifier, publicKey).then(function() {
console.log('Key changed for', identifier);
this.trigger('keychange:' + identifier);
resolve(true);
}.bind(this));
}.bind(this));
} else {
resolve(false);
}
}.bind(this));
}.bind(this));
},
loadIdentityKey: function(identifier) {
if (identifier === null || identifier === undefined) {
throw new Error("Tried to get identity key for undefined/null key");
}
var number = textsecure.utils.unencodeNumber(identifier)[0];
return new Promise(function(resolve) {
var identityKey = new IdentityKey({id: number});
identityKey.fetch().always(function() {
resolve(identityKey.get('publicKey'));
});
});
},
saveIdentity: function(identifier, publicKey) {
if (identifier === null || identifier === undefined) {
throw new Error("Tried to put identity key for undefined/null key");
}
if (!(publicKey instanceof ArrayBuffer)) {
publicKey = convertToArrayBuffer(publicKey);
}
var number = textsecure.utils.unencodeNumber(identifier)[0];
return new Promise(function(resolve, reject) {
var identityKey = new IdentityKey({id: number});
identityKey.fetch().always(function() {
var oldpublicKey = identityKey.get('publicKey');
if (!oldpublicKey) {
// Lookup failed, or the current key was removed, so save this one.
identityKey.save({publicKey: publicKey}).then(resolve);
} else {
// Key exists, if it matches do nothing, else throw
if (equalArrayBuffers(oldpublicKey, publicKey)) {
resolve();
} else {
reject(new Error("Attempted to overwrite a different identity key"));
}
}
});
});
},
removeIdentityKey: function(number) {
return new Promise(function(resolve, reject) {
var identityKey = new IdentityKey({id: number});
identityKey.fetch().then(function() {
identityKey.save({publicKey: undefined});
}).fail(function() {
reject(new Error("Tried to remove identity for unknown number"));
});
resolve(textsecure.storage.protocol.removeAllSessions(number));
});
},
getGroup: function(groupId) {
if (groupId === null || groupId === undefined) {
throw new Error("Tried to get group for undefined/null id");
}
return new Promise(function(resolve) {
var group = new Group({id: groupId});
group.fetch().always(function() {
resolve(group.get('data'));
});
});
},
putGroup: function(groupId, group) {
if (groupId === null || groupId === undefined) {
throw new Error("Tried to put group key for undefined/null id");
}
if (group === null || group === undefined) {
throw new Error("Tried to put undefined/null group object");
}
var group = new Group({id: groupId, data: group});
return new Promise(function(resolve) {
group.save().always(resolve);
});
},
removeGroup: function(groupId) {
if (groupId === null || groupId === undefined) {
throw new Error("Tried to remove group key for undefined/null id");
}
return new Promise(function(resolve) {
var group = new Group({id: groupId});
group.destroy().always(resolve);
});
},
};
_.extend(SignalProtocolStore.prototype, Backbone.Events);
window.SignalProtocolStore = SignalProtocolStore;
})();