Move js files around for libtextsecure split
This commit is contained in:
parent
444b765dfc
commit
8ad1a38b5b
19 changed files with 2764 additions and 62 deletions
334
js/api.js
334
js/api.js
|
@ -1,334 +0,0 @@
|
|||
/* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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;
|
||||
}();
|
142
js/crypto.js
142
js/crypto.js
|
@ -1,142 +0,0 @@
|
|||
/* vim: ts=4:sw=4
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
;(function() {
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
})();
|
76
js/errors.js
76
js/errors.js
|
@ -1,76 +0,0 @@
|
|||
/* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
;(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;
|
||||
|
||||
})();
|
306
js/helpers.js
306
js/helpers.js
|
@ -1,306 +0,0 @@
|
|||
/* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
2723
js/libtextsecure.js
Normal file
2723
js/libtextsecure.js
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,25 +0,0 @@
|
|||
;(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
|
||||
};
|
||||
})();
|
804
js/protocol.js
804
js/protocol.js
|
@ -1,804 +0,0 @@
|
|||
/* vim: ts=4:sw=4
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
;(function() {
|
||||
|
||||
'use strict';
|
||||
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;
|
||||
}();
|
||||
|
||||
})();
|
|
@ -1,402 +0,0 @@
|
|||
/* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// 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 makeAttachmentPointer;
|
||||
var refreshGroup = function(number, groupId, devicesForNumber) {
|
||||
groupId = getString(groupId);
|
||||
|
||||
var doUpdate = false;
|
||||
for (var i in devicesForNumber) {
|
||||
if (textsecure.storage.groups.needUpdateByDeviceRegistrationId(groupId, number, devicesForNumber[i].encodedNumber, devicesForNumber[i].registrationId))
|
||||
doUpdate = true;
|
||||
}
|
||||
if (!doUpdate)
|
||||
return Promise.resolve(true);
|
||||
|
||||
var group = textsecure.storage.groups.getGroup(groupId);
|
||||
var numberIndex = group.numbers.indexOf(number);
|
||||
if (numberIndex < 0) // This is potentially a multi-message rare racing-AJAX race
|
||||
return Promise.reject("Tried to refresh group to non-member");
|
||||
|
||||
var proto = new textsecure.protobuf.PushMessageContent();
|
||||
proto.group = new textsecure.protobuf.PushMessageContent.GroupContext();
|
||||
|
||||
proto.group.id = toArrayBuffer(group.id);
|
||||
proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE;
|
||||
proto.group.members = group.numbers;
|
||||
proto.group.name = group.name === undefined ? null : group.name;
|
||||
|
||||
if (group.avatar !== undefined) {
|
||||
return makeAttachmentPointer(group.avatar).then(function(attachment) {
|
||||
proto.group.avatar = attachment;
|
||||
return sendMessageToDevices(Date.now(), number, devicesForNumber, proto);
|
||||
});
|
||||
} else {
|
||||
return sendMessageToDevices(Date.now(), number, devicesForNumber, proto);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
doSendMessage(number, devicesForNumber, recurse);
|
||||
}
|
||||
}
|
||||
|
||||
doSendMessage = function(number, devicesForNumber, recurse) {
|
||||
var groupUpdate = Promise.resolve(true);
|
||||
if (message.group && message.group.id)
|
||||
groupUpdate = refreshGroup(number, message.group.id, devicesForNumber);
|
||||
return groupUpdate.then(function() {
|
||||
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();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var 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;
|
||||
}();
|
|
@ -1,69 +0,0 @@
|
|||
/* vim: ts=4:sw=4
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
;(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);
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
/* vim: ts=4:sw=4
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
;(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);
|
||||
};
|
||||
})();
|
|
@ -1,135 +0,0 @@
|
|||
/* vim: ts=4:sw=4
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
;(function() {
|
||||
/*********************
|
||||
*** Group Storage ***
|
||||
*********************/
|
||||
window.textsecure = window.textsecure || {};
|
||||
window.textsecure.storage = window.textsecure.storage || {};
|
||||
|
||||
window.textsecure.storage.groups = {
|
||||
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);
|
||||
}
|
||||
|
||||
if (!haveMe)
|
||||
finalNumbers.push(me);
|
||||
|
||||
var groupObject = {numbers: finalNumbers, numberRegistrationIds: {}};
|
||||
for (var i in finalNumbers)
|
||||
groupObject.numberRegistrationIds[finalNumbers[i]] = {};
|
||||
|
||||
textsecure.storage.putEncrypted("group" + groupId, groupObject);
|
||||
|
||||
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);
|
||||
delete group.numberRegistrationIds[number];
|
||||
textsecure.storage.putEncrypted("group" + groupId, group);
|
||||
}
|
||||
|
||||
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);
|
||||
group.numberRegistrationIds[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
|
||||
},
|
||||
|
||||
needUpdateByDeviceRegistrationId: function(groupId, number, encodedNumber, registrationId) {
|
||||
var group = textsecure.storage.getEncrypted("group" + groupId);
|
||||
if (group === undefined)
|
||||
throw new Error("Unknown group for device registration id");
|
||||
|
||||
if (group.numberRegistrationIds[number] === undefined)
|
||||
throw new Error("Unknown number in group for device registration id");
|
||||
|
||||
if (group.numberRegistrationIds[number][encodedNumber] == registrationId)
|
||||
return false;
|
||||
|
||||
var needUpdate = group.numberRegistrationIds[number][encodedNumber] !== undefined;
|
||||
group.numberRegistrationIds[number][encodedNumber] = registrationId;
|
||||
textsecure.storage.putEncrypted("group" + groupId, group);
|
||||
return needUpdate;
|
||||
},
|
||||
};
|
||||
|
||||
})();
|
|
@ -1,97 +0,0 @@
|
|||
/* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
;(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, "=");
|
||||
}
|
||||
};
|
||||
}());
|
|
@ -1,134 +0,0 @@
|
|||
/* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
;(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);
|
||||
};
|
||||
};
|
||||
|
||||
}());
|
|
@ -1,81 +0,0 @@
|
|||
/* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
;(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;
|
||||
};
|
||||
})();
|
Loading…
Add table
Add a link
Reference in a new issue