diff --git a/Gruntfile.js b/Gruntfile.js
index d9ea8ba9b..05ede6716 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -53,6 +53,25 @@ module.exports = function(grunt) {
'test/_test.js'
],
dest: 'test/test.js',
+ },
+ libtextsecure: {
+ src: [
+ 'libtextsecure/protobufs.js',
+ 'libtextsecure/websocket.js',
+ 'libtextsecure/websocket-resources.js',
+ 'libtextsecure/helpers.js',
+ 'libtextsecure/errors.js',
+ 'libtextsecure/stringview.js',
+ 'libtextsecure/storage.js',
+ 'libtextsecure/storage/devices.js',
+ 'libtextsecure/storage/groups.js',
+ 'libtextsecure/api.js',
+ 'libtextsecure/crypto.js',
+ 'libtextsecure/protocol.js',
+ 'libtextsecure/sendmessage.js',
+ ],
+ dest: 'js/libtextsecure.js',
+ },
}
},
sass: {
diff --git a/background.html b/background.html
index 847e6e3b6..0049bce40 100644
--- a/background.html
+++ b/background.html
@@ -16,27 +16,17 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
diff --git a/index.html b/index.html
index e81c946aa..5d00483b7 100644
--- a/index.html
+++ b/index.html
@@ -130,31 +130,20 @@
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/js/libtextsecure.js b/js/libtextsecure.js
new file mode 100644
index 000000000..01e72938a
--- /dev/null
+++ b/js/libtextsecure.js
@@ -0,0 +1,2723 @@
+;(function() {
+
+ function loadProtoBufs(filename) {
+ return dcodeIO.ProtoBuf.loadProtoFile({root: 'protos', file: filename}).build('textsecure');
+ };
+
+ var pushMessages = loadProtoBufs('IncomingPushMessageSignal.proto');
+ var protocolMessages = loadProtoBufs('WhisperTextProtocol.proto');
+ var subProtocolMessages = loadProtoBufs('SubProtocol.proto');
+ var deviceMessages = loadProtoBufs('DeviceMessages.proto');
+
+ window.textsecure = window.textsecure || {};
+ window.textsecure.protobuf = {
+ IncomingPushMessageSignal : pushMessages.IncomingPushMessageSignal,
+ PushMessageContent : pushMessages.PushMessageContent,
+ WhisperMessage : protocolMessages.WhisperMessage,
+ PreKeyWhisperMessage : protocolMessages.PreKeyWhisperMessage,
+ DeviceInit : deviceMessages.DeviceInit,
+ IdentityKey : deviceMessages.IdentityKey,
+ DeviceControl : deviceMessages.DeviceControl,
+ WebSocketResponseMessage : subProtocolMessages.WebSocketResponseMessage,
+ WebSocketRequestMessage : subProtocolMessages.WebSocketRequestMessage,
+ WebSocketMessage : subProtocolMessages.WebSocketMessage
+ };
+})();
+
+/* vim: ts=4:sw=4:expandtab
+ *
+ * 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';
+
+ /*
+ * var socket = textsecure.websocket(url);
+ *
+ * Returns an adamantium-reinforced super socket, capable of sending
+ * app-level keep alives and automatically reconnecting.
+ *
+ */
+
+ window.textsecure.websocket = function (url) {
+ var socketWrapper = {
+ onmessage : function() {},
+ ondisconnect : function() {},
+ };
+ var socket;
+ var keepAliveTimer;
+ var reconnectSemaphore = 0;
+ var reconnectTimeout = 1000;
+
+ function resetKeepAliveTimer() {
+ clearTimeout(keepAliveTimer);
+ keepAliveTimer = setTimeout(function() {
+ socket.send(
+ new textsecure.protobuf.WebSocketMessage({
+ type: textsecure.protobuf.WebSocketMessage.Type.REQUEST,
+ request: { verb: 'GET', path: '/v1/keepalive' }
+ }).encode().toArrayBuffer()
+ );
+
+ resetKeepAliveTimer();
+ }, 15000);
+ };
+
+ function reconnect(e) {
+ reconnectSemaphore--;
+ setTimeout(connect, reconnectTimeout);
+ socketWrapper.ondisconnect(e);
+ };
+
+ function connect() {
+ clearTimeout(keepAliveTimer);
+ if (++reconnectSemaphore <= 0) { return; }
+
+ if (socket) { socket.close(); }
+ socket = new WebSocket(url);
+
+ socket.onerror = reconnect;
+ socket.onclose = reconnect;
+ socket.onopen = resetKeepAliveTimer;
+
+ socket.onmessage = function(response) {
+ socketWrapper.onmessage(response);
+ resetKeepAliveTimer();
+ };
+
+ socketWrapper.send = function(msg) {
+ socket.send(msg);
+ }
+ }
+
+ connect();
+ return socketWrapper;
+ };
+})();
+
+/* vim: ts=4:sw=4:expandtab
+ *
+ * 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';
+
+ /*
+ * WebSocket-Resources
+ *
+ * Create a request-response interface over websockets using the
+ * WebSocket-Resources sub-protocol[1].
+ *
+ * var client = new WebSocketResource(socket, function(request) {
+ * request.respond(200, 'OK');
+ * });
+ *
+ * client.sendRequest({
+ * verb: 'PUT',
+ * path: '/v1/messages',
+ * body: '{ some: "json" }',
+ * success: function(message, status, request) {...},
+ * error: function(message, status, request) {...}
+ * });
+ *
+ * 1. https://github.com/WhisperSystems/WebSocket-Resources
+ *
+ */
+
+ var Request = function(options) {
+ this.verb = options.verb || options.type;
+ this.path = options.path || options.url;
+ this.body = options.body || options.data;
+ this.success = options.success
+ this.error = options.error
+ this.id = options.id;
+
+ if (this.id === undefined) {
+ var bits = new Uint32Array(2);
+ window.crypto.getRandomValues(bits);
+ this.id = dcodeIO.Long.fromBits(bits[0], bits[1], true);
+ }
+ };
+
+ var IncomingWebSocketRequest = function(options) {
+ var request = new Request(options);
+ var socket = options.socket;
+
+ this.verb = request.verb;
+ this.path = request.path;
+ this.body = request.body;
+
+ this.respond = function(status, message) {
+ socket.send(
+ new textsecure.protobuf.WebSocketMessage({
+ type: textsecure.protobuf.WebSocketMessage.Type.RESPONSE,
+ response: { id: request.id, message: message, status: status }
+ }).encode().toArrayBuffer()
+ );
+ };
+ };
+
+ var outgoing = {};
+ var OutgoingWebSocketRequest = function(options, socket) {
+ var request = new Request(options);
+ outgoing[request.id] = request;
+ socket.send(
+ new textsecure.protobuf.WebSocketMessage({
+ type: textsecure.protobuf.WebSocketMessage.Type.REQUEST,
+ request: {
+ verb : request.verb,
+ path : request.path,
+ body : request.body,
+ id : request.id
+ }
+ }).encode().toArrayBuffer()
+ );
+ };
+
+ window.WebSocketResource = function(socket, handleRequest) {
+ this.sendRequest = function(options) {
+ return new OutgoingWebSocketRequest(options, socket);
+ };
+
+ socket.onmessage = function(socketMessage) {
+ var blob = socketMessage.data;
+ var reader = new FileReader();
+ reader.onload = function() {
+ var message = textsecure.protobuf.WebSocketMessage.decode(reader.result);
+ if (message.type === textsecure.protobuf.WebSocketMessage.Type.REQUEST ) {
+ handleRequest(
+ new IncomingWebSocketRequest({
+ verb : message.request.verb,
+ path : message.request.path,
+ body : message.request.body,
+ id : message.request.id,
+ socket : socket
+ })
+ );
+ }
+ else if (message.type === textsecure.protobuf.WebSocketMessage.Type.RESPONSE ) {
+ var response = message.response;
+ var request = outgoing[response.id];
+ if (request) {
+ request.response = response;
+ var callback = request.error;
+ if (response.status >= 200 && response.status < 300) {
+ callback = request.success;
+ }
+
+ if (typeof callback === 'function') {
+ callback(response.message, response.status, request);
+ }
+ } else {
+ throw 'Received response for unknown request ' + message.response.id;
+ }
+ }
+ };
+ reader.readAsArrayBuffer(blob);
+ };
+ };
+
+}());
+
+/* vim: ts=4:sw=4:expandtab
+ *
+ * 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 .
+ */
+
+window.textsecure = window.textsecure || {};
+
+/*********************************
+ *** Type conversion utilities ***
+ *********************************/
+// Strings/arrays
+//TODO: Throw all this shit in favor of consistent types
+//TODO: Namespace
+var StaticByteBufferProto = new dcodeIO.ByteBuffer().__proto__;
+var StaticArrayBufferProto = new ArrayBuffer().__proto__;
+var StaticUint8ArrayProto = new Uint8Array().__proto__;
+function getString(thing) {
+ if (thing === Object(thing)) {
+ if (thing.__proto__ == StaticUint8ArrayProto)
+ return String.fromCharCode.apply(null, thing);
+ if (thing.__proto__ == StaticArrayBufferProto)
+ return getString(new Uint8Array(thing));
+ if (thing.__proto__ == StaticByteBufferProto)
+ return thing.toString("binary");
+ }
+ return thing;
+}
+
+function getStringable(thing) {
+ return (typeof thing == "string" || typeof thing == "number" || typeof thing == "boolean" ||
+ (thing === Object(thing) &&
+ (thing.__proto__ == StaticArrayBufferProto ||
+ thing.__proto__ == StaticUint8ArrayProto ||
+ thing.__proto__ == StaticByteBufferProto)));
+}
+
+function isEqual(a, b, mayBeShort) {
+ // TODO: Special-case arraybuffers, etc
+ if (a === undefined || b === undefined)
+ return false;
+ a = getString(a);
+ b = getString(b);
+ var maxLength = mayBeShort ? Math.min(a.length, b.length) : Math.max(a.length, b.length);
+ if (maxLength < 5)
+ throw new Error("a/b compare too short");
+ return a.substring(0, Math.min(maxLength, a.length)) == b.substring(0, Math.min(maxLength, b.length));
+}
+
+function toArrayBuffer(thing) {
+ //TODO: Optimize this for specific cases
+ if (thing === undefined)
+ return undefined;
+ if (thing === Object(thing) && thing.__proto__ == StaticArrayBufferProto)
+ return thing;
+
+ 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;
+ }
+
+ if (!getStringable(thing))
+ throw new Error("Tried to convert a non-stringable thing of type " + typeof thing + " to an array buffer");
+ var str = getString(thing);
+ 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;
+}
+
+// Number formatting utils
+window.textsecure.utils = function() {
+ var self = {};
+ self.unencodeNumber = function(number) {
+ return number.split(".");
+ };
+
+ self.isNumberSane = function(number) {
+ return number[0] == "+" &&
+ /^[0-9]+$/.test(number.substring(1));
+ }
+
+ /**************************
+ *** JSON'ing Utilities ***
+ **************************/
+ function ensureStringed(thing) {
+ if (getStringable(thing))
+ return getString(thing);
+ else if (thing instanceof Array) {
+ var res = [];
+ for (var i = 0; i < thing.length; i++)
+ res[i] = ensureStringed(thing[i]);
+ return res;
+ } else if (thing === Object(thing)) {
+ var res = {};
+ for (var key in thing)
+ res[key] = ensureStringed(thing[key]);
+ return res;
+ }
+ throw new Error("unsure of how to jsonify object of type " + typeof thing);
+
+ }
+
+ self.jsonThing = function(thing) {
+ return JSON.stringify(ensureStringed(thing));
+ }
+
+ return self;
+}();
+
+window.textsecure.throwHumanError = function(error, type, humanError) {
+ var e = new Error(error);
+ if (type !== undefined)
+ e.name = type;
+ e.humanError = humanError;
+ throw e;
+}
+
+var handleAttachment = function(attachment) {
+ function getAttachment() {
+ return textsecure.api.getAttachment(attachment.id.toString());
+ }
+
+ function decryptAttachment(encrypted) {
+ return textsecure.protocol.decryptAttachment(
+ encrypted,
+ attachment.key.toArrayBuffer()
+ );
+ }
+
+ function updateAttachment(data) {
+ attachment.data = data;
+ }
+
+ return getAttachment().
+ then(decryptAttachment).
+ then(updateAttachment);
+};
+
+textsecure.processDecrypted = function(decrypted, source) {
+
+ // Now that its decrypted, validate the message and clean it up for consumer processing
+ // Note that messages may (generally) only perform one action and we ignore remaining fields
+ // after the first action.
+
+ if (decrypted.flags == null)
+ decrypted.flags = 0;
+
+ if ((decrypted.flags & textsecure.protobuf.PushMessageContent.Flags.END_SESSION)
+ == textsecure.protobuf.PushMessageContent.Flags.END_SESSION)
+ return;
+ if (decrypted.flags != 0) {
+ throw new Error("Unknown flags in message");
+ }
+
+ var promises = [];
+
+ if (decrypted.group !== null) {
+ decrypted.group.id = getString(decrypted.group.id);
+ var existingGroup = textsecure.storage.groups.getNumbers(decrypted.group.id);
+ if (existingGroup === undefined) {
+ if (decrypted.group.type != textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE) {
+ throw new Error("Got message for unknown group");
+ }
+ textsecure.storage.groups.createNewGroup(decrypted.group.members, decrypted.group.id);
+ if (decrypted.group.avatar !== null) {
+ promises.push(handleAttachment(decrypted.group.avatar));
+ }
+ } else {
+ var fromIndex = existingGroup.indexOf(source);
+
+ if (fromIndex < 0) {
+ //TODO: This could be indication of a race...
+ throw new Error("Sender was not a member of the group they were sending from");
+ }
+
+ switch(decrypted.group.type) {
+ case textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE:
+ if (decrypted.group.avatar !== null)
+ promises.push(handleAttachment(decrypted.group.avatar));
+
+ if (decrypted.group.members.filter(function(number) { return !textsecure.utils.isNumberSane(number); }).length != 0)
+ throw new Error("Invalid number in new group members");
+
+ if (existingGroup.filter(function(number) { decrypted.group.members.indexOf(number) < 0 }).length != 0)
+ throw new Error("Attempted to remove numbers from group with an UPDATE");
+ decrypted.group.added = decrypted.group.members.filter(function(number) { return existingGroup.indexOf(number) < 0; });
+
+ var newGroup = textsecure.storage.groups.addNumbers(decrypted.group.id, decrypted.group.added);
+ if (newGroup.length != decrypted.group.members.length ||
+ newGroup.filter(function(number) { return decrypted.group.members.indexOf(number) < 0; }).length != 0) {
+ throw new Error("Error calculating group member difference");
+ }
+
+ //TODO: Also follow this path if avatar + name haven't changed (ie we should start storing those)
+ if (decrypted.group.avatar === null && decrypted.group.added.length == 0 && decrypted.group.name === null) {
+ return;
+ }
+
+ decrypted.body = null;
+ decrypted.attachments = [];
+
+ break;
+ case textsecure.protobuf.PushMessageContent.GroupContext.Type.QUIT:
+ textsecure.storage.groups.removeNumber(decrypted.group.id, source);
+
+ decrypted.body = null;
+ decrypted.attachments = [];
+ case textsecure.protobuf.PushMessageContent.GroupContext.Type.DELIVER:
+ decrypted.group.name = null;
+ decrypted.group.members = [];
+ decrypted.group.avatar = null;
+
+ break;
+ default:
+ throw new Error("Unknown group message type");
+ }
+ }
+ }
+
+ for (var i in decrypted.attachments) {
+ promises.push(handleAttachment(decrypted.attachments[i]));
+ }
+ return Promise.all(promises).then(function() {
+ return decrypted;
+ });
+}
+
+window.textsecure.registerSingleDevice = function(number, verificationCode, stepDone) {
+ var signalingKey = textsecure.crypto.getRandomBytes(32 + 20);
+ textsecure.storage.putEncrypted('signaling_key', signalingKey);
+
+ var password = btoa(getString(textsecure.crypto.getRandomBytes(16)));
+ password = password.substring(0, password.length - 2);
+ textsecure.storage.putEncrypted("password", password);
+
+ var registrationId = new Uint16Array(textsecure.crypto.getRandomBytes(2))[0];
+ registrationId = registrationId & 0x3fff;
+ textsecure.storage.putUnencrypted("registrationId", registrationId);
+
+ return textsecure.api.confirmCode(number, verificationCode, password, signalingKey, registrationId, true).then(function() {
+ var numberId = number + ".1";
+ textsecure.storage.putUnencrypted("number_id", numberId);
+ textsecure.storage.putUnencrypted("regionCode", libphonenumber.util.getRegionCodeForNumber(number));
+ stepDone(1);
+
+ return textsecure.protocol.generateKeys().then(function(keys) {
+ stepDone(2);
+ return textsecure.api.registerKeys(keys).then(function() {
+ stepDone(3);
+ });
+ });
+ });
+}
+
+window.textsecure.registerSecondDevice = function(encodedDeviceInit, cryptoInfo, stepDone) {
+ var deviceInit = textsecure.protobuf.DeviceInit.decode(encodedDeviceInit, 'binary');
+ return cryptoInfo.decryptAndHandleDeviceInit(deviceInit).then(function(identityKey) {
+ if (identityKey.server != textsecure.api.relay)
+ throw new Error("Unknown relay used by master");
+ var number = identityKey.phoneNumber;
+
+ stepDone(1);
+
+ var signalingKey = textsecure.crypto.getRandomBytes(32 + 20);
+ textsecure.storage.putEncrypted('signaling_key', signalingKey);
+
+ var password = btoa(getString(textsecure.crypto.getRandomBytes(16)));
+ password = password.substring(0, password.length - 2);
+ textsecure.storage.putEncrypted("password", password);
+
+ var registrationId = new Uint16Array(textsecure.crypto.getRandomBytes(2))[0];
+ registrationId = registrationId & 0x3fff;
+ textsecure.storage.putUnencrypted("registrationId", registrationId);
+
+ return textsecure.api.confirmCode(number, identityKey.provisioningCode, password, signalingKey, registrationId, false).then(function(result) {
+ var numberId = number + "." + result;
+ textsecure.storage.putUnencrypted("number_id", numberId);
+ textsecure.storage.putUnencrypted("regionCode", libphonenumber.util.getRegion(number));
+ stepDone(2);
+
+ return textsecure.protocol.generateKeys().then(function(keys) {
+ stepDone(3);
+ return textsecure.api.registerKeys(keys).then(function() {
+ stepDone(4);
+ //TODO: Send DeviceControl.NEW_DEVICE_REGISTERED to all other devices
+ });
+ });
+ });
+ });
+};
+
+/* vim: ts=4:sw=4:expandtab
+ *
+ * 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';
+
+ var registeredFunctions = {};
+ var Type = {
+ SEND_MESSAGE: 1,
+ INIT_SESSION: 2,
+ };
+ window.textsecure = window.textsecure || {};
+ window.textsecure.replay = {
+ Type: Type,
+ registerFunction: function(func, functionCode) {
+ registeredFunctions[functionCode] = func;
+ }
+ };
+
+ function ReplayableError(options) {
+ options = options || {};
+ this.name = options.name || 'ReplayableError';
+ this.functionCode = options.functionCode;
+ this.args = options.args;
+ }
+ ReplayableError.prototype = new Error();
+ ReplayableError.prototype.constructor = ReplayableError;
+
+ ReplayableError.prototype.replay = function() {
+ var args = Array.prototype.slice.call(arguments);
+ args.shift();
+ args = this.args.concat(args);
+
+ registeredFunctions[this.functionCode].apply(window, args);
+ };
+
+ function IncomingIdentityKeyError(number, message) {
+ ReplayableError.call(this, {
+ functionCode : Type.INIT_SESSION,
+ args : [number, message]
+ });
+ this.name = 'IncomingIdentityKeyError';
+ this.message = "The identity of the sender has changed. This may be malicious, or the sender may have simply reinstalled TextSecure.";
+ }
+ IncomingIdentityKeyError.prototype = new ReplayableError();
+ IncomingIdentityKeyError.prototype.constructor = IncomingIdentityKeyError;
+
+ function OutgoingIdentityKeyError(number, message) {
+ ReplayableError.call(this, {
+ functionCode : Type.SEND_MESSAGE,
+ args : [number, message]
+ });
+ this.name = 'OutgoingIdentityKeyError';
+ this.message = "The identity of the destination has changed. This may be malicious, or the destination may have simply reinstalled TextSecure.";
+ }
+ OutgoingIdentityKeyError.prototype = new ReplayableError();
+ OutgoingIdentityKeyError.prototype.constructor = OutgoingIdentityKeyError;
+
+ window.textsecure.IncomingIdentityKeyError = IncomingIdentityKeyError;
+ window.textsecure.OutgoingIdentityKeyError = OutgoingIdentityKeyError;
+ window.textsecure.ReplayableError = ReplayableError;
+
+})();
+
+/* vim: ts=4:sw=4:expandtab
+ *
+ * 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.StringView = {
+
+ /*
+ * These functions from the Mozilla Developer Network
+ * and have been placed in the public domain.
+ * https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding
+ * https://developer.mozilla.org/en-US/docs/MDN/About#Copyrights_and_licenses
+ */
+
+ b64ToUint6: function(nChr) {
+ return nChr > 64 && nChr < 91 ?
+ nChr - 65
+ : nChr > 96 && nChr < 123 ?
+ nChr - 71
+ : nChr > 47 && nChr < 58 ?
+ nChr + 4
+ : nChr === 43 ?
+ 62
+ : nChr === 47 ?
+ 63
+ :
+ 0;
+ },
+
+ base64ToBytes: function(sBase64, nBlocksSize) {
+ var
+ sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ""), nInLen = sB64Enc.length,
+ nOutLen = nBlocksSize ? Math.ceil((nInLen * 3 + 1 >> 2) / nBlocksSize) * nBlocksSize : nInLen * 3 + 1 >> 2;
+ var aBBytes = new ArrayBuffer(nOutLen);
+ var taBytes = new Uint8Array(aBBytes);
+
+ for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) {
+ nMod4 = nInIdx & 3;
+ nUint24 |= StringView.b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4;
+ if (nMod4 === 3 || nInLen - nInIdx === 1) {
+ for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) {
+ taBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255;
+ }
+ nUint24 = 0;
+ }
+ }
+ return aBBytes;
+ },
+
+ uint6ToB64: function(nUint6) {
+ return nUint6 < 26 ?
+ nUint6 + 65
+ : nUint6 < 52 ?
+ nUint6 + 71
+ : nUint6 < 62 ?
+ nUint6 - 4
+ : nUint6 === 62 ?
+ 43
+ : nUint6 === 63 ?
+ 47
+ :
+ 65;
+ },
+
+ bytesToBase64: function(aBytes) {
+ var nMod3, sB64Enc = "";
+ for (var nLen = aBytes.length, nUint24 = 0, nIdx = 0; nIdx < nLen; nIdx++) {
+ nMod3 = nIdx % 3;
+ if (nIdx > 0 && (nIdx * 4 / 3) % 76 === 0) { sB64Enc += "\r\n"; }
+ nUint24 |= aBytes[nIdx] << (16 >>> nMod3 & 24);
+ if (nMod3 === 2 || aBytes.length - nIdx === 1) {
+ sB64Enc += String.fromCharCode(
+ StringView.uint6ToB64(nUint24 >>> 18 & 63),
+ StringView.uint6ToB64(nUint24 >>> 12 & 63),
+ StringView.uint6ToB64(nUint24 >>> 6 & 63),
+ StringView.uint6ToB64(nUint24 & 63)
+ );
+ nUint24 = 0;
+ }
+ }
+ return sB64Enc.replace(/A(?=A$|$)/g, "=");
+ }
+ };
+}());
+
+/* 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 .
+ */
+
+'use strict';
+
+;(function() {
+
+ /************************************************
+ *** Utilities to store data in local storage ***
+ ************************************************/
+ window.textsecure = window.textsecure || {};
+ window.textsecure.storage = window.textsecure.storage || {};
+
+ window.textsecure.storage = {
+
+ /*****************************
+ *** Base Storage Routines ***
+ *****************************/
+ putEncrypted: function(key, value) {
+ //TODO
+ if (value === undefined)
+ throw new Error("Tried to store undefined");
+ localStorage.setItem("e" + key, textsecure.utils.jsonThing(value));
+ },
+
+ getEncrypted: function(key, defaultValue) {
+ //TODO
+ var value = localStorage.getItem("e" + key);
+ if (value === null)
+ return defaultValue;
+ return JSON.parse(value);
+ },
+
+ removeEncrypted: function(key) {
+ localStorage.removeItem("e" + key);
+ },
+
+ putUnencrypted: function(key, value) {
+ if (value === undefined)
+ throw new Error("Tried to store undefined");
+ localStorage.setItem("u" + key, textsecure.utils.jsonThing(value));
+ },
+
+ getUnencrypted: function(key, defaultValue) {
+ var value = localStorage.getItem("u" + key);
+ if (value === null)
+ return defaultValue;
+ return JSON.parse(value);
+ },
+
+ removeUnencrypted: function(key) {
+ localStorage.removeItem("u" + key);
+ }
+ };
+})();
+
+
+/* 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 .
+ */
+
+'use strict';
+
+;(function() {
+ /**********************
+ *** Device Storage ***
+ **********************/
+ window.textsecure = window.textsecure || {};
+ window.textsecure.storage = window.textsecure.storage || {};
+
+ window.textsecure.storage.devices = {
+ saveDeviceObject: function(deviceObject) {
+ return internalSaveDeviceObject(deviceObject, false);
+ },
+
+ saveKeysToDeviceObject: function(deviceObject) {
+ return internalSaveDeviceObject(deviceObject, true);
+ },
+
+ getDeviceObjectsForNumber: function(number) {
+ var map = textsecure.storage.getEncrypted("devices" + number);
+ return map === undefined ? [] : map.devices;
+ },
+
+ getDeviceObject: function(encodedNumber) {
+ var number = textsecure.utils.unencodeNumber(encodedNumber);
+ var devices = textsecure.storage.devices.getDeviceObjectsForNumber(number[0]);
+ if (devices === undefined)
+ return undefined;
+
+ for (var i in devices)
+ if (devices[i].encodedNumber == encodedNumber)
+ return devices[i];
+
+ return undefined;
+ },
+
+ removeDeviceIdsForNumber: function(number, deviceIdsToRemove) {
+ var map = textsecure.storage.getEncrypted("devices" + number);
+ if (map === undefined)
+ throw new Error("Tried to remove device for unknown number");
+
+ var newDevices = [];
+ var devicesRemoved = 0;
+ for (var i in map.devices) {
+ var keep = true;
+ for (var j in deviceIdsToRemove)
+ if (map.devices[i].encodedNumber == number + "." + deviceIdsToRemove[j])
+ keep = false;
+
+ if (keep)
+ newDevices.push(map.devices[i]);
+ else
+ devicesRemoved++;
+ }
+
+ if (devicesRemoved != deviceIdsToRemove.length)
+ throw new Error("Tried to remove unknown device");
+ }
+ };
+
+ var internalSaveDeviceObject = function(deviceObject, onlyKeys) {
+ 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 (map === undefined)
+ map = { devices: [deviceObject], identityKey: deviceObject.identityKey };
+ else if (map.identityKey != getString(deviceObject.identityKey))
+ throw new Error("Identity key changed");
+ else {
+ var updated = false;
+ for (var i in map.devices) {
+ if (map.devices[i].encodedNumber == deviceObject.encodedNumber) {
+ if (!onlyKeys)
+ map.devices[i] = deviceObject;
+ else {
+ map.devices[i].preKey = deviceObject.preKey;
+ map.devices[i].preKeyId = deviceObject.preKeyId;
+ map.devices[i].signedKey = deviceObject.signedKey;
+ map.devices[i].signedKeyId = deviceObject.signedKeyId;
+ map.devices[i].registrationId = deviceObject.registrationId;
+ }
+ updated = true;
+ }
+ }
+
+ if (!updated)
+ map.devices.push(deviceObject);
+ }
+
+ textsecure.storage.putEncrypted("devices" + number, map);
+ };
+})();
+
+/* 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 .
+ */
+
+'use strict';
+
+;(function() {
+ /*********************
+ *** Group Storage ***
+ *********************/
+ window.textsecure = window.textsecure || {};
+ window.textsecure.storage = window.textsecure.storage || {};
+
+ window.textsecure.storage.groups = {
+ getGroupListForNumber: function(number) {
+ return textsecure.storage.getEncrypted("groupMembership" + number, []);
+ },
+
+ createNewGroup: function(numbers, groupId) {
+ if (groupId !== undefined && textsecure.storage.getEncrypted("group" + groupId) !== undefined) {
+ throw new Error("Tried to recreate group");
+ }
+
+ while (groupId === undefined || textsecure.storage.getEncrypted("group" + groupId) !== undefined) {
+ groupId = getString(textsecure.crypto.getRandomBytes(16));
+ }
+
+ var me = textsecure.utils.unencodeNumber(textsecure.storage.getUnencrypted("number_id"))[0];
+ var haveMe = false;
+ var finalNumbers = [];
+ for (var i in numbers) {
+ var number = numbers[i];
+ if (!textsecure.utils.isNumberSane(number))
+ throw new Error("Invalid number in group");
+ if (number == me)
+ haveMe = true;
+ if (finalNumbers.indexOf(number) < 0) {
+ finalNumbers.push(number);
+ addGroupToNumber(groupId, number);
+ }
+ }
+
+ if (!haveMe)
+ finalNumbers.push(me);
+
+ textsecure.storage.putEncrypted("group" + groupId, {numbers: finalNumbers});
+
+ return {id: groupId, numbers: finalNumbers};
+ },
+
+ getNumbers: function(groupId) {
+ var group = textsecure.storage.getEncrypted("group" + groupId);
+ if (group === undefined)
+ return undefined;
+
+ return group.numbers;
+ },
+
+ removeNumber: function(groupId, number) {
+ var group = textsecure.storage.getEncrypted("group" + groupId);
+ if (group === undefined)
+ return undefined;
+
+ var me = textsecure.utils.unencodeNumber(textsecure.storage.getUnencrypted("number_id"))[0];
+ if (number == me)
+ throw new Error("Cannot remove ourselves from a group, leave the group instead");
+
+ var i = group.numbers.indexOf(number);
+ if (i > -1) {
+ group.numbers.slice(i, 1);
+ textsecure.storage.putEncrypted("group" + groupId, group);
+ removeGroupFromNumber(groupId, number);
+ }
+
+ return group.numbers;
+ },
+
+ addNumbers: function(groupId, numbers) {
+ var group = textsecure.storage.getEncrypted("group" + groupId);
+ if (group === undefined)
+ return undefined;
+
+ for (var i in numbers) {
+ var number = numbers[i];
+ if (!textsecure.utils.isNumberSane(number))
+ throw new Error("Invalid number in set to add to group");
+ if (group.numbers.indexOf(number) < 0) {
+ group.numbers.push(number);
+ addGroupToNumber(groupId, number);
+ }
+ }
+
+ textsecure.storage.putEncrypted("group" + groupId, group);
+ return group.numbers;
+ },
+
+ deleteGroup: function(groupId) {
+ textsecure.storage.removeEncrypted("group" + groupId);
+ },
+
+ getGroup: function(groupId) {
+ var group = textsecure.storage.getEncrypted("group" + groupId);
+ if (group === undefined)
+ return undefined;
+
+ return { id: groupId, numbers: group.numbers }; //TODO: avatar/name tracking
+ }
+ };
+
+ var addGroupToNumber = function(groupId, number) {
+ var membership = textsecure.storage.getEncrypted("groupMembership" + number, [groupId]);
+ if (membership.indexOf(groupId) < 0)
+ membership.push(groupId);
+ textsecure.storage.putEncrypted("groupMembership" + number, membership);
+ }
+
+ var removeGroupFromNumber = function(groupId, number) {
+ var membership = textsecure.storage.getEncrypted("groupMembership" + number, [groupId]);
+ membership = membership.filter(function(group) { return group != groupId; });
+ if (membership.length == 0)
+ textsecure.storage.removeEncrypted("groupMembership" + number);
+ else
+ textsecure.storage.putEncrypted("groupMembership" + number, membership);
+ }
+
+})();
+
+/* vim: ts=4:sw=4:expandtab
+ *
+ * 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 .
+ */
+
+window.textsecure = window.textsecure || {};
+
+window.textsecure.api = function () {
+ 'use strict';
+
+ var self = {};
+
+ /************************************************
+ *** Utilities to communicate with the server ***
+ ************************************************/
+ // Staging server
+ var URL_BASE = "https://textsecure-service-staging.whispersystems.org";
+ self.relay = "textsecure-service-staging.whispersystems.org";
+ var ATTACHMENT_HOST = "whispersystems-textsecure-attachments-staging.s3.amazonaws.com";
+
+ // This is the real server
+ //var URL_BASE = "https://textsecure-service.whispersystems.org";
+
+ var URL_CALLS = {};
+ URL_CALLS.accounts = "/v1/accounts";
+ URL_CALLS.devices = "/v1/devices";
+ URL_CALLS.keys = "/v2/keys";
+ URL_CALLS.push = "/v1/websocket";
+ URL_CALLS.temp_push = "/v1/temp_websocket";
+ URL_CALLS.messages = "/v1/messages";
+ URL_CALLS.attachment = "/v1/attachments";
+
+ /**
+ * REQUIRED PARAMS:
+ * call: URL_CALLS entry
+ * httpType: POST/GET/PUT/etc
+ * OPTIONAL PARAMS:
+ * success_callback: function(response object) called on success
+ * error_callback: function(http status code = -1 or != 200) called on failure
+ * urlParameters: crap appended to the url (probably including a leading /)
+ * user: user name to be sent in a basic auth header
+ * password: password to be sent in a basic auth headerA
+ * do_auth: alternative to user/password where user/password are figured out automagically
+ * jsonData: JSON data sent in the request body
+ */
+ var doAjax = function (param) {
+ if (param.urlParameters === undefined) {
+ param.urlParameters = "";
+ }
+
+ if (param.do_auth) {
+ param.user = textsecure.storage.getUnencrypted("number_id");
+ param.password = textsecure.storage.getEncrypted("password");
+ }
+
+ return new Promise(function (resolve, reject) {
+ $.ajax(URL_BASE + URL_CALLS[param.call] + param.urlParameters, {
+ type : param.httpType,
+ data : param.jsonData && textsecure.utils.jsonThing(param.jsonData),
+ contentType : 'application/json; charset=utf-8',
+ dataType : 'json',
+
+ beforeSend : function (xhr) {
+ if (param.user !== undefined &&
+ param.password !== undefined)
+ xhr.setRequestHeader("Authorization", "Basic " + btoa(getString(param.user) + ":" + getString(param.password)));
+ },
+
+ success : function(response, textStatus, jqXHR) {
+ resolve(response);
+ },
+
+ error : function(jqXHR, textStatus, errorThrown) {
+ var code = jqXHR.status;
+ if (code === 200) {
+ // happens sometimes when we get no response
+ // (TODO: Fix server to return 204? instead)
+ resolve(null);
+ return;
+ }
+ if (code > 999 || code < 100)
+ code = -1;
+ try {
+ switch (code) {
+ case -1:
+ textsecure.throwHumanError(code, "HTTPError",
+ "Failed to connect to the server, please check your network connection.");
+ case 413:
+ textsecure.throwHumanError(code, "HTTPError",
+ "Rate limit exceeded, please try again later.");
+ case 403:
+ textsecure.throwHumanError(code, "HTTPError",
+ "Invalid code, please try again.");
+ case 417:
+ // TODO: This shouldn't be a thing?, but its in the API doc?
+ textsecure.throwHumanError(code, "HTTPError",
+ "Number already registered.");
+ case 401:
+ textsecure.throwHumanError(code, "HTTPError",
+ "Invalid authentication, most likely someone re-registered and invalidated our registration.");
+ case 404:
+ textsecure.throwHumanError(code, "HTTPError",
+ "Number is not registered with TextSecure.");
+ default:
+ textsecure.throwHumanError(code, "HTTPError",
+ "The server rejected our query, please file a bug report.");
+ }
+ } catch (e) {
+ if (jqXHR.responseJSON)
+ e.response = jqXHR.responseJSON;
+ reject(e);
+ }
+ }
+ });
+ });
+ };
+
+ function requestVerificationCode(number, transport) {
+ return doAjax({
+ call : 'accounts',
+ httpType : 'GET',
+ urlParameters : '/' + transport + '/code/' + number,
+ });
+ };
+ self.requestVerificationSMS = function(number) {
+ return requestVerificationCode(number, 'sms');
+ };
+ self.requestVerificationVoice = function(number) {
+ return requestVerificationCode(number, 'voice');
+ };
+
+ self.confirmCode = function(number, code, password,
+ signaling_key, registrationId, single_device) {
+ var call = single_device ? 'accounts' : 'devices';
+ var urlPrefix = single_device ? '/code/' : '/';
+
+ return doAjax({
+ call : call,
+ httpType : 'PUT',
+ urlParameters : urlPrefix + code,
+ user : number,
+ password : password,
+ jsonData : { signalingKey : btoa(getString(signaling_key)),
+ supportsSms : false,
+ fetchesMessages : true,
+ registrationId : registrationId}
+ });
+ };
+
+ self.registerKeys = function(genKeys) {
+ var keys = {};
+ keys.identityKey = btoa(getString(genKeys.identityKey));
+ keys.signedPreKey = {keyId: genKeys.signedPreKey.keyId, publicKey: btoa(getString(genKeys.signedPreKey.publicKey)),
+ signature: btoa(getString(genKeys.signedPreKey.signature))};
+
+ keys.preKeys = [];
+ var j = 0;
+ for (var i in genKeys.preKeys)
+ keys.preKeys[j++] = {keyId: i, publicKey: btoa(getString(genKeys.preKeys[i].publicKey))};
+
+ //TODO: This is just to make the server happy (v2 clients should choke on publicKey),
+ // it needs removed before release
+ keys.lastResortKey = {keyId: 0x7fffFFFF, publicKey: btoa("42")};
+
+ return doAjax({
+ call : 'keys',
+ httpType : 'PUT',
+ do_auth : true,
+ jsonData : keys,
+ });
+ };
+
+ self.getKeysForNumber = function(number, deviceId) {
+ if (deviceId === undefined)
+ deviceId = "*";
+
+ return doAjax({
+ call : 'keys',
+ httpType : 'GET',
+ do_auth : true,
+ urlParameters : "/" + number + "/" + deviceId,
+ }).then(function(res) {
+ var promises = [];
+ res.identityKey = StringView.base64ToBytes(res.identityKey);
+ for (var i = 0; i < res.devices.length; i++) {
+ res.devices[i].signedPreKey.publicKey = StringView.base64ToBytes(res.devices[i].signedPreKey.publicKey);
+ res.devices[i].signedPreKey.signature = StringView.base64ToBytes(res.devices[i].signedPreKey.signature);
+ promises[i] = window.textsecure.crypto.Ed25519Verify(res.identityKey, res.devices[i].signedPreKey.publicKey, res.devices[i].signedPreKey.signature);
+ res.devices[i].preKey.publicKey = StringView.base64ToBytes(res.devices[i].preKey.publicKey);
+ //TODO: Is this still needed?
+ //if (res.devices[i].keyId === undefined)
+ // res.devices[i].keyId = 0;
+ }
+ return Promise.all(promises).then(function() {
+ return res;
+ });
+ });
+ };
+
+ self.sendMessages = function(destination, messageArray) {
+ //TODO: Do this conversion somewhere else?
+ for (var i = 0; i < messageArray.length; i++)
+ messageArray[i].body = btoa(messageArray[i].body);
+ var jsonData = { messages: messageArray };
+ if (messageArray[0].relay !== undefined)
+ jsonData.relay = messageArray[0].relay;
+ jsonData.timestamp = messageArray[0].timestamp;
+
+ return doAjax({
+ call : 'messages',
+ httpType : 'PUT',
+ urlParameters : '/' + destination,
+ do_auth : true,
+ jsonData : jsonData,
+ });
+ };
+
+ self.getAttachment = function(id) {
+ return doAjax({
+ call : 'attachment',
+ httpType : 'GET',
+ urlParameters : '/' + id,
+ do_auth : true,
+ }).then(function(response) {
+ return new Promise(function(resolve, reject) {
+ $.ajax(response.location, {
+ type : "GET",
+ xhrFields: {
+ responseType: "arraybuffer"
+ },
+ headers: {
+ "Content-Type": "application/octet-stream"
+ },
+
+ success : function(response, textStatus, jqXHR) {
+ resolve(response);
+ },
+
+ error : function(jqXHR, textStatus, errorThrown) {
+ var code = jqXHR.status;
+ if (code > 999 || code < 100)
+ code = -1;
+
+ var e = new Error(code);
+ e.name = "HTTPError";
+ if (jqXHR.responseJSON)
+ e.response = jqXHR.responseJSON;
+ reject(e);
+ }
+ });
+ });
+ });
+ };
+
+ var id_regex = RegExp( "^https:\/\/" + ATTACHMENT_HOST + "\/(\\d+)\?");
+ self.putAttachment = function(encryptedBin) {
+ return doAjax({
+ call : 'attachment',
+ httpType : 'GET',
+ do_auth : true,
+ }).then(function(response) {
+ return new Promise(function(resolve, reject) {
+ $.ajax(response.location, {
+ type : "PUT",
+ headers : {"Content-Type" : "application/octet-stream"},
+ data : encryptedBin,
+ processData : false,
+ success : function() {
+ try {
+ // Parse the id as a string from the location url
+ // (workaround for ids too large for Javascript numbers)
+ var id = response.location.match(id_regex)[1];
+ resolve(id);
+ } catch(e) {
+ reject(e);
+ }
+ },
+ error : function(jqXHR, textStatus, errorThrown) {
+ var code = jqXHR.status;
+ if (code > 999 || code < 100)
+ code = -1;
+
+ var e = new Error(code);
+ e.name = "HTTPError";
+ if (jqXHR.responseJSON)
+ e.response = jqXHR.responseJSON;
+ reject(e);
+ }
+ });
+ });
+ });
+ };
+
+ var getWebsocket = function(url, auth, reconnectTimeout) {
+ var URL = URL_BASE.replace(/^http/g, 'ws') + url + '/?';
+ var params = '';
+ if (auth) {
+ var user = textsecure.storage.getUnencrypted("number_id");
+ var password = textsecure.storage.getEncrypted("password");
+ var params = $.param({
+ login: '+' + user.substring(1),
+ password: password
+ });
+ }
+ return window.textsecure.websocket(URL+params)
+ }
+
+ self.getMessageWebsocket = function() {
+ return getWebsocket(URL_CALLS['push'], true, 1000);
+ }
+
+ self.getTempWebsocket = function() {
+ //XXX
+ var socketWrapper = { onmessage: function() {}, ondisconnect: function() {}, onconnect: function() {} };
+ setTimeout(function() {
+ socketWrapper.onmessage({uuid: "404-42-magic"});
+ }, 1000);
+ return socketWrapper;
+ //return getWebsocket(URL_CALLS['temp_push'], false, 5000);
+ }
+
+ return self;
+}();
+
+/* 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() {
+ window.textsecure = window.textsecure || {};
+
+ /*
+ * textsecure.crypto
+ * glues together various implementations into a single interface
+ * for all low-level crypto operations,
+ */
+
+ function curve25519() {
+ // use native client opportunistically, since it's faster
+ return textsecure.nativeclient || window.curve25519;
+ }
+
+ window.textsecure.crypto = {
+ getRandomBytes: function(size) {
+ // At some point we might consider XORing in hashes of random
+ // UI events to strengthen ourselves against RNG flaws in crypto.getRandomValues
+ // ie maybe take a look at how Gibson does it at https://www.grc.com/r&d/js.htm
+ var array = new Uint8Array(size);
+ window.crypto.getRandomValues(array);
+ return array.buffer;
+ },
+ encrypt: function(key, data, iv) {
+ return window.crypto.subtle.importKey('raw', key, {name: 'AES-CBC'}, false, ['encrypt']).then(function(key) {
+ return window.crypto.subtle.encrypt({name: 'AES-CBC', iv: new Uint8Array(iv)}, key, data);
+ });
+ },
+ decrypt: function(key, data, iv) {
+ return window.crypto.subtle.importKey('raw', key, {name: 'AES-CBC'}, false, ['decrypt']).then(function(key) {
+ return window.crypto.subtle.decrypt({name: 'AES-CBC', iv: new Uint8Array(iv)}, key, data);
+ });
+ },
+ sign: function(key, data) {
+ return window.crypto.subtle.importKey('raw', key, {name: 'HMAC', hash: {name: 'SHA-256'}}, false, ['sign']).then(function(key) {
+ return window.crypto.subtle.sign( {name: 'HMAC', hash: 'SHA-256'}, key, data);
+ });
+ },
+
+ HKDF: function(input, salt, info) {
+ // Specific implementation of RFC 5869 that only returns the first 3 32-byte chunks
+ // TODO: We dont always need the third chunk, we might skip it
+ return window.textsecure.crypto.sign(salt, input).then(function(PRK) {
+ var infoBuffer = new ArrayBuffer(info.byteLength + 1 + 32);
+ var infoArray = new Uint8Array(infoBuffer);
+ infoArray.set(new Uint8Array(info), 32);
+ infoArray[infoArray.length - 1] = 1;
+ return window.textsecure.crypto.sign(PRK, infoBuffer.slice(32)).then(function(T1) {
+ infoArray.set(new Uint8Array(T1));
+ infoArray[infoArray.length - 1] = 2;
+ return window.textsecure.crypto.sign(PRK, infoBuffer).then(function(T2) {
+ infoArray.set(new Uint8Array(T2));
+ infoArray[infoArray.length - 1] = 3;
+ return window.textsecure.crypto.sign(PRK, infoBuffer).then(function(T3) {
+ return [ T1, T2, T3 ];
+ });
+ });
+ });
+ });
+ },
+
+ // Curve 25519 crypto
+ createKeyPair: function(privKey) {
+ if (privKey === undefined) {
+ privKey = textsecure.crypto.getRandomBytes(32);
+ }
+ if (privKey.byteLength != 32) {
+ throw new Error("Invalid private key");
+ }
+
+ return curve25519().keyPair(privKey).then(function(raw_keys) {
+ // prepend version byte
+ var origPub = new Uint8Array(raw_keys.pubKey);
+ var pub = new Uint8Array(33);
+ pub.set(origPub, 1);
+ pub[0] = 5;
+
+ return { pubKey: pub.buffer, privKey: raw_keys.privKey };
+ });
+ },
+ ECDHE: function(pubKey, privKey) {
+ pubKey = validatePubKeyFormat(pubKey);
+ if (privKey === undefined || privKey.byteLength != 32)
+ throw new Error("Invalid private key");
+
+ if (pubKey === undefined || pubKey.byteLength != 32)
+ throw new Error("Invalid public key");
+
+ return curve25519().sharedSecret(pubKey, privKey);
+ },
+ Ed25519Sign: function(privKey, message) {
+ if (privKey === undefined || privKey.byteLength != 32)
+ throw new Error("Invalid private key");
+
+ if (message === undefined)
+ throw new Error("Invalid message");
+
+ return curve25519().sign(privKey, message);
+ },
+ Ed25519Verify: function(pubKey, msg, sig) {
+ pubKey = validatePubKeyFormat(pubKey);
+
+ if (pubKey === undefined || pubKey.byteLength != 32)
+ throw new Error("Invalid public key");
+
+ if (msg === undefined)
+ throw new Error("Invalid message");
+
+ if (sig === undefined || sig.byteLength != 64)
+ throw new Error("Invalid signature");
+
+ return curve25519().verify(pubKey, msg, sig);
+ }
+ };
+
+ var validatePubKeyFormat = function(pubKey) {
+ if (pubKey === undefined || ((pubKey.byteLength != 33 || new Uint8Array(pubKey)[0] != 5) && pubKey.byteLength != 32))
+ throw new Error("Invalid public key");
+ if (pubKey.byteLength == 33) {
+ return pubKey.slice(1);
+ } else {
+ console.error("WARNING: Expected pubkey of length 33, please report the ST and client that generated the pubkey");
+ return pubKey;
+ }
+ };
+
+})();
+
+/* 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.textsecure = window.textsecure || {};
+
+window.textsecure.protocol = function() {
+ var self = {};
+
+ /******************************
+ *** Random constants/utils ***
+ ******************************/
+ // We consider messages lost after a week and might throw away keys at that point
+ // (also the time between signedPreKey regenerations)
+ var MESSAGE_LOST_THRESHOLD_MS = 1000*60*60*24*7;
+
+ function objectContainsKeys(object) {
+ var count = 0;
+ for (var key in object) {
+ count++;
+ break;
+ }
+ return count != 0;
+ }
+
+ /***************************
+ *** Key/session storage ***
+ ***************************/
+ var crypto_storage = {};
+
+ crypto_storage.putKeyPair = function(keyName, keyPair) {
+ textsecure.storage.putEncrypted("25519Key" + keyName, keyPair);
+ }
+
+ crypto_storage.getNewStoredKeyPair = function(keyName) {
+ return textsecure.crypto.createKeyPair().then(function(keyPair) {
+ crypto_storage.putKeyPair(keyName, keyPair);
+ return keyPair;
+ });
+ }
+
+ crypto_storage.getStoredKeyPair = function(keyName) {
+ var res = textsecure.storage.getEncrypted("25519Key" + keyName);
+ if (res === undefined)
+ return undefined;
+ return { pubKey: toArrayBuffer(res.pubKey), privKey: toArrayBuffer(res.privKey) };
+ }
+
+ crypto_storage.removeStoredKeyPair = function(keyName) {
+ textsecure.storage.removeEncrypted("25519Key" + keyName);
+ }
+
+ crypto_storage.getIdentityKey = function() {
+ return this.getStoredKeyPair("identityKey");
+ }
+
+ crypto_storage.saveSession = function(encodedNumber, session, registrationId) {
+ var device = textsecure.storage.devices.getDeviceObject(encodedNumber);
+ if (device === undefined)
+ device = { sessions: {}, encodedNumber: encodedNumber };
+
+ if (registrationId !== undefined)
+ device.registrationId = registrationId;
+
+ crypto_storage.saveSessionAndDevice(device, session);
+ }
+
+ crypto_storage.saveSessionAndDevice = function(device, session) {
+ if (device.sessions === undefined)
+ device.sessions = {};
+ var sessions = device.sessions;
+
+ 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));
+
+ if (!doDeleteSession) {
+ var keysLeft = false;
+ for (var key in session) {
+ if (key != "indexInfo" && key != "oldRatchetList" && key != "currentRatchet") {
+ keysLeft = true;
+ break;
+ }
+ }
+ doDeleteSession = !keysLeft;
+ console.log((doDeleteSession ? "Deleting " : "Not deleting ") + "closed session which has not yet timed out");
+ } else
+ console.log("Deleting closed session due to timeout (created at " + session.indexInfo.closed + ")");
+ }
+
+ if (doDeleteSession)
+ delete sessions[getString(session.indexInfo.baseKey)];
+ else
+ sessions[getString(session.indexInfo.baseKey)] = session;
+
+ var openSessionRemaining = false;
+ for (var key in sessions)
+ if (sessions[key].indexInfo.closed == -1)
+ openSessionRemaining = true;
+ if (!openSessionRemaining)
+ try {
+ delete device['registrationId'];
+ } catch(_) {}
+
+ textsecure.storage.devices.saveDeviceObject(device);
+ }
+
+ var getSessions = function(encodedNumber) {
+ var device = textsecure.storage.devices.getDeviceObject(encodedNumber);
+ if (device === undefined || device.sessions === undefined)
+ return undefined;
+ return device.sessions;
+ }
+
+ crypto_storage.getOpenSession = function(encodedNumber) {
+ var sessions = getSessions(encodedNumber);
+ if (sessions === undefined)
+ return undefined;
+
+ for (var key in sessions)
+ if (sessions[key].indexInfo.closed == -1)
+ return sessions[key];
+ return undefined;
+ }
+
+ crypto_storage.getSessionByRemoteEphemeralKey = function(encodedNumber, remoteEphemeralKey) {
+ var sessions = getSessions(encodedNumber);
+ if (sessions === undefined)
+ return undefined;
+
+ var searchKey = getString(remoteEphemeralKey);
+
+ var openSession = undefined;
+ for (var key in sessions) {
+ if (sessions[key].indexInfo.closed == -1) {
+ if (openSession !== undefined)
+ throw new Error("Datastore inconsistensy: multiple open sessions for " + encodedNumber);
+ openSession = sessions[key];
+ }
+ if (sessions[key][searchKey] !== undefined)
+ return sessions[key];
+ }
+ if (openSession !== undefined)
+ return openSession;
+
+ return undefined;
+ }
+
+ crypto_storage.getSessionOrIdentityKeyByBaseKey = function(encodedNumber, baseKey) {
+ var sessions = getSessions(encodedNumber);
+ var device = textsecure.storage.devices.getDeviceObject(encodedNumber);
+ if (device === undefined)
+ return undefined;
+
+ var preferredSession = device.sessions && device.sessions[getString(baseKey)];
+ if (preferredSession !== undefined)
+ return preferredSession;
+
+ if (device.identityKey !== undefined)
+ return { indexInfo: { remoteIdentityKey: device.identityKey } };
+
+ throw new Error("Datastore inconsistency: device was stored without identity key");
+ }
+
+ /*****************************
+ *** Internal Crypto stuff ***
+ *****************************/
+ var HKDF = function(input, salt, info) {
+ // HKDF for TextSecure has a bit of additional handling - salts always end up being 32 bytes
+ if (salt == '')
+ salt = new ArrayBuffer(32);
+ if (salt.byteLength != 32)
+ throw new Error("Got salt of incorrect length");
+
+ info = toArrayBuffer(info); // TODO: maybe convert calls?
+
+ return textsecure.crypto.HKDF(input, salt, info);
+ }
+
+ var verifyMAC = function(data, key, mac) {
+ return textsecure.crypto.sign(key, data).then(function(calculated_mac) {
+ if (!isEqual(calculated_mac, mac, true))
+ throw new Error("Bad MAC");
+ });
+ }
+
+ /******************************
+ *** Ratchet implementation ***
+ ******************************/
+ var calculateRatchet = function(session, remoteKey, sending) {
+ var ratchet = session.currentRatchet;
+
+ return textsecure.crypto.ECDHE(remoteKey, toArrayBuffer(ratchet.ephemeralKeyPair.privKey)).then(function(sharedSecret) {
+ return HKDF(sharedSecret, toArrayBuffer(ratchet.rootKey), "WhisperRatchet").then(function(masterKey) {
+ if (sending)
+ session[getString(ratchet.ephemeralKeyPair.pubKey)] = { messageKeys: {}, chainKey: { counter: -1, key: masterKey[1] } };
+ else
+ session[getString(remoteKey)] = { messageKeys: {}, chainKey: { counter: -1, key: masterKey[1] } };
+ ratchet.rootKey = masterKey[0];
+ });
+ });
+ }
+
+ var initSession = function(isInitiator, ourEphemeralKey, ourSignedKey, encodedNumber, theirIdentityPubKey, theirEphemeralPubKey, theirSignedPubKey) {
+ var ourIdentityKey = crypto_storage.getIdentityKey();
+
+ if (isInitiator) {
+ if (ourSignedKey !== undefined)
+ throw new Error("Invalid call to initSession");
+ ourSignedKey = ourEphemeralKey;
+ } else {
+ if (theirSignedPubKey !== undefined)
+ throw new Error("Invalid call to initSession");
+ theirSignedPubKey = theirEphemeralPubKey;
+ }
+
+ var sharedSecret;
+ if (ourEphemeralKey === undefined || theirEphemeralPubKey === undefined)
+ sharedSecret = new Uint8Array(32 * 4);
+ else
+ sharedSecret = new Uint8Array(32 * 5);
+
+ for (var i = 0; i < 32; i++)
+ sharedSecret[i] = 0xff;
+
+ return textsecure.crypto.ECDHE(theirSignedPubKey, ourIdentityKey.privKey).then(function(ecRes1) {
+ function finishInit() {
+ return textsecure.crypto.ECDHE(theirSignedPubKey, ourSignedKey.privKey).then(function(ecRes) {
+ sharedSecret.set(new Uint8Array(ecRes), 32 * 3);
+
+ return HKDF(sharedSecret.buffer, '', "WhisperText").then(function(masterKey) {
+ var session = {currentRatchet: { rootKey: masterKey[0], lastRemoteEphemeralKey: theirSignedPubKey, previousCounter: 0 },
+ indexInfo: { remoteIdentityKey: theirIdentityPubKey, closed: -1 },
+ oldRatchetList: []
+ };
+ if (!isInitiator)
+ session.indexInfo.baseKey = theirEphemeralPubKey;
+ else
+ session.indexInfo.baseKey = ourEphemeralKey.pubKey;
+
+ // If we're initiating we go ahead and set our first sending ephemeral key now,
+ // otherwise we figure it out when we first maybeStepRatchet with the remote's ephemeral key
+ if (isInitiator) {
+ return textsecure.crypto.createKeyPair().then(function(ourSendingEphemeralKey) {
+ session.currentRatchet.ephemeralKeyPair = ourSendingEphemeralKey;
+ return calculateRatchet(session, theirSignedPubKey, true).then(function() {
+ return session;
+ });
+ });
+ } else {
+ session.currentRatchet.ephemeralKeyPair = ourSignedKey;
+ return session;
+ }
+ });
+ });
+ }
+
+ var promise;
+ if (ourEphemeralKey === undefined || theirEphemeralPubKey === undefined)
+ promise = Promise.resolve(new ArrayBuffer(0));
+ else
+ promise = textsecure.crypto.ECDHE(theirEphemeralPubKey, ourEphemeralKey.privKey);
+ return promise.then(function(ecRes4) {
+ sharedSecret.set(new Uint8Array(ecRes4), 32 * 4);
+
+ if (isInitiator)
+ return textsecure.crypto.ECDHE(theirIdentityPubKey, ourSignedKey.privKey).then(function(ecRes2) {
+ sharedSecret.set(new Uint8Array(ecRes1), 32);
+ sharedSecret.set(new Uint8Array(ecRes2), 32 * 2);
+ }).then(finishInit);
+ else
+ return textsecure.crypto.ECDHE(theirIdentityPubKey, ourSignedKey.privKey).then(function(ecRes2) {
+ sharedSecret.set(new Uint8Array(ecRes1), 32 * 2);
+ sharedSecret.set(new Uint8Array(ecRes2), 32)
+ }).then(finishInit);
+ });
+ });
+ }
+
+ var removeOldChains = function(session) {
+ // Sending ratchets are always removed when we step because we never need them again
+ // Receiving ratchets are either removed if we step with all keys used up to previousCounter
+ // and are otherwise added to the oldRatchetList, which we parse here and remove ratchets
+ // older than a week (we assume the message was lost and move on with our lives at that point)
+ var newList = [];
+ for (var i = 0; i < session.oldRatchetList.length; i++) {
+ var entry = session.oldRatchetList[i];
+ var ratchet = getString(entry.ephemeralKey);
+ console.log("Checking old chain with added time " + (entry.added/1000));
+ if ((!objectContainsKeys(session[ratchet].messageKeys) && (session[ratchet].chainKey === undefined || session[ratchet].chainKey.key === undefined))
+ || entry.added < new Date().getTime() - MESSAGE_LOST_THRESHOLD_MS) {
+ delete session[ratchet];
+ console.log("...deleted");
+ } else
+ newList[newList.length] = entry;
+ }
+ session.oldRatchetList = newList;
+ }
+
+ var closeSession = function(session, sessionClosedByRemote) {
+ if (session.indexInfo.closed > -1)
+ return;
+
+ // After this has run, we can still receive messages on ratchet chains which
+ // were already open (unless we know we dont need them),
+ // but we cannot send messages or step the ratchet
+
+ // Delete current sending ratchet
+ delete session[getString(session.currentRatchet.ephemeralKeyPair.pubKey)];
+ // Move all receive ratchets to the oldRatchetList to mark them for deletion
+ for (var i in session) {
+ if (session[i].chainKey !== undefined && session[i].chainKey.key !== undefined) {
+ if (!sessionClosedByRemote)
+ session.oldRatchetList[session.oldRatchetList.length] = { added: new Date().getTime(), ephemeralKey: i };
+ else
+ delete session[i].chainKey.key;
+ }
+ }
+ // Delete current root key and our ephemeral key pair to disallow ratchet stepping
+ delete session.currentRatchet['rootKey'];
+ delete session.currentRatchet['ephemeralKeyPair'];
+ session.indexInfo.closed = new Date().getTime();
+ removeOldChains(session);
+ }
+
+ self.closeOpenSessionForDevice = function(encodedNumber) {
+ var session = crypto_storage.getOpenSession(encodedNumber);
+ if (session === undefined)
+ return;
+
+ closeSession(session);
+ crypto_storage.saveSession(encodedNumber, session);
+ }
+
+ var initSessionFromPreKeyWhisperMessage;
+ var decryptWhisperMessage;
+ var handlePreKeyWhisperMessage = function(from, encodedMessage) {
+ var preKeyProto = textsecure.protobuf.PreKeyWhisperMessage.decode(encodedMessage, 'binary');
+ return initSessionFromPreKeyWhisperMessage(from, preKeyProto).then(function(sessions) {
+ return decryptWhisperMessage(from, getString(preKeyProto.message), sessions[0], preKeyProto.registrationId).then(function(result) {
+ if (sessions[1] !== undefined)
+ sessions[1]();
+ return result;
+ });
+ });
+ }
+
+ var wipeIdentityAndTryMessageAgain = function(from, encodedMessage, message_id) {
+ // Wipe identity key!
+ textsecure.storage.removeEncrypted("devices" + from.split('.')[0]);
+ return handlePreKeyWhisperMessage(from, encodedMessage).then(
+ function(pushMessageContent) {
+ extension.trigger('message:decrypted', {
+ message_id : message_id,
+ data : pushMessageContent
+ });
+ }
+ );
+ }
+ textsecure.replay.registerFunction(wipeIdentityAndTryMessageAgain, textsecure.replay.Type.INIT_SESSION);
+
+ initSessionFromPreKeyWhisperMessage = function(encodedNumber, message) {
+ var preKeyPair = crypto_storage.getStoredKeyPair("preKey" + message.preKeyId);
+ var signedPreKeyPair = crypto_storage.getStoredKeyPair("signedKey" + message.signedPreKeyId);
+
+ var session = crypto_storage.getSessionOrIdentityKeyByBaseKey(encodedNumber, toArrayBuffer(message.baseKey));
+ var open_session = crypto_storage.getOpenSession(encodedNumber);
+ if (signedPreKeyPair === undefined) {
+ // Session may or may not be the right one, but if its not, we can't do anything about it
+ // ...fall through and let decryptWhisperMessage handle that case
+ if (session !== undefined && session.currentRatchet !== undefined)
+ return Promise.resolve([session, undefined]);
+ else
+ throw new Error("Missing Signed PreKey for PreKeyWhisperMessage");
+ }
+ if (session !== undefined) {
+ // Duplicate PreKeyMessage for session:
+ if (isEqual(session.indexInfo.baseKey, message.baseKey, false))
+ return Promise.resolve([session, undefined]);
+
+ // We already had a session/known identity key:
+ if (isEqual(session.indexInfo.remoteIdentityKey, message.identityKey, false)) {
+ // If the identity key matches the previous one, close the previous one and use the new one
+ if (open_session !== undefined)
+ closeSession(open_session); // To be returned and saved later
+ } else {
+ // ...otherwise create an error that the UI will pick up and ask the user if they want to re-negotiate
+ throw new textsecure.IncomingIdentityKeyError(encodedNumber, getString(message.encode()));
+ }
+ }
+ return initSession(false, preKeyPair, signedPreKeyPair, encodedNumber, toArrayBuffer(message.identityKey), toArrayBuffer(message.baseKey), undefined)
+ .then(function(new_session) {
+ // Note that the session is not actually saved until the very end of decryptWhisperMessage
+ // ... to ensure that the sender actually holds the private keys for all reported pubkeys
+ return [new_session, function() {
+ if (open_session !== undefined)
+ crypto_storage.saveSession(encodedNumber, open_session);
+ crypto_storage.removeStoredKeyPair("preKey" + message.preKeyId);
+ }];
+ });;
+ }
+
+ var fillMessageKeys = function(chain, counter) {
+ if (chain.chainKey.counter + 1000 < counter) //TODO: maybe 1000 is too low/high in some cases?
+ return Promise.resolve(); // Stalker, much?
+
+ if (chain.chainKey.counter >= counter)
+ return Promise.resolve(); // Already calculated
+
+ if (chain.chainKey.key === undefined)
+ throw new Error("Got invalid request to extend chain after it was already closed");
+
+ var key = toArrayBuffer(chain.chainKey.key);
+ var byteArray = new Uint8Array(1);
+ byteArray[0] = 1;
+ return textsecure.crypto.sign(key, byteArray.buffer).then(function(mac) {
+ byteArray[0] = 2;
+ return textsecure.crypto.sign(key, byteArray.buffer).then(function(key) {
+ chain.messageKeys[chain.chainKey.counter + 1] = mac;
+ chain.chainKey.key = key
+ chain.chainKey.counter += 1;
+ return fillMessageKeys(chain, counter);
+ });
+ });
+ }
+
+ var maybeStepRatchet = function(session, remoteKey, previousCounter) {
+ if (session[getString(remoteKey)] !== undefined)
+ return Promise.resolve();
+
+ var ratchet = session.currentRatchet;
+
+ var finish = function() {
+ return calculateRatchet(session, remoteKey, false).then(function() {
+ // Now swap the ephemeral key and calculate the new sending chain
+ var previousRatchet = getString(ratchet.ephemeralKeyPair.pubKey);
+ if (session[previousRatchet] !== undefined) {
+ ratchet.previousCounter = session[previousRatchet].chainKey.counter;
+ delete session[previousRatchet];
+ }
+
+ return textsecure.crypto.createKeyPair().then(function(keyPair) {
+ ratchet.ephemeralKeyPair = keyPair;
+ return calculateRatchet(session, remoteKey, true).then(function() {
+ ratchet.lastRemoteEphemeralKey = remoteKey;
+ });
+ });
+ });
+ }
+
+ var previousRatchet = session[getString(ratchet.lastRemoteEphemeralKey)];
+ if (previousRatchet !== undefined) {
+ return fillMessageKeys(previousRatchet, previousCounter).then(function() {
+ delete previousRatchet.chainKey.key;
+ if (!objectContainsKeys(previousRatchet.messageKeys))
+ delete session[getString(ratchet.lastRemoteEphemeralKey)];
+ else
+ session.oldRatchetList[session.oldRatchetList.length] = { added: new Date().getTime(), ephemeralKey: ratchet.lastRemoteEphemeralKey };
+ }).then(finish);
+ } else
+ return finish();
+ }
+
+ // returns decrypted protobuf
+ decryptWhisperMessage = function(encodedNumber, messageBytes, session, registrationId) {
+ if (messageBytes[0] != String.fromCharCode((3 << 4) | 3))
+ throw new Error("Bad version number on WhisperMessage");
+
+ var messageProto = messageBytes.substring(1, messageBytes.length - 8);
+ var mac = messageBytes.substring(messageBytes.length - 8, messageBytes.length);
+
+ var message = textsecure.protobuf.WhisperMessage.decode(messageProto, 'binary');
+ var remoteEphemeralKey = toArrayBuffer(message.ephemeralKey);
+
+ if (session === undefined) {
+ var session = crypto_storage.getSessionByRemoteEphemeralKey(encodedNumber, remoteEphemeralKey);
+ if (session === undefined)
+ throw new Error("No session found to decrypt message from " + encodedNumber);
+ }
+
+ return maybeStepRatchet(session, remoteEphemeralKey, message.previousCounter).then(function() {
+ var chain = session[getString(message.ephemeralKey)];
+
+ return fillMessageKeys(chain, message.counter).then(function() {
+ return HKDF(toArrayBuffer(chain.messageKeys[message.counter]), '', "WhisperMessageKeys").then(function(keys) {
+ delete chain.messageKeys[message.counter];
+
+ var messageProtoArray = toArrayBuffer(messageProto);
+ var macInput = new Uint8Array(messageProtoArray.byteLength + 33*2 + 1);
+ macInput.set(new Uint8Array(toArrayBuffer(session.indexInfo.remoteIdentityKey)));
+ macInput.set(new Uint8Array(toArrayBuffer(crypto_storage.getIdentityKey().pubKey)), 33);
+ macInput[33*2] = (3 << 4) | 3;
+ macInput.set(new Uint8Array(messageProtoArray), 33*2 + 1);
+
+ return verifyMAC(macInput.buffer, keys[1], mac).then(function() {
+ return window.textsecure.crypto.decrypt(keys[0], toArrayBuffer(message.ciphertext), keys[2].slice(0, 16))
+ .then(function(paddedPlaintext) {
+
+ paddedPlaintext = new Uint8Array(paddedPlaintext);
+ var plaintext;
+ for (var i = paddedPlaintext.length - 1; i >= 0; i--) {
+ if (paddedPlaintext[i] == 0x80) {
+ plaintext = new Uint8Array(i);
+ plaintext.set(paddedPlaintext.subarray(0, i));
+ plaintext = plaintext.buffer;
+ break;
+ } else if (paddedPlaintext[i] != 0x00)
+ throw new Error('Invalid padding');
+ }
+
+ delete session['pendingPreKey'];
+
+ var finalMessage = textsecure.protobuf.PushMessageContent.decode(plaintext);
+
+ if ((finalMessage.flags & textsecure.protobuf.PushMessageContent.Flags.END_SESSION)
+ == textsecure.protobuf.PushMessageContent.Flags.END_SESSION)
+ closeSession(session, true);
+
+ removeOldChains(session);
+
+ crypto_storage.saveSession(encodedNumber, session, registrationId);
+ return finalMessage;
+ });
+ });
+ });
+ });
+ });
+ }
+
+ /*************************
+ *** Public crypto API ***
+ *************************/
+ // Decrypts message into a raw string
+ self.decryptWebsocketMessage = function(message) {
+ var signaling_key = textsecure.storage.getEncrypted("signaling_key"); //TODO: in crypto_storage
+ var aes_key = toArrayBuffer(signaling_key.substring(0, 32));
+ var mac_key = toArrayBuffer(signaling_key.substring(32, 32 + 20));
+
+ var decodedMessage = message.toArrayBuffer();
+ if (new Uint8Array(decodedMessage)[0] != 1)
+ throw new Error("Got bad version number: " + decodedMessage[0]);
+
+ var iv = decodedMessage.slice(1, 1 + 16);
+ var ciphertext = decodedMessage.slice(1 + 16, decodedMessage.byteLength - 10);
+ var ivAndCiphertext = decodedMessage.slice(0, decodedMessage.byteLength - 10);
+ var mac = decodedMessage.slice(decodedMessage.byteLength - 10, decodedMessage.byteLength);
+
+ return verifyMAC(ivAndCiphertext, mac_key, mac).then(function() {
+ return window.textsecure.crypto.decrypt(aes_key, ciphertext, iv);
+ });
+ };
+
+ self.decryptAttachment = function(encryptedBin, keys) {
+ var aes_key = keys.slice(0, 32);
+ var mac_key = keys.slice(32, 64);
+
+ var iv = encryptedBin.slice(0, 16);
+ var ciphertext = encryptedBin.slice(16, encryptedBin.byteLength - 32);
+ var ivAndCiphertext = encryptedBin.slice(0, encryptedBin.byteLength - 32);
+ var mac = encryptedBin.slice(encryptedBin.byteLength - 32, encryptedBin.byteLength);
+
+ return verifyMAC(ivAndCiphertext, mac_key, mac).then(function() {
+ return window.textsecure.crypto.decrypt(aes_key, ciphertext, iv);
+ });
+ };
+
+ self.encryptAttachment = function(plaintext, keys, iv) {
+ var aes_key = keys.slice(0, 32);
+ var mac_key = keys.slice(32, 64);
+
+ return window.textsecure.crypto.encrypt(aes_key, plaintext, iv).then(function(ciphertext) {
+ var ivAndCiphertext = new Uint8Array(16 + ciphertext.byteLength);
+ ivAndCiphertext.set(new Uint8Array(iv));
+ ivAndCiphertext.set(new Uint8Array(ciphertext), 16);
+
+ return textsecure.crypto.sign(mac_key, ivAndCiphertext.buffer).then(function(mac) {
+ var encryptedBin = new Uint8Array(16 + ciphertext.byteLength + 32);
+ encryptedBin.set(ivAndCiphertext);
+ encryptedBin.set(new Uint8Array(mac), 16 + ciphertext.byteLength);
+ return encryptedBin.buffer;
+ });
+ });
+ };
+
+ self.handleIncomingPushMessageProto = function(proto) {
+ switch(proto.type) {
+ case textsecure.protobuf.IncomingPushMessageSignal.Type.PLAINTEXT:
+ 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 decryptWhisperMessage(from, getString(proto.message));
+ case textsecure.protobuf.IncomingPushMessageSignal.Type.PREKEY_BUNDLE:
+ if (proto.message.readUint8() != ((3 << 4) | 3))
+ throw new Error("Bad version byte");
+ var from = proto.source + "." + (proto.sourceDevice == null ? 0 : proto.sourceDevice);
+ return handlePreKeyWhisperMessage(from, getString(proto.message));
+ case textsecure.protobuf.IncomingPushMessageSignal.Type.RECEIPT:
+ return Promise.resolve(null);
+ default:
+ return new Promise(function(resolve, reject) { reject(new Error("Unknown message type")); });
+ }
+ }
+
+ // return Promise(encoded [PreKey]WhisperMessage)
+ self.encryptMessageFor = function(deviceObject, pushMessageContent) {
+ var session = crypto_storage.getOpenSession(deviceObject.encodedNumber);
+
+ var doEncryptPushMessageContent = function() {
+ var msg = new textsecure.protobuf.WhisperMessage();
+ var plaintext = toArrayBuffer(pushMessageContent.encode());
+
+ var paddedPlaintext = new Uint8Array(Math.ceil((plaintext.byteLength + 1) / 160.0) * 160 - 1);
+ paddedPlaintext.set(new Uint8Array(plaintext));
+ paddedPlaintext[plaintext.byteLength] = 0x80;
+
+ msg.ephemeralKey = toArrayBuffer(session.currentRatchet.ephemeralKeyPair.pubKey);
+ var chain = session[getString(msg.ephemeralKey)];
+
+ return fillMessageKeys(chain, chain.chainKey.counter + 1).then(function() {
+ return HKDF(toArrayBuffer(chain.messageKeys[chain.chainKey.counter]), '', "WhisperMessageKeys").then(function(keys) {
+ delete chain.messageKeys[chain.chainKey.counter];
+ msg.counter = chain.chainKey.counter;
+ msg.previousCounter = session.currentRatchet.previousCounter;
+
+ return window.textsecure.crypto.encrypt(keys[0], paddedPlaintext.buffer, keys[2].slice(0, 16)).then(function(ciphertext) {
+ msg.ciphertext = ciphertext;
+ var encodedMsg = toArrayBuffer(msg.encode());
+
+ var macInput = new Uint8Array(encodedMsg.byteLength + 33*2 + 1);
+ macInput.set(new Uint8Array(toArrayBuffer(crypto_storage.getIdentityKey().pubKey)));
+ macInput.set(new Uint8Array(toArrayBuffer(session.indexInfo.remoteIdentityKey)), 33);
+ macInput[33*2] = (3 << 4) | 3;
+ macInput.set(new Uint8Array(encodedMsg), 33*2 + 1);
+
+ return textsecure.crypto.sign(keys[1], macInput.buffer).then(function(mac) {
+ var result = new Uint8Array(encodedMsg.byteLength + 9);
+ result[0] = (3 << 4) | 3;
+ 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['preKey'];
+ delete deviceObject['preKeyId'];
+ } catch(_) {}
+
+ removeOldChains(session);
+
+ crypto_storage.saveSessionAndDevice(deviceObject, session);
+ return result;
+ });
+ });
+ });
+ });
+ }
+
+ var preKeyMsg = new textsecure.protobuf.PreKeyWhisperMessage();
+ preKeyMsg.identityKey = toArrayBuffer(crypto_storage.getIdentityKey().pubKey);
+ preKeyMsg.registrationId = textsecure.storage.getUnencrypted("registrationId");
+
+ if (session === undefined) {
+ return textsecure.crypto.createKeyPair().then(function(baseKey) {
+ preKeyMsg.preKeyId = deviceObject.preKeyId;
+ preKeyMsg.signedPreKeyId = deviceObject.signedKeyId;
+ preKeyMsg.baseKey = toArrayBuffer(baseKey.pubKey);
+ return initSession(true, baseKey, undefined, deviceObject.encodedNumber,
+ toArrayBuffer(deviceObject.identityKey), toArrayBuffer(deviceObject.preKey), toArrayBuffer(deviceObject.signedKey))
+ .then(function(new_session) {
+ session = new_session;
+ session.pendingPreKey = { preKeyId: deviceObject.preKeyId, signedKeyId: deviceObject.signedKeyId, baseKey: baseKey.pubKey };
+ return doEncryptPushMessageContent().then(function(message) {
+ preKeyMsg.message = message;
+ var result = String.fromCharCode((3 << 4) | 3) + getString(preKeyMsg.encode());
+ return {type: 3, body: result};
+ });
+ });
+ });
+ } else
+ return doEncryptPushMessageContent().then(function(message) {
+ if (session.pendingPreKey !== undefined) {
+ preKeyMsg.baseKey = toArrayBuffer(session.pendingPreKey.baseKey);
+ preKeyMsg.preKeyId = session.pendingPreKey.preKeyId;
+ preKeyMsg.signedPreKeyId = session.pendingPreKey.signedKeyId;
+ preKeyMsg.message = message;
+
+ var result = String.fromCharCode((3 << 4) | 3) + getString(preKeyMsg.encode());
+ return {type: 3, body: result};
+ } else
+ return {type: 1, body: getString(message)};
+ });
+ }
+
+ var GENERATE_KEYS_KEYS_GENERATED = 100;
+ self.generateKeys = function() {
+ var identityKeyPair = crypto_storage.getIdentityKey();
+ var identityKeyCalculated = function(identityKeyPair) {
+ var firstPreKeyId = textsecure.storage.getEncrypted("maxPreKeyId", 0);
+ textsecure.storage.putEncrypted("maxPreKeyId", firstPreKeyId + GENERATE_KEYS_KEYS_GENERATED);
+
+ var signedKeyId = textsecure.storage.getEncrypted("signedKeyId", 0);
+ textsecure.storage.putEncrypted("signedKeyId", signedKeyId + 1);
+
+ var keys = {};
+ keys.identityKey = identityKeyPair.pubKey;
+ keys.preKeys = [];
+
+ var generateKey = function(keyId) {
+ return crypto_storage.getNewStoredKeyPair("preKey" + keyId, false).then(function(keyPair) {
+ keys.preKeys[keyId] = {keyId: keyId, publicKey: keyPair.pubKey};
+ });
+ };
+
+ var promises = [];
+ for (var i = firstPreKeyId; i < firstPreKeyId + GENERATE_KEYS_KEYS_GENERATED; i++)
+ promises[i] = generateKey(i);
+
+ promises[firstPreKeyId + GENERATE_KEYS_KEYS_GENERATED] = crypto_storage.getNewStoredKeyPair("signedKey" + signedKeyId).then(function(keyPair) {
+ return textsecure.crypto.Ed25519Sign(identityKeyPair.privKey, keyPair.pubKey).then(function(sig) {
+ keys.signedPreKey = {keyId: signedKeyId, publicKey: keyPair.pubKey, signature: sig};
+ });
+ });
+
+ //TODO: Process by date added and agressively call generateKeys when we get near maxPreKeyId in a message
+ crypto_storage.removeStoredKeyPair("signedKey" + (signedKeyId - 2));
+
+ return Promise.all(promises).then(function() {
+ return keys;
+ });
+ }
+ if (identityKeyPair === undefined)
+ return crypto_storage.getNewStoredKeyPair("identityKey").then(function(keyPair) { return identityKeyCalculated(keyPair); });
+ else
+ return identityKeyCalculated(identityKeyPair);
+ }
+
+ //TODO: Dont always update prekeys here
+ if (textsecure.storage.getEncrypted("lastSignedKeyUpdate", Date.now()) < Date.now() - MESSAGE_LOST_THRESHOLD_MS) {
+ new Promise(function(resolve) { resolve(self.generateKeys()); });
+ }
+
+
+ self.prepareTempWebsocket = function() {
+ var socketInfo = {};
+ var keyPair;
+
+ socketInfo.decryptAndHandleDeviceInit = function(deviceInit) {
+ var masterEphemeral = toArrayBuffer(deviceInit.masterEphemeralPubKey);
+ var message = toArrayBuffer(deviceInit.identityKeyMessage);
+
+ return textsecure.crypto.ECDHE(masterEphemeral, keyPair.privKey).then(function(ecRes) {
+ return HKDF(ecRes, masterEphemeral, "WhisperDeviceInit").then(function(keys) {
+ if (new Uint8Array(message)[0] != (3 << 4) | 3)
+ throw new Error("Bad version number on IdentityKeyMessage");
+
+ var iv = message.slice(1, 16 + 1);
+ var mac = message.slice(message.length - 32, message.length);
+ var ivAndCiphertext = message.slice(0, message.length - 32);
+ var ciphertext = message.slice(16 + 1, message.length - 32);
+
+ return verifyMAC(ivAndCiphertext, ecRes[1], mac).then(function() {
+ window.textsecure.crypto.decrypt(ecRes[0], ciphertext, iv).then(function(plaintext) {
+ var identityKeyMsg = textsecure.protobuf.IdentityKey.decode(plaintext);
+
+ textsecure.crypto.createKeyPair(toArrayBuffer(identityKeyMsg.identityKey)).then(function(identityKeyPair) {
+ crypto_storage.putKeyPair("identityKey", identityKeyPair);
+ identityKeyMsg.identityKey = null;
+
+ return identityKeyMsg;
+ });
+ });
+ });
+ });
+ });
+ }
+
+ return textsecure.crypto.createKeyPair().then(function(newKeyPair) {
+ keyPair = newKeyPair;
+ socketInfo.pubKey = keyPair.pubKey;
+ return socketInfo;
+ });
+ }
+
+ return self;
+}();
+
+})();
+
+/* vim: ts=4:sw=4:expandtab
+ *
+ * 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 .
+ */
+// sendMessage(numbers = [], message = PushMessageContentProto, callback(success/failure map))
+window.textsecure.messaging = function() {
+ 'use strict';
+
+ var self = {};
+
+ function getKeysForNumber(number, updateDevices) {
+ var handleResult = function(response) {
+ for (var i in response.devices) {
+ if (updateDevices === undefined || updateDevices.indexOf(response.devices[i].deviceId) > -1)
+ textsecure.storage.devices.saveKeysToDeviceObject({
+ encodedNumber: number + "." + response.devices[i].deviceId,
+ identityKey: response.identityKey,
+ preKey: response.devices[i].preKey.publicKey,
+ preKeyId: response.devices[i].preKey.keyId,
+ signedKey: response.devices[i].signedPreKey.publicKey,
+ signedKeyId: response.devices[i].signedPreKey.keyId,
+ registrationId: response.devices[i].registrationId
+ });
+ }
+ };
+
+ var promises = [];
+ if (updateDevices !== undefined)
+ for (var i in updateDevices)
+ promises[promises.length] = textsecure.api.getKeysForNumber(number, updateDevices[i]).then(handleResult);
+ else
+ return textsecure.api.getKeysForNumber(number).then(handleResult);
+
+ return Promise.all(promises);
+ }
+
+ // success_callback(server success/failure map), error_callback(error_msg)
+ // message == PushMessageContentProto (NOT STRING)
+ function sendMessageToDevices(timestamp, number, deviceObjectList, message, success_callback, error_callback) {
+ var jsonData = [];
+ var relay = undefined;
+ var promises = [];
+
+ var addEncryptionFor = function(i) {
+ if (deviceObjectList[i].relay !== undefined) {
+ if (relay === undefined)
+ relay = deviceObjectList[i].relay;
+ else if (relay != deviceObjectList[i].relay)
+ return new Promise(function() { throw new Error("Mismatched relays for number " + number); });
+ } else {
+ if (relay === undefined)
+ relay = "";
+ else if (relay != "")
+ return new Promise(function() { throw new Error("Mismatched relays for number " + number); });
+ }
+
+ return textsecure.protocol.encryptMessageFor(deviceObjectList[i], message).then(function(encryptedMsg) {
+ jsonData[i] = {
+ type: encryptedMsg.type,
+ destinationDeviceId: textsecure.utils.unencodeNumber(deviceObjectList[i].encodedNumber)[1],
+ destinationRegistrationId: deviceObjectList[i].registrationId,
+ body: encryptedMsg.body,
+ timestamp: timestamp
+ };
+
+ if (deviceObjectList[i].relay !== undefined)
+ jsonData[i].relay = deviceObjectList[i].relay;
+ });
+ }
+
+ for (var i = 0; i < deviceObjectList.length; i++)
+ promises[i] = addEncryptionFor(i);
+ return Promise.all(promises).then(function() {
+ return textsecure.api.sendMessages(number, jsonData);
+ });
+ }
+
+ var sendGroupProto;
+ var makeAttachmentPointer;
+ var refreshGroups = function(number) {
+ var groups = textsecure.storage.groups.getGroupListForNumber(number);
+ var promises = [];
+ for (var i in groups) {
+ var group = textsecure.storage.groups.getGroup(groups[i]);
+
+ 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;
+ promises.push(sendGroupProto([number], proto));
+ });
+ } else {
+ promises.push(sendGroupProto([number], proto));
+ }
+ }
+ return Promise.all(promises);
+ }
+
+ var tryMessageAgain = function(number, encodedMessage, message_id) {
+ var message = new Whisper.MessageCollection().add({id: message_id});
+ message.fetch().then(function() {
+ textsecure.storage.removeEncrypted("devices" + number);
+ var proto = textsecure.protobuf.PushMessageContent.decode(encodedMessage, 'binary');
+ sendMessageProto(message.get('sent_at'), [number], proto, function(res) {
+ if (res.failure.length > 0) {
+ message.set('errors', res.failure);
+ }
+ else {
+ message.set('errors', []);
+ }
+ message.save().then(function(){
+ extension.trigger('message', message); // notify frontend listeners
+ });
+ });
+ });
+ };
+ textsecure.replay.registerFunction(tryMessageAgain, textsecure.replay.Type.SEND_MESSAGE);
+
+ var sendMessageProto = function(timestamp, numbers, message, callback) {
+ var numbersCompleted = 0;
+ var errors = [];
+ var successfulNumbers = [];
+
+ var numberCompleted = function() {
+ numbersCompleted++;
+ if (numbersCompleted >= numbers.length)
+ callback({success: successfulNumbers, failure: errors});
+ }
+
+ var registerError = function(number, message, error) {
+ if (error) {
+ if (error.humanError)
+ message = error.humanError;
+ } else
+ error = new Error(message);
+ errors[errors.length] = { number: number, reason: message, error: error };
+ numberCompleted();
+ }
+
+ var doSendMessage;
+ var reloadDevicesAndSend = function(number, recurse) {
+ return function() {
+ var devicesForNumber = textsecure.storage.devices.getDeviceObjectsForNumber(number);
+ if (devicesForNumber.length == 0)
+ return registerError(number, "Got empty device list when loading device keys", null);
+ refreshGroups(number).then(function() {
+ doSendMessage(number, devicesForNumber, recurse);
+ });
+ }
+ }
+
+ doSendMessage = function(number, devicesForNumber, recurse) {
+ return sendMessageToDevices(timestamp, number, devicesForNumber, message).then(function(result) {
+ successfulNumbers[successfulNumbers.length] = number;
+ numberCompleted();
+ }).catch(function(error) {
+ if (error instanceof Error && error.name == "HTTPError" && (error.message == 410 || error.message == 409)) {
+ if (!recurse)
+ return registerError(number, "Hit retry limit attempting to reload device list", error);
+
+ if (error.message == 409)
+ textsecure.storage.devices.removeDeviceIdsForNumber(number, error.response.extraDevices);
+
+ var resetDevices = ((error.message == 410) ? error.response.staleDevices : error.response.missingDevices);
+ getKeysForNumber(number, resetDevices)
+ .then(reloadDevicesAndSend(number, false))
+ .catch(function(error) {
+ if (error.message !== "Identity key changed")
+ registerError(number, "Failed to reload device keys", error);
+ else {
+ error = new textsecure.OutgoingIdentityKeyError(number, getString(message.encode()));
+ registerError(number, "Identity key changed", error);
+ }
+ });
+ } else
+ registerError(number, "Failed to create or send message", error);
+ });
+ }
+
+ _.each(numbers, function(number) {
+ var devicesForNumber = textsecure.storage.devices.getDeviceObjectsForNumber(number);
+
+ var promises = [];
+ for (var j in devicesForNumber)
+ if (devicesForNumber[j].registrationId === undefined)
+ promises[promises.length] = getKeysForNumber(number, [parseInt(textsecure.utils.unencodeNumber(devicesForNumber[j].encodedNumber)[1])]);
+
+ Promise.all(promises).then(function() {
+ devicesForNumber = textsecure.storage.devices.getDeviceObjectsForNumber(number);
+
+ if (devicesForNumber.length == 0) {
+ getKeysForNumber(number)
+ .then(reloadDevicesAndSend(number, true))
+ .catch(function(error) {
+ registerError(number, "Failed to retreive new device keys for number " + number, error);
+ });
+ } else
+ doSendMessage(number, devicesForNumber, true);
+ });
+ });
+ }
+
+ makeAttachmentPointer = function(attachment) {
+ var proto = new textsecure.protobuf.PushMessageContent.AttachmentPointer();
+ proto.key = textsecure.crypto.getRandomBytes(64);
+
+ var iv = textsecure.crypto.getRandomBytes(16);
+ return textsecure.protocol.encryptAttachment(attachment.data, proto.key, iv).then(function(encryptedBin) {
+ return textsecure.api.putAttachment(encryptedBin).then(function(id) {
+ proto.id = id;
+ proto.contentType = attachment.contentType;
+ return proto;
+ });
+ });
+ }
+
+ var sendIndividualProto = function(number, proto, timestamp) {
+ return new Promise(function(resolve, reject) {
+ sendMessageProto(timestamp, [number], proto, function(res) {
+ if (res.failure.length > 0)
+ reject(res.failure);
+ else
+ resolve();
+ });
+ });
+ }
+
+ sendGroupProto = function(numbers, proto, timestamp) {
+ timestamp = timestamp || Date.now();
+ var me = textsecure.utils.unencodeNumber(textsecure.storage.getUnencrypted("number_id"))[0];
+ numbers = numbers.filter(function(number) { return number != me; });
+
+ return new Promise(function(resolve, reject) {
+ sendMessageProto(timestamp, numbers, proto, function(res) {
+ if (res.failure.length > 0)
+ reject(res.failure);
+ else
+ resolve();
+ });
+ });
+ }
+
+ self.sendMessageToNumber = function(number, messageText, attachments, timestamp) {
+ var proto = new textsecure.protobuf.PushMessageContent();
+ proto.body = messageText;
+
+ var promises = [];
+ for (var i in attachments)
+ promises.push(makeAttachmentPointer(attachments[i]));
+ return Promise.all(promises).then(function(attachmentsArray) {
+ proto.attachments = attachmentsArray;
+ return sendIndividualProto(number, proto, timestamp);
+ });
+ }
+
+ self.closeSession = function(number) {
+ var proto = new textsecure.protobuf.PushMessageContent();
+ proto.body = "TERMINATE";
+ proto.flags = textsecure.protobuf.PushMessageContent.Flags.END_SESSION;
+ return sendIndividualProto(number, proto).then(function(res) {
+ var devices = textsecure.storage.devices.getDeviceObjectsForNumber(number);
+ for (var i in devices)
+ textsecure.protocol.closeOpenSessionForDevice(devices[i].encodedNumber);
+
+ return res;
+ });
+ }
+
+ self.sendMessageToGroup = function(groupId, messageText, attachments, timestamp) {
+ var proto = new textsecure.protobuf.PushMessageContent();
+ proto.body = messageText;
+ proto.group = new textsecure.protobuf.PushMessageContent.GroupContext();
+ proto.group.id = toArrayBuffer(groupId);
+ proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.DELIVER;
+
+ var numbers = textsecure.storage.groups.getNumbers(groupId);
+ if (numbers === undefined)
+ return new Promise(function(resolve, reject) { reject(new Error("Unknown Group")); });
+
+ var promises = [];
+ for (var i in attachments)
+ promises.push(makeAttachmentPointer(attachments[i]));
+ return Promise.all(promises).then(function(attachmentsArray) {
+ proto.attachments = attachmentsArray;
+ return sendGroupProto(numbers, proto, timestamp);
+ });
+ }
+
+ self.createGroup = function(numbers, name, avatar) {
+ var proto = new textsecure.protobuf.PushMessageContent();
+ proto.group = new textsecure.protobuf.PushMessageContent.GroupContext();
+
+ var group = textsecure.storage.groups.createNewGroup(numbers);
+ proto.group.id = toArrayBuffer(group.id);
+ var numbers = group.numbers;
+
+ proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE;
+ proto.group.members = numbers;
+ proto.group.name = name;
+
+ if (avatar !== undefined) {
+ return makeAttachmentPointer(avatar).then(function(attachment) {
+ proto.group.avatar = attachment;
+ return sendGroupProto(numbers, proto).then(function() {
+ return proto.group.id;
+ });
+ });
+ } else {
+ return sendGroupProto(numbers, proto).then(function() {
+ return proto.group.id;
+ });
+ }
+ }
+
+ self.addNumberToGroup = function(groupId, number) {
+ var proto = new textsecure.protobuf.PushMessageContent();
+ proto.group = new textsecure.protobuf.PushMessageContent.GroupContext();
+ proto.group.id = toArrayBuffer(groupId);
+ proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE;
+
+ var numbers = textsecure.storage.groups.addNumbers(groupId, [number]);
+ if (numbers === undefined)
+ return new Promise(function(resolve, reject) { reject(new Error("Unknown Group")); });
+ proto.group.members = numbers;
+
+ return sendGroupProto(numbers, proto);
+ }
+
+ self.setGroupName = function(groupId, name) {
+ var proto = new textsecure.protobuf.PushMessageContent();
+ proto.group = new textsecure.protobuf.PushMessageContent.GroupContext();
+ proto.group.id = toArrayBuffer(groupId);
+ proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE;
+ proto.group.name = name;
+
+ var numbers = textsecure.storage.groups.getNumbers(groupId);
+ if (numbers === undefined)
+ return new Promise(function(resolve, reject) { reject(new Error("Unknown Group")); });
+ proto.group.members = numbers;
+
+ return sendGroupProto(numbers, proto);
+ }
+
+ self.setGroupAvatar = function(groupId, avatar) {
+ var proto = new textsecure.protobuf.PushMessageContent();
+ proto.group = new textsecure.protobuf.PushMessageContent.GroupContext();
+ proto.group.id = toArrayBuffer(groupId);
+ proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE;
+
+ var numbers = textsecure.storage.groups.getNumbers(groupId);
+ if (numbers === undefined)
+ return new Promise(function(resolve, reject) { reject(new Error("Unknown Group")); });
+ proto.group.members = numbers;
+
+ return makeAttachmentPointer(avatar).then(function(attachment) {
+ proto.group.avatar = attachment;
+ return sendGroupProto(numbers, proto);
+ });
+ }
+
+ self.leaveGroup = function(groupId) {
+ var proto = new textsecure.protobuf.PushMessageContent();
+ proto.group = new textsecure.protobuf.PushMessageContent.GroupContext();
+ proto.group.id = toArrayBuffer(groupId);
+ proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.QUIT;
+
+ var numbers = textsecure.storage.groups.getNumbers(groupId);
+ if (numbers === undefined)
+ return new Promise(function(resolve, reject) { reject(new Error("Unknown Group")); });
+ textsecure.storage.groups.deleteGroup(groupId);
+
+ return sendGroupProto(numbers, proto);
+ }
+
+ return self;
+}();
diff --git a/js/api.js b/libtextsecure/api.js
similarity index 100%
rename from js/api.js
rename to libtextsecure/api.js
diff --git a/js/crypto.js b/libtextsecure/crypto.js
similarity index 100%
rename from js/crypto.js
rename to libtextsecure/crypto.js
diff --git a/js/errors.js b/libtextsecure/errors.js
similarity index 100%
rename from js/errors.js
rename to libtextsecure/errors.js
diff --git a/js/helpers.js b/libtextsecure/helpers.js
similarity index 100%
rename from js/helpers.js
rename to libtextsecure/helpers.js
diff --git a/js/protobufs.js b/libtextsecure/protobufs.js
similarity index 100%
rename from js/protobufs.js
rename to libtextsecure/protobufs.js
diff --git a/js/protocol.js b/libtextsecure/protocol.js
similarity index 100%
rename from js/protocol.js
rename to libtextsecure/protocol.js
diff --git a/js/sendmessage.js b/libtextsecure/sendmessage.js
similarity index 100%
rename from js/sendmessage.js
rename to libtextsecure/sendmessage.js
diff --git a/js/storage.js b/libtextsecure/storage.js
similarity index 100%
rename from js/storage.js
rename to libtextsecure/storage.js
diff --git a/js/storage/devices.js b/libtextsecure/storage/devices.js
similarity index 100%
rename from js/storage/devices.js
rename to libtextsecure/storage/devices.js
diff --git a/js/storage/groups.js b/libtextsecure/storage/groups.js
similarity index 100%
rename from js/storage/groups.js
rename to libtextsecure/storage/groups.js
diff --git a/js/stringview.js b/libtextsecure/stringview.js
similarity index 100%
rename from js/stringview.js
rename to libtextsecure/stringview.js
diff --git a/js/websocket-resources.js b/libtextsecure/websocket-resources.js
similarity index 100%
rename from js/websocket-resources.js
rename to libtextsecure/websocket-resources.js
diff --git a/js/websocket.js b/libtextsecure/websocket.js
similarity index 100%
rename from js/websocket.js
rename to libtextsecure/websocket.js
diff --git a/options.html b/options.html
index 2bcd98e90..b56dbd2b4 100644
--- a/options.html
+++ b/options.html
@@ -95,27 +95,17 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
diff --git a/test/index.html b/test/index.html
index 9bd8e99b0..ac5de142d 100644
--- a/test/index.html
+++ b/test/index.html
@@ -125,27 +125,18 @@
-
-
-
-
-
-
-
-
-
-
+
+
-
-
+
+
+
+
-
-
-