signal_protocol_store.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. /*
  2. * vim: ts=4:sw=4:expandtab
  3. */
  4. ;(function() {
  5. 'use strict';
  6. var StaticByteBufferProto = new dcodeIO.ByteBuffer().__proto__;
  7. var StaticArrayBufferProto = new ArrayBuffer().__proto__;
  8. var StaticUint8ArrayProto = new Uint8Array().__proto__;
  9. function isStringable(thing) {
  10. return (thing === Object(thing) &&
  11. (thing.__proto__ == StaticArrayBufferProto ||
  12. thing.__proto__ == StaticUint8ArrayProto ||
  13. thing.__proto__ == StaticByteBufferProto));
  14. }
  15. function convertToArrayBuffer(thing) {
  16. if (thing === undefined) {
  17. return undefined;
  18. }
  19. if (thing === Object(thing)) {
  20. if (thing.__proto__ == StaticArrayBufferProto) {
  21. return thing;
  22. }
  23. //TODO: Several more cases here...
  24. }
  25. if (thing instanceof Array) {
  26. // Assuming Uint16Array from curve25519
  27. var res = new ArrayBuffer(thing.length * 2);
  28. var uint = new Uint16Array(res);
  29. for (var i = 0; i < thing.length; i++) {
  30. uint[i] = thing[i];
  31. }
  32. return res;
  33. }
  34. var str;
  35. if (isStringable(thing)) {
  36. str = stringObject(thing);
  37. } else if (typeof thing == "string") {
  38. str = thing;
  39. } else {
  40. throw new Error("Tried to convert a non-stringable thing of type " + typeof thing + " to an array buffer");
  41. }
  42. var res = new ArrayBuffer(str.length);
  43. var uint = new Uint8Array(res);
  44. for (var i = 0; i < str.length; i++) {
  45. uint[i] = str.charCodeAt(i);
  46. }
  47. return res;
  48. }
  49. function equalArrayBuffers(ab1, ab2) {
  50. if (!(ab1 instanceof ArrayBuffer && ab2 instanceof ArrayBuffer)) {
  51. return false;
  52. }
  53. if (ab1.byteLength !== ab2.byteLength) {
  54. return false;
  55. }
  56. var result = true;
  57. var ta1 = new Uint8Array(ab1);
  58. var ta2 = new Uint8Array(ab2);
  59. for (var i = 0; i < ab1.byteLength; ++i) {
  60. if (ta1[i] !== ta2[i]) { result = false; }
  61. }
  62. return result;
  63. }
  64. var Model = Backbone.Model.extend({ database: Whisper.Database });
  65. var PreKey = Model.extend({ storeName: 'preKeys' });
  66. var SignedPreKey = Model.extend({ storeName: 'signedPreKeys' });
  67. var Session = Model.extend({ storeName: 'sessions' });
  68. var SessionCollection = Backbone.Collection.extend({
  69. storeName: 'sessions',
  70. database: Whisper.Database,
  71. model: Session,
  72. fetchSessionsForNumber: function(number) {
  73. return this.fetch({range: [number + '.1', number + '.' + ':']});
  74. }
  75. });
  76. var IdentityKey = Model.extend({ storeName: 'identityKeys' });
  77. var Group = Model.extend({ storeName: 'groups' });
  78. var Item = Model.extend({ storeName: 'items' });
  79. function SignalProtocolStore() {}
  80. SignalProtocolStore.prototype = {
  81. constructor: SignalProtocolStore,
  82. getIdentityKeyPair: function() {
  83. var item = new Item({id: 'identityKey'});
  84. return new Promise(function(resolve) {
  85. item.fetch().then(function() {
  86. resolve(item.get('value'));
  87. });
  88. });
  89. },
  90. getLocalRegistrationId: function() {
  91. var item = new Item({id: 'registrationId'});
  92. return new Promise(function(resolve) {
  93. item.fetch().then(function() {
  94. resolve(item.get('value'));
  95. });
  96. });
  97. },
  98. /* Returns a prekeypair object or undefined */
  99. loadPreKey: function(keyId) {
  100. var prekey = new PreKey({id: keyId});
  101. return new Promise(function(resolve) {
  102. prekey.fetch().then(function() {
  103. resolve({
  104. pubKey: prekey.attributes.publicKey,
  105. privKey: prekey.attributes.privateKey
  106. });
  107. }).fail(resolve);
  108. });
  109. },
  110. storePreKey: function(keyId, keyPair) {
  111. var prekey = new PreKey({
  112. id : keyId,
  113. publicKey : keyPair.pubKey,
  114. privateKey : keyPair.privKey
  115. });
  116. return new Promise(function(resolve) {
  117. prekey.save().always(function() {
  118. resolve();
  119. });
  120. });
  121. },
  122. removePreKey: function(keyId) {
  123. var prekey = new PreKey({id: keyId});
  124. new Promise(function(resolve) {
  125. getAccountManager().refreshPreKeys().then(resolve);
  126. });
  127. return new Promise(function(resolve) {
  128. prekey.destroy().then(function() {
  129. resolve();
  130. });
  131. });
  132. },
  133. /* Returns a signed keypair object or undefined */
  134. loadSignedPreKey: function(keyId) {
  135. var prekey = new SignedPreKey({id: keyId});
  136. return new Promise(function(resolve) {
  137. prekey.fetch().then(function() {
  138. resolve({
  139. pubKey: prekey.attributes.publicKey,
  140. privKey: prekey.attributes.privateKey
  141. });
  142. }).fail(resolve);
  143. });
  144. },
  145. storeSignedPreKey: function(keyId, keyPair) {
  146. var prekey = new SignedPreKey({
  147. id : keyId,
  148. publicKey : keyPair.pubKey,
  149. privateKey : keyPair.privKey
  150. });
  151. return new Promise(function(resolve) {
  152. prekey.save().always(function() {
  153. resolve();
  154. });
  155. });
  156. },
  157. removeSignedPreKey: function(keyId) {
  158. var prekey = new SignedPreKey({id: keyId});
  159. return new Promise(function(resolve) {
  160. prekey.destroy().then(function() {
  161. resolve();
  162. });
  163. });
  164. },
  165. loadSession: function(encodedNumber) {
  166. if (encodedNumber === null || encodedNumber === undefined) {
  167. throw new Error("Tried to get session for undefined/null number");
  168. }
  169. return new Promise(function(resolve) {
  170. var session = new Session({id: encodedNumber});
  171. session.fetch().always(function() {
  172. resolve(session.get('record'));
  173. });
  174. });
  175. },
  176. storeSession: function(encodedNumber, record) {
  177. if (encodedNumber === null || encodedNumber === undefined) {
  178. throw new Error("Tried to put session for undefined/null number");
  179. }
  180. return new Promise(function(resolve) {
  181. var number = textsecure.utils.unencodeNumber(encodedNumber)[0];
  182. var deviceId = parseInt(textsecure.utils.unencodeNumber(encodedNumber)[1]);
  183. var session = new Session({id: encodedNumber});
  184. session.fetch().always(function() {
  185. session.save({
  186. record: record,
  187. deviceId: deviceId,
  188. number: number
  189. }).fail(function(e) {
  190. console.log('Failed to save session', encodedNumber, e);
  191. }).always(function() {
  192. resolve();
  193. });
  194. });
  195. });
  196. },
  197. getDeviceIds: function(number) {
  198. if (number === null || number === undefined) {
  199. throw new Error("Tried to get device ids for undefined/null number");
  200. }
  201. return new Promise(function(resolve) {
  202. var sessions = new SessionCollection();
  203. sessions.fetchSessionsForNumber(number).always(function() {
  204. resolve(sessions.pluck('deviceId'));
  205. });
  206. });
  207. },
  208. removeSession: function(encodedNumber) {
  209. return new Promise(function(resolve) {
  210. var session = new Session({id: encodedNumber});
  211. session.fetch().then(function() {
  212. session.destroy().then(resolve);
  213. });
  214. });
  215. },
  216. removeAllSessions: function(number) {
  217. if (number === null || number === undefined) {
  218. throw new Error("Tried to remove sessions for undefined/null number");
  219. }
  220. return new Promise(function(resolve) {
  221. var sessions = new SessionCollection();
  222. sessions.fetchSessionsForNumber(number).always(function() {
  223. var promises = [];
  224. while (sessions.length > 0) {
  225. promises.push(new Promise(function(res) {
  226. sessions.pop().destroy().then(res);
  227. }));
  228. }
  229. Promise.all(promises).then(resolve);
  230. });
  231. });
  232. },
  233. clearSessionStore: function() {
  234. return new Promise(function(resolve) {
  235. var sessions = new SessionCollection();
  236. sessions.sync('delete', sessions, {}).always(resolve);
  237. });
  238. },
  239. isTrustedIdentity: function(identifier, publicKey) {
  240. if (identifier === null || identifier === undefined) {
  241. throw new Error("Tried to get identity key for undefined/null key");
  242. }
  243. var number = textsecure.utils.unencodeNumber(identifier)[0];
  244. return new Promise(function(resolve) {
  245. var identityKey = new IdentityKey({id: number});
  246. identityKey.fetch().always(function() {
  247. var oldpublicKey = identityKey.get('publicKey');
  248. if (!oldpublicKey || equalArrayBuffers(oldpublicKey, publicKey)) {
  249. resolve(true);
  250. } else if (!storage.get('safety-numbers-approval', true)) {
  251. this.removeIdentityKey(identifier).then(function() {
  252. this.saveIdentity(identifier, publicKey).then(function() {
  253. console.log('Key changed for', identifier);
  254. this.trigger('keychange:' + identifier);
  255. resolve(true);
  256. }.bind(this));
  257. }.bind(this));
  258. } else {
  259. resolve(false);
  260. }
  261. }.bind(this));
  262. }.bind(this));
  263. },
  264. loadIdentityKey: function(identifier) {
  265. if (identifier === null || identifier === undefined) {
  266. throw new Error("Tried to get identity key for undefined/null key");
  267. }
  268. var number = textsecure.utils.unencodeNumber(identifier)[0];
  269. return new Promise(function(resolve) {
  270. var identityKey = new IdentityKey({id: number});
  271. identityKey.fetch().always(function() {
  272. resolve(identityKey.get('publicKey'));
  273. });
  274. });
  275. },
  276. saveIdentity: function(identifier, publicKey) {
  277. if (identifier === null || identifier === undefined) {
  278. throw new Error("Tried to put identity key for undefined/null key");
  279. }
  280. if (!(publicKey instanceof ArrayBuffer)) {
  281. publicKey = convertToArrayBuffer(publicKey);
  282. }
  283. var number = textsecure.utils.unencodeNumber(identifier)[0];
  284. return new Promise(function(resolve, reject) {
  285. var identityKey = new IdentityKey({id: number});
  286. identityKey.fetch().always(function() {
  287. var oldpublicKey = identityKey.get('publicKey');
  288. if (!oldpublicKey) {
  289. // Lookup failed, or the current key was removed, so save this one.
  290. identityKey.save({publicKey: publicKey}).then(resolve);
  291. } else {
  292. // Key exists, if it matches do nothing, else throw
  293. if (equalArrayBuffers(oldpublicKey, publicKey)) {
  294. resolve();
  295. } else {
  296. reject(new Error("Attempted to overwrite a different identity key"));
  297. }
  298. }
  299. });
  300. });
  301. },
  302. removeIdentityKey: function(number) {
  303. return new Promise(function(resolve, reject) {
  304. var identityKey = new IdentityKey({id: number});
  305. identityKey.fetch().then(function() {
  306. identityKey.save({publicKey: undefined});
  307. }).fail(function() {
  308. reject(new Error("Tried to remove identity for unknown number"));
  309. });
  310. resolve(textsecure.storage.protocol.removeAllSessions(number));
  311. });
  312. },
  313. getGroup: function(groupId) {
  314. if (groupId === null || groupId === undefined) {
  315. throw new Error("Tried to get group for undefined/null id");
  316. }
  317. return new Promise(function(resolve) {
  318. var group = new Group({id: groupId});
  319. group.fetch().always(function() {
  320. resolve(group.get('data'));
  321. });
  322. });
  323. },
  324. putGroup: function(groupId, group) {
  325. if (groupId === null || groupId === undefined) {
  326. throw new Error("Tried to put group key for undefined/null id");
  327. }
  328. if (group === null || group === undefined) {
  329. throw new Error("Tried to put undefined/null group object");
  330. }
  331. var group = new Group({id: groupId, data: group});
  332. return new Promise(function(resolve) {
  333. group.save().always(resolve);
  334. });
  335. },
  336. removeGroup: function(groupId) {
  337. if (groupId === null || groupId === undefined) {
  338. throw new Error("Tried to remove group key for undefined/null id");
  339. }
  340. return new Promise(function(resolve) {
  341. var group = new Group({id: groupId});
  342. group.destroy().always(resolve);
  343. });
  344. },
  345. };
  346. _.extend(SignalProtocolStore.prototype, Backbone.Events);
  347. window.SignalProtocolStore = SignalProtocolStore;
  348. })();