mirror of
https://github.com/oxen-io/session-desktop.git
synced 2023-12-14 02:12:57 +01:00
commit
1d0b4c9572
149 changed files with 35633 additions and 46306 deletions
|
@ -87,11 +87,8 @@ module.exports = grunt => {
|
|||
'libtextsecure/stringview.js',
|
||||
'libtextsecure/event_target.js',
|
||||
'libtextsecure/account_manager.js',
|
||||
'libtextsecure/websocket-resources.js',
|
||||
'libtextsecure/http-resources.js',
|
||||
'libtextsecure/message_receiver.js',
|
||||
'libtextsecure/sendmessage.js',
|
||||
'libtextsecure/sync_request.js',
|
||||
'libtextsecure/contacts_parser.js',
|
||||
'libtextsecure/ProvisioningCipher.js',
|
||||
'libtextsecure/task_with_timeout.js',
|
||||
|
@ -100,7 +97,6 @@ module.exports = grunt => {
|
|||
},
|
||||
libloki: {
|
||||
src: [
|
||||
'libloki/api.js',
|
||||
'libloki/crypto.js',
|
||||
'libloki/service_nodes.js',
|
||||
'libloki/storage.js',
|
||||
|
@ -118,7 +114,6 @@ module.exports = grunt => {
|
|||
libtextsecuretest: {
|
||||
src: [
|
||||
'node_modules/jquery/dist/jquery.js',
|
||||
'components/mock-socket/dist/mock-socket.js',
|
||||
'node_modules/mocha/mocha.js',
|
||||
'node_modules/chai/chai.js',
|
||||
'libtextsecure/test/_test.js',
|
||||
|
|
|
@ -457,20 +457,6 @@
|
|||
"unverify": {
|
||||
"message": "Mark As Not Verified"
|
||||
},
|
||||
"isVerified": {
|
||||
"message": "You marked your safety number with $name$ verified",
|
||||
"description": "Summary state shown at top of the safety number screen if user has verified contact.",
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"content": "$1",
|
||||
"example": "Bob"
|
||||
}
|
||||
},
|
||||
"androidKey": "MessageRecord_you_marked_your_safety_number_with_s_verified",
|
||||
"androidReplace": {
|
||||
"%s": "$name$"
|
||||
}
|
||||
},
|
||||
"isNotVerified": {
|
||||
"message": "You have not verified your safety number with $name$.",
|
||||
"description": "Summary state shown at top of the safety number screen if user has not verified contact.",
|
||||
|
@ -1045,18 +1031,6 @@
|
|||
"description": "Confirmation dialog text that tells the user what will happen if they delete the contact.",
|
||||
"androidKey": "activity_home_delete_conversation_dialog_message"
|
||||
},
|
||||
"sessionResetFailed": {
|
||||
"message": "Secure session reset failed",
|
||||
"description": "your secure session could not been transmitted to the other participant."
|
||||
},
|
||||
"sessionResetOngoing": {
|
||||
"message": "Secure session reset in progress",
|
||||
"description": "your secure session is currently being reset, waiting for the reset acknowledgment."
|
||||
},
|
||||
"sessionEnded": {
|
||||
"message": "Secure session reset succeeded",
|
||||
"description": "This is a past tense, informational message. In other words, your secure session has been reset."
|
||||
},
|
||||
"quoteThumbnailAlt": {
|
||||
"message": "Thumbnail of image from quoted message",
|
||||
"description": "Used in alt tag of thumbnail images inside of an embedded message quote"
|
||||
|
|
|
@ -179,8 +179,6 @@
|
|||
|
||||
<!-- CRYPTO -->
|
||||
<script type='text/javascript' src='js/wall_clock_listener.js'></script>
|
||||
<script type='text/javascript' src='js/rotate_signed_prekey_listener.js'></script>
|
||||
<script type='text/javascript' src='js/keychange_listener.js'></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class='app-loading-screen'>
|
||||
|
|
|
@ -182,8 +182,6 @@
|
|||
|
||||
<!-- CRYPTO -->
|
||||
<script type='text/javascript' src='js/wall_clock_listener.js'></script>
|
||||
<script type='text/javascript' src='js/rotate_signed_prekey_listener.js'></script>
|
||||
<script type='text/javascript' src='js/keychange_listener.js'></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class='app-loading-screen'>
|
||||
|
|
635
components/mock-socket/dist/mock-socket.js
vendored
635
components/mock-socket/dist/mock-socket.js
vendored
|
@ -1,635 +0,0 @@
|
|||
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
|
||||
// Starting point for browserify and throws important objects into the window object
|
||||
var Service = require('./service');
|
||||
var MockServer = require('./mock-server');
|
||||
var MockSocket = require('./mock-socket');
|
||||
var globalContext = require('./helpers/global-context');
|
||||
|
||||
globalContext.SocketService = Service;
|
||||
globalContext.MockSocket = MockSocket;
|
||||
globalContext.MockServer = MockServer;
|
||||
|
||||
},{"./helpers/global-context":3,"./mock-server":7,"./mock-socket":8,"./service":9}],2:[function(require,module,exports){
|
||||
var globalContext = require('./global-context');
|
||||
|
||||
/*
|
||||
* This delay allows the thread to finish assigning its on* methods
|
||||
* before invoking the delay callback. This is purely a timing hack.
|
||||
* http://geekabyte.blogspot.com/2014/01/javascript-effect-of-setting-settimeout.html
|
||||
*
|
||||
* @param {callback: function} the callback which will be invoked after the timeout
|
||||
* @parma {context: object} the context in which to invoke the function
|
||||
*/
|
||||
function delay(callback, context) {
|
||||
globalContext.setTimeout(function(context) {
|
||||
callback.call(context);
|
||||
}, 4, context);
|
||||
}
|
||||
|
||||
module.exports = delay;
|
||||
|
||||
},{"./global-context":3}],3:[function(require,module,exports){
|
||||
(function (global){
|
||||
/*
|
||||
* Determines the global context. This should be either window (in the)
|
||||
* case where we are in a browser) or global (in the case where we are in
|
||||
* node)
|
||||
*/
|
||||
var globalContext;
|
||||
|
||||
if(typeof window === 'undefined') {
|
||||
globalContext = global;
|
||||
}
|
||||
else {
|
||||
globalContext = window;
|
||||
}
|
||||
|
||||
if (!globalContext) {
|
||||
throw new Error('Unable to set the global context to either window or global.');
|
||||
}
|
||||
|
||||
module.exports = globalContext;
|
||||
|
||||
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
|
||||
},{}],4:[function(require,module,exports){
|
||||
/*
|
||||
* This is a mock websocket event message that is passed into the onopen,
|
||||
* opmessage, etc functions.
|
||||
*
|
||||
* @param {name: string} The name of the event
|
||||
* @param {data: *} The data to send.
|
||||
* @param {origin: string} The url of the place where the event is originating.
|
||||
*/
|
||||
function socketEventMessage(name, data, origin) {
|
||||
var ports = null;
|
||||
var source = null;
|
||||
var bubbles = false;
|
||||
var cancelable = false;
|
||||
var lastEventId = '';
|
||||
var targetPlacehold = null;
|
||||
|
||||
try {
|
||||
var messageEvent = new MessageEvent(name);
|
||||
messageEvent.initMessageEvent(name, bubbles, cancelable, data, origin, lastEventId);
|
||||
|
||||
Object.defineProperties(messageEvent, {
|
||||
target: {
|
||||
get: function() { return targetPlacehold; },
|
||||
set: function(value) { targetPlacehold = value; }
|
||||
},
|
||||
srcElement: {
|
||||
get: function() { return this.target; }
|
||||
},
|
||||
currentTarget: {
|
||||
get: function() { return this.target; }
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
// We are unable to create a MessageEvent Object. This should only be happening in PhantomJS.
|
||||
var messageEvent = {
|
||||
type : name,
|
||||
bubbles : bubbles,
|
||||
cancelable : cancelable,
|
||||
data : data,
|
||||
origin : origin,
|
||||
lastEventId : lastEventId,
|
||||
source : source,
|
||||
ports : ports,
|
||||
defaultPrevented : false,
|
||||
returnValue : true,
|
||||
clipboardData : undefined
|
||||
};
|
||||
|
||||
Object.defineProperties(messageEvent, {
|
||||
target: {
|
||||
get: function() { return targetPlacehold; },
|
||||
set: function(value) { targetPlacehold = value; }
|
||||
},
|
||||
srcElement: {
|
||||
get: function() { return this.target; }
|
||||
},
|
||||
currentTarget: {
|
||||
get: function() { return this.target; }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return messageEvent;
|
||||
}
|
||||
|
||||
module.exports = socketEventMessage;
|
||||
|
||||
},{}],5:[function(require,module,exports){
|
||||
/*
|
||||
* The native websocket object will transform urls without a pathname to have just a /.
|
||||
* As an example: ws://localhost:8080 would actually be ws://localhost:8080/ but ws://example.com/foo would not
|
||||
* change. This function does this transformation to stay inline with the native websocket implementation.
|
||||
*
|
||||
* @param {url: string} The url to transform.
|
||||
*/
|
||||
function urlTransform(url) {
|
||||
var urlPath = urlParse('path', url);
|
||||
var urlQuery = urlParse('?', url);
|
||||
|
||||
urlQuery = (urlQuery) ? '?' + urlQuery : '';
|
||||
|
||||
if(urlPath === '') {
|
||||
return url.split('?')[0] + '/' + urlQuery;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
/*
|
||||
* The following functions (isNumeric & urlParse) was taken from
|
||||
* https://github.com/websanova/js-url/blob/764ed8d94012a79bfa91026f2a62fe3383a5a49e/url.js
|
||||
* which is shared via the MIT license with minimal modifications.
|
||||
*/
|
||||
function isNumeric(arg) {
|
||||
return !isNaN(parseFloat(arg)) && isFinite(arg);
|
||||
}
|
||||
|
||||
function urlParse(arg, url) {
|
||||
var _ls = url || window.location.toString();
|
||||
|
||||
if (!arg) { return _ls; }
|
||||
else { arg = arg.toString(); }
|
||||
|
||||
if (_ls.substring(0,2) === '//') { _ls = 'http:' + _ls; }
|
||||
else if (_ls.split('://').length === 1) { _ls = 'http://' + _ls; }
|
||||
|
||||
url = _ls.split('/');
|
||||
var _l = {auth:''}, host = url[2].split('@');
|
||||
|
||||
if (host.length === 1) { host = host[0].split(':'); }
|
||||
else { _l.auth = host[0]; host = host[1].split(':'); }
|
||||
|
||||
_l.protocol=url[0];
|
||||
_l.hostname=host[0];
|
||||
_l.port=(host[1] || ((_l.protocol.split(':')[0].toLowerCase() === 'https') ? '443' : '80'));
|
||||
_l.pathname=( (url.length > 3 ? '/' : '') + url.slice(3, url.length).join('/').split('?')[0].split('#')[0]);
|
||||
var _p = _l.pathname;
|
||||
|
||||
if (_p.charAt(_p.length-1) === '/') { _p=_p.substring(0, _p.length-1); }
|
||||
var _h = _l.hostname, _hs = _h.split('.'), _ps = _p.split('/');
|
||||
|
||||
if (arg === 'hostname') { return _h; }
|
||||
else if (arg === 'domain') {
|
||||
if (/^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/.test(_h)) { return _h; }
|
||||
return _hs.slice(-2).join('.');
|
||||
}
|
||||
//else if (arg === 'tld') { return _hs.slice(-1).join('.'); }
|
||||
else if (arg === 'sub') { return _hs.slice(0, _hs.length - 2).join('.'); }
|
||||
else if (arg === 'port') { return _l.port; }
|
||||
else if (arg === 'protocol') { return _l.protocol.split(':')[0]; }
|
||||
else if (arg === 'auth') { return _l.auth; }
|
||||
else if (arg === 'user') { return _l.auth.split(':')[0]; }
|
||||
else if (arg === 'pass') { return _l.auth.split(':')[1] || ''; }
|
||||
else if (arg === 'path') { return _l.pathname; }
|
||||
else if (arg.charAt(0) === '.') {
|
||||
arg = arg.substring(1);
|
||||
if(isNumeric(arg)) {arg = parseInt(arg, 10); return _hs[arg < 0 ? _hs.length + arg : arg-1] || ''; }
|
||||
}
|
||||
else if (isNumeric(arg)) { arg = parseInt(arg, 10); return _ps[arg < 0 ? _ps.length + arg : arg] || ''; }
|
||||
else if (arg === 'file') { return _ps.slice(-1)[0]; }
|
||||
else if (arg === 'filename') { return _ps.slice(-1)[0].split('.')[0]; }
|
||||
else if (arg === 'fileext') { return _ps.slice(-1)[0].split('.')[1] || ''; }
|
||||
else if (arg.charAt(0) === '?' || arg.charAt(0) === '#') {
|
||||
var params = _ls, param = null;
|
||||
|
||||
if(arg.charAt(0) === '?') { params = (params.split('?')[1] || '').split('#')[0]; }
|
||||
else if(arg.charAt(0) === '#') { params = (params.split('#')[1] || ''); }
|
||||
|
||||
if(!arg.charAt(1)) { return params; }
|
||||
|
||||
arg = arg.substring(1);
|
||||
params = params.split('&');
|
||||
|
||||
for(var i=0,ii=params.length; i<ii; i++) {
|
||||
param = params[i].split('=');
|
||||
if(param[0] === arg) { return param[1] || ''; }
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
module.exports = urlTransform;
|
||||
|
||||
},{}],6:[function(require,module,exports){
|
||||
/*
|
||||
* This defines four methods: onopen, onmessage, onerror, and onclose. This is done this way instead of
|
||||
* just placing the methods on the prototype because we need to capture the callback when it is defined like so:
|
||||
*
|
||||
* mockSocket.onopen = function() { // this is what we need to store };
|
||||
*
|
||||
* The only way is to capture the callback via the custom setter below and then place them into the correct
|
||||
* namespace so they get invoked at the right time.
|
||||
*
|
||||
* @param {websocket: object} The websocket object which we want to define these properties onto
|
||||
*/
|
||||
function webSocketProperties(websocket) {
|
||||
var eventMessageSource = function(callback) {
|
||||
return function(event) {
|
||||
event.target = websocket;
|
||||
callback.apply(websocket, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
Object.defineProperties(websocket, {
|
||||
onopen: {
|
||||
enumerable: true,
|
||||
get: function() { return this._onopen; },
|
||||
set: function(callback) {
|
||||
this._onopen = eventMessageSource(callback);
|
||||
this.service.setCallbackObserver('clientOnOpen', this._onopen, websocket);
|
||||
}
|
||||
},
|
||||
onmessage: {
|
||||
enumerable: true,
|
||||
get: function() { return this._onmessage; },
|
||||
set: function(callback) {
|
||||
this._onmessage = eventMessageSource(callback);
|
||||
this.service.setCallbackObserver('clientOnMessage', this._onmessage, websocket);
|
||||
}
|
||||
},
|
||||
onclose: {
|
||||
enumerable: true,
|
||||
get: function() { return this._onclose; },
|
||||
set: function(callback) {
|
||||
this._onclose = eventMessageSource(callback);
|
||||
this.service.setCallbackObserver('clientOnclose', this._onclose, websocket);
|
||||
}
|
||||
},
|
||||
onerror: {
|
||||
enumerable: true,
|
||||
get: function() { return this._onerror; },
|
||||
set: function(callback) {
|
||||
this._onerror = eventMessageSource(callback);
|
||||
this.service.setCallbackObserver('clientOnError', this._onerror, websocket);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = webSocketProperties;
|
||||
|
||||
},{}],7:[function(require,module,exports){
|
||||
var Service = require('./service');
|
||||
var delay = require('./helpers/delay');
|
||||
var urlTransform = require('./helpers/url-transform');
|
||||
var socketMessageEvent = require('./helpers/message-event');
|
||||
var globalContext = require('./helpers/global-context');
|
||||
|
||||
function MockServer(url) {
|
||||
var service = new Service();
|
||||
this.url = urlTransform(url);
|
||||
|
||||
globalContext.MockSocket.services[this.url] = service;
|
||||
|
||||
this.service = service;
|
||||
service.server = this;
|
||||
}
|
||||
|
||||
MockServer.prototype = {
|
||||
service: null,
|
||||
|
||||
/*
|
||||
* This is the main function for the mock server to subscribe to the on events.
|
||||
*
|
||||
* ie: mockServer.on('connection', function() { console.log('a mock client connected'); });
|
||||
*
|
||||
* @param {type: string}: The event key to subscribe to. Valid keys are: connection, message, and close.
|
||||
* @param {callback: function}: The callback which should be called when a certain event is fired.
|
||||
*/
|
||||
on: function(type, callback) {
|
||||
var observerKey;
|
||||
|
||||
if(typeof callback !== 'function' || typeof type !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch(type) {
|
||||
case 'connection':
|
||||
observerKey = 'clientHasJoined';
|
||||
break;
|
||||
case 'message':
|
||||
observerKey = 'clientHasSentMessage';
|
||||
break;
|
||||
case 'close':
|
||||
observerKey = 'clientHasLeft';
|
||||
break;
|
||||
}
|
||||
|
||||
// Make sure that the observerKey is valid before observing on it.
|
||||
if(typeof observerKey === 'string') {
|
||||
this.service.clearAll(observerKey);
|
||||
this.service.setCallbackObserver(observerKey, callback, this);
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* This send function will notify all mock clients via their onmessage callbacks that the server
|
||||
* has a message for them.
|
||||
*
|
||||
* @param {data: *}: Any javascript object which will be crafted into a MessageObject.
|
||||
*/
|
||||
send: function(data) {
|
||||
delay(function() {
|
||||
this.service.sendMessageToClients(socketMessageEvent('message', data, this.url));
|
||||
}, this);
|
||||
},
|
||||
|
||||
/*
|
||||
* Notifies all mock clients that the server is closing and their onclose callbacks should fire.
|
||||
*/
|
||||
close: function() {
|
||||
delay(function() {
|
||||
this.service.closeConnectionFromServer(socketMessageEvent('close', null, this.url));
|
||||
}, this);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = MockServer;
|
||||
|
||||
},{"./helpers/delay":2,"./helpers/global-context":3,"./helpers/message-event":4,"./helpers/url-transform":5,"./service":9}],8:[function(require,module,exports){
|
||||
var delay = require('./helpers/delay');
|
||||
var urlTransform = require('./helpers/url-transform');
|
||||
var socketMessageEvent = require('./helpers/message-event');
|
||||
var globalContext = require('./helpers/global-context');
|
||||
var webSocketProperties = require('./helpers/websocket-properties');
|
||||
|
||||
function MockSocket(url) {
|
||||
this.binaryType = 'blob';
|
||||
this.url = urlTransform(url);
|
||||
this.readyState = globalContext.MockSocket.CONNECTING;
|
||||
this.service = globalContext.MockSocket.services[this.url];
|
||||
|
||||
webSocketProperties(this);
|
||||
|
||||
delay(function() {
|
||||
// Let the service know that we are both ready to change our ready state and that
|
||||
// this client is connecting to the mock server.
|
||||
this.service.clientIsConnecting(this, this._updateReadyState);
|
||||
}, this);
|
||||
}
|
||||
|
||||
MockSocket.CONNECTING = 0;
|
||||
MockSocket.OPEN = 1;
|
||||
MockSocket.CLOSING = 2;
|
||||
MockSocket.LOADING = 3;
|
||||
MockSocket.CLOSED = 4;
|
||||
MockSocket.services = {};
|
||||
|
||||
MockSocket.prototype = {
|
||||
|
||||
/*
|
||||
* Holds the on*** callback functions. These are really just for the custom
|
||||
* getters that are defined in the helpers/websocket-properties. Accessing these properties is not advised.
|
||||
*/
|
||||
_onopen : null,
|
||||
_onmessage : null,
|
||||
_onerror : null,
|
||||
_onclose : null,
|
||||
|
||||
/*
|
||||
* This holds reference to the service object. The service object is how we can
|
||||
* communicate with the backend via the pub/sub model.
|
||||
*
|
||||
* The service has properties which we can use to observe or notifiy with.
|
||||
* this.service.notify('foo') & this.service.observe('foo', callback, context)
|
||||
*/
|
||||
service: null,
|
||||
|
||||
/*
|
||||
* This is a mock for the native send function found on the WebSocket object. It notifies the
|
||||
* service that it has sent a message.
|
||||
*
|
||||
* @param {data: *}: Any javascript object which will be crafted into a MessageObject.
|
||||
*/
|
||||
send: function(data) {
|
||||
delay(function() {
|
||||
this.service.sendMessageToServer(socketMessageEvent('message', data, this.url));
|
||||
}, this);
|
||||
},
|
||||
|
||||
/*
|
||||
* This is a mock for the native close function found on the WebSocket object. It notifies the
|
||||
* service that it is closing the connection.
|
||||
*/
|
||||
close: function() {
|
||||
delay(function() {
|
||||
this.service.closeConnectionFromClient(socketMessageEvent('close', null, this.url), this);
|
||||
}, this);
|
||||
},
|
||||
|
||||
/*
|
||||
* This is a private method that can be used to change the readyState. This is used
|
||||
* like this: this.protocol.subject.observe('updateReadyState', this._updateReadyState, this);
|
||||
* so that the service and the server can change the readyState simply be notifing a namespace.
|
||||
*
|
||||
* @param {newReadyState: number}: The new ready state. Must be 0-4
|
||||
*/
|
||||
_updateReadyState: function(newReadyState) {
|
||||
if(newReadyState >= 0 && newReadyState <= 4) {
|
||||
this.readyState = newReadyState;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = MockSocket;
|
||||
|
||||
},{"./helpers/delay":2,"./helpers/global-context":3,"./helpers/message-event":4,"./helpers/url-transform":5,"./helpers/websocket-properties":6}],9:[function(require,module,exports){
|
||||
var socketMessageEvent = require('./helpers/message-event');
|
||||
var globalContext = require('./helpers/global-context');
|
||||
|
||||
function SocketService() {
|
||||
this.list = {};
|
||||
}
|
||||
|
||||
SocketService.prototype = {
|
||||
server: null,
|
||||
|
||||
/*
|
||||
* This notifies the mock server that a client is connecting and also sets up
|
||||
* the ready state observer.
|
||||
*
|
||||
* @param {client: object} the context of the client
|
||||
* @param {readyStateFunction: function} the function that will be invoked on a ready state change
|
||||
*/
|
||||
clientIsConnecting: function(client, readyStateFunction) {
|
||||
this.observe('updateReadyState', readyStateFunction, client);
|
||||
|
||||
// if the server has not been set then we notify the onclose method of this client
|
||||
if(!this.server) {
|
||||
this.notify(client, 'updateReadyState', globalContext.MockSocket.CLOSED);
|
||||
this.notifyOnlyFor(client, 'clientOnError');
|
||||
return false;
|
||||
}
|
||||
|
||||
this.notifyOnlyFor(client, 'updateReadyState', globalContext.MockSocket.OPEN);
|
||||
this.notify('clientHasJoined', this.server);
|
||||
this.notifyOnlyFor(client, 'clientOnOpen', socketMessageEvent('open', null, this.server.url));
|
||||
},
|
||||
|
||||
/*
|
||||
* Closes a connection from the server's perspective. This should
|
||||
* close all clients.
|
||||
*
|
||||
* @param {messageEvent: object} the mock message event.
|
||||
*/
|
||||
closeConnectionFromServer: function(messageEvent) {
|
||||
this.notify('updateReadyState', globalContext.MockSocket.CLOSING);
|
||||
this.notify('clientOnclose', messageEvent);
|
||||
this.notify('updateReadyState', globalContext.MockSocket.CLOSED);
|
||||
this.notify('clientHasLeft');
|
||||
},
|
||||
|
||||
/*
|
||||
* Closes a connection from the clients perspective. This
|
||||
* should only close the client who initiated the close and not
|
||||
* all of the other clients.
|
||||
*
|
||||
* @param {messageEvent: object} the mock message event.
|
||||
* @param {client: object} the context of the client
|
||||
*/
|
||||
closeConnectionFromClient: function(messageEvent, client) {
|
||||
if(client.readyState === globalContext.MockSocket.OPEN) {
|
||||
this.notifyOnlyFor(client, 'updateReadyState', globalContext.MockSocket.CLOSING);
|
||||
this.notifyOnlyFor(client, 'clientOnclose', messageEvent);
|
||||
this.notifyOnlyFor(client, 'updateReadyState', globalContext.MockSocket.CLOSED);
|
||||
this.notify('clientHasLeft');
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* Notifies the mock server that a client has sent a message.
|
||||
*
|
||||
* @param {messageEvent: object} the mock message event.
|
||||
*/
|
||||
sendMessageToServer: function(messageEvent) {
|
||||
this.notify('clientHasSentMessage', messageEvent.data, messageEvent);
|
||||
},
|
||||
|
||||
/*
|
||||
* Notifies all clients that the server has sent a message
|
||||
*
|
||||
* @param {messageEvent: object} the mock message event.
|
||||
*/
|
||||
sendMessageToClients: function(messageEvent) {
|
||||
this.notify('clientOnMessage', messageEvent);
|
||||
},
|
||||
|
||||
/*
|
||||
* Setup the callback function observers for both the server and client.
|
||||
*
|
||||
* @param {observerKey: string} either: connection, message or close
|
||||
* @param {callback: function} the callback to be invoked
|
||||
* @param {server: object} the context of the server
|
||||
*/
|
||||
setCallbackObserver: function(observerKey, callback, server) {
|
||||
this.observe(observerKey, callback, server);
|
||||
},
|
||||
|
||||
/*
|
||||
* Binds a callback to a namespace. If notify is called on a namespace all "observers" will be
|
||||
* fired with the context that is passed in.
|
||||
*
|
||||
* @param {namespace: string}
|
||||
* @param {callback: function}
|
||||
* @param {context: object}
|
||||
*/
|
||||
observe: function(namespace, callback, context) {
|
||||
|
||||
// Make sure the arguments are of the correct type
|
||||
if( typeof namespace !== 'string' || typeof callback !== 'function' || (context && typeof context !== 'object')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If a namespace has not been created before then we need to "initialize" the namespace
|
||||
if(!this.list[namespace]) {
|
||||
this.list[namespace] = [];
|
||||
}
|
||||
|
||||
this.list[namespace].push({callback: callback, context: context});
|
||||
},
|
||||
|
||||
/*
|
||||
* Remove all observers from a given namespace.
|
||||
*
|
||||
* @param {namespace: string} The namespace to clear.
|
||||
*/
|
||||
clearAll: function(namespace) {
|
||||
|
||||
if(!this.verifyNamespaceArg(namespace)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.list[namespace] = [];
|
||||
},
|
||||
|
||||
/*
|
||||
* Notify all callbacks that have been bound to the given namespace.
|
||||
*
|
||||
* @param {namespace: string} The namespace to notify observers on.
|
||||
* @param {namespace: url} The url to notify observers on.
|
||||
*/
|
||||
notify: function(namespace) {
|
||||
|
||||
// This strips the namespace from the list of args as we dont want to pass that into the callback.
|
||||
var argumentsForCallback = Array.prototype.slice.call(arguments, 1);
|
||||
|
||||
if(!this.verifyNamespaceArg(namespace)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Loop over all of the observers and fire the callback function with the context.
|
||||
for(var i = 0, len = this.list[namespace].length; i < len; i++) {
|
||||
this.list[namespace][i].callback.apply(this.list[namespace][i].context, argumentsForCallback);
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* Notify only the callback of the given context and namespace.
|
||||
*
|
||||
* @param {context: object} the context to match against.
|
||||
* @param {namespace: string} The namespace to notify observers on.
|
||||
*/
|
||||
notifyOnlyFor: function(context, namespace) {
|
||||
|
||||
// This strips the namespace from the list of args as we dont want to pass that into the callback.
|
||||
var argumentsForCallback = Array.prototype.slice.call(arguments, 2);
|
||||
|
||||
if(!this.verifyNamespaceArg(namespace)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Loop over all of the observers and fire the callback function with the context.
|
||||
for(var i = 0, len = this.list[namespace].length; i < len; i++) {
|
||||
if(this.list[namespace][i].context === context) {
|
||||
this.list[namespace][i].callback.apply(this.list[namespace][i].context, argumentsForCallback);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* Verifies that the namespace is valid.
|
||||
*
|
||||
* @param {namespace: string} The namespace to verify.
|
||||
*/
|
||||
verifyNamespaceArg: function(namespace) {
|
||||
if(typeof namespace !== 'string' || !this.list[namespace]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = SocketService;
|
||||
|
||||
},{"./helpers/global-context":3,"./helpers/message-event":4}]},{},[1]);
|
109
js/background.js
109
js/background.js
|
@ -7,7 +7,6 @@
|
|||
storage,
|
||||
textsecure,
|
||||
Whisper,
|
||||
libloki,
|
||||
libsession,
|
||||
libsignal,
|
||||
BlockedNumberController,
|
||||
|
@ -60,7 +59,6 @@
|
|||
'save.svg',
|
||||
'shield.svg',
|
||||
'timer.svg',
|
||||
'verified-check.svg',
|
||||
'video.svg',
|
||||
'warning.svg',
|
||||
'x.svg',
|
||||
|
@ -111,14 +109,7 @@
|
|||
|
||||
// start a background worker for ecc
|
||||
textsecure.startWorker('js/libsignal-protocol-worker.js');
|
||||
Whisper.KeyChangeListener.init(textsecure.storage.protocol);
|
||||
let messageReceiver;
|
||||
window.getSocketStatus = () => {
|
||||
if (messageReceiver) {
|
||||
return messageReceiver.getStatus();
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
Whisper.events = _.clone(Backbone.Events);
|
||||
Whisper.events.isListenedTo = eventName =>
|
||||
Whisper.events._events ? !!Whisper.events._events[eventName] : false;
|
||||
|
@ -132,6 +123,7 @@
|
|||
const user = {
|
||||
regionCode: window.storage.get('regionCode'),
|
||||
ourNumber: textsecure.storage.user.getNumber(),
|
||||
ourPrimary: window.textsecure.storage.get('primaryDevicePubKey'),
|
||||
isSecondaryDevice: !!textsecure.storage.get('isSecondaryDevice'),
|
||||
};
|
||||
Whisper.events.trigger('userChanged', user);
|
||||
|
@ -178,7 +170,6 @@
|
|||
return;
|
||||
}
|
||||
const ourKey = textsecure.storage.user.getNumber();
|
||||
window.feeds = [];
|
||||
window.lokiMessageAPI = new window.LokiMessageAPI();
|
||||
// singleton to relay events to libtextsecure/message_receiver
|
||||
window.lokiPublicChatAPI = new window.LokiPublicChatAPI(ourKey);
|
||||
|
@ -478,10 +469,6 @@
|
|||
await window.Signal.Data.removeMessage(message.id, {
|
||||
Message: Whisper.Message,
|
||||
});
|
||||
const conversation = message.getConversation();
|
||||
if (conversation) {
|
||||
await conversation.updateLastMessage();
|
||||
}
|
||||
})
|
||||
);
|
||||
window.log.info('Cleanup: complete');
|
||||
|
@ -495,9 +482,6 @@
|
|||
storage.put('link-preview-setting', false);
|
||||
});
|
||||
|
||||
// listeners
|
||||
Whisper.RotateSignedPreKeyListener.init(Whisper.events, newVersion);
|
||||
|
||||
connect(true);
|
||||
});
|
||||
|
||||
|
@ -516,9 +500,6 @@
|
|||
Whisper.Registration.isDone() &&
|
||||
!Whisper.Registration.ongoingSecondaryDeviceRegistration()
|
||||
) {
|
||||
// listeners
|
||||
Whisper.RotateSignedPreKeyListener.init(Whisper.events, newVersion);
|
||||
|
||||
connect();
|
||||
appView.openInbox({
|
||||
initialLoadComplete,
|
||||
|
@ -962,10 +943,11 @@
|
|||
|
||||
Whisper.events.on(
|
||||
'publicMessageSent',
|
||||
({ pubKey, timestamp, serverId, serverTimestamp }) => {
|
||||
({ identifier, pubKey, timestamp, serverId, serverTimestamp }) => {
|
||||
try {
|
||||
const conversation = window.getConversationController().get(pubKey);
|
||||
conversation.onPublicMessageSent(
|
||||
identifier,
|
||||
pubKey,
|
||||
timestamp,
|
||||
serverId,
|
||||
|
@ -1006,7 +988,6 @@
|
|||
});
|
||||
|
||||
Whisper.events.on('devicePairingRequestRejected', async pubKey => {
|
||||
await libloki.storage.removeContactPreKeyBundle(pubKey);
|
||||
await libsession.Protocols.MultiDeviceProtocol.removePairingAuthorisations(
|
||||
pubKey
|
||||
);
|
||||
|
@ -1038,9 +1019,6 @@
|
|||
});
|
||||
}
|
||||
|
||||
window.getSyncRequest = () =>
|
||||
new textsecure.SyncRequest(textsecure.messaging, messageReceiver);
|
||||
|
||||
let disconnectTimer = null;
|
||||
function onOffline() {
|
||||
window.log.info('offline');
|
||||
|
@ -1060,7 +1038,7 @@
|
|||
window.removeEventListener('online', onOnline);
|
||||
window.addEventListener('offline', onOffline);
|
||||
|
||||
if (disconnectTimer && isSocketOnline()) {
|
||||
if (disconnectTimer) {
|
||||
window.log.warn('Already online. Had a blip in online/offline status.');
|
||||
clearTimeout(disconnectTimer);
|
||||
disconnectTimer = null;
|
||||
|
@ -1074,13 +1052,6 @@
|
|||
connect();
|
||||
}
|
||||
|
||||
function isSocketOnline() {
|
||||
const socketStatus = window.getSocketStatus();
|
||||
return (
|
||||
socketStatus === WebSocket.CONNECTING || socketStatus === WebSocket.OPEN
|
||||
);
|
||||
}
|
||||
|
||||
async function disconnect() {
|
||||
window.log.info('disconnect');
|
||||
|
||||
|
@ -1124,14 +1095,8 @@
|
|||
if (messageReceiver) {
|
||||
await messageReceiver.close();
|
||||
}
|
||||
const mySignalingKey = storage.get('signaling_key');
|
||||
|
||||
connectCount += 1;
|
||||
const options = {
|
||||
retryCached: connectCount === 1,
|
||||
serverTrustRoot: window.getServerTrustRoot(),
|
||||
};
|
||||
|
||||
Whisper.Notifications.disable(); // avoid notification flood until empty
|
||||
setTimeout(() => {
|
||||
Whisper.Notifications.enable();
|
||||
|
@ -1151,19 +1116,18 @@
|
|||
window.getDefaultFileServer()
|
||||
);
|
||||
window.lokiPublicChatAPI = null;
|
||||
window.feeds = [];
|
||||
messageReceiver = new textsecure.MessageReceiver(mySignalingKey, options);
|
||||
messageReceiver = new textsecure.MessageReceiver();
|
||||
messageReceiver.addEventListener(
|
||||
'message',
|
||||
window.DataMessageReceiver.handleMessageEvent
|
||||
);
|
||||
window.textsecure.messaging = new textsecure.MessageSender();
|
||||
window.textsecure.messaging = true;
|
||||
return;
|
||||
}
|
||||
|
||||
initAPIs();
|
||||
await initSpecialConversations();
|
||||
messageReceiver = new textsecure.MessageReceiver(mySignalingKey, options);
|
||||
messageReceiver = new textsecure.MessageReceiver();
|
||||
messageReceiver.addEventListener(
|
||||
'message',
|
||||
window.DataMessageReceiver.handleMessageEvent
|
||||
|
@ -1180,64 +1144,7 @@
|
|||
logger: window.log,
|
||||
});
|
||||
|
||||
window.textsecure.messaging = new textsecure.MessageSender();
|
||||
|
||||
// On startup after upgrading to a new version, request a contact sync
|
||||
// (but only if we're not the primary device)
|
||||
if (
|
||||
!firstRun &&
|
||||
connectCount === 1 &&
|
||||
newVersion &&
|
||||
// eslint-disable-next-line eqeqeq
|
||||
textsecure.storage.user.getDeviceId() != '1'
|
||||
) {
|
||||
window.getSyncRequest();
|
||||
}
|
||||
|
||||
const deviceId = textsecure.storage.user.getDeviceId();
|
||||
if (firstRun === true && deviceId !== '1') {
|
||||
const hasThemeSetting = Boolean(storage.get('theme-setting'));
|
||||
if (!hasThemeSetting && textsecure.storage.get('userAgent') === 'OWI') {
|
||||
storage.put('theme-setting', 'ios');
|
||||
}
|
||||
const syncRequest = new textsecure.SyncRequest(
|
||||
textsecure.messaging,
|
||||
messageReceiver
|
||||
);
|
||||
Whisper.events.trigger('contactsync:begin');
|
||||
syncRequest.addEventListener('success', () => {
|
||||
window.log.info('sync successful');
|
||||
storage.put('synced_at', Date.now());
|
||||
Whisper.events.trigger('contactsync');
|
||||
});
|
||||
syncRequest.addEventListener('timeout', () => {
|
||||
window.log.error('sync timed out');
|
||||
Whisper.events.trigger('contactsync');
|
||||
});
|
||||
|
||||
if (Whisper.Import.isComplete()) {
|
||||
const { CONFIGURATION } = textsecure.protobuf.SyncMessage.Request.Type;
|
||||
const { RequestSyncMessage } = window.libsession.Messages.Outgoing;
|
||||
|
||||
const requestConfigurationSyncMessage = new RequestSyncMessage({
|
||||
timestamp: Date.now(),
|
||||
reqestType: CONFIGURATION,
|
||||
});
|
||||
await libsession
|
||||
.getMessageQueue()
|
||||
.sendSyncMessage(requestConfigurationSyncMessage);
|
||||
// sending of the message is handled in the 'private' case below
|
||||
}
|
||||
}
|
||||
|
||||
libsession.Protocols.SessionProtocol.checkSessionRequestExpiry().catch(
|
||||
e => {
|
||||
window.log.error(
|
||||
'Error occured which checking for session request expiry',
|
||||
e
|
||||
);
|
||||
}
|
||||
);
|
||||
window.textsecure.messaging = true;
|
||||
|
||||
storage.onready(async () => {
|
||||
idleDetector.start();
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
/* global Whisper, SignalProtocolStore, _ */
|
||||
|
||||
/* eslint-disable more/no-then */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
Whisper.KeyChangeListener = {
|
||||
init(signalProtocolStore) {
|
||||
if (!(signalProtocolStore instanceof SignalProtocolStore)) {
|
||||
throw new Error('KeyChangeListener requires a SignalProtocolStore');
|
||||
}
|
||||
|
||||
signalProtocolStore.on('keychange', async id => {
|
||||
const conversation = await window
|
||||
.getConversationController()
|
||||
.getOrCreateAndWait(id, 'private');
|
||||
conversation.addKeyChange(id);
|
||||
|
||||
const groups = await window
|
||||
.getConversationController()
|
||||
.getAllGroupsInvolvingId(id);
|
||||
_.forEach(groups, group => {
|
||||
group.addKeyChange(id);
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
})();
|
16
js/models/conversations.d.ts
vendored
16
js/models/conversations.d.ts
vendored
|
@ -35,7 +35,6 @@ export interface ConversationModel
|
|||
// Save model changes to the database
|
||||
commit: () => Promise<void>;
|
||||
notify: (message: MessageModel) => void;
|
||||
isSessionResetReceived: () => boolean;
|
||||
updateExpirationTimer: (
|
||||
expireTimer: number | null,
|
||||
source?: string,
|
||||
|
@ -43,8 +42,6 @@ export interface ConversationModel
|
|||
options?: object
|
||||
) => Promise<void>;
|
||||
isPrivate: () => boolean;
|
||||
isVerified: () => boolean;
|
||||
toggleVerified: () => Promise<void>;
|
||||
getProfile: (id: string) => Promise<any>;
|
||||
getProfiles: () => Promise<any>;
|
||||
setProfileKey: (key: string) => Promise<void>;
|
||||
|
@ -65,11 +62,9 @@ export interface ConversationModel
|
|||
isRss: () => boolean;
|
||||
isBlocked: () => boolean;
|
||||
isClosable: () => boolean;
|
||||
isModerator: (id: string) => boolean;
|
||||
isAdmin: (id: string) => boolean;
|
||||
throttledBumpTyping: () => void;
|
||||
|
||||
messageCollection: Backbone.Collection<MessageModel>;
|
||||
|
||||
// types to make more specific
|
||||
sendMessage: (
|
||||
body: any,
|
||||
|
@ -82,17 +77,12 @@ export interface ConversationModel
|
|||
updateGroupAdmins: any;
|
||||
setLokiProfile: any;
|
||||
getLokiProfile: any;
|
||||
onSessionResetReceived: any;
|
||||
setVerifiedDefault: any;
|
||||
setVerified: any;
|
||||
setUnverified: any;
|
||||
getNumber: any;
|
||||
getProfileName: any;
|
||||
getAvatarPath: any;
|
||||
markRead: (timestamp: number) => Promise<void>;
|
||||
showChannelLightbox: any;
|
||||
deletePublicMessages: any;
|
||||
getMessagesWithTimestamp: any;
|
||||
makeQuote: any;
|
||||
unblock: any;
|
||||
deleteContact: any;
|
||||
|
@ -103,7 +93,9 @@ export interface ConversationModel
|
|||
block: any;
|
||||
copyPublicKey: any;
|
||||
getAvatar: any;
|
||||
notifyTyping: any;
|
||||
notifyTyping: (
|
||||
{ isTyping, sender } = { isTyping: boolean, sender: string }
|
||||
) => any;
|
||||
setSecondaryStatus: any;
|
||||
queueJob: any;
|
||||
onUpdateGroupName: any;
|
||||
|
|
|
@ -37,23 +37,11 @@
|
|||
deleteAttachmentData,
|
||||
} = window.Signal.Migrations;
|
||||
|
||||
// Possible session reset states
|
||||
const SessionResetEnum = Object.freeze({
|
||||
// No ongoing reset
|
||||
none: 0,
|
||||
// we initiated the session reset
|
||||
initiated: 1,
|
||||
// we received the session reset
|
||||
request_received: 2,
|
||||
});
|
||||
|
||||
Whisper.Conversation = Backbone.Model.extend({
|
||||
storeName: 'conversations',
|
||||
defaults() {
|
||||
return {
|
||||
unreadCount: 0,
|
||||
verified: textsecure.storage.protocol.VerifiedStatus.DEFAULT,
|
||||
sessionResetStatus: SessionResetEnum.none,
|
||||
groupAdmins: [],
|
||||
isKickedFromGroup: false,
|
||||
profileSharing: false,
|
||||
|
@ -70,10 +58,6 @@
|
|||
return `group(${this.id})`;
|
||||
},
|
||||
|
||||
handleMessageError(message, errors) {
|
||||
this.trigger('messageError', message, errors);
|
||||
},
|
||||
|
||||
getContactCollection() {
|
||||
const collection = new Backbone.Collection();
|
||||
const collator = new Intl.Collator();
|
||||
|
@ -87,7 +71,6 @@
|
|||
|
||||
initialize() {
|
||||
this.ourNumber = textsecure.storage.user.getNumber();
|
||||
this.verifiedEnum = textsecure.storage.protocol.VerifiedStatus;
|
||||
|
||||
// This may be overridden by ConversationController.getOrCreate, and signify
|
||||
// our first save to the database. Or first fetch from the database.
|
||||
|
@ -98,9 +81,6 @@
|
|||
conversation: this,
|
||||
});
|
||||
|
||||
this.messageCollection.on('change:errors', this.handleMessageError, this);
|
||||
this.messageCollection.on('send-error', this.onMessageError, this);
|
||||
|
||||
this.throttledBumpTyping = _.throttle(this.bumpTyping, 300);
|
||||
const debouncedUpdateLastMessage = _.debounce(
|
||||
this.updateLastMessage.bind(this),
|
||||
|
@ -111,15 +91,7 @@
|
|||
'add remove destroy',
|
||||
debouncedUpdateLastMessage
|
||||
);
|
||||
this.listenTo(this.messageCollection, 'sent', this.updateLastMessage);
|
||||
this.listenTo(
|
||||
this.messageCollection,
|
||||
'send-error',
|
||||
this.updateLastMessage
|
||||
);
|
||||
|
||||
this.on('newmessage', this.onNewMessage);
|
||||
this.on('change:profileKey', this.onChangeProfileKey);
|
||||
|
||||
// Listening for out-of-band data updates
|
||||
this.on('updateMessage', this.updateAndMerge);
|
||||
|
@ -217,7 +189,7 @@
|
|||
: BlockedNumberController.blockGroup(this.id);
|
||||
await promise;
|
||||
this.commit();
|
||||
await textsecure.messaging.sendBlockedListSyncMessage();
|
||||
await libsession.Utils.SyncMessageUtils.sendBlockedListSyncMessage();
|
||||
},
|
||||
async unblock() {
|
||||
if (!this.id || this.isPublic() || this.isRss()) {
|
||||
|
@ -228,24 +200,16 @@
|
|||
: BlockedNumberController.unblockGroup(this.id);
|
||||
await promise;
|
||||
this.commit();
|
||||
await textsecure.messaging.sendBlockedListSyncMessage();
|
||||
await libsession.Utils.SyncMessageUtils.sendBlockedListSyncMessage();
|
||||
},
|
||||
async bumpTyping() {
|
||||
if (this.isPublic() || this.isMediumGroup()) {
|
||||
return;
|
||||
}
|
||||
// We don't send typing messages if the setting is disabled or we do not have a session
|
||||
// We don't send typing messages if the setting is disabled
|
||||
// or we blocked that user
|
||||
const devicePubkey = new libsession.Types.PubKey(this.id);
|
||||
const hasSession = await libsession.Protocols.SessionProtocol.hasSession(
|
||||
devicePubkey
|
||||
);
|
||||
|
||||
if (
|
||||
!storage.get('typing-indicators-setting') ||
|
||||
!hasSession ||
|
||||
this.isBlocked()
|
||||
) {
|
||||
if (!storage.get('typing-indicators-setting') || this.isBlocked()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -419,13 +383,17 @@
|
|||
await Promise.all(messages.map(m => m.setCalculatingPoW()));
|
||||
},
|
||||
|
||||
async onPublicMessageSent(pubKey, timestamp, serverId, serverTimestamp) {
|
||||
const messages = this.getMessagesWithTimestamp(pubKey, timestamp);
|
||||
if (messages && messages.length === 1) {
|
||||
await messages[0].setIsPublic(true);
|
||||
await messages[0].setServerId(serverId);
|
||||
await messages[0].setServerTimestamp(serverTimestamp);
|
||||
async onPublicMessageSent(identifier, serverId, serverTimestamp) {
|
||||
const registeredMessage = window.getMessageController().get(identifier);
|
||||
|
||||
if (!registeredMessage || !registeredMessage.message) {
|
||||
return null;
|
||||
}
|
||||
const model = registeredMessage.message;
|
||||
await model.setIsPublic(true);
|
||||
await model.setServerId(serverId);
|
||||
await model.setServerTimestamp(serverTimestamp);
|
||||
return undefined;
|
||||
},
|
||||
|
||||
async onNewMessage(message) {
|
||||
|
@ -457,11 +425,19 @@
|
|||
format() {
|
||||
return this.cachedProps;
|
||||
},
|
||||
getGroupAdmins() {
|
||||
return this.get('groupAdmins') || this.get('moderators');
|
||||
},
|
||||
getProps() {
|
||||
const { format } = PhoneNumber;
|
||||
const regionCode = storage.get('regionCode');
|
||||
const typingKeys = Object.keys(this.contactTypingTimers || {});
|
||||
|
||||
const groupAdmins = this.getGroupAdmins();
|
||||
|
||||
const members =
|
||||
this.isGroup() && !this.isPublic() ? this.get('members') : undefined;
|
||||
|
||||
const result = {
|
||||
id: this.id,
|
||||
isArchived: this.get('isArchived'),
|
||||
|
@ -494,7 +470,8 @@
|
|||
hasNickname: !!this.getNickname(),
|
||||
isKickedFromGroup: !!this.get('isKickedFromGroup'),
|
||||
left: !!this.get('left'),
|
||||
|
||||
groupAdmins,
|
||||
members,
|
||||
onClick: () => this.trigger('select', this),
|
||||
onBlockContact: () => this.block(),
|
||||
onUnblockContact: () => this.unblock(),
|
||||
|
@ -515,126 +492,6 @@
|
|||
return result;
|
||||
},
|
||||
|
||||
onMessageError() {
|
||||
this.updateVerified();
|
||||
},
|
||||
safeGetVerified() {
|
||||
const promise = textsecure.storage.protocol.getVerified(this.id);
|
||||
return promise.catch(
|
||||
() => textsecure.storage.protocol.VerifiedStatus.DEFAULT
|
||||
);
|
||||
},
|
||||
async updateVerified() {
|
||||
if (this.isPrivate()) {
|
||||
await this.initialPromise;
|
||||
const verified = await this.safeGetVerified();
|
||||
|
||||
this.set({ verified });
|
||||
|
||||
// we don't await here because we don't need to wait for this to finish
|
||||
this.commit();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await this.fetchContacts();
|
||||
await Promise.all(
|
||||
this.contactCollection.map(async contact => {
|
||||
if (!contact.isMe()) {
|
||||
await contact.updateVerified();
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
setVerifiedDefault(options) {
|
||||
const { DEFAULT } = this.verifiedEnum;
|
||||
return this.queueJob(() => this._setVerified(DEFAULT, options));
|
||||
},
|
||||
setVerified(options) {
|
||||
const { VERIFIED } = this.verifiedEnum;
|
||||
return this.queueJob(() => this._setVerified(VERIFIED, options));
|
||||
},
|
||||
setUnverified(options) {
|
||||
const { UNVERIFIED } = this.verifiedEnum;
|
||||
return this.queueJob(() => this._setVerified(UNVERIFIED, options));
|
||||
},
|
||||
async _setVerified(verified, providedOptions) {
|
||||
const options = providedOptions || {};
|
||||
_.defaults(options, {
|
||||
viaSyncMessage: false,
|
||||
viaContactSync: false,
|
||||
key: null,
|
||||
});
|
||||
|
||||
const { VERIFIED, UNVERIFIED } = this.verifiedEnum;
|
||||
|
||||
if (!this.isPrivate()) {
|
||||
throw new Error(
|
||||
'You cannot verify a group conversation. ' +
|
||||
'You must verify individual contacts.'
|
||||
);
|
||||
}
|
||||
|
||||
const beginningVerified = this.get('verified');
|
||||
let keyChange;
|
||||
if (options.viaSyncMessage) {
|
||||
// handle the incoming key from the sync messages - need different
|
||||
// behavior if that key doesn't match the current key
|
||||
keyChange = await textsecure.storage.protocol.processVerifiedMessage(
|
||||
this.id,
|
||||
verified,
|
||||
options.key
|
||||
);
|
||||
} else {
|
||||
keyChange = await textsecure.storage.protocol.setVerified(
|
||||
this.id,
|
||||
verified
|
||||
);
|
||||
}
|
||||
|
||||
this.set({ verified });
|
||||
await this.commit();
|
||||
|
||||
// Three situations result in a verification notice in the conversation:
|
||||
// 1) The message came from an explicit verification in another client (not
|
||||
// a contact sync)
|
||||
// 2) The verification value received by the contact sync is different
|
||||
// from what we have on record (and it's not a transition to UNVERIFIED)
|
||||
// 3) Our local verification status is VERIFIED and it hasn't changed,
|
||||
// but the key did change (Key1/VERIFIED to Key2/VERIFIED - but we don't
|
||||
// want to show DEFAULT->DEFAULT or UNVERIFIED->UNVERIFIED)
|
||||
if (
|
||||
!options.viaContactSync ||
|
||||
(beginningVerified !== verified && verified !== UNVERIFIED) ||
|
||||
(keyChange && verified === VERIFIED)
|
||||
) {
|
||||
await this.addVerifiedChange(this.id, verified === VERIFIED, {
|
||||
local: !options.viaSyncMessage,
|
||||
});
|
||||
}
|
||||
if (!options.viaSyncMessage) {
|
||||
await this.sendVerifySyncMessage(this.id, verified);
|
||||
}
|
||||
},
|
||||
async sendVerifySyncMessage(number, state) {
|
||||
const key = await textsecure.storage.protocol.loadIdentityKey(number);
|
||||
return textsecure.messaging.syncVerification(number, state, key);
|
||||
},
|
||||
isVerified() {
|
||||
if (this.isPrivate()) {
|
||||
return this.get('verified') === this.verifiedEnum.VERIFIED;
|
||||
}
|
||||
if (!this.contactCollection.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.contactCollection.every(contact => {
|
||||
if (contact.isMe()) {
|
||||
return true;
|
||||
}
|
||||
return contact.isVerified();
|
||||
});
|
||||
},
|
||||
async getPrimaryConversation() {
|
||||
if (!this.isSecondaryDevice()) {
|
||||
// This is already the primary conversation
|
||||
|
@ -671,209 +528,17 @@
|
|||
}
|
||||
},
|
||||
async updateGroupAdmins(groupAdmins) {
|
||||
this.set({ groupAdmins });
|
||||
await this.commit();
|
||||
},
|
||||
isUnverified() {
|
||||
if (this.isPrivate()) {
|
||||
const verified = this.get('verified');
|
||||
return (
|
||||
verified !== this.verifiedEnum.VERIFIED &&
|
||||
verified !== this.verifiedEnum.DEFAULT
|
||||
);
|
||||
}
|
||||
if (!this.contactCollection.length) {
|
||||
return true;
|
||||
}
|
||||
const existingAdmins = _.sortBy(this.getGroupAdmins());
|
||||
const newAdmins = _.sortBy(groupAdmins);
|
||||
|
||||
return this.contactCollection.any(contact => {
|
||||
if (contact.isMe()) {
|
||||
return false;
|
||||
}
|
||||
return contact.isUnverified();
|
||||
});
|
||||
},
|
||||
getUnverified() {
|
||||
if (this.isPrivate()) {
|
||||
return this.isUnverified()
|
||||
? new Backbone.Collection([this])
|
||||
: new Backbone.Collection();
|
||||
}
|
||||
return new Backbone.Collection(
|
||||
this.contactCollection.filter(contact => {
|
||||
if (contact.isMe()) {
|
||||
return false;
|
||||
}
|
||||
return contact.isUnverified();
|
||||
})
|
||||
);
|
||||
},
|
||||
setApproved() {
|
||||
if (!this.isPrivate()) {
|
||||
throw new Error(
|
||||
'You cannot set a group conversation as trusted. ' +
|
||||
'You must set individual contacts as trusted.'
|
||||
);
|
||||
}
|
||||
|
||||
return textsecure.storage.protocol.setApproval(this.id, true);
|
||||
},
|
||||
safeIsUntrusted() {
|
||||
return textsecure.storage.protocol
|
||||
.isUntrusted(this.id)
|
||||
.catch(() => false);
|
||||
},
|
||||
isUntrusted() {
|
||||
if (this.isPrivate()) {
|
||||
return this.safeIsUntrusted();
|
||||
}
|
||||
if (!this.contactCollection.length) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
this.contactCollection.map(contact => {
|
||||
if (contact.isMe()) {
|
||||
return false;
|
||||
}
|
||||
return contact.safeIsUntrusted();
|
||||
})
|
||||
).then(results => _.any(results, result => result));
|
||||
},
|
||||
getUntrusted() {
|
||||
// This is a bit ugly because isUntrusted() is async. Could do the work to cache
|
||||
// it locally, but we really only need it for this call.
|
||||
if (this.isPrivate()) {
|
||||
return this.isUntrusted().then(untrusted => {
|
||||
if (untrusted) {
|
||||
return new Backbone.Collection([this]);
|
||||
}
|
||||
|
||||
return new Backbone.Collection();
|
||||
});
|
||||
}
|
||||
return Promise.all(
|
||||
this.contactCollection.map(contact => {
|
||||
if (contact.isMe()) {
|
||||
return [false, contact];
|
||||
}
|
||||
return Promise.all([contact.isUntrusted(), contact]);
|
||||
})
|
||||
).then(results => {
|
||||
const filtered = _.filter(results, result => {
|
||||
const untrusted = result[0];
|
||||
return untrusted;
|
||||
});
|
||||
return new Backbone.Collection(
|
||||
_.map(filtered, result => {
|
||||
const contact = result[1];
|
||||
return contact;
|
||||
})
|
||||
);
|
||||
});
|
||||
},
|
||||
toggleVerified() {
|
||||
if (this.isVerified()) {
|
||||
return this.setVerifiedDefault();
|
||||
}
|
||||
return this.setVerified();
|
||||
},
|
||||
|
||||
async addKeyChange(keyChangedId) {
|
||||
window.log.info(
|
||||
'adding key change advisory for',
|
||||
this.idForLogging(),
|
||||
keyChangedId,
|
||||
this.get('timestamp')
|
||||
);
|
||||
|
||||
const timestamp = Date.now();
|
||||
const message = {
|
||||
conversationId: this.id,
|
||||
type: 'keychange',
|
||||
sent_at: this.get('timestamp'),
|
||||
received_at: timestamp,
|
||||
key_changed: keyChangedId,
|
||||
unread: 1,
|
||||
};
|
||||
|
||||
// no commit() here as this is not a message model object
|
||||
const id = await window.Signal.Data.saveMessage(message, {
|
||||
Message: Whisper.Message,
|
||||
});
|
||||
|
||||
this.trigger(
|
||||
'newmessage',
|
||||
new Whisper.Message({
|
||||
...message,
|
||||
id,
|
||||
})
|
||||
);
|
||||
},
|
||||
// Remove the message locally from our conversation
|
||||
async _removeMessage(id) {
|
||||
await window.Signal.Data.removeMessage(id, { Message: Whisper.Message });
|
||||
const existing = this.messageCollection.get(id);
|
||||
if (existing) {
|
||||
this.messageCollection.remove(id);
|
||||
existing.trigger('destroy');
|
||||
}
|
||||
},
|
||||
async addVerifiedChange(verifiedChangeId, verified, providedOptions) {
|
||||
const options = providedOptions || {};
|
||||
_.defaults(options, { local: true });
|
||||
|
||||
if (this.isMe()) {
|
||||
if (_.isEqual(existingAdmins, newAdmins)) {
|
||||
window.log.info(
|
||||
'refusing to add verified change advisory for our own number'
|
||||
'Skipping updates of groupAdmins/moderators. No change detected.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const lastMessage = this.get('timestamp') || Date.now();
|
||||
|
||||
window.log.info(
|
||||
'adding verified change advisory for',
|
||||
this.idForLogging(),
|
||||
verifiedChangeId,
|
||||
lastMessage
|
||||
);
|
||||
|
||||
const timestamp = Date.now();
|
||||
const message = {
|
||||
conversationId: this.id,
|
||||
type: 'verified-change',
|
||||
sent_at: lastMessage,
|
||||
received_at: timestamp,
|
||||
verifiedChanged: verifiedChangeId,
|
||||
verified,
|
||||
local: options.local,
|
||||
unread: 1,
|
||||
};
|
||||
|
||||
// no commit() here as this is not a message model object
|
||||
const id = await window.Signal.Data.saveMessage(message, {
|
||||
Message: Whisper.Message,
|
||||
});
|
||||
|
||||
this.trigger(
|
||||
'newmessage',
|
||||
new Whisper.Message({
|
||||
...message,
|
||||
id,
|
||||
})
|
||||
);
|
||||
|
||||
if (this.isPrivate()) {
|
||||
window
|
||||
.getConversationController()
|
||||
.getAllGroupsInvolvingId(this.id)
|
||||
.then(groups => {
|
||||
_.forEach(groups, group => {
|
||||
group.addVerifiedChange(this.id, verified, options);
|
||||
});
|
||||
});
|
||||
}
|
||||
this.set({ groupAdmins });
|
||||
await this.commit();
|
||||
},
|
||||
|
||||
async onReadMessage(message, readAt) {
|
||||
|
@ -1168,8 +833,7 @@
|
|||
attachments,
|
||||
quote,
|
||||
preview,
|
||||
groupInvitation = null,
|
||||
otherOptions = {}
|
||||
groupInvitation = null
|
||||
) {
|
||||
this.clearTypingTimers();
|
||||
|
||||
|
@ -1211,12 +875,9 @@
|
|||
messageWithSchema.destination = destination;
|
||||
}
|
||||
|
||||
const { sessionRestoration = false } = otherOptions;
|
||||
|
||||
const attributes = {
|
||||
...messageWithSchema,
|
||||
groupInvitation,
|
||||
sessionRestoration,
|
||||
id: window.getGuid(),
|
||||
};
|
||||
|
||||
|
@ -1461,109 +1122,6 @@
|
|||
isSearchable() {
|
||||
return !this.get('left');
|
||||
},
|
||||
async setSessionResetStatus(newStatus) {
|
||||
// Ensure that the new status is a valid SessionResetEnum value
|
||||
if (!(newStatus in Object.values(SessionResetEnum))) {
|
||||
return;
|
||||
}
|
||||
if (this.get('sessionResetStatus') !== newStatus) {
|
||||
this.set({ sessionResetStatus: newStatus });
|
||||
await this.commit();
|
||||
}
|
||||
},
|
||||
async onSessionResetInitiated() {
|
||||
await this.setSessionResetStatus(SessionResetEnum.initiated);
|
||||
},
|
||||
async onSessionResetReceived() {
|
||||
await this.createAndStoreEndSessionMessage({
|
||||
type: 'incoming',
|
||||
endSessionType: 'ongoing',
|
||||
});
|
||||
await this.setSessionResetStatus(SessionResetEnum.request_received);
|
||||
},
|
||||
|
||||
isSessionResetReceived() {
|
||||
return (
|
||||
this.get('sessionResetStatus') === SessionResetEnum.request_received
|
||||
);
|
||||
},
|
||||
|
||||
isSessionResetOngoing() {
|
||||
return this.get('sessionResetStatus') !== SessionResetEnum.none;
|
||||
},
|
||||
|
||||
async createAndStoreEndSessionMessage(attributes) {
|
||||
const now = Date.now();
|
||||
const message = this.messageCollection.add({
|
||||
conversationId: this.id,
|
||||
type: 'outgoing',
|
||||
sent_at: now,
|
||||
received_at: now,
|
||||
destination: this.id,
|
||||
recipients: this.getRecipients(),
|
||||
flags: textsecure.protobuf.DataMessage.Flags.END_SESSION,
|
||||
...attributes,
|
||||
});
|
||||
|
||||
const id = await message.commit();
|
||||
message.set({ id });
|
||||
window.Whisper.events.trigger('messageAdded', {
|
||||
conversationKey: this.id,
|
||||
messageModel: message,
|
||||
});
|
||||
return message;
|
||||
},
|
||||
|
||||
async onNewSessionAdopted() {
|
||||
if (this.get('sessionResetStatus') === SessionResetEnum.initiated) {
|
||||
// send empty message to confirm that we have adopted the new session
|
||||
const user = new libsession.Types.PubKey(this.id);
|
||||
|
||||
const sessionEstablished = new window.libsession.Messages.Outgoing.SessionEstablishedMessage(
|
||||
{ timestamp: Date.now() }
|
||||
);
|
||||
await libsession.getMessageQueue().send(user, sessionEstablished);
|
||||
}
|
||||
await this.createAndStoreEndSessionMessage({
|
||||
type: 'incoming',
|
||||
endSessionType: 'done',
|
||||
});
|
||||
await this.setSessionResetStatus(SessionResetEnum.none);
|
||||
},
|
||||
|
||||
async endSession() {
|
||||
if (this.isPrivate()) {
|
||||
// Only create a new message if *we* initiated the session reset.
|
||||
// On the receiver side, the actual message containing the END_SESSION flag
|
||||
// will ensure the "session reset" message will be added to their conversation.
|
||||
if (
|
||||
this.get('sessionResetStatus') !== SessionResetEnum.request_received
|
||||
) {
|
||||
await this.onSessionResetInitiated();
|
||||
// const message = await this.createAndStoreEndSessionMessage({
|
||||
// type: 'outgoing',
|
||||
// endSessionType: 'ongoing',
|
||||
// });
|
||||
// window.log.info('resetting secure session');
|
||||
// const device = new libsession.Types.PubKey(this.id);
|
||||
// const preKeyBundle = await window.libloki.storage.getPreKeyBundleForContact(
|
||||
// device.key
|
||||
// );
|
||||
// // const endSessionMessage = new libsession.Messages.Outgoing.EndSessionMessage(
|
||||
// // {
|
||||
// // timestamp: message.get('sent_at'),
|
||||
// // preKeyBundle,
|
||||
// // }
|
||||
// // );
|
||||
|
||||
// // await libsession.getMessageQueue().send(device, endSessionMessage);
|
||||
// // // TODO handle errors to reset session reset status with the new pipeline
|
||||
// // if (message.hasErrors()) {
|
||||
// // await this.setSessionResetStatus(SessionResetEnum.none);
|
||||
// // }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async commit() {
|
||||
await window.Signal.Data.updateConversation(this.id, this.attributes, {
|
||||
|
@ -1678,19 +1236,11 @@
|
|||
return;
|
||||
}
|
||||
|
||||
const devicePubkey = new libsession.Types.PubKey(this.id);
|
||||
const hasSession = await libsession.Protocols.SessionProtocol.hasSession(
|
||||
devicePubkey
|
||||
);
|
||||
if (!hasSession) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isPrivate() && read.length && options.sendReadReceipts) {
|
||||
window.log.info(`Sending ${read.length} read receipts`);
|
||||
// Because syncReadMessages sends to our other devices, and sendReadReceipts goes
|
||||
// to a contact, we need accessKeys for both.
|
||||
await textsecure.messaging.syncReadMessages(read);
|
||||
await libsession.Utils.SyncMessageUtils.syncReadMessages(read);
|
||||
|
||||
if (storage.get('read-receipt-setting')) {
|
||||
await Promise.all(
|
||||
|
@ -1823,35 +1373,17 @@
|
|||
await this.commit();
|
||||
}
|
||||
},
|
||||
isModerator(pubKey) {
|
||||
isAdmin(pubKey) {
|
||||
if (!this.isPublic()) {
|
||||
return false;
|
||||
}
|
||||
if (!pubKey) {
|
||||
throw new Error('isModerator() pubKey is falsy');
|
||||
throw new Error('isAdmin() pubKey is falsy');
|
||||
}
|
||||
const moderators = this.get('moderators');
|
||||
return Array.isArray(moderators) && moderators.includes(pubKey);
|
||||
const groupAdmins = this.getGroupAdmins();
|
||||
return Array.isArray(groupAdmins) && groupAdmins.includes(pubKey);
|
||||
},
|
||||
async setModerators(moderators) {
|
||||
if (!this.isPublic()) {
|
||||
return;
|
||||
}
|
||||
// TODO: compare array properly
|
||||
if (!_.isEqual(this.get('moderators'), moderators)) {
|
||||
this.set({ moderators });
|
||||
await this.commit();
|
||||
}
|
||||
},
|
||||
|
||||
// SIGNAL PROFILES
|
||||
|
||||
onChangeProfileKey() {
|
||||
if (this.isPrivate()) {
|
||||
this.getProfiles();
|
||||
}
|
||||
},
|
||||
|
||||
getProfiles() {
|
||||
// request all conversation members' keys
|
||||
let ids = [];
|
||||
|
@ -2064,13 +1596,6 @@
|
|||
},
|
||||
|
||||
removeMessage(messageId) {
|
||||
const message = this.messageCollection.models.find(
|
||||
msg => msg.id === messageId
|
||||
);
|
||||
if (message) {
|
||||
message.trigger('unload');
|
||||
this.messageCollection.remove(messageId);
|
||||
}
|
||||
window.Signal.Data.removeMessage(messageId, {
|
||||
Message: Whisper.Message,
|
||||
});
|
||||
|
@ -2102,7 +1627,6 @@
|
|||
MessageCollection: Whisper.MessageCollection,
|
||||
});
|
||||
|
||||
this.messageCollection.reset([]);
|
||||
window.Whisper.events.trigger('conversationReset', {
|
||||
conversationKey: this.id,
|
||||
});
|
||||
|
@ -2287,9 +1811,7 @@
|
|||
})
|
||||
);
|
||||
},
|
||||
notifyTyping(options = {}) {
|
||||
const { isTyping, sender, senderDevice } = options;
|
||||
|
||||
notifyTyping({ isTyping, sender }) {
|
||||
// We don't do anything with typing messages from our other devices
|
||||
if (sender === this.ourNumber) {
|
||||
return;
|
||||
|
@ -2312,10 +1834,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
const identifier = `${sender}.${senderDevice}`;
|
||||
|
||||
this.contactTypingTimers = this.contactTypingTimers || {};
|
||||
const record = this.contactTypingTimers[identifier];
|
||||
const record = this.contactTypingTimers[sender];
|
||||
|
||||
if (record) {
|
||||
clearTimeout(record.timer);
|
||||
|
@ -2326,16 +1846,13 @@
|
|||
// 'change' causes a re-render of this conversation's list item in the left pane
|
||||
|
||||
if (isTyping) {
|
||||
this.contactTypingTimers[identifier] = this.contactTypingTimers[
|
||||
identifier
|
||||
] || {
|
||||
this.contactTypingTimers[sender] = this.contactTypingTimers[sender] || {
|
||||
timestamp: Date.now(),
|
||||
sender,
|
||||
senderDevice,
|
||||
};
|
||||
|
||||
this.contactTypingTimers[identifier].timer = setTimeout(
|
||||
this.clearContactTypingTimer.bind(this, identifier),
|
||||
this.contactTypingTimers[sender].timer = setTimeout(
|
||||
this.clearContactTypingTimer.bind(this, sender),
|
||||
15 * 1000
|
||||
);
|
||||
if (!record) {
|
||||
|
@ -2344,7 +1861,7 @@
|
|||
this.commit();
|
||||
}
|
||||
} else {
|
||||
delete this.contactTypingTimers[identifier];
|
||||
delete this.contactTypingTimers[sender];
|
||||
if (record) {
|
||||
// User was previously typing, and is no longer. State change!
|
||||
this.trigger('typing-update');
|
||||
|
@ -2353,13 +1870,13 @@
|
|||
}
|
||||
},
|
||||
|
||||
clearContactTypingTimer(identifier) {
|
||||
clearContactTypingTimer(sender) {
|
||||
this.contactTypingTimers = this.contactTypingTimers || {};
|
||||
const record = this.contactTypingTimers[identifier];
|
||||
const record = this.contactTypingTimers[sender];
|
||||
|
||||
if (record) {
|
||||
clearTimeout(record.timer);
|
||||
delete this.contactTypingTimers[identifier];
|
||||
delete this.contactTypingTimers[sender];
|
||||
|
||||
// User was previously typing, but timed out or we received message. State change!
|
||||
this.trigger('typing-update');
|
||||
|
|
76
js/models/messages.d.ts
vendored
76
js/models/messages.d.ts
vendored
|
@ -1,3 +1,4 @@
|
|||
import { LocalizerType } from '../../ts/types/Util';
|
||||
import { ConversationModel } from './conversations';
|
||||
|
||||
type MessageModelType = 'incoming' | 'outgoing';
|
||||
|
@ -7,12 +8,10 @@ type MessageDeliveryStatus =
|
|||
| 'delivered'
|
||||
| 'read'
|
||||
| 'error';
|
||||
export type EndSessionType = 'done' | 'ongoing';
|
||||
|
||||
interface MessageAttributes {
|
||||
id: number;
|
||||
source: string;
|
||||
endSessionType: EndSessionType;
|
||||
quote: any;
|
||||
expireTimer: number;
|
||||
received_at: number;
|
||||
|
@ -45,12 +44,79 @@ interface MessageAttributes {
|
|||
status: MessageDeliveryStatus;
|
||||
}
|
||||
|
||||
export interface MessageRegularProps {
|
||||
disableMenu?: boolean;
|
||||
isDeletable: boolean;
|
||||
isAdmin?: boolean;
|
||||
weAreAdmin?: boolean;
|
||||
text?: string;
|
||||
bodyPending?: boolean;
|
||||
id: string;
|
||||
collapseMetadata?: boolean;
|
||||
direction: 'incoming' | 'outgoing';
|
||||
timestamp: number;
|
||||
serverTimestamp?: number;
|
||||
status?: 'sending' | 'sent' | 'delivered' | 'read' | 'error' | 'pow';
|
||||
// What if changed this over to a single contact like quote, and put the events on it?
|
||||
contact?: Contact & {
|
||||
hasSignalAccount: boolean;
|
||||
onSendMessage?: () => void;
|
||||
onClick?: () => void;
|
||||
};
|
||||
authorName?: string;
|
||||
authorProfileName?: string;
|
||||
/** Note: this should be formatted for display */
|
||||
authorPhoneNumber: string;
|
||||
conversationType: 'group' | 'direct';
|
||||
attachments?: Array<AttachmentType>;
|
||||
quote?: {
|
||||
text: string;
|
||||
attachment?: QuotedAttachmentType;
|
||||
isFromMe: boolean;
|
||||
authorPhoneNumber: string;
|
||||
authorProfileName?: string;
|
||||
authorName?: string;
|
||||
messageId?: string;
|
||||
onClick: (data: any) => void;
|
||||
referencedMessageNotFound: boolean;
|
||||
};
|
||||
previews: Array<LinkPreviewType>;
|
||||
authorAvatarPath?: string;
|
||||
isExpired: boolean;
|
||||
expirationLength?: number;
|
||||
expirationTimestamp?: number;
|
||||
convoId: string;
|
||||
isPublic?: boolean;
|
||||
isRss?: boolean;
|
||||
selected: boolean;
|
||||
isKickedFromGroup: boolean;
|
||||
// whether or not to show check boxes
|
||||
multiSelectMode: boolean;
|
||||
firstMessageOfSeries: boolean;
|
||||
isUnread: boolean;
|
||||
isQuotedMessageToAnimate?: boolean;
|
||||
|
||||
onClickAttachment?: (attachment: AttachmentType) => void;
|
||||
onClickLinkPreview?: (url: string) => void;
|
||||
onCopyText?: () => void;
|
||||
onSelectMessage: (messageId: string) => void;
|
||||
onReply?: (messagId: number) => void;
|
||||
onRetrySend?: () => void;
|
||||
onDownload?: (attachment: AttachmentType) => void;
|
||||
onDeleteMessage: (messageId: string) => void;
|
||||
onCopyPubKey?: () => void;
|
||||
onBanUser?: () => void;
|
||||
onShowDetail: () => void;
|
||||
onShowUserDetails: (userPubKey: string) => void;
|
||||
markRead: (readAt: number) => Promise<void>;
|
||||
theme: DefaultTheme;
|
||||
}
|
||||
|
||||
export interface MessageModel extends Backbone.Model<MessageAttributes> {
|
||||
idForLogging: () => string;
|
||||
isGroupUpdate: () => boolean;
|
||||
isExpirationTimerUpdate: () => boolean;
|
||||
getNotificationText: () => string;
|
||||
isEndSession: () => boolean;
|
||||
markRead: () => void;
|
||||
merge: (other: MessageModel) => void;
|
||||
saveErrors: (error: any) => void;
|
||||
|
@ -62,11 +128,9 @@ export interface MessageModel extends Backbone.Model<MessageAttributes> {
|
|||
handleMessageSentSuccess: (sentMessage: any, wrappedEnvelope: any) => any;
|
||||
handleMessageSentFailure: (sentMessage: any, error: any) => any;
|
||||
|
||||
propsForMessage?: any;
|
||||
propsForMessage?: MessageRegularProps;
|
||||
propsForTimerNotification?: any;
|
||||
propsForResetSessionNotification?: any;
|
||||
propsForGroupInvitation?: any;
|
||||
propsForGroupNotification?: any;
|
||||
propsForVerificationNotification?: any;
|
||||
firstMessageOfSeries: boolean;
|
||||
}
|
||||
|
|
|
@ -60,16 +60,18 @@
|
|||
this.setToExpire();
|
||||
// Keep props ready
|
||||
const generateProps = (triggerEvent = true) => {
|
||||
if (this.isExpirationTimerUpdate()) {
|
||||
// handle disabled message types first:
|
||||
if (
|
||||
this.isSessionRestoration() ||
|
||||
this.isVerifiedChange() ||
|
||||
this.isEndSession()
|
||||
) {
|
||||
// do nothing
|
||||
return;
|
||||
} else if (this.isExpirationTimerUpdate()) {
|
||||
this.propsForTimerNotification = this.getPropsForTimerNotification();
|
||||
} else if (this.isVerifiedChange()) {
|
||||
this.propsForVerificationNotification = this.getPropsForVerificationNotification();
|
||||
} else if (this.isEndSession()) {
|
||||
this.propsForResetSessionNotification = this.getPropsForResetSessionNotification();
|
||||
} else if (this.isGroupUpdate()) {
|
||||
this.propsForGroupNotification = this.getPropsForGroupNotification();
|
||||
} else if (this.isSessionRestoration()) {
|
||||
// do nothing
|
||||
} else if (this.isGroupInvitation()) {
|
||||
this.propsForGroupInvitation = this.getPropsForGroupInvitation();
|
||||
} else {
|
||||
|
@ -120,26 +122,33 @@
|
|||
window.log.warn(`Message missing attributes: ${missing}`);
|
||||
}
|
||||
},
|
||||
isEndSession() {
|
||||
const endSessionFlag = textsecure.protobuf.DataMessage.Flags.END_SESSION;
|
||||
// eslint-disable-next-line no-bitwise
|
||||
return !!(this.get('flags') & endSessionFlag);
|
||||
},
|
||||
getEndSessionTranslationKey() {
|
||||
const sessionType = this.get('endSessionType');
|
||||
if (sessionType === 'ongoing') {
|
||||
return 'sessionResetOngoing';
|
||||
} else if (sessionType === 'failed') {
|
||||
return 'sessionResetFailed';
|
||||
}
|
||||
return 'sessionEnded';
|
||||
},
|
||||
isExpirationTimerUpdate() {
|
||||
const expirationTimerFlag =
|
||||
textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE;
|
||||
// eslint-disable-next-line no-bitwise
|
||||
return !!(this.get('flags') & expirationTimerFlag);
|
||||
},
|
||||
// Those 3 functions is just used to filter out messages of this type.
|
||||
// This type is not used anymore, so we filter out existing message in DB with this flag set.
|
||||
// isEndSession() isVerifiedChange() and isSessionRestoration()
|
||||
isEndSession() {
|
||||
const endSessionFlag = textsecure.protobuf.DataMessage.Flags.END_SESSION;
|
||||
// eslint-disable-next-line no-bitwise
|
||||
return !!(this.get('flags') & endSessionFlag);
|
||||
},
|
||||
isVerifiedChange() {
|
||||
return this.get('type') === 'verified-change';
|
||||
},
|
||||
isSessionRestoration() {
|
||||
const sessionRestoreFlag =
|
||||
textsecure.protobuf.DataMessage.Flags.SESSION_RESTORE;
|
||||
/* eslint-disable no-bitwise */
|
||||
return (
|
||||
!!this.get('sessionRestoration') ||
|
||||
!!(this.get('flags') & sessionRestoreFlag)
|
||||
);
|
||||
/* eslint-enable no-bitwise */
|
||||
},
|
||||
isGroupUpdate() {
|
||||
return !!this.get('group_update');
|
||||
},
|
||||
|
@ -222,9 +231,6 @@
|
|||
}
|
||||
return messages.join(' ');
|
||||
}
|
||||
if (this.isEndSession()) {
|
||||
return i18n(this.getEndSessionTranslationKey());
|
||||
}
|
||||
if (this.isIncoming() && this.hasErrors()) {
|
||||
return i18n('incomingError');
|
||||
}
|
||||
|
@ -233,25 +239,13 @@
|
|||
}
|
||||
return this.get('body');
|
||||
},
|
||||
isVerifiedChange() {
|
||||
return this.get('type') === 'verified-change';
|
||||
},
|
||||
|
||||
isKeyChange() {
|
||||
return this.get('type') === 'keychange';
|
||||
},
|
||||
isGroupInvitation() {
|
||||
return !!this.get('groupInvitation');
|
||||
},
|
||||
isSessionRestoration() {
|
||||
const sessionRestoreFlag =
|
||||
textsecure.protobuf.DataMessage.Flags.SESSION_RESTORE;
|
||||
/* eslint-disable no-bitwise */
|
||||
return (
|
||||
!!this.get('sessionRestoration') ||
|
||||
!!(this.get('flags') & sessionRestoreFlag)
|
||||
);
|
||||
/* eslint-enable no-bitwise */
|
||||
},
|
||||
getNotificationText() {
|
||||
let description = this.getDescription();
|
||||
if (description) {
|
||||
|
@ -347,22 +341,6 @@
|
|||
|
||||
return basicProps;
|
||||
},
|
||||
getPropsForVerificationNotification() {
|
||||
const type = this.get('verified') ? 'markVerified' : 'markNotVerified';
|
||||
const isLocal = this.get('local');
|
||||
const phoneNumber = this.get('verifiedChanged');
|
||||
|
||||
return {
|
||||
type,
|
||||
isLocal,
|
||||
contact: this.findAndFormatContact(phoneNumber),
|
||||
};
|
||||
},
|
||||
getPropsForResetSessionNotification() {
|
||||
return {
|
||||
sessionResetMessageKey: this.getEndSessionTranslationKey(),
|
||||
};
|
||||
},
|
||||
getPropsForGroupInvitation() {
|
||||
const invitation = this.get('groupInvitation');
|
||||
|
||||
|
@ -568,11 +546,11 @@
|
|||
// for the public group chat
|
||||
const conversation = this.getConversation();
|
||||
|
||||
const isModerator =
|
||||
conversation && !!conversation.isModerator(phoneNumber);
|
||||
const isAdmin = conversation && !!conversation.isAdmin(phoneNumber);
|
||||
|
||||
const convoId = conversation ? conversation.id : undefined;
|
||||
const isGroup = !!conversation && !conversation.isPrivate();
|
||||
const isPublic = !!this.get('isPublic');
|
||||
|
||||
const attachments = this.get('attachments') || [];
|
||||
|
||||
|
@ -600,15 +578,11 @@
|
|||
isUnread: this.isUnread(),
|
||||
expirationLength,
|
||||
expirationTimestamp,
|
||||
isPublic: !!this.get('isPublic'),
|
||||
isPublic,
|
||||
isRss: !!this.get('isRss'),
|
||||
isKickedFromGroup:
|
||||
conversation && conversation.get('isKickedFromGroup'),
|
||||
isDeletable:
|
||||
!this.get('isPublic') ||
|
||||
isModerator ||
|
||||
phoneNumber === textsecure.storage.user.getNumber(),
|
||||
isModerator,
|
||||
isAdmin, // if the sender is an admin (not us)
|
||||
|
||||
onCopyText: () => this.copyText(),
|
||||
onCopyPubKey: () => this.copyPubKey(),
|
||||
|
@ -1154,7 +1128,6 @@
|
|||
e.number === number &&
|
||||
(e.name === 'MessageError' ||
|
||||
e.name === 'SendMessageNetworkError' ||
|
||||
e.name === 'SignedPreKeyRotationError' ||
|
||||
e.name === 'OutgoingIdentityKeyError')
|
||||
);
|
||||
this.set({ errors: errors[1] });
|
||||
|
@ -1265,9 +1238,7 @@
|
|||
async handleMessageSentFailure(sentMessage, error) {
|
||||
if (error instanceof Error) {
|
||||
this.saveErrors(error);
|
||||
if (error.name === 'SignedPreKeyRotationError') {
|
||||
await window.getAccountManager().rotateSignedPreKey();
|
||||
} else if (error.name === 'OutgoingIdentityKeyError') {
|
||||
if (error.name === 'OutgoingIdentityKeyError') {
|
||||
const c = window.getConversationController().get(sentMessage.device);
|
||||
await c.getProfiles();
|
||||
}
|
||||
|
@ -1519,20 +1490,13 @@
|
|||
});
|
||||
errors = errors.concat(this.get('errors') || []);
|
||||
|
||||
if (this.isEndSession()) {
|
||||
this.set({ endSessionType: 'failed' });
|
||||
}
|
||||
|
||||
this.set({ errors });
|
||||
await this.commit();
|
||||
},
|
||||
hasNetworkError() {
|
||||
const error = _.find(
|
||||
this.get('errors'),
|
||||
e =>
|
||||
e.name === 'MessageError' ||
|
||||
e.name === 'SendMessageNetworkError' ||
|
||||
e.name === 'SignedPreKeyRotationError'
|
||||
e => e.name === 'MessageError' || e.name === 'SendMessageNetworkError'
|
||||
);
|
||||
return !!error;
|
||||
},
|
||||
|
|
|
@ -231,14 +231,7 @@ async function _finishJob(message, id) {
|
|||
});
|
||||
const conversation = message.getConversation();
|
||||
if (conversation) {
|
||||
const fromConversation = conversation.messageCollection.get(message.id);
|
||||
|
||||
if (fromConversation && message !== fromConversation) {
|
||||
fromConversation.set(message.attributes);
|
||||
fromConversation.commit();
|
||||
} else {
|
||||
message.commit();
|
||||
}
|
||||
message.commit();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
1
js/modules/data.d.ts
vendored
1
js/modules/data.d.ts
vendored
|
@ -8,7 +8,6 @@ export type IdentityKey = {
|
|||
id: string;
|
||||
publicKey: ArrayBuffer;
|
||||
firstUse: boolean;
|
||||
verified: number;
|
||||
nonblockingApproval: boolean;
|
||||
secretKey?: string; // found in medium groups
|
||||
};
|
||||
|
|
|
@ -1204,7 +1204,7 @@ class LokiPublicChannelAPI {
|
|||
}
|
||||
|
||||
if (this.running) {
|
||||
await this.conversation.setModerators(moderators || []);
|
||||
await this.conversation.updateGroupAdmins(moderators || []);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1813,7 +1813,6 @@ class LokiPublicChannelAPI {
|
|||
const messageData = {
|
||||
serverId: adnMessage.id,
|
||||
clientVerified: true,
|
||||
isSessionRequest: false,
|
||||
source: pubKey,
|
||||
sourceDevice: 1,
|
||||
timestamp, // sender timestamp
|
||||
|
|
|
@ -37,11 +37,7 @@ class LokiMessageAPI {
|
|||
* Temporarily i've made it so `MessageSender` handles open group sends and calls this function for regular sends.
|
||||
*/
|
||||
async sendMessage(pubKey, data, messageTimeStamp, ttl, options = {}) {
|
||||
const {
|
||||
isPublic = false,
|
||||
numConnections = DEFAULT_CONNECTIONS,
|
||||
publicSendData = null,
|
||||
} = options;
|
||||
const { isPublic = false, numConnections = DEFAULT_CONNECTIONS } = options;
|
||||
// Data required to identify a message in a conversation
|
||||
const messageEventData = {
|
||||
pubKey,
|
||||
|
@ -49,20 +45,9 @@ class LokiMessageAPI {
|
|||
};
|
||||
|
||||
if (isPublic) {
|
||||
if (!publicSendData) {
|
||||
throw new window.textsecure.PublicChatError(
|
||||
'Missing public send data for public chat message'
|
||||
);
|
||||
}
|
||||
const res = await publicSendData.sendMessage(data, messageTimeStamp);
|
||||
if (res === false) {
|
||||
throw new window.textsecure.PublicChatError(
|
||||
'Failed to send public chat message'
|
||||
);
|
||||
}
|
||||
messageEventData.serverId = res.serverId;
|
||||
messageEventData.serverTimestamp = res.serverTimestamp;
|
||||
window.Whisper.events.trigger('publicMessageSent', messageEventData);
|
||||
window.log.warn(
|
||||
'this sendMessage() should not be called anymore with an open group message'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
module.exports = {
|
||||
CURRENT_VERSION: 3,
|
||||
|
||||
// This matches Envelope.Type.CIPHERTEXT
|
||||
WHISPER_TYPE: 1,
|
||||
// This matches Envelope.Type.PREKEY_BUNDLE
|
||||
PREKEY_TYPE: 3,
|
||||
|
||||
SENDERKEY_TYPE: 4,
|
||||
SENDERKEY_DISTRIBUTION_TYPE: 5,
|
||||
|
||||
ENCRYPTED_MESSAGE_OVERHEAD: 53,
|
||||
|
||||
FALLBACK_MESSAGE: 101,
|
||||
};
|
42
js/modules/metadata/SecretSessionCipher.d.ts
vendored
42
js/modules/metadata/SecretSessionCipher.d.ts
vendored
|
@ -1,42 +0,0 @@
|
|||
import { SignalService } from '../../protobuf';
|
||||
import { CipherTextObject } from '../../../libtextsecure/libsignal-protocol';
|
||||
|
||||
export interface SecretSessionCipherConstructor {
|
||||
new (storage: any): SecretSessionCipherInterface;
|
||||
}
|
||||
|
||||
export interface SecretSessionCipherInterface {
|
||||
encrypt(
|
||||
destinationPubkey: string,
|
||||
senderCertificate: SignalService.SenderCertificate,
|
||||
innerEncryptedMessage: CipherTextObject
|
||||
): Promise<ArrayBuffer>;
|
||||
decrypt(
|
||||
cipherText: ArrayBuffer,
|
||||
me: { number: string; deviceId: number }
|
||||
): Promise<{
|
||||
isMe?: boolean;
|
||||
sender: string;
|
||||
content: ArrayBuffer;
|
||||
type: SignalService.Envelope.Type;
|
||||
}>;
|
||||
}
|
||||
|
||||
export declare class SecretSessionCipher
|
||||
implements SecretSessionCipherInterface {
|
||||
constructor(storage: any);
|
||||
public encrypt(
|
||||
destinationPubkey: string,
|
||||
senderCertificate: SignalService.SenderCertificate,
|
||||
innerEncryptedMessage: CipherTextObject
|
||||
): Promise<ArrayBuffer>;
|
||||
public decrypt(
|
||||
cipherText: ArrayBuffer,
|
||||
me: { number: string; deviceId: number }
|
||||
): Promise<{
|
||||
isMe?: boolean;
|
||||
sender: string;
|
||||
content: ArrayBuffer;
|
||||
type: SignalService.Envelope.Type;
|
||||
}>;
|
||||
}
|
|
@ -1,550 +0,0 @@
|
|||
/* global libsignal, textsecure, dcodeIO, libloki */
|
||||
|
||||
/* eslint-disable no-bitwise */
|
||||
|
||||
const CiphertextMessage = require('./CiphertextMessage');
|
||||
const {
|
||||
bytesFromString,
|
||||
concatenateBytes,
|
||||
constantTimeEqual,
|
||||
decryptAesCtr,
|
||||
encryptAesCtr,
|
||||
fromEncodedBinaryToArrayBuffer,
|
||||
getViewOfArrayBuffer,
|
||||
getZeroes,
|
||||
highBitsToInt,
|
||||
hmacSha256,
|
||||
intsToByteHighAndLow,
|
||||
splitBytes,
|
||||
trimBytes,
|
||||
} = require('../crypto');
|
||||
|
||||
const REVOKED_CERTIFICATES = [];
|
||||
|
||||
function SecretSessionCipher(storage) {
|
||||
this.storage = storage;
|
||||
|
||||
// We do this on construction because libsignal won't be available when this file loads
|
||||
const { SessionCipher } = libsignal;
|
||||
this.SessionCipher = SessionCipher;
|
||||
}
|
||||
|
||||
const CIPHERTEXT_VERSION = 1;
|
||||
const UNIDENTIFIED_DELIVERY_PREFIX = 'UnidentifiedDelivery';
|
||||
|
||||
// public CertificateValidator(ECPublicKey trustRoot)
|
||||
function createCertificateValidator(trustRoot) {
|
||||
return {
|
||||
// public void validate(SenderCertificate certificate, long validationTime)
|
||||
async validate(certificate, validationTime) {
|
||||
const serverCertificate = certificate.signer;
|
||||
|
||||
await libsignal.Curve.async.verifySignature(
|
||||
trustRoot,
|
||||
serverCertificate.certificate,
|
||||
serverCertificate.signature
|
||||
);
|
||||
|
||||
const serverCertId = serverCertificate.certificate.id;
|
||||
if (REVOKED_CERTIFICATES.includes(serverCertId)) {
|
||||
throw new Error(
|
||||
`Server certificate id ${serverCertId} has been revoked`
|
||||
);
|
||||
}
|
||||
|
||||
await libsignal.Curve.async.verifySignature(
|
||||
serverCertificate.key,
|
||||
certificate.certificate,
|
||||
certificate.signature
|
||||
);
|
||||
|
||||
if (validationTime > certificate.expires) {
|
||||
throw new Error('Certificate is expired');
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function _decodePoint(serialized, offset = 0) {
|
||||
const view =
|
||||
offset > 0
|
||||
? getViewOfArrayBuffer(serialized, offset, serialized.byteLength)
|
||||
: serialized;
|
||||
|
||||
return libsignal.Curve.validatePubKeyFormat(view);
|
||||
}
|
||||
|
||||
// public ServerCertificate(byte[] serialized)
|
||||
function _createServerCertificateFromBuffer(serialized) {
|
||||
const wrapper = textsecure.protobuf.ServerCertificate.decode(serialized);
|
||||
|
||||
if (!wrapper.certificate || !wrapper.signature) {
|
||||
throw new Error('Missing fields');
|
||||
}
|
||||
|
||||
const certificate = textsecure.protobuf.ServerCertificate.Certificate.decode(
|
||||
wrapper.certificate.toArrayBuffer()
|
||||
);
|
||||
|
||||
if (!certificate.id || !certificate.key) {
|
||||
throw new Error('Missing fields');
|
||||
}
|
||||
|
||||
return {
|
||||
id: certificate.id,
|
||||
key: certificate.key.toArrayBuffer(),
|
||||
serialized,
|
||||
certificate: wrapper.certificate.toArrayBuffer(),
|
||||
|
||||
signature: wrapper.signature.toArrayBuffer(),
|
||||
};
|
||||
}
|
||||
|
||||
// public SenderCertificate(byte[] serialized)
|
||||
function _createSenderCertificateFromBuffer(serialized) {
|
||||
const cert = textsecure.protobuf.SenderCertificate.decode(serialized);
|
||||
|
||||
if (!cert.senderDevice || !cert.sender) {
|
||||
throw new Error('Missing fields');
|
||||
}
|
||||
|
||||
return {
|
||||
sender: cert.sender,
|
||||
senderDevice: cert.senderDevice,
|
||||
|
||||
certificate: cert.toArrayBuffer(),
|
||||
|
||||
serialized,
|
||||
};
|
||||
}
|
||||
|
||||
// public UnidentifiedSenderMessage(byte[] serialized)
|
||||
function _createUnidentifiedSenderMessageFromBuffer(serialized) {
|
||||
const version = highBitsToInt(serialized[0]);
|
||||
|
||||
if (version > CIPHERTEXT_VERSION) {
|
||||
throw new Error(`Unknown version: ${this.version}`);
|
||||
}
|
||||
|
||||
const view = getViewOfArrayBuffer(serialized, 1, serialized.byteLength);
|
||||
const unidentifiedSenderMessage = textsecure.protobuf.UnidentifiedSenderMessage.decode(
|
||||
view
|
||||
);
|
||||
|
||||
if (
|
||||
!unidentifiedSenderMessage.ephemeralPublic ||
|
||||
!unidentifiedSenderMessage.encryptedStatic ||
|
||||
!unidentifiedSenderMessage.encryptedMessage
|
||||
) {
|
||||
throw new Error('Missing fields');
|
||||
}
|
||||
|
||||
return {
|
||||
version,
|
||||
|
||||
ephemeralPublic: unidentifiedSenderMessage.ephemeralPublic.toArrayBuffer(),
|
||||
encryptedStatic: unidentifiedSenderMessage.encryptedStatic.toArrayBuffer(),
|
||||
encryptedMessage: unidentifiedSenderMessage.encryptedMessage.toArrayBuffer(),
|
||||
|
||||
serialized,
|
||||
};
|
||||
}
|
||||
|
||||
// public UnidentifiedSenderMessage(
|
||||
// ECPublicKey ephemeral, byte[] encryptedStatic, byte[] encryptedMessage) {
|
||||
function _createUnidentifiedSenderMessage(
|
||||
ephemeralPublic,
|
||||
encryptedStatic,
|
||||
encryptedMessage
|
||||
) {
|
||||
const versionBytes = new Uint8Array([
|
||||
intsToByteHighAndLow(CIPHERTEXT_VERSION, CIPHERTEXT_VERSION),
|
||||
]);
|
||||
const unidentifiedSenderMessage = new textsecure.protobuf.UnidentifiedSenderMessage();
|
||||
|
||||
unidentifiedSenderMessage.encryptedMessage = encryptedMessage;
|
||||
unidentifiedSenderMessage.encryptedStatic = encryptedStatic;
|
||||
unidentifiedSenderMessage.ephemeralPublic = ephemeralPublic;
|
||||
|
||||
const messageBytes = unidentifiedSenderMessage.encode().toArrayBuffer();
|
||||
|
||||
return {
|
||||
version: CIPHERTEXT_VERSION,
|
||||
|
||||
ephemeralPublic,
|
||||
encryptedStatic,
|
||||
encryptedMessage,
|
||||
|
||||
serialized: concatenateBytes(versionBytes, messageBytes),
|
||||
};
|
||||
}
|
||||
|
||||
// public UnidentifiedSenderMessageContent(byte[] serialized)
|
||||
function _createUnidentifiedSenderMessageContentFromBuffer(serialized) {
|
||||
const TypeEnum = textsecure.protobuf.UnidentifiedSenderMessage.Message.Type;
|
||||
|
||||
const message = textsecure.protobuf.UnidentifiedSenderMessage.Message.decode(
|
||||
serialized
|
||||
);
|
||||
|
||||
if (!message.type || !message.senderCertificate || !message.content) {
|
||||
throw new Error('Missing fields');
|
||||
}
|
||||
|
||||
let type;
|
||||
switch (message.type) {
|
||||
case TypeEnum.MESSAGE:
|
||||
type = CiphertextMessage.WHISPER_TYPE;
|
||||
break;
|
||||
case TypeEnum.PREKEY_MESSAGE:
|
||||
type = CiphertextMessage.PREKEY_TYPE;
|
||||
break;
|
||||
case TypeEnum.FALLBACK_MESSAGE:
|
||||
type = CiphertextMessage.FALLBACK_MESSAGE;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown type: ${message.type}`);
|
||||
}
|
||||
|
||||
return {
|
||||
type,
|
||||
senderCertificate: _createSenderCertificateFromBuffer(
|
||||
message.senderCertificate.toArrayBuffer()
|
||||
),
|
||||
content: message.content.toArrayBuffer(),
|
||||
|
||||
serialized,
|
||||
};
|
||||
}
|
||||
|
||||
// private int getProtoType(int type)
|
||||
function _getProtoMessageType(type) {
|
||||
const TypeEnum = textsecure.protobuf.UnidentifiedSenderMessage.Message.Type;
|
||||
|
||||
switch (type) {
|
||||
case CiphertextMessage.WHISPER_TYPE:
|
||||
return TypeEnum.MESSAGE;
|
||||
case CiphertextMessage.PREKEY_TYPE:
|
||||
return TypeEnum.PREKEY_MESSAGE;
|
||||
case CiphertextMessage.FALLBACK_MESSAGE:
|
||||
return TypeEnum.FALLBACK_MESSAGE;
|
||||
default:
|
||||
throw new Error(`_getProtoMessageType: type '${type}' does not exist`);
|
||||
}
|
||||
}
|
||||
|
||||
// public UnidentifiedSenderMessageContent(
|
||||
// int type, SenderCertificate senderCertificate, byte[] content)
|
||||
function _createUnidentifiedSenderMessageContent(
|
||||
type,
|
||||
senderCertificate,
|
||||
content
|
||||
) {
|
||||
const innerMessage = new textsecure.protobuf.UnidentifiedSenderMessage.Message();
|
||||
innerMessage.type = _getProtoMessageType(type);
|
||||
innerMessage.senderCertificate = senderCertificate;
|
||||
innerMessage.content = content;
|
||||
|
||||
return {
|
||||
type,
|
||||
senderCertificate,
|
||||
content,
|
||||
|
||||
serialized: innerMessage.encode().toArrayBuffer(),
|
||||
};
|
||||
}
|
||||
|
||||
SecretSessionCipher.prototype = {
|
||||
async encrypt(destinationPubkey, senderCertificate, innerEncryptedMessage) {
|
||||
// Capture this.xxx variables to replicate Java's implicit this syntax
|
||||
const _calculateEphemeralKeys = this._calculateEphemeralKeys.bind(this);
|
||||
const _encryptWithSecretKeys = this._encryptWithSecretKeys.bind(this);
|
||||
const _calculateStaticKeys = this._calculateStaticKeys.bind(this);
|
||||
|
||||
const ourIdentity = await this.storage.getIdentityKeyPair();
|
||||
const theirIdentity = dcodeIO.ByteBuffer.wrap(
|
||||
destinationPubkey,
|
||||
'hex'
|
||||
).toArrayBuffer();
|
||||
|
||||
const ephemeral = await libsignal.Curve.async.generateKeyPair();
|
||||
const ephemeralSalt = concatenateBytes(
|
||||
bytesFromString(UNIDENTIFIED_DELIVERY_PREFIX),
|
||||
theirIdentity,
|
||||
ephemeral.pubKey
|
||||
);
|
||||
const ephemeralKeys = await _calculateEphemeralKeys(
|
||||
theirIdentity,
|
||||
ephemeral.privKey,
|
||||
ephemeralSalt
|
||||
);
|
||||
const staticKeyCiphertext = await _encryptWithSecretKeys(
|
||||
ephemeralKeys.cipherKey,
|
||||
ephemeralKeys.macKey,
|
||||
ourIdentity.pubKey
|
||||
);
|
||||
|
||||
const staticSalt = concatenateBytes(
|
||||
ephemeralKeys.chainKey,
|
||||
staticKeyCiphertext
|
||||
);
|
||||
const staticKeys = await _calculateStaticKeys(
|
||||
theirIdentity,
|
||||
ourIdentity.privKey,
|
||||
staticSalt
|
||||
);
|
||||
const content = _createUnidentifiedSenderMessageContent(
|
||||
innerEncryptedMessage.type,
|
||||
senderCertificate,
|
||||
fromEncodedBinaryToArrayBuffer(innerEncryptedMessage.body)
|
||||
);
|
||||
const messageBytes = await _encryptWithSecretKeys(
|
||||
staticKeys.cipherKey,
|
||||
staticKeys.macKey,
|
||||
content.serialized
|
||||
);
|
||||
|
||||
const unidentifiedSenderMessage = _createUnidentifiedSenderMessage(
|
||||
ephemeral.pubKey,
|
||||
staticKeyCiphertext,
|
||||
messageBytes
|
||||
);
|
||||
|
||||
return unidentifiedSenderMessage.serialized;
|
||||
},
|
||||
|
||||
// public Pair<SignalProtocolAddress, byte[]> decrypt(
|
||||
// CertificateValidator validator, byte[] ciphertext, long timestamp)
|
||||
async decrypt(ciphertext, me) {
|
||||
// Capture this.xxx variables to replicate Java's implicit this syntax
|
||||
const signalProtocolStore = this.storage;
|
||||
const _calculateEphemeralKeys = this._calculateEphemeralKeys.bind(this);
|
||||
const _calculateStaticKeys = this._calculateStaticKeys.bind(this);
|
||||
const _decryptWithUnidentifiedSenderMessage = this._decryptWithUnidentifiedSenderMessage.bind(
|
||||
this
|
||||
);
|
||||
const _decryptWithSecretKeys = this._decryptWithSecretKeys.bind(this);
|
||||
|
||||
const ourIdentity = await signalProtocolStore.getIdentityKeyPair();
|
||||
|
||||
const wrapper = _createUnidentifiedSenderMessageFromBuffer(ciphertext);
|
||||
const ephemeralSalt = concatenateBytes(
|
||||
bytesFromString(UNIDENTIFIED_DELIVERY_PREFIX),
|
||||
ourIdentity.pubKey,
|
||||
wrapper.ephemeralPublic
|
||||
);
|
||||
const ephemeralKeys = await _calculateEphemeralKeys(
|
||||
wrapper.ephemeralPublic,
|
||||
ourIdentity.privKey,
|
||||
ephemeralSalt
|
||||
);
|
||||
const staticKeyBytes = await _decryptWithSecretKeys(
|
||||
ephemeralKeys.cipherKey,
|
||||
ephemeralKeys.macKey,
|
||||
wrapper.encryptedStatic
|
||||
);
|
||||
|
||||
const staticKey = _decodePoint(staticKeyBytes, 0);
|
||||
const staticSalt = concatenateBytes(
|
||||
ephemeralKeys.chainKey,
|
||||
wrapper.encryptedStatic
|
||||
);
|
||||
const staticKeys = await _calculateStaticKeys(
|
||||
staticKey,
|
||||
ourIdentity.privKey,
|
||||
staticSalt
|
||||
);
|
||||
const messageBytes = await _decryptWithSecretKeys(
|
||||
staticKeys.cipherKey,
|
||||
staticKeys.macKey,
|
||||
wrapper.encryptedMessage
|
||||
);
|
||||
|
||||
const content = _createUnidentifiedSenderMessageContentFromBuffer(
|
||||
messageBytes
|
||||
);
|
||||
|
||||
const { sender, senderDevice } = content.senderCertificate;
|
||||
const { number, deviceId } = me || {};
|
||||
if (sender === number && senderDevice === deviceId) {
|
||||
return {
|
||||
isMe: true,
|
||||
};
|
||||
}
|
||||
const address = new libsignal.SignalProtocolAddress(sender, senderDevice);
|
||||
|
||||
try {
|
||||
return {
|
||||
sender: address,
|
||||
content: await _decryptWithUnidentifiedSenderMessage(content),
|
||||
type: content.type,
|
||||
};
|
||||
} catch (error) {
|
||||
if (!error) {
|
||||
// eslint-disable-next-line no-ex-assign
|
||||
error = new Error('Decryption error was falsey!');
|
||||
}
|
||||
|
||||
error.sender = address;
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// public int getSessionVersion(SignalProtocolAddress remoteAddress) {
|
||||
getSessionVersion(remoteAddress) {
|
||||
const { SessionCipher } = this;
|
||||
const signalProtocolStore = this.storage;
|
||||
|
||||
const cipher = new SessionCipher(signalProtocolStore, remoteAddress);
|
||||
|
||||
return cipher.getSessionVersion();
|
||||
},
|
||||
|
||||
// public int getRemoteRegistrationId(SignalProtocolAddress remoteAddress) {
|
||||
getRemoteRegistrationId(remoteAddress) {
|
||||
const { SessionCipher } = this;
|
||||
const signalProtocolStore = this.storage;
|
||||
|
||||
const cipher = new SessionCipher(signalProtocolStore, remoteAddress);
|
||||
|
||||
return cipher.getRemoteRegistrationId();
|
||||
},
|
||||
|
||||
closeOpenSessionForDevice(remoteAddress) {
|
||||
const { SessionCipher } = this;
|
||||
const signalProtocolStore = this.storage;
|
||||
|
||||
const cipher = new SessionCipher(signalProtocolStore, remoteAddress);
|
||||
|
||||
return cipher.closeOpenSessionForDevice();
|
||||
},
|
||||
|
||||
// private EphemeralKeys calculateEphemeralKeys(
|
||||
// ECPublicKey ephemeralPublic, ECPrivateKey ephemeralPrivate, byte[] salt)
|
||||
async _calculateEphemeralKeys(ephemeralPublic, ephemeralPrivate, salt) {
|
||||
const ephemeralSecret = await libsignal.Curve.async.calculateAgreement(
|
||||
ephemeralPublic,
|
||||
ephemeralPrivate
|
||||
);
|
||||
const ephemeralDerivedParts = await libsignal.HKDF.deriveSecrets(
|
||||
ephemeralSecret,
|
||||
salt,
|
||||
new ArrayBuffer()
|
||||
);
|
||||
|
||||
// private EphemeralKeys(byte[] chainKey, byte[] cipherKey, byte[] macKey)
|
||||
return {
|
||||
chainKey: ephemeralDerivedParts[0],
|
||||
cipherKey: ephemeralDerivedParts[1],
|
||||
macKey: ephemeralDerivedParts[2],
|
||||
};
|
||||
},
|
||||
|
||||
// private StaticKeys calculateStaticKeys(
|
||||
// ECPublicKey staticPublic, ECPrivateKey staticPrivate, byte[] salt)
|
||||
async _calculateStaticKeys(staticPublic, staticPrivate, salt) {
|
||||
const staticSecret = await libsignal.Curve.async.calculateAgreement(
|
||||
staticPublic,
|
||||
staticPrivate
|
||||
);
|
||||
const staticDerivedParts = await libsignal.HKDF.deriveSecrets(
|
||||
staticSecret,
|
||||
salt,
|
||||
new ArrayBuffer()
|
||||
);
|
||||
|
||||
// private StaticKeys(byte[] cipherKey, byte[] macKey)
|
||||
return {
|
||||
cipherKey: staticDerivedParts[1],
|
||||
macKey: staticDerivedParts[2],
|
||||
};
|
||||
},
|
||||
|
||||
// private byte[] decrypt(UnidentifiedSenderMessageContent message)
|
||||
_decryptWithUnidentifiedSenderMessage(message) {
|
||||
const signalProtocolStore = this.storage;
|
||||
|
||||
const sender = new libsignal.SignalProtocolAddress(
|
||||
message.senderCertificate.sender,
|
||||
message.senderCertificate.senderDevice
|
||||
);
|
||||
|
||||
switch (message.type) {
|
||||
case CiphertextMessage.WHISPER_TYPE:
|
||||
return new libloki.crypto.LokiSessionCipher(
|
||||
signalProtocolStore,
|
||||
sender
|
||||
).decryptWhisperMessage(message.content);
|
||||
case CiphertextMessage.PREKEY_TYPE:
|
||||
return new libloki.crypto.LokiSessionCipher(
|
||||
signalProtocolStore,
|
||||
sender
|
||||
).decryptPreKeyWhisperMessage(message.content);
|
||||
case CiphertextMessage.FALLBACK_MESSAGE:
|
||||
return new libloki.crypto.FallBackSessionCipher(sender).decrypt(
|
||||
message.content
|
||||
);
|
||||
default:
|
||||
throw new Error(`Unknown type: ${message.type}`);
|
||||
}
|
||||
},
|
||||
|
||||
// private byte[] encrypt(
|
||||
// SecretKeySpec cipherKey, SecretKeySpec macKey, byte[] plaintext)
|
||||
async _encryptWithSecretKeys(cipherKey, macKey, plaintext) {
|
||||
// Cipher const cipher = Cipher.getInstance('AES/CTR/NoPadding');
|
||||
// cipher.init(Cipher.ENCRYPT_MODE, cipherKey, new IvParameterSpec(new byte[16]));
|
||||
|
||||
// Mac const mac = Mac.getInstance('HmacSHA256');
|
||||
// mac.init(macKey);
|
||||
|
||||
// byte[] const ciphertext = cipher.doFinal(plaintext);
|
||||
const ciphertext = await encryptAesCtr(cipherKey, plaintext, getZeroes(16));
|
||||
|
||||
// byte[] const ourFullMac = mac.doFinal(ciphertext);
|
||||
const ourFullMac = await hmacSha256(macKey, ciphertext);
|
||||
const ourMac = trimBytes(ourFullMac, 10);
|
||||
|
||||
return concatenateBytes(ciphertext, ourMac);
|
||||
},
|
||||
|
||||
// private byte[] decrypt(
|
||||
// SecretKeySpec cipherKey, SecretKeySpec macKey, byte[] ciphertext)
|
||||
async _decryptWithSecretKeys(cipherKey, macKey, ciphertext) {
|
||||
if (ciphertext.byteLength < 10) {
|
||||
throw new Error('Ciphertext not long enough for MAC!');
|
||||
}
|
||||
|
||||
const ciphertextParts = splitBytes(
|
||||
ciphertext,
|
||||
ciphertext.byteLength - 10,
|
||||
10
|
||||
);
|
||||
|
||||
// Mac const mac = Mac.getInstance('HmacSHA256');
|
||||
// mac.init(macKey);
|
||||
|
||||
// byte[] const digest = mac.doFinal(ciphertextParts[0]);
|
||||
const digest = await hmacSha256(macKey, ciphertextParts[0]);
|
||||
const ourMac = trimBytes(digest, 10);
|
||||
const theirMac = ciphertextParts[1];
|
||||
|
||||
if (!constantTimeEqual(ourMac, theirMac)) {
|
||||
throw new Error('Bad mac!');
|
||||
}
|
||||
|
||||
// Cipher const cipher = Cipher.getInstance('AES/CTR/NoPadding');
|
||||
// cipher.init(Cipher.DECRYPT_MODE, cipherKey, new IvParameterSpec(new byte[16]));
|
||||
|
||||
// return cipher.doFinal(ciphertextParts[0]);
|
||||
return decryptAesCtr(cipherKey, ciphertextParts[0], getZeroes(16));
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
SecretSessionCipher,
|
||||
createCertificateValidator,
|
||||
_createServerCertificateFromBuffer,
|
||||
_createSenderCertificateFromBuffer,
|
||||
};
|
9
js/modules/signal.d.ts
vendored
9
js/modules/signal.d.ts
vendored
|
@ -1,9 +0,0 @@
|
|||
import { SecretSessionCipherConstructor } from './metadata/SecretSessionCipher';
|
||||
|
||||
interface Metadata {
|
||||
SecretSessionCipher: SecretSessionCipherConstructor;
|
||||
}
|
||||
|
||||
export interface SignalInterface {
|
||||
Metadata: Metadata;
|
||||
}
|
|
@ -10,7 +10,6 @@ const OS = require('../../ts/OS');
|
|||
const Settings = require('./settings');
|
||||
const Util = require('../../ts/util');
|
||||
const { migrateToSQL } = require('./migrate_to_sql');
|
||||
const Metadata = require('./metadata/SecretSessionCipher');
|
||||
const LinkPreviews = require('./link_previews');
|
||||
const AttachmentDownloads = require('./attachment_downloads');
|
||||
|
||||
|
@ -32,9 +31,6 @@ const { UserDetailsDialog } = require('../../ts/components/UserDetailsDialog');
|
|||
const {
|
||||
DevicePairingDialog,
|
||||
} = require('../../ts/components/DevicePairingDialog');
|
||||
const {
|
||||
SessionConversation,
|
||||
} = require('../../ts/components/session/conversation/SessionConversation');
|
||||
const { SessionModal } = require('../../ts/components/session/SessionModal');
|
||||
const {
|
||||
SessionSeedModal,
|
||||
|
@ -244,7 +240,6 @@ exports.setup = (options = {}) => {
|
|||
AddModeratorsDialog,
|
||||
RemoveModeratorsDialog,
|
||||
GroupInvitation,
|
||||
SessionConversation,
|
||||
SessionConfirm,
|
||||
SessionModal,
|
||||
SessionSeedModal,
|
||||
|
@ -299,7 +294,6 @@ exports.setup = (options = {}) => {
|
|||
Emoji,
|
||||
IndexedDB,
|
||||
LinkPreviews,
|
||||
Metadata,
|
||||
migrateToSQL,
|
||||
Migrations,
|
||||
Notifications,
|
||||
|
|
|
@ -1,356 +0,0 @@
|
|||
const fetch = require('node-fetch');
|
||||
const { Agent } = require('https');
|
||||
|
||||
/* global Buffer, setTimeout, log, _ */
|
||||
|
||||
/* eslint-disable more/no-then, no-bitwise, no-nested-ternary */
|
||||
|
||||
function _btoa(str) {
|
||||
let buffer;
|
||||
|
||||
if (str instanceof Buffer) {
|
||||
buffer = str;
|
||||
} else {
|
||||
buffer = Buffer.from(str.toString(), 'binary');
|
||||
}
|
||||
|
||||
return buffer.toString('base64');
|
||||
}
|
||||
|
||||
const _call = object => Object.prototype.toString.call(object);
|
||||
|
||||
const ArrayBufferToString = _call(new ArrayBuffer());
|
||||
const Uint8ArrayToString = _call(new Uint8Array());
|
||||
|
||||
function _getString(thing) {
|
||||
if (typeof thing !== 'string') {
|
||||
if (_call(thing) === Uint8ArrayToString) {
|
||||
return String.fromCharCode.apply(null, thing);
|
||||
}
|
||||
if (_call(thing) === ArrayBufferToString) {
|
||||
return _getString(new Uint8Array(thing));
|
||||
}
|
||||
}
|
||||
return thing;
|
||||
}
|
||||
|
||||
function _validateResponse(response, schema) {
|
||||
try {
|
||||
// eslint-disable-next-line guard-for-in, no-restricted-syntax
|
||||
for (const i in schema) {
|
||||
switch (schema[i]) {
|
||||
case 'object':
|
||||
case 'string':
|
||||
case 'number':
|
||||
// eslint-disable-next-line valid-typeof
|
||||
if (typeof response[i] !== schema[i]) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
} catch (ex) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const FIVE_MINUTES = 1000 * 60 * 5;
|
||||
const agents = {
|
||||
unauth: null,
|
||||
auth: null,
|
||||
};
|
||||
|
||||
function getContentType(response) {
|
||||
if (response.headers && response.headers.get) {
|
||||
return response.headers.get('content-type');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function _promiseAjax(providedUrl, options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = providedUrl || `${options.host}/${options.path}`;
|
||||
if (options.disableLogs) {
|
||||
log.info(
|
||||
`${options.type} [REDACTED_URL]${
|
||||
options.unauthenticated ? ' (unauth)' : ''
|
||||
}`
|
||||
);
|
||||
} else {
|
||||
log.info(
|
||||
`${options.type} ${url}${options.unauthenticated ? ' (unauth)' : ''}`
|
||||
);
|
||||
}
|
||||
|
||||
const timeout =
|
||||
typeof options.timeout !== 'undefined' ? options.timeout : 10000;
|
||||
|
||||
const { proxyUrl } = options;
|
||||
const agentType = options.unauthenticated ? 'unauth' : 'auth';
|
||||
const cacheKey = `${proxyUrl}-${agentType}`;
|
||||
|
||||
const { timestamp } = agents[cacheKey] || {};
|
||||
if (!timestamp || timestamp + FIVE_MINUTES < Date.now()) {
|
||||
if (timestamp) {
|
||||
log.info(`Cycling agent for type ${cacheKey}`);
|
||||
}
|
||||
agents[cacheKey] = {
|
||||
agent: new Agent({ keepAlive: true }),
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
const { agent } = agents[cacheKey];
|
||||
|
||||
const fetchOptions = {
|
||||
method: options.type,
|
||||
body: options.data || null,
|
||||
headers: {
|
||||
'User-Agent': 'Session',
|
||||
'X-Loki-Messenger-Agent': 'OWD',
|
||||
...options.headers,
|
||||
},
|
||||
redirect: options.redirect,
|
||||
agent,
|
||||
ca: options.certificateAuthority,
|
||||
timeout,
|
||||
};
|
||||
|
||||
if (fetchOptions.body instanceof ArrayBuffer) {
|
||||
// node-fetch doesn't support ArrayBuffer, only node Buffer
|
||||
const contentLength = fetchOptions.body.byteLength;
|
||||
fetchOptions.body = Buffer.from(fetchOptions.body);
|
||||
|
||||
// node-fetch doesn't set content-length like S3 requires
|
||||
fetchOptions.headers['Content-Length'] = contentLength;
|
||||
}
|
||||
|
||||
const { accessKey, unauthenticated } = options;
|
||||
if (unauthenticated) {
|
||||
if (!accessKey) {
|
||||
throw new Error(
|
||||
'_promiseAjax: mode is aunathenticated, but accessKey was not provided'
|
||||
);
|
||||
}
|
||||
// Access key is already a Base64 string
|
||||
fetchOptions.headers['Unidentified-Access-Key'] = accessKey;
|
||||
} else if (options.user && options.password) {
|
||||
const user = _getString(options.user);
|
||||
const password = _getString(options.password);
|
||||
const auth = _btoa(`${user}:${password}`);
|
||||
fetchOptions.headers.Authorization = `Basic ${auth}`;
|
||||
}
|
||||
|
||||
if (options.contentType) {
|
||||
fetchOptions.headers['Content-Type'] = options.contentType;
|
||||
}
|
||||
|
||||
fetch(url, fetchOptions)
|
||||
.then(response => {
|
||||
let resultPromise;
|
||||
if (
|
||||
options.responseType === 'json' &&
|
||||
response.headers.get('Content-Type') === 'application/json'
|
||||
) {
|
||||
resultPromise = response.json();
|
||||
} else if (
|
||||
options.responseType === 'arraybuffer' ||
|
||||
options.responseType === 'arraybufferwithdetails'
|
||||
) {
|
||||
resultPromise = response.buffer();
|
||||
} else {
|
||||
resultPromise = response.text();
|
||||
}
|
||||
|
||||
return resultPromise.then(result => {
|
||||
if (
|
||||
options.responseType === 'arraybuffer' ||
|
||||
options.responseType === 'arraybufferwithdetails'
|
||||
) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
result = result.buffer.slice(
|
||||
result.byteOffset,
|
||||
result.byteOffset + result.byteLength
|
||||
);
|
||||
}
|
||||
if (options.responseType === 'json') {
|
||||
if (options.validateResponse) {
|
||||
if (!_validateResponse(result, options.validateResponse)) {
|
||||
if (options.disableLogs) {
|
||||
log.info(
|
||||
options.type,
|
||||
'[REDACTED_URL]',
|
||||
response.status,
|
||||
'Error'
|
||||
);
|
||||
} else {
|
||||
log.error(options.type, url, response.status, 'Error');
|
||||
}
|
||||
return reject(
|
||||
HTTPError(
|
||||
'promiseAjax: invalid response',
|
||||
response.status,
|
||||
result,
|
||||
options.stack
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (response.status >= 0 && response.status < 400) {
|
||||
if (options.disableLogs) {
|
||||
log.info(
|
||||
options.type,
|
||||
'[REDACTED_URL]',
|
||||
response.status,
|
||||
'Success'
|
||||
);
|
||||
} else {
|
||||
log.info(options.type, url, response.status, 'Success');
|
||||
}
|
||||
if (options.responseType === 'arraybufferwithdetails') {
|
||||
return resolve({
|
||||
data: result,
|
||||
contentType: getContentType(response),
|
||||
response,
|
||||
});
|
||||
}
|
||||
return resolve(result, response.status);
|
||||
}
|
||||
|
||||
if (options.disableLogs) {
|
||||
log.info(options.type, '[REDACTED_URL]', response.status, 'Error');
|
||||
} else {
|
||||
log.error(options.type, url, response.status, 'Error');
|
||||
}
|
||||
return reject(
|
||||
HTTPError(
|
||||
'promiseAjax: error response',
|
||||
response.status,
|
||||
result,
|
||||
options.stack
|
||||
)
|
||||
);
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
if (options.disableLogs) {
|
||||
log.error(options.type, '[REDACTED_URL]', 0, 'Error');
|
||||
} else {
|
||||
log.error(options.type, url, 0, 'Error');
|
||||
}
|
||||
const stack = `${e.stack}\nInitial stack:\n${options.stack}`;
|
||||
reject(HTTPError('promiseAjax catch', 0, e.toString(), stack));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function _retryAjax(url, options, providedLimit, providedCount) {
|
||||
const count = (providedCount || 0) + 1;
|
||||
const limit = providedLimit || 3;
|
||||
return _promiseAjax(url, options).catch(e => {
|
||||
if (e.name === 'HTTPError' && e.code === -1 && count < limit) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve(_retryAjax(url, options, limit, count));
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
function _outerAjax(url, options) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
options.stack = new Error().stack; // just in case, save stack here.
|
||||
return _retryAjax(url, options);
|
||||
}
|
||||
|
||||
function HTTPError(message, providedCode, response, stack) {
|
||||
const code = providedCode > 999 || providedCode < 100 ? -1 : providedCode;
|
||||
const e = new Error(`${message}; code: ${code}`);
|
||||
e.name = 'HTTPError';
|
||||
e.code = code;
|
||||
e.stack += `\nOriginal stack:\n${stack}`;
|
||||
if (response) {
|
||||
e.response = response;
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initialize,
|
||||
};
|
||||
|
||||
// We first set up the data that won't change during this session of the app
|
||||
function initialize() {
|
||||
// Thanks to function-hoisting, we can put this return statement before all of the
|
||||
// below function definitions.
|
||||
return {
|
||||
connect,
|
||||
};
|
||||
|
||||
// Then we connect to the server with user-specific information. This is the only API
|
||||
// exposed to the browser context, ensuring that it can't connect to arbitrary
|
||||
// locations.
|
||||
function connect() {
|
||||
// Thanks, function hoisting!
|
||||
return {
|
||||
getAttachment,
|
||||
getProxiedSize,
|
||||
makeProxiedRequest,
|
||||
};
|
||||
|
||||
function getAttachment(fileUrl) {
|
||||
return _outerAjax(fileUrl, {
|
||||
contentType: 'application/octet-stream',
|
||||
responseType: 'arraybuffer',
|
||||
timeout: 0,
|
||||
type: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-shadow
|
||||
async function getProxiedSize(url) {
|
||||
const result = await _outerAjax(url, {
|
||||
processData: false,
|
||||
responseType: 'arraybufferwithdetails',
|
||||
proxyUrl: '',
|
||||
type: 'HEAD',
|
||||
disableLogs: true,
|
||||
});
|
||||
|
||||
const { response } = result;
|
||||
if (!response.headers || !response.headers.get) {
|
||||
throw new Error('getProxiedSize: Problem retrieving header value');
|
||||
}
|
||||
|
||||
const size = response.headers.get('content-length');
|
||||
return parseInt(size, 10);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-shadow
|
||||
function makeProxiedRequest(url, options = {}) {
|
||||
const { returnArrayBuffer, start, end } = options;
|
||||
let headers;
|
||||
|
||||
if (_.isNumber(start) && _.isNumber(end)) {
|
||||
headers = {
|
||||
Range: `bytes=${start}-${end}`,
|
||||
};
|
||||
}
|
||||
|
||||
return _outerAjax(url, {
|
||||
processData: false,
|
||||
responseType: returnArrayBuffer ? 'arraybufferwithdetails' : null,
|
||||
proxyUrl: '',
|
||||
type: 'GET',
|
||||
redirect: 'follow',
|
||||
disableLogs: true,
|
||||
headers,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,101 +0,0 @@
|
|||
/* global Whisper, storage, getAccountManager */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
const ROTATION_INTERVAL = 48 * 60 * 60 * 1000;
|
||||
let timeout;
|
||||
let scheduledTime;
|
||||
let shouldStop = false;
|
||||
|
||||
function scheduleNextRotation() {
|
||||
const now = Date.now();
|
||||
const nextTime = now + ROTATION_INTERVAL;
|
||||
storage.put('nextSignedKeyRotationTime', nextTime);
|
||||
}
|
||||
|
||||
function run() {
|
||||
if (shouldStop) {
|
||||
return;
|
||||
}
|
||||
window.log.info('Rotating signed prekey...');
|
||||
getAccountManager()
|
||||
.rotateSignedPreKey()
|
||||
.catch(() => {
|
||||
window.log.error(
|
||||
'rotateSignedPrekey() failed. Trying again in five seconds'
|
||||
);
|
||||
setTimeout(runWhenOnline, 5000);
|
||||
});
|
||||
scheduleNextRotation();
|
||||
setTimeoutForNextRun();
|
||||
}
|
||||
|
||||
function runWhenOnline() {
|
||||
if (navigator.onLine) {
|
||||
run();
|
||||
} else {
|
||||
window.log.info(
|
||||
'We are offline; keys will be rotated when we are next online'
|
||||
);
|
||||
const listener = () => {
|
||||
window.removeEventListener('online', listener);
|
||||
run();
|
||||
};
|
||||
window.addEventListener('online', listener);
|
||||
}
|
||||
}
|
||||
|
||||
function setTimeoutForNextRun() {
|
||||
const now = Date.now();
|
||||
const time = storage.get('nextSignedKeyRotationTime', now);
|
||||
|
||||
if (scheduledTime !== time || !timeout) {
|
||||
window.log.info(
|
||||
'Next signed key rotation scheduled for',
|
||||
new Date(time).toISOString()
|
||||
);
|
||||
}
|
||||
|
||||
scheduledTime = time;
|
||||
let waitTime = time - now;
|
||||
if (waitTime < 0) {
|
||||
waitTime = 0;
|
||||
}
|
||||
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(runWhenOnline, waitTime);
|
||||
}
|
||||
function onTimeTravel() {
|
||||
if (Whisper.Registration.isDone()) {
|
||||
setTimeoutForNextRun();
|
||||
}
|
||||
}
|
||||
let initComplete;
|
||||
Whisper.RotateSignedPreKeyListener = {
|
||||
init(events, newVersion) {
|
||||
if (initComplete) {
|
||||
window.log.warn('Rotate signed prekey listener: Already initialized');
|
||||
return;
|
||||
}
|
||||
initComplete = true;
|
||||
shouldStop = false;
|
||||
|
||||
if (newVersion) {
|
||||
runWhenOnline();
|
||||
} else {
|
||||
setTimeoutForNextRun();
|
||||
}
|
||||
|
||||
events.on('timetravel', onTimeTravel);
|
||||
},
|
||||
stop(events) {
|
||||
initComplete = false;
|
||||
shouldStop = true;
|
||||
events.off('timetravel', onTimeTravel);
|
||||
clearTimeout(timeout);
|
||||
},
|
||||
};
|
||||
})();
|
|
@ -3,7 +3,6 @@
|
|||
dcodeIO,
|
||||
Backbone,
|
||||
_,
|
||||
libsignal,
|
||||
textsecure,
|
||||
stringObject,
|
||||
BlockedNumberController
|
||||
|
@ -15,29 +14,11 @@
|
|||
(function() {
|
||||
'use strict';
|
||||
|
||||
const TIMESTAMP_THRESHOLD = 5 * 1000; // 5 seconds
|
||||
const Direction = {
|
||||
SENDING: 1,
|
||||
RECEIVING: 2,
|
||||
};
|
||||
|
||||
const VerifiedStatus = {
|
||||
DEFAULT: 0,
|
||||
VERIFIED: 1,
|
||||
UNVERIFIED: 2,
|
||||
};
|
||||
|
||||
function validateVerifiedStatus(status) {
|
||||
if (
|
||||
status === VerifiedStatus.DEFAULT ||
|
||||
status === VerifiedStatus.VERIFIED ||
|
||||
status === VerifiedStatus.UNVERIFIED
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const StaticByteBufferProto = new dcodeIO.ByteBuffer().__proto__;
|
||||
const StaticArrayBufferProto = new ArrayBuffer().__proto__;
|
||||
const StaticUint8ArrayProto = new Uint8Array().__proto__;
|
||||
|
@ -113,7 +94,6 @@
|
|||
'publicKey',
|
||||
'firstUse',
|
||||
'timestamp',
|
||||
'verified',
|
||||
'nonblockingApproval',
|
||||
],
|
||||
validate(attrs) {
|
||||
|
@ -144,9 +124,6 @@
|
|||
if (typeof attrs.timestamp !== 'number' || !(attrs.timestamp >= 0)) {
|
||||
return new Error('Invalid identity key timestamp');
|
||||
}
|
||||
if (!validateVerifiedStatus(attrs.verified)) {
|
||||
return new Error('Invalid identity key verified');
|
||||
}
|
||||
if (typeof attrs.nonblockingApproval !== 'boolean') {
|
||||
return new Error('Invalid identity key nonblockingApproval');
|
||||
}
|
||||
|
@ -210,223 +187,21 @@
|
|||
window.log.error('Could not load identityKey from SignalData');
|
||||
return undefined;
|
||||
},
|
||||
async getLocalRegistrationId() {
|
||||
const item = await window.Signal.Data.getItemById('registrationId');
|
||||
if (item) {
|
||||
return item.value;
|
||||
}
|
||||
|
||||
return 1;
|
||||
},
|
||||
|
||||
// PreKeys
|
||||
|
||||
async loadPreKey(keyId) {
|
||||
const key = this.preKeys[keyId];
|
||||
if (key) {
|
||||
window.log.info('Successfully fetched prekey:', keyId);
|
||||
return {
|
||||
pubKey: key.publicKey,
|
||||
privKey: key.privateKey,
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
async loadPreKeyForContact(contactPubKey) {
|
||||
const key = await window.Signal.Data.getPreKeyByRecipient(contactPubKey);
|
||||
|
||||
if (key) {
|
||||
window.log.info(
|
||||
'Successfully fetched prekey for recipient:',
|
||||
contactPubKey
|
||||
);
|
||||
return {
|
||||
pubKey: key.publicKey,
|
||||
privKey: key.privateKey,
|
||||
keyId: key.id,
|
||||
recipient: key.recipient,
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
async storePreKey(keyId, keyPair, contactPubKey) {
|
||||
const data = {
|
||||
id: keyId,
|
||||
publicKey: keyPair.pubKey,
|
||||
privateKey: keyPair.privKey,
|
||||
recipient: contactPubKey,
|
||||
};
|
||||
|
||||
this.preKeys[keyId] = data;
|
||||
await window.Signal.Data.createOrUpdatePreKey(data);
|
||||
},
|
||||
async removePreKey(keyId) {
|
||||
try {
|
||||
this.trigger('removePreKey');
|
||||
} catch (error) {
|
||||
window.log.error(
|
||||
'removePreKey error triggering removePreKey:',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
}
|
||||
|
||||
delete this.preKeys[keyId];
|
||||
await window.Signal.Data.removePreKeyById(keyId);
|
||||
},
|
||||
async clearPreKeyStore() {
|
||||
this.preKeys = Object.create(null);
|
||||
await window.Signal.Data.removeAllPreKeys();
|
||||
},
|
||||
|
||||
// Signed PreKeys
|
||||
/* Returns a signed keypair object or undefined */
|
||||
async loadSignedPreKey(keyId) {
|
||||
const key = this.signedPreKeys[keyId];
|
||||
if (key) {
|
||||
window.log.info('Successfully fetched signed prekey:', key.id);
|
||||
return {
|
||||
pubKey: key.publicKey,
|
||||
privKey: key.privateKey,
|
||||
created_at: key.created_at,
|
||||
keyId: key.id,
|
||||
confirmed: key.confirmed,
|
||||
signature: key.signature,
|
||||
};
|
||||
}
|
||||
|
||||
window.log.error('Failed to fetch signed prekey:', keyId);
|
||||
return undefined;
|
||||
},
|
||||
async loadSignedPreKeys() {
|
||||
if (arguments.length > 0) {
|
||||
throw new Error('loadSignedPreKeys takes no arguments');
|
||||
}
|
||||
|
||||
const keys = Object.values(this.signedPreKeys);
|
||||
return keys.map(prekey => ({
|
||||
pubKey: prekey.publicKey,
|
||||
privKey: prekey.privateKey,
|
||||
created_at: prekey.created_at,
|
||||
keyId: prekey.id,
|
||||
confirmed: prekey.confirmed,
|
||||
signature: prekey.signature,
|
||||
}));
|
||||
},
|
||||
async storeSignedPreKey(keyId, keyPair, confirmed, signature) {
|
||||
const data = {
|
||||
id: keyId,
|
||||
publicKey: keyPair.pubKey,
|
||||
privateKey: keyPair.privKey,
|
||||
created_at: Date.now(),
|
||||
confirmed: Boolean(confirmed),
|
||||
signature,
|
||||
};
|
||||
|
||||
this.signedPreKeys[keyId] = data;
|
||||
await window.Signal.Data.createOrUpdateSignedPreKey(data);
|
||||
},
|
||||
async removeSignedPreKey(keyId) {
|
||||
delete this.signedPreKeys[keyId];
|
||||
await window.Signal.Data.removeSignedPreKeyById(keyId);
|
||||
},
|
||||
async clearSignedPreKeysStore() {
|
||||
this.signedPreKeys = Object.create(null);
|
||||
await window.Signal.Data.removeAllSignedPreKeys();
|
||||
},
|
||||
|
||||
// Sessions
|
||||
|
||||
async loadSession(encodedNumber) {
|
||||
if (encodedNumber === null || encodedNumber === undefined) {
|
||||
throw new Error('Tried to get session for undefined/null number');
|
||||
}
|
||||
|
||||
const session = this.sessions[encodedNumber];
|
||||
if (session) {
|
||||
return session.record;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
async storeSession(encodedNumber, record) {
|
||||
if (encodedNumber === null || encodedNumber === undefined) {
|
||||
throw new Error('Tried to put session for undefined/null number');
|
||||
}
|
||||
const unencoded = textsecure.utils.unencodeNumber(encodedNumber);
|
||||
const number = unencoded[0];
|
||||
const deviceId = parseInt(unencoded[1], 10);
|
||||
|
||||
const data = {
|
||||
id: encodedNumber,
|
||||
number,
|
||||
deviceId,
|
||||
record,
|
||||
};
|
||||
|
||||
this.sessions[encodedNumber] = data;
|
||||
await window.Signal.Data.createOrUpdateSession(data);
|
||||
},
|
||||
async getDeviceIds(number) {
|
||||
if (number === null || number === undefined) {
|
||||
throw new Error('Tried to get device ids for undefined/null number');
|
||||
}
|
||||
|
||||
const allSessions = Object.values(this.sessions);
|
||||
const sessions = allSessions.filter(session => session.number === number);
|
||||
return _.pluck(sessions, 'deviceId');
|
||||
},
|
||||
async removeAllSessions(number) {
|
||||
if (number === null || number === undefined) {
|
||||
throw new Error('Tried to remove sessions for undefined/null number');
|
||||
}
|
||||
|
||||
const allSessions = Object.values(this.sessions);
|
||||
for (let i = 0, max = allSessions.length; i < max; i += 1) {
|
||||
const session = allSessions[i];
|
||||
if (session.number === number) {
|
||||
delete this.sessions[session.id];
|
||||
}
|
||||
}
|
||||
await window.Signal.Data.removeSessionsByNumber(number);
|
||||
},
|
||||
async archiveSiblingSessions(identifier) {
|
||||
const address = libsignal.SignalProtocolAddress.fromString(identifier);
|
||||
|
||||
const deviceIds = await this.getDeviceIds(address.getName());
|
||||
const siblings = _.without(deviceIds, address.getDeviceId());
|
||||
|
||||
await Promise.all(
|
||||
siblings.map(async deviceId => {
|
||||
const sibling = new libsignal.SignalProtocolAddress(
|
||||
address.getName(),
|
||||
deviceId
|
||||
);
|
||||
window.log.info('closing session for', sibling.toString());
|
||||
const sessionCipher = new libsignal.SessionCipher(
|
||||
textsecure.storage.protocol,
|
||||
sibling
|
||||
);
|
||||
await sessionCipher.closeOpenSessionForDevice();
|
||||
})
|
||||
);
|
||||
},
|
||||
async archiveAllSessions(number) {
|
||||
const deviceIds = await this.getDeviceIds(number);
|
||||
|
||||
await Promise.all(
|
||||
deviceIds.map(async deviceId => {
|
||||
const address = new libsignal.SignalProtocolAddress(number, deviceId);
|
||||
window.log.info('closing session for', address.toString());
|
||||
const sessionCipher = new libsignal.SessionCipher(
|
||||
textsecure.storage.protocol,
|
||||
address
|
||||
);
|
||||
await sessionCipher.closeOpenSessionForDevice();
|
||||
})
|
||||
);
|
||||
},
|
||||
async clearSessionStore() {
|
||||
this.sessions = Object.create(null);
|
||||
window.Signal.Data.removeAllSessions();
|
||||
|
@ -434,58 +209,6 @@
|
|||
|
||||
// Identity Keys
|
||||
|
||||
async isTrustedIdentity(identifier, publicKey, direction) {
|
||||
if (identifier === null || identifier === undefined) {
|
||||
throw new Error('Tried to get identity key for undefined/null key');
|
||||
}
|
||||
const number = textsecure.utils.unencodeNumber(identifier)[0];
|
||||
const isOurNumber = number === textsecure.storage.user.getNumber();
|
||||
|
||||
const identityRecord = this.identityKeys[number];
|
||||
|
||||
if (isOurNumber) {
|
||||
const existing = identityRecord ? identityRecord.publicKey : null;
|
||||
return equalArrayBuffers(existing, publicKey);
|
||||
}
|
||||
|
||||
switch (direction) {
|
||||
case Direction.SENDING:
|
||||
return this.isTrustedForSending(publicKey, identityRecord);
|
||||
case Direction.RECEIVING:
|
||||
return true;
|
||||
default:
|
||||
throw new Error(`Unknown direction: ${direction}`);
|
||||
}
|
||||
},
|
||||
isTrustedForSending(publicKey, identityRecord) {
|
||||
if (!identityRecord) {
|
||||
window.log.info(
|
||||
'isTrustedForSending: No previous record, returning true...'
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
const existing = identityRecord.publicKey;
|
||||
|
||||
if (!existing) {
|
||||
window.log.info('isTrustedForSending: Nothing here, returning true...');
|
||||
return true;
|
||||
}
|
||||
if (!equalArrayBuffers(existing, publicKey)) {
|
||||
window.log.info("isTrustedForSending: Identity keys don't match...");
|
||||
return false;
|
||||
}
|
||||
if (identityRecord.verified === VerifiedStatus.UNVERIFIED) {
|
||||
window.log.error('Needs unverified approval!');
|
||||
return false;
|
||||
}
|
||||
if (this.isNonBlockingApprovalRequired(identityRecord)) {
|
||||
window.log.error('isTrustedForSending: Needs non-blocking approval!');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
async loadIdentityKey(identifier) {
|
||||
if (identifier === null || identifier === undefined) {
|
||||
throw new Error('Tried to get identity key for undefined/null key');
|
||||
|
@ -528,7 +251,6 @@
|
|||
publicKey,
|
||||
firstUse: true,
|
||||
timestamp: Date.now(),
|
||||
verified: VerifiedStatus.DEFAULT,
|
||||
nonblockingApproval,
|
||||
});
|
||||
|
||||
|
@ -538,55 +260,20 @@
|
|||
const oldpublicKey = identityRecord.publicKey;
|
||||
if (!equalArrayBuffers(oldpublicKey, publicKey)) {
|
||||
window.log.info('Replacing existing identity...');
|
||||
const previousStatus = identityRecord.verified;
|
||||
let verifiedStatus;
|
||||
if (
|
||||
previousStatus === VerifiedStatus.VERIFIED ||
|
||||
previousStatus === VerifiedStatus.UNVERIFIED
|
||||
) {
|
||||
verifiedStatus = VerifiedStatus.UNVERIFIED;
|
||||
} else {
|
||||
verifiedStatus = VerifiedStatus.DEFAULT;
|
||||
}
|
||||
|
||||
await this._saveIdentityKey({
|
||||
id: number,
|
||||
publicKey,
|
||||
firstUse: false,
|
||||
timestamp: Date.now(),
|
||||
verified: verifiedStatus,
|
||||
nonblockingApproval,
|
||||
});
|
||||
|
||||
try {
|
||||
this.trigger('keychange', number);
|
||||
} catch (error) {
|
||||
window.log.error(
|
||||
'saveIdentity error triggering keychange:',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
}
|
||||
await this.archiveSiblingSessions(identifier);
|
||||
|
||||
return true;
|
||||
} else if (this.isNonBlockingApprovalRequired(identityRecord)) {
|
||||
window.log.info('Setting approval status...');
|
||||
|
||||
identityRecord.nonblockingApproval = nonblockingApproval;
|
||||
await this._saveIdentityKey(identityRecord);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
isNonBlockingApprovalRequired(identityRecord) {
|
||||
return (
|
||||
!identityRecord.firstUse &&
|
||||
Date.now() - identityRecord.timestamp < TIMESTAMP_THRESHOLD &&
|
||||
!identityRecord.nonblockingApproval
|
||||
);
|
||||
},
|
||||
async saveIdentityWithAttributes(identifier, attributes) {
|
||||
if (identifier === null || identifier === undefined) {
|
||||
throw new Error('Tried to put identity key for undefined/null key');
|
||||
|
@ -626,230 +313,9 @@
|
|||
identityRecord.nonblockingApproval = nonblockingApproval;
|
||||
await this._saveIdentityKey(identityRecord);
|
||||
},
|
||||
async setVerified(number, verifiedStatus, publicKey) {
|
||||
if (number === null || number === undefined) {
|
||||
throw new Error('Tried to set verified for undefined/null key');
|
||||
}
|
||||
if (!validateVerifiedStatus(verifiedStatus)) {
|
||||
throw new Error('Invalid verified status');
|
||||
}
|
||||
if (arguments.length > 2 && !(publicKey instanceof ArrayBuffer)) {
|
||||
throw new Error('Invalid public key');
|
||||
}
|
||||
|
||||
const identityRecord = this.identityKeys[number];
|
||||
if (!identityRecord) {
|
||||
throw new Error(`No identity record for ${number}`);
|
||||
}
|
||||
|
||||
if (
|
||||
!publicKey ||
|
||||
equalArrayBuffers(identityRecord.publicKey, publicKey)
|
||||
) {
|
||||
identityRecord.verified = verifiedStatus;
|
||||
|
||||
const model = new IdentityRecord(identityRecord);
|
||||
if (model.isValid()) {
|
||||
await this._saveIdentityKey(identityRecord);
|
||||
} else {
|
||||
throw identityRecord.validationError;
|
||||
}
|
||||
} else {
|
||||
window.log.info('No identity record for specified publicKey');
|
||||
}
|
||||
},
|
||||
async getVerified(number) {
|
||||
if (number === null || number === undefined) {
|
||||
throw new Error('Tried to set verified for undefined/null key');
|
||||
}
|
||||
|
||||
const identityRecord = this.identityKeys[number];
|
||||
if (!identityRecord) {
|
||||
throw new Error(`No identity record for ${number}`);
|
||||
}
|
||||
|
||||
const verifiedStatus = identityRecord.verified;
|
||||
if (validateVerifiedStatus(verifiedStatus)) {
|
||||
return verifiedStatus;
|
||||
}
|
||||
|
||||
return VerifiedStatus.DEFAULT;
|
||||
},
|
||||
// Resolves to true if a new identity key was saved
|
||||
processContactSyncVerificationState(identifier, verifiedStatus, publicKey) {
|
||||
if (verifiedStatus === VerifiedStatus.UNVERIFIED) {
|
||||
return this.processUnverifiedMessage(
|
||||
identifier,
|
||||
verifiedStatus,
|
||||
publicKey
|
||||
);
|
||||
}
|
||||
return this.processVerifiedMessage(identifier, verifiedStatus, publicKey);
|
||||
},
|
||||
// This function encapsulates the non-Java behavior, since the mobile apps don't
|
||||
// currently receive contact syncs and therefore will see a verify sync with
|
||||
// UNVERIFIED status
|
||||
async processUnverifiedMessage(number, verifiedStatus, publicKey) {
|
||||
if (number === null || number === undefined) {
|
||||
throw new Error('Tried to set verified for undefined/null key');
|
||||
}
|
||||
if (publicKey !== undefined && !(publicKey instanceof ArrayBuffer)) {
|
||||
throw new Error('Invalid public key');
|
||||
}
|
||||
|
||||
const identityRecord = this.identityKeys[number];
|
||||
const isPresent = Boolean(identityRecord);
|
||||
let isEqual = false;
|
||||
|
||||
if (isPresent && publicKey) {
|
||||
isEqual = equalArrayBuffers(publicKey, identityRecord.publicKey);
|
||||
}
|
||||
|
||||
if (
|
||||
isPresent &&
|
||||
isEqual &&
|
||||
identityRecord.verified !== VerifiedStatus.UNVERIFIED
|
||||
) {
|
||||
await textsecure.storage.protocol.setVerified(
|
||||
number,
|
||||
verifiedStatus,
|
||||
publicKey
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isPresent || !isEqual) {
|
||||
await textsecure.storage.protocol.saveIdentityWithAttributes(number, {
|
||||
publicKey,
|
||||
verified: verifiedStatus,
|
||||
firstUse: false,
|
||||
timestamp: Date.now(),
|
||||
nonblockingApproval: true,
|
||||
});
|
||||
|
||||
if (isPresent && !isEqual) {
|
||||
try {
|
||||
this.trigger('keychange', number);
|
||||
} catch (error) {
|
||||
window.log.error(
|
||||
'processUnverifiedMessage error triggering keychange:',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
}
|
||||
|
||||
await this.archiveAllSessions(number);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// The situation which could get us here is:
|
||||
// 1. had a previous key
|
||||
// 2. new key is the same
|
||||
// 3. desired new status is same as what we had before
|
||||
return false;
|
||||
},
|
||||
// This matches the Java method as of
|
||||
// https://github.com/signalapp/Signal-Android/blob/d0bb68e1378f689e4d10ac6a46014164992ca4e4/src/org/thoughtcrime/securesms/util/IdentityUtil.java#L188
|
||||
async processVerifiedMessage(number, verifiedStatus, publicKey) {
|
||||
if (number === null || number === undefined) {
|
||||
throw new Error('Tried to set verified for undefined/null key');
|
||||
}
|
||||
if (!validateVerifiedStatus(verifiedStatus)) {
|
||||
throw new Error('Invalid verified status');
|
||||
}
|
||||
if (publicKey !== undefined && !(publicKey instanceof ArrayBuffer)) {
|
||||
throw new Error('Invalid public key');
|
||||
}
|
||||
|
||||
const identityRecord = this.identityKeys[number];
|
||||
|
||||
const isPresent = Boolean(identityRecord);
|
||||
let isEqual = false;
|
||||
|
||||
if (isPresent && publicKey) {
|
||||
isEqual = equalArrayBuffers(publicKey, identityRecord.publicKey);
|
||||
}
|
||||
|
||||
if (!isPresent && verifiedStatus === VerifiedStatus.DEFAULT) {
|
||||
window.log.info('No existing record for default status');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
isPresent &&
|
||||
isEqual &&
|
||||
identityRecord.verified !== VerifiedStatus.DEFAULT &&
|
||||
verifiedStatus === VerifiedStatus.DEFAULT
|
||||
) {
|
||||
await textsecure.storage.protocol.setVerified(
|
||||
number,
|
||||
verifiedStatus,
|
||||
publicKey
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
verifiedStatus === VerifiedStatus.VERIFIED &&
|
||||
(!isPresent ||
|
||||
(isPresent && !isEqual) ||
|
||||
(isPresent && identityRecord.verified !== VerifiedStatus.VERIFIED))
|
||||
) {
|
||||
await textsecure.storage.protocol.saveIdentityWithAttributes(number, {
|
||||
publicKey,
|
||||
verified: verifiedStatus,
|
||||
firstUse: false,
|
||||
timestamp: Date.now(),
|
||||
nonblockingApproval: true,
|
||||
});
|
||||
|
||||
if (isPresent && !isEqual) {
|
||||
try {
|
||||
this.trigger('keychange', number);
|
||||
} catch (error) {
|
||||
window.log.error(
|
||||
'processVerifiedMessage error triggering keychange:',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
}
|
||||
|
||||
await this.archiveAllSessions(number);
|
||||
|
||||
// true signifies that we overwrote a previous key with a new one
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// We get here if we got a new key and the status is DEFAULT. If the
|
||||
// message is out of date, we don't want to lose whatever more-secure
|
||||
// state we had before.
|
||||
return false;
|
||||
},
|
||||
async isUntrusted(number) {
|
||||
if (number === null || number === undefined) {
|
||||
throw new Error('Tried to set verified for undefined/null key');
|
||||
}
|
||||
|
||||
const identityRecord = this.identityKeys[number];
|
||||
if (!identityRecord) {
|
||||
throw new Error(`No identity record for ${number}`);
|
||||
}
|
||||
|
||||
if (
|
||||
Date.now() - identityRecord.timestamp < TIMESTAMP_THRESHOLD &&
|
||||
!identityRecord.nonblockingApproval &&
|
||||
!identityRecord.firstUse
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
async removeIdentityKey(number) {
|
||||
delete this.identityKeys[number];
|
||||
await window.Signal.Data.removeIdentityKeyById(number);
|
||||
await textsecure.storage.protocol.removeAllSessions(number);
|
||||
},
|
||||
|
||||
// Not yet processed messages - for resiliency
|
||||
|
@ -905,5 +371,4 @@
|
|||
|
||||
window.SignalProtocolStore = SignalProtocolStore;
|
||||
window.SignalProtocolStore.prototype.Direction = Direction;
|
||||
window.SignalProtocolStore.prototype.VerifiedStatus = VerifiedStatus;
|
||||
})();
|
||||
|
|
|
@ -80,10 +80,6 @@
|
|||
this.showMessageDetail
|
||||
);
|
||||
|
||||
this.lazyUpdateVerified = _.debounce(
|
||||
this.model.updateVerified.bind(this.model),
|
||||
1000 // one second
|
||||
);
|
||||
this.throttledGetProfiles = _.throttle(
|
||||
this.model.getProfiles.bind(this.model),
|
||||
1000 * 60 * 5 // five minutes
|
||||
|
@ -273,69 +269,6 @@
|
|||
model.trigger('unload');
|
||||
});
|
||||
},
|
||||
|
||||
markAllAsVerifiedDefault(unverified) {
|
||||
return Promise.all(
|
||||
unverified.map(contact => {
|
||||
if (contact.isUnverified()) {
|
||||
return contact.setVerifiedDefault();
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
markAllAsApproved(untrusted) {
|
||||
return Promise.all(untrusted.map(contact => contact.setApproved()));
|
||||
},
|
||||
|
||||
openSafetyNumberScreens(unverified) {
|
||||
if (unverified.length === 1) {
|
||||
this.showSafetyNumber(unverified.at(0));
|
||||
return;
|
||||
}
|
||||
|
||||
this.showMembers(null, unverified, { needVerify: true });
|
||||
},
|
||||
|
||||
onVerifiedChange() {
|
||||
if (this.model.isUnverified()) {
|
||||
const unverified = this.model.getUnverified();
|
||||
let message;
|
||||
if (!unverified.length) {
|
||||
return;
|
||||
}
|
||||
if (unverified.length > 1) {
|
||||
message = i18n('multipleNoLongerVerified');
|
||||
} else {
|
||||
message = i18n('noLongerVerified', unverified.at(0).getTitle());
|
||||
}
|
||||
|
||||
// Need to re-add, since unverified set may have changed
|
||||
if (this.banner) {
|
||||
this.banner.remove();
|
||||
this.banner = null;
|
||||
}
|
||||
|
||||
this.banner = new Whisper.BannerView({
|
||||
message,
|
||||
onDismiss: () => {
|
||||
this.markAllAsVerifiedDefault(unverified);
|
||||
},
|
||||
onClick: () => {
|
||||
this.openSafetyNumberScreens(unverified);
|
||||
},
|
||||
});
|
||||
|
||||
const container = this.$('.discussion-container');
|
||||
container.append(this.banner.el);
|
||||
} else if (this.banner) {
|
||||
this.banner.remove();
|
||||
this.banner = null;
|
||||
}
|
||||
},
|
||||
|
||||
renderTypingBubble() {
|
||||
const timers = this.model.contactTypingTimers || {};
|
||||
const records = _.values(timers);
|
||||
|
@ -385,17 +318,6 @@
|
|||
$('.conversation-stack').addClass('conversation-stack-no-border');
|
||||
}
|
||||
|
||||
// const statusPromise = this.throttledGetProfiles();
|
||||
// // eslint-disable-next-line more/no-then
|
||||
// this.statusFetch = statusPromise.then(() =>
|
||||
// // eslint-disable-next-line more/no-then
|
||||
// this.model.updateVerified().then(() => {
|
||||
// this.onVerifiedChange();
|
||||
// this.statusFetch = null;
|
||||
// window.log.info('done with status fetch');
|
||||
// })
|
||||
// );
|
||||
|
||||
// We schedule our catch-up decrypt right after any in-progress fetch of
|
||||
// messages from the database, then ensure that the loading screen is only
|
||||
// dismissed when that is complete.
|
||||
|
@ -439,8 +361,6 @@
|
|||
|
||||
addMessage(message) {
|
||||
// This is debounced, so it won't hit the database too often.
|
||||
this.lazyUpdateVerified();
|
||||
|
||||
// We do this here because we don't want convo.messageCollection to have
|
||||
// anything in it unless it has an associated view. This is so, when we
|
||||
// fetch on open, it's clean.
|
||||
|
@ -558,28 +478,7 @@
|
|||
},
|
||||
|
||||
forceSend({ contact, message }) {
|
||||
window.confirmationDialog({
|
||||
message: i18n('identityKeyErrorOnSend', [
|
||||
contact.getTitle(),
|
||||
contact.getTitle(),
|
||||
]),
|
||||
messageSub: i18n('youMayWishToVerifyContact'),
|
||||
okText: i18n('sendAnyway'),
|
||||
resolve: async () => {
|
||||
await contact.updateVerified();
|
||||
|
||||
if (contact.isUnverified()) {
|
||||
await contact.setVerifiedDefault();
|
||||
}
|
||||
|
||||
const untrusted = await contact.isUntrusted();
|
||||
if (untrusted) {
|
||||
await contact.setApproved();
|
||||
}
|
||||
|
||||
message.resend(contact.id);
|
||||
},
|
||||
});
|
||||
message.resend(contact.id);
|
||||
},
|
||||
|
||||
showContactDetail({ contact, hasSignalAccount }) {
|
||||
|
@ -646,38 +545,6 @@
|
|||
}
|
||||
},
|
||||
|
||||
showSendConfirmationDialog(e, contacts) {
|
||||
let message;
|
||||
const isUnverified = this.model.isUnverified();
|
||||
|
||||
if (contacts.length > 1) {
|
||||
if (isUnverified) {
|
||||
message = i18n('changedSinceVerifiedMultiple');
|
||||
} else {
|
||||
message = i18n('changedRecentlyMultiple');
|
||||
}
|
||||
} else {
|
||||
const contactName = contacts.at(0).getTitle();
|
||||
if (isUnverified) {
|
||||
message = i18n('changedSinceVerified', [contactName, contactName]);
|
||||
} else {
|
||||
message = i18n('changedRecently', [contactName, contactName]);
|
||||
}
|
||||
}
|
||||
|
||||
window.confirmationDialog({
|
||||
title: i18n('changedSinceVerifiedTitle'),
|
||||
message,
|
||||
okText: i18n('sendAnyway'),
|
||||
resolve: () => {
|
||||
this.checkUnverifiedSendMessage(e, { force: true });
|
||||
},
|
||||
reject: () => {
|
||||
this.focusMessageFieldAndClearDisabled();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
stripQuery(text, cursorPos) {
|
||||
const end = text.slice(cursorPos).search(/[^a-fA-F0-9]/);
|
||||
const mentionEnd = end === -1 ? text.length : cursorPos + end;
|
||||
|
@ -721,74 +588,6 @@
|
|||
this.$messageField.trigger('input');
|
||||
},
|
||||
|
||||
async handleSubmitPressed(e, options = {}) {
|
||||
if (this.memberView.membersShown()) {
|
||||
const member = this.memberView.selectedMember();
|
||||
this.selectMember(member);
|
||||
} else {
|
||||
await this.checkUnverifiedSendMessage(e, options);
|
||||
}
|
||||
},
|
||||
|
||||
async checkUnverifiedSendMessage(e, options = {}) {
|
||||
e.preventDefault();
|
||||
this.sendStart = Date.now();
|
||||
this.$messageField.attr('disabled', true);
|
||||
|
||||
_.defaults(options, { force: false });
|
||||
|
||||
// This will go to the trust store for the latest identity key information,
|
||||
// and may result in the display of a new banner for this conversation.
|
||||
try {
|
||||
await this.model.updateVerified();
|
||||
const contacts = this.model.getUnverified();
|
||||
if (!contacts.length) {
|
||||
this.checkUntrustedSendMessage(e, options);
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.force) {
|
||||
await this.markAllAsVerifiedDefault(contacts);
|
||||
this.checkUnverifiedSendMessage(e, options);
|
||||
return;
|
||||
}
|
||||
|
||||
this.showSendConfirmationDialog(e, contacts);
|
||||
} catch (error) {
|
||||
this.focusMessageFieldAndClearDisabled();
|
||||
window.log.error(
|
||||
'checkUnverifiedSendMessage error:',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
async checkUntrustedSendMessage(e, options = {}) {
|
||||
_.defaults(options, { force: false });
|
||||
|
||||
try {
|
||||
const contacts = await this.model.getUntrusted();
|
||||
if (!contacts.length) {
|
||||
this.sendMessage(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.force) {
|
||||
await this.markAllAsApproved(contacts);
|
||||
this.sendMessage(e);
|
||||
return;
|
||||
}
|
||||
|
||||
this.showSendConfirmationDialog(e, contacts);
|
||||
} catch (error) {
|
||||
this.focusMessageFieldAndClearDisabled();
|
||||
window.log.error(
|
||||
'checkUntrustedSendMessage error:',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
async sendMessage(e) {
|
||||
this.removeLastSeenIndicator();
|
||||
this.model.clearTypingTimers();
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
this.titleText = i18n('updateGroupDialogTitle', this.groupName);
|
||||
// I'd much prefer to integrate mods with groupAdmins
|
||||
// but lets discuss first...
|
||||
this.isAdmin = groupConvo.isModerator(
|
||||
this.isAdmin = groupConvo.isAdmin(
|
||||
window.storage.get('primaryDevicePubKey')
|
||||
);
|
||||
}
|
||||
|
@ -92,7 +92,7 @@
|
|||
this.titleText = i18n('updateGroupDialogTitle', this.groupName);
|
||||
// I'd much prefer to integrate mods with groupAdmins
|
||||
// but lets discuss first...
|
||||
this.isAdmin = groupConvo.isModerator(
|
||||
this.isAdmin = groupConvo.isAdmin(
|
||||
window.storage.get('primaryDevicePubKey')
|
||||
);
|
||||
// zero out contactList for now
|
||||
|
|
|
@ -87,7 +87,7 @@
|
|||
// Do not trigger an update if there is too many members
|
||||
if (
|
||||
newMembers.length + existingMembers.length >
|
||||
window.CONSTANTS.MEDIUM_GROUP_SIZE_LIMIT
|
||||
window.CONSTANTS.CLOSED_GROUP_SIZE_LIMIT
|
||||
) {
|
||||
window.libsession.Utils.ToastUtils.pushTooManyMembers();
|
||||
return;
|
||||
|
|
127
libloki/api.js
127
libloki/api.js
|
@ -1,127 +0,0 @@
|
|||
/* global window, textsecure, libsession */
|
||||
/* eslint-disable no-bitwise */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
window.libloki = window.libloki || {};
|
||||
|
||||
const DebugFlagsEnum = {
|
||||
GROUP_SYNC_MESSAGES: 1,
|
||||
CONTACT_SYNC_MESSAGES: 2,
|
||||
FALLBACK_MESSAGES: 8,
|
||||
SESSION_BACKGROUND_MESSAGE: 32,
|
||||
GROUP_REQUEST_INFO: 64,
|
||||
// If you add any new flag, be sure it is bitwise safe! (unique and 2 multiples)
|
||||
ALL: 65535,
|
||||
};
|
||||
|
||||
const debugFlags = DebugFlagsEnum.ALL;
|
||||
|
||||
const debugLogFn = (...args) => {
|
||||
if (window.lokiFeatureFlags.debugMessageLogs) {
|
||||
window.log.warn(...args);
|
||||
}
|
||||
};
|
||||
|
||||
function logGroupSync(...args) {
|
||||
if (debugFlags & DebugFlagsEnum.GROUP_SYNC_MESSAGES) {
|
||||
debugLogFn(...args);
|
||||
}
|
||||
}
|
||||
|
||||
function logGroupRequestInfo(...args) {
|
||||
if (debugFlags & DebugFlagsEnum.GROUP_REQUEST_INFO) {
|
||||
debugLogFn(...args);
|
||||
}
|
||||
}
|
||||
|
||||
function logContactSync(...args) {
|
||||
if (debugFlags & DebugFlagsEnum.CONTACT_SYNC_MESSAGES) {
|
||||
debugLogFn(...args);
|
||||
}
|
||||
}
|
||||
|
||||
function logBackgroundMessage(...args) {
|
||||
if (debugFlags & DebugFlagsEnum.SESSION_BACKGROUND_MESSAGE) {
|
||||
debugLogFn(...args);
|
||||
}
|
||||
}
|
||||
|
||||
async function createContactSyncMessage(sessionContacts) {
|
||||
if (sessionContacts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rawContacts = await Promise.all(
|
||||
sessionContacts.map(async conversation => {
|
||||
const profile = conversation.getLokiProfile();
|
||||
const name = profile
|
||||
? profile.displayName
|
||||
: conversation.getProfileName();
|
||||
const status = await conversation.safeGetVerified();
|
||||
|
||||
return {
|
||||
name,
|
||||
number: conversation.getNumber(),
|
||||
nickname: conversation.getNickname(),
|
||||
blocked: conversation.isBlocked(),
|
||||
expireTimer: conversation.get('expireTimer'),
|
||||
verifiedStatus: status,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return new libsession.Messages.Outgoing.ContactSyncMessage({
|
||||
timestamp: Date.now(),
|
||||
rawContacts,
|
||||
});
|
||||
}
|
||||
|
||||
function createGroupSyncMessage(sessionGroup) {
|
||||
// We are getting a single open group here
|
||||
|
||||
const rawGroup = {
|
||||
id: sessionGroup.id,
|
||||
name: sessionGroup.get('name'),
|
||||
members: sessionGroup.get('members') || [],
|
||||
blocked: sessionGroup.isBlocked(),
|
||||
expireTimer: sessionGroup.get('expireTimer'),
|
||||
admins: sessionGroup.get('groupAdmins') || [],
|
||||
};
|
||||
|
||||
return new libsession.Messages.Outgoing.ClosedGroupSyncMessage({
|
||||
timestamp: Date.now(),
|
||||
rawGroup,
|
||||
});
|
||||
}
|
||||
|
||||
async function sendSessionRequestsToMembers(members = []) {
|
||||
// For every member, trigger a session request if needed
|
||||
members.forEach(async memberStr => {
|
||||
const ourPubKey = textsecure.storage.user.getNumber();
|
||||
if (memberStr !== ourPubKey) {
|
||||
const memberPubkey = new libsession.Types.PubKey(memberStr);
|
||||
await window
|
||||
.getConversationController()
|
||||
.getOrCreateAndWait(memberStr, 'private');
|
||||
await libsession.Protocols.SessionProtocol.sendSessionRequestIfNeeded(
|
||||
memberPubkey
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const debug = {
|
||||
logContactSync,
|
||||
logGroupSync,
|
||||
logBackgroundMessage,
|
||||
logGroupRequestInfo,
|
||||
};
|
||||
|
||||
window.libloki.api = {
|
||||
sendSessionRequestsToMembers,
|
||||
createContactSyncMessage,
|
||||
createGroupSyncMessage,
|
||||
debug,
|
||||
};
|
||||
})();
|
3
libloki/crypto.d.ts
vendored
3
libloki/crypto.d.ts
vendored
|
@ -10,9 +10,6 @@ export interface CryptoInterface {
|
|||
DHEncrypt: any;
|
||||
DecryptGCM: any; // AES-GCM
|
||||
EncryptGCM: any; // AES-GCM
|
||||
FallBackDecryptionError: any;
|
||||
FallBackSessionCipher: any;
|
||||
LokiSessionCipher: any;
|
||||
PairingType: PairingTypeEnum;
|
||||
_decodeSnodeAddressToPubKey: any;
|
||||
decryptForPubkey: any;
|
||||
|
|
|
@ -15,8 +15,6 @@
|
|||
(function() {
|
||||
window.libloki = window.libloki || {};
|
||||
|
||||
class FallBackDecryptionError extends Error {}
|
||||
|
||||
const IV_LENGTH = 16;
|
||||
const NONCE_LENGTH = 12;
|
||||
|
||||
|
@ -131,54 +129,6 @@
|
|||
return libsignal.crypto.decrypt(symmetricKey, ciphertext, iv);
|
||||
}
|
||||
|
||||
class FallBackSessionCipher {
|
||||
constructor(address) {
|
||||
this.identityKeyString = address.getName();
|
||||
this.pubKey = StringView.hexToArrayBuffer(address.getName());
|
||||
}
|
||||
|
||||
// Should we use ephemeral key pairs here rather than long term keys on each side?
|
||||
async encrypt(plaintext) {
|
||||
const myKeyPair = await textsecure.storage.protocol.getIdentityKeyPair();
|
||||
if (!myKeyPair) {
|
||||
throw new Error('Failed to get keypair for encryption');
|
||||
}
|
||||
const myPrivateKey = myKeyPair.privKey;
|
||||
const symmetricKey = await libsignal.Curve.async.calculateAgreement(
|
||||
this.pubKey,
|
||||
myPrivateKey
|
||||
);
|
||||
const ivAndCiphertext = await DHEncrypt(symmetricKey, plaintext);
|
||||
const binaryIvAndCiphertext = dcodeIO.ByteBuffer.wrap(
|
||||
ivAndCiphertext
|
||||
).toString('binary');
|
||||
return {
|
||||
type: textsecure.protobuf.Envelope.Type.FALLBACK_MESSAGE,
|
||||
body: binaryIvAndCiphertext,
|
||||
registrationId: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async decrypt(ivAndCiphertext) {
|
||||
const myKeyPair = await textsecure.storage.protocol.getIdentityKeyPair();
|
||||
if (!myKeyPair) {
|
||||
throw new Error('Failed to get keypair for decryption');
|
||||
}
|
||||
const myPrivateKey = myKeyPair.privKey;
|
||||
const symmetricKey = await libsignal.Curve.async.calculateAgreement(
|
||||
this.pubKey,
|
||||
myPrivateKey
|
||||
);
|
||||
try {
|
||||
return await DHDecrypt(symmetricKey, ivAndCiphertext);
|
||||
} catch (e) {
|
||||
throw new FallBackDecryptionError(
|
||||
`Could not decrypt message from ${this.identityKeyString} using FallBack encryption.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const base32zIndex = Multibase.names.indexOf('base32z');
|
||||
const base32zCode = Multibase.codes[base32zIndex];
|
||||
|
||||
|
@ -354,167 +304,17 @@
|
|||
GRANT: 2,
|
||||
});
|
||||
|
||||
/**
|
||||
* A wrapper around Signal's SessionCipher.
|
||||
* This handles specific session reset logic that we need.
|
||||
*/
|
||||
class LokiSessionCipher {
|
||||
constructor(storage, protocolAddress) {
|
||||
this.storage = storage;
|
||||
this.protocolAddress = protocolAddress;
|
||||
this.sessionCipher = new libsignal.SessionCipher(
|
||||
storage,
|
||||
protocolAddress
|
||||
);
|
||||
this.TYPE = Object.freeze({
|
||||
MESSAGE: 1,
|
||||
PREKEY: 2,
|
||||
});
|
||||
}
|
||||
|
||||
decryptWhisperMessage(buffer, encoding) {
|
||||
return this._decryptMessage(this.TYPE.MESSAGE, buffer, encoding);
|
||||
}
|
||||
|
||||
decryptPreKeyWhisperMessage(buffer, encoding) {
|
||||
return this._decryptMessage(this.TYPE.PREKEY, buffer, encoding);
|
||||
}
|
||||
|
||||
async _decryptMessage(type, buffer, encoding) {
|
||||
// Capture active session
|
||||
const activeSessionBaseKey = await this._getCurrentSessionBaseKey();
|
||||
|
||||
if (type === this.TYPE.PREKEY && !activeSessionBaseKey) {
|
||||
const wrapped = dcodeIO.ByteBuffer.wrap(buffer);
|
||||
await window.libloki.storage.verifyFriendRequestAcceptPreKey(
|
||||
this.protocolAddress.getName(),
|
||||
wrapped
|
||||
);
|
||||
}
|
||||
|
||||
const decryptFunction =
|
||||
type === this.TYPE.PREKEY
|
||||
? this.sessionCipher.decryptPreKeyWhisperMessage
|
||||
: this.sessionCipher.decryptWhisperMessage;
|
||||
const result = await decryptFunction(buffer, encoding);
|
||||
|
||||
// Handle session reset
|
||||
// This needs to be done synchronously so that the next time we decrypt a message,
|
||||
// we have the correct session
|
||||
try {
|
||||
await this._handleSessionResetIfNeeded(activeSessionBaseKey);
|
||||
} catch (e) {
|
||||
window.log.info('Failed to handle session reset: ', e);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async _handleSessionResetIfNeeded(previousSessionBaseKey) {
|
||||
if (!previousSessionBaseKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
let conversation;
|
||||
try {
|
||||
conversation = await window
|
||||
.getConversationController()
|
||||
.getOrCreateAndWait(this.protocolAddress.getName(), 'private');
|
||||
} catch (e) {
|
||||
window.log.info(
|
||||
'Error getting conversation: ',
|
||||
this.protocolAddress.getName()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (conversation.isSessionResetOngoing()) {
|
||||
const currentSessionBaseKey = await this._getCurrentSessionBaseKey();
|
||||
if (currentSessionBaseKey !== previousSessionBaseKey) {
|
||||
if (conversation.isSessionResetReceived()) {
|
||||
// The other user used an old session to contact us; wait for them to switch to a new one.
|
||||
await this._restoreSession(previousSessionBaseKey);
|
||||
} else {
|
||||
// Our session reset was successful; we initiated one and got a new session back from the other user.
|
||||
await this._deleteAllSessionExcept(currentSessionBaseKey);
|
||||
await conversation.onNewSessionAdopted();
|
||||
}
|
||||
} else if (conversation.isSessionResetReceived()) {
|
||||
// Our session reset was successful; we received a message with the same session from the other user.
|
||||
await this._deleteAllSessionExcept(previousSessionBaseKey);
|
||||
await conversation.onNewSessionAdopted();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async _getCurrentSessionBaseKey() {
|
||||
const record = await this.sessionCipher.getRecord(
|
||||
this.protocolAddress.toString()
|
||||
);
|
||||
if (!record) {
|
||||
return null;
|
||||
}
|
||||
const openSession = record.getOpenSession();
|
||||
if (!openSession) {
|
||||
return null;
|
||||
}
|
||||
const { baseKey } = openSession.indexInfo;
|
||||
return baseKey;
|
||||
}
|
||||
|
||||
async _restoreSession(sessionBaseKey) {
|
||||
const record = await this.sessionCipher.getRecord(
|
||||
this.protocolAddress.toString()
|
||||
);
|
||||
if (!record) {
|
||||
return;
|
||||
}
|
||||
record.archiveCurrentState();
|
||||
|
||||
const sessionToRestore = record.sessions[sessionBaseKey];
|
||||
if (!sessionToRestore) {
|
||||
throw new Error(`Cannot find session with base key ${sessionBaseKey}`);
|
||||
}
|
||||
|
||||
record.promoteState(sessionToRestore);
|
||||
record.updateSessionState(sessionToRestore);
|
||||
await this.storage.storeSession(
|
||||
this.protocolAddress.toString(),
|
||||
record.serialize()
|
||||
);
|
||||
}
|
||||
|
||||
async _deleteAllSessionExcept(sessionBaseKey) {
|
||||
const record = await this.sessionCipher.getRecord(
|
||||
this.protocolAddress.toString()
|
||||
);
|
||||
if (!record) {
|
||||
return;
|
||||
}
|
||||
const sessionToKeep = record.sessions[sessionBaseKey];
|
||||
record.sessions = {};
|
||||
record.updateSessionState(sessionToKeep);
|
||||
await this.storage.storeSession(
|
||||
this.protocolAddress.toString(),
|
||||
record.serialize()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
window.libloki.crypto = {
|
||||
DHEncrypt,
|
||||
EncryptGCM, // AES-GCM
|
||||
DHDecrypt,
|
||||
DecryptGCM, // AES-GCM
|
||||
FallBackSessionCipher,
|
||||
FallBackDecryptionError,
|
||||
decryptToken,
|
||||
generateSignatureForPairing,
|
||||
verifyPairingSignature,
|
||||
verifyAuthorisation,
|
||||
validateAuthorisation,
|
||||
PairingType,
|
||||
LokiSessionCipher,
|
||||
generateEphemeralKeyPair,
|
||||
encryptForPubkey,
|
||||
decryptForPubkey,
|
||||
|
|
|
@ -1,85 +1,9 @@
|
|||
/* global window, libsignal, textsecure */
|
||||
/* global window */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
window.libloki = window.libloki || {};
|
||||
|
||||
async function getPreKeyBundleForContact(pubKey) {
|
||||
const myKeyPair = await textsecure.storage.protocol.getIdentityKeyPair();
|
||||
const identityKey = myKeyPair.pubKey;
|
||||
|
||||
// Retrieve ids. The ids stored are always the latest generated + 1
|
||||
const signedKeyId = textsecure.storage.get('signedKeyId', 2) - 1;
|
||||
|
||||
const [signedKey, preKey] = await Promise.all([
|
||||
textsecure.storage.protocol.loadSignedPreKey(signedKeyId),
|
||||
new Promise(async resolve => {
|
||||
// retrieve existing prekey if we already generated one for that recipient
|
||||
const storedPreKey = await textsecure.storage.protocol.loadPreKeyForContact(
|
||||
pubKey
|
||||
);
|
||||
if (storedPreKey) {
|
||||
resolve({ pubKey: storedPreKey.pubKey, keyId: storedPreKey.keyId });
|
||||
} else {
|
||||
// generate and store new prekey
|
||||
const preKeyId = textsecure.storage.get('maxPreKeyId', 1);
|
||||
textsecure.storage.put('maxPreKeyId', preKeyId + 1);
|
||||
const newPreKey = await libsignal.KeyHelper.generatePreKey(preKeyId);
|
||||
await textsecure.storage.protocol.storePreKey(
|
||||
newPreKey.keyId,
|
||||
newPreKey.keyPair,
|
||||
pubKey
|
||||
);
|
||||
resolve({ pubKey: newPreKey.keyPair.pubKey, keyId: preKeyId });
|
||||
}
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
identityKey: new Uint8Array(identityKey),
|
||||
deviceId: 1, // TODO: fetch from somewhere
|
||||
preKeyId: preKey.keyId,
|
||||
signedKeyId,
|
||||
preKey: new Uint8Array(preKey.pubKey),
|
||||
signedKey: new Uint8Array(signedKey.pubKey),
|
||||
signature: new Uint8Array(signedKey.signature),
|
||||
};
|
||||
}
|
||||
async function removeContactPreKeyBundle(pubKey) {
|
||||
await Promise.all([
|
||||
textsecure.storage.protocol.removeContactPreKey(pubKey),
|
||||
textsecure.storage.protocol.removeContactSignedPreKey(pubKey),
|
||||
]);
|
||||
}
|
||||
|
||||
async function verifyFriendRequestAcceptPreKey(pubKey, buffer) {
|
||||
const storedPreKey = await textsecure.storage.protocol.loadPreKeyForContact(
|
||||
pubKey
|
||||
);
|
||||
if (!storedPreKey) {
|
||||
throw new Error(
|
||||
'Received a friend request from a pubkey for which no prekey bundle was created'
|
||||
);
|
||||
}
|
||||
// need to pop the version
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const version = buffer.readUint8();
|
||||
const preKeyProto = window.textsecure.protobuf.PreKeyWhisperMessage.decode(
|
||||
buffer
|
||||
);
|
||||
if (!preKeyProto) {
|
||||
throw new Error(
|
||||
'Could not decode PreKeyWhisperMessage while attempting to match the preKeyId'
|
||||
);
|
||||
}
|
||||
const { preKeyId } = preKeyProto;
|
||||
if (storedPreKey.keyId !== preKeyId) {
|
||||
throw new Error(
|
||||
'Received a preKeyWhisperMessage (friend request accept) from an unknown source'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getGuardNodes() {
|
||||
return window.Signal.Data.getGuardNodes();
|
||||
}
|
||||
|
@ -89,134 +13,7 @@
|
|||
}
|
||||
|
||||
window.libloki.storage = {
|
||||
getPreKeyBundleForContact,
|
||||
removeContactPreKeyBundle,
|
||||
verifyFriendRequestAcceptPreKey,
|
||||
getGuardNodes,
|
||||
updateGuardNodes,
|
||||
};
|
||||
|
||||
// Libloki protocol store
|
||||
|
||||
const store = window.SignalProtocolStore.prototype;
|
||||
|
||||
store.storeContactPreKey = async (pubKey, preKey) => {
|
||||
const key = {
|
||||
// id: (autoincrement)
|
||||
identityKeyString: pubKey,
|
||||
publicKey: preKey.publicKey,
|
||||
keyId: preKey.keyId,
|
||||
};
|
||||
|
||||
await window.Signal.Data.createOrUpdateContactPreKey(key);
|
||||
};
|
||||
|
||||
store.loadContactPreKey = async pubKey => {
|
||||
const preKey = await window.Signal.Data.getContactPreKeyByIdentityKey(
|
||||
pubKey
|
||||
);
|
||||
if (preKey) {
|
||||
return {
|
||||
id: preKey.id,
|
||||
keyId: preKey.keyId,
|
||||
publicKey: preKey.publicKey,
|
||||
identityKeyString: preKey.identityKeyString,
|
||||
};
|
||||
}
|
||||
|
||||
window.log.warn('Failed to fetch contact prekey:', pubKey);
|
||||
return undefined;
|
||||
};
|
||||
|
||||
store.loadContactPreKeys = async filters => {
|
||||
const { keyId, identityKeyString } = filters;
|
||||
const keys = await window.Signal.Data.getContactPreKeys(
|
||||
keyId,
|
||||
identityKeyString
|
||||
);
|
||||
if (keys) {
|
||||
return keys.map(preKey => ({
|
||||
id: preKey.id,
|
||||
keyId: preKey.keyId,
|
||||
publicKey: preKey.publicKey,
|
||||
identityKeyString: preKey.identityKeyString,
|
||||
}));
|
||||
}
|
||||
|
||||
window.log.warn('Failed to fetch signed prekey with filters', filters);
|
||||
return undefined;
|
||||
};
|
||||
|
||||
store.removeContactPreKey = async pubKey => {
|
||||
await window.Signal.Data.removeContactPreKeyByIdentityKey(pubKey);
|
||||
};
|
||||
|
||||
store.clearContactPreKeysStore = async () => {
|
||||
await window.Signal.Data.removeAllContactPreKeys();
|
||||
};
|
||||
|
||||
store.storeContactSignedPreKey = async (pubKey, signedPreKey) => {
|
||||
const key = {
|
||||
// id: (autoincrement)
|
||||
identityKeyString: pubKey,
|
||||
keyId: signedPreKey.keyId,
|
||||
publicKey: signedPreKey.publicKey,
|
||||
signature: signedPreKey.signature,
|
||||
created_at: Date.now(),
|
||||
confirmed: false,
|
||||
};
|
||||
await window.Signal.Data.createOrUpdateContactSignedPreKey(key);
|
||||
};
|
||||
|
||||
store.loadContactSignedPreKey = async pubKey => {
|
||||
const preKey = await window.Signal.Data.getContactSignedPreKeyByIdentityKey(
|
||||
pubKey
|
||||
);
|
||||
if (preKey) {
|
||||
return {
|
||||
id: preKey.id,
|
||||
identityKeyString: preKey.identityKeyString,
|
||||
publicKey: preKey.publicKey,
|
||||
signature: preKey.signature,
|
||||
created_at: preKey.created_at,
|
||||
keyId: preKey.keyId,
|
||||
confirmed: preKey.confirmed,
|
||||
};
|
||||
}
|
||||
window.log.warn('Failed to fetch contact signed prekey:', pubKey);
|
||||
return undefined;
|
||||
};
|
||||
|
||||
store.loadContactSignedPreKeys = async filters => {
|
||||
const { keyId, identityKeyString } = filters;
|
||||
const keys = await window.Signal.Data.getContactSignedPreKeys(
|
||||
keyId,
|
||||
identityKeyString
|
||||
);
|
||||
if (keys) {
|
||||
return keys.map(preKey => ({
|
||||
id: preKey.id,
|
||||
identityKeyString: preKey.identityKeyString,
|
||||
publicKey: preKey.publicKey,
|
||||
signature: preKey.signature,
|
||||
created_at: preKey.created_at,
|
||||
keyId: preKey.keyId,
|
||||
confirmed: preKey.confirmed,
|
||||
}));
|
||||
}
|
||||
|
||||
window.log.warn(
|
||||
'Failed to fetch contact signed prekey with filters',
|
||||
filters
|
||||
);
|
||||
return undefined;
|
||||
};
|
||||
|
||||
store.removeContactSignedPreKey = async pubKey => {
|
||||
await window.Signal.Data.removeContactSignedPreKeyByIdentityKey(pubKey);
|
||||
};
|
||||
|
||||
store.clearContactSignedPreKeysStore = async () => {
|
||||
await window.Signal.Data.removeAllContactSignedPreKeys();
|
||||
};
|
||||
})();
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
/* global libsignal, libloki, textsecure, StringView, dcodeIO */
|
||||
|
||||
'use strict';
|
||||
|
||||
describe('Crypto', () => {
|
||||
describe('FallBackSessionCipher', () => {
|
||||
let fallbackCipher;
|
||||
let identityKey;
|
||||
let address;
|
||||
const store = textsecure.storage.protocol;
|
||||
|
||||
before(async () => {
|
||||
clearDatabase();
|
||||
identityKey = await libsignal.KeyHelper.generateIdentityKeyPair();
|
||||
store.put('identityKey', identityKey);
|
||||
const key = libsignal.crypto.getRandomBytes(32);
|
||||
const pubKeyString = StringView.arrayBufferToHex(key);
|
||||
address = new libsignal.SignalProtocolAddress(pubKeyString, 1);
|
||||
fallbackCipher = new libloki.crypto.FallBackSessionCipher(address);
|
||||
});
|
||||
|
||||
it('should encrypt fallback cipher messages as fallback messages', async () => {
|
||||
const buffer = new ArrayBuffer(10);
|
||||
const { type } = await fallbackCipher.encrypt(buffer);
|
||||
assert.strictEqual(
|
||||
type,
|
||||
textsecure.protobuf.Envelope.Type.FALLBACK_MESSAGE
|
||||
);
|
||||
});
|
||||
|
||||
it('should encrypt and then decrypt a message with the same result', async () => {
|
||||
const arr = new Uint8Array([1, 2, 3, 4, 5]);
|
||||
const { body } = await fallbackCipher.encrypt(arr.buffer);
|
||||
const bufferBody = dcodeIO.ByteBuffer.wrap(
|
||||
body,
|
||||
'binary'
|
||||
).toArrayBuffer();
|
||||
const result = await fallbackCipher.decrypt(bufferBody);
|
||||
assert.deepEqual(result, arr.buffer);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -15,7 +15,6 @@
|
|||
<script type="text/javascript" src="test.js"></script>
|
||||
<script type="text/javascript" src="components.js"></script>
|
||||
|
||||
<script type="text/javascript" src="../../libtextsecure/test/in_memory_signal_protocol_store.js"></script>
|
||||
<script type="text/javascript" src="../../libloki/proof-of-work.js"></script>
|
||||
<script type="text/javascript" src="../../libtextsecure/helpers.js" data-cover></script>
|
||||
<script type="text/javascript" src="../../libtextsecure/storage.js" data-cover></script>
|
||||
|
@ -24,15 +23,12 @@
|
|||
<script type="text/javascript" src="../../libtextsecure/protobufs.js" data-cover></script>
|
||||
<script type="text/javascript" src="../../libtextsecure/stringview.js" data-cover></script>
|
||||
|
||||
<script type="text/javascript" src="../api.js" data-cover></script>
|
||||
<script type="text/javascript" src="../crypto.js" data-cover></script>
|
||||
<script type="text/javascript" src="../service_nodes.js" data-cover></script>
|
||||
<script type="text/javascript" src="../storage.js" data-cover></script>
|
||||
|
||||
<script type="text/javascript" src="crypto_test.js"></script>
|
||||
<script type="text/javascript" src="proof-of-work_test.js"></script>
|
||||
<script type="text/javascript" src="service_nodes_test.js"></script>
|
||||
<script type="text/javascript" src="storage_test.js"></script>
|
||||
<script type="text/javascript" src="messages.js"></script>
|
||||
|
||||
<!-- Comment out to turn off code coverage. Useful for getting real callstacks. -->
|
||||
|
|
|
@ -1,84 +0,0 @@
|
|||
/* global libsignal, libloki, textsecure, StringView */
|
||||
|
||||
'use strict';
|
||||
|
||||
describe('Storage', () => {
|
||||
let testKey;
|
||||
const store = textsecure.storage.protocol;
|
||||
|
||||
describe('#getPreKeyBundleForContact', () => {
|
||||
beforeEach(async () => {
|
||||
clearDatabase();
|
||||
testKey = {
|
||||
pubKey: libsignal.crypto.getRandomBytes(33),
|
||||
privKey: libsignal.crypto.getRandomBytes(32),
|
||||
};
|
||||
textsecure.storage.put('signedKeyId', 2);
|
||||
await store.storeSignedPreKey(1, testKey);
|
||||
});
|
||||
|
||||
it('should generate a new prekey bundle for a new contact', async () => {
|
||||
const pubKey = libsignal.crypto.getRandomBytes(32);
|
||||
const pubKeyString = StringView.arrayBufferToHex(pubKey);
|
||||
const preKeyIdBefore = textsecure.storage.get('maxPreKeyId', 1);
|
||||
const newBundle = await libloki.storage.getPreKeyBundleForContact(
|
||||
pubKeyString
|
||||
);
|
||||
const preKeyIdAfter = textsecure.storage.get('maxPreKeyId', 1);
|
||||
assert.strictEqual(preKeyIdAfter, preKeyIdBefore + 1);
|
||||
|
||||
const testKeyArray = new Uint8Array(testKey.pubKey);
|
||||
assert.isDefined(newBundle);
|
||||
assert.isDefined(newBundle.identityKey);
|
||||
assert.isDefined(newBundle.deviceId);
|
||||
assert.isDefined(newBundle.preKeyId);
|
||||
assert.isDefined(newBundle.signedKeyId);
|
||||
assert.isDefined(newBundle.preKey);
|
||||
assert.isDefined(newBundle.signedKey);
|
||||
assert.isDefined(newBundle.signature);
|
||||
assert.strictEqual(
|
||||
testKeyArray.byteLength,
|
||||
newBundle.signedKey.byteLength
|
||||
);
|
||||
for (let i = 0; i !== testKeyArray.byteLength; i += 1) {
|
||||
assert.strictEqual(testKeyArray[i], newBundle.signedKey[i]);
|
||||
}
|
||||
});
|
||||
|
||||
it('should return the same prekey bundle after creating a contact', async () => {
|
||||
const pubKey = libsignal.crypto.getRandomBytes(32);
|
||||
const pubKeyString = StringView.arrayBufferToHex(pubKey);
|
||||
const bundle1 = await libloki.storage.getPreKeyBundleForContact(
|
||||
pubKeyString
|
||||
);
|
||||
const bundle2 = await libloki.storage.getPreKeyBundleForContact(
|
||||
pubKeyString
|
||||
);
|
||||
assert.isDefined(bundle1);
|
||||
assert.isDefined(bundle2);
|
||||
assert.deepEqual(bundle1, bundle2);
|
||||
});
|
||||
|
||||
it('should save the signed keys and prekeys from a bundle', async () => {
|
||||
const pubKey = libsignal.crypto.getRandomBytes(32);
|
||||
const pubKeyString = StringView.arrayBufferToHex(pubKey);
|
||||
const preKeyIdBefore = textsecure.storage.get('maxPreKeyId', 1);
|
||||
const newBundle = await libloki.storage.getPreKeyBundleForContact(
|
||||
pubKeyString
|
||||
);
|
||||
const preKeyIdAfter = textsecure.storage.get('maxPreKeyId', 1);
|
||||
assert.strictEqual(preKeyIdAfter, preKeyIdBefore + 1);
|
||||
|
||||
const testKeyArray = new Uint8Array(testKey.pubKey);
|
||||
assert.isDefined(newBundle);
|
||||
assert.isDefined(newBundle.identityKey);
|
||||
assert.isDefined(newBundle.deviceId);
|
||||
assert.isDefined(newBundle.preKeyId);
|
||||
assert.isDefined(newBundle.signedKeyId);
|
||||
assert.isDefined(newBundle.preKey);
|
||||
assert.isDefined(newBundle.signedKey);
|
||||
assert.isDefined(newBundle.signature);
|
||||
assert.deepEqual(testKeyArray, newBundle.signedKey);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -49,8 +49,6 @@
|
|||
registerSingleDevice(mnemonic, mnemonicLanguage, profileName) {
|
||||
const createAccount = this.createAccount.bind(this);
|
||||
const clearSessionsAndPreKeys = this.clearSessionsAndPreKeys.bind(this);
|
||||
const generateKeys = this.generateKeys.bind(this, 0);
|
||||
const confirmKeys = this.confirmKeys.bind(this);
|
||||
const registrationDone = this.registrationDone.bind(this);
|
||||
let generateKeypair;
|
||||
if (mnemonic) {
|
||||
|
@ -73,8 +71,6 @@
|
|||
createAccount(identityKeyPair)
|
||||
.then(() => this.saveRecoveryPhrase(mnemonic))
|
||||
.then(clearSessionsAndPreKeys)
|
||||
.then(generateKeys)
|
||||
.then(confirmKeys)
|
||||
.then(() => {
|
||||
const pubKeyString = StringView.arrayBufferToHex(
|
||||
identityKeyPair.pubKey
|
||||
|
@ -84,163 +80,16 @@
|
|||
)
|
||||
);
|
||||
},
|
||||
rotateSignedPreKey() {
|
||||
return this.queueTask(() => {
|
||||
const signedKeyId = textsecure.storage.get('signedKeyId', 1);
|
||||
if (typeof signedKeyId !== 'number') {
|
||||
throw new Error('Invalid signedKeyId');
|
||||
}
|
||||
|
||||
const store = textsecure.storage.protocol;
|
||||
const { cleanSignedPreKeys } = this;
|
||||
|
||||
return store
|
||||
.getIdentityKeyPair()
|
||||
.then(
|
||||
identityKey =>
|
||||
libsignal.KeyHelper.generateSignedPreKey(
|
||||
identityKey,
|
||||
signedKeyId
|
||||
),
|
||||
() => {
|
||||
// We swallow any error here, because we don't want to get into
|
||||
// a loop of repeated retries.
|
||||
window.log.error(
|
||||
'Failed to get identity key. Canceling key rotation.'
|
||||
);
|
||||
}
|
||||
)
|
||||
.then(res => {
|
||||
if (!res) {
|
||||
return null;
|
||||
}
|
||||
window.log.info('Saving new signed prekey', res.keyId);
|
||||
return Promise.all([
|
||||
textsecure.storage.put('signedKeyId', signedKeyId + 1),
|
||||
store.storeSignedPreKey(
|
||||
res.keyId,
|
||||
res.keyPair,
|
||||
undefined,
|
||||
res.signature
|
||||
),
|
||||
])
|
||||
.then(() => {
|
||||
const confirmed = true;
|
||||
window.log.info('Confirming new signed prekey', res.keyId);
|
||||
return Promise.all([
|
||||
textsecure.storage.remove('signedKeyRotationRejected'),
|
||||
store.storeSignedPreKey(
|
||||
res.keyId,
|
||||
res.keyPair,
|
||||
confirmed,
|
||||
res.signature
|
||||
),
|
||||
]);
|
||||
})
|
||||
.then(() => cleanSignedPreKeys());
|
||||
})
|
||||
.catch(e => {
|
||||
window.log.error(
|
||||
'rotateSignedPrekey error:',
|
||||
e && e.stack ? e.stack : e
|
||||
);
|
||||
|
||||
if (
|
||||
e instanceof Error &&
|
||||
e.name === 'HTTPError' &&
|
||||
e.code >= 400 &&
|
||||
e.code <= 599
|
||||
) {
|
||||
const rejections =
|
||||
1 + textsecure.storage.get('signedKeyRotationRejected', 0);
|
||||
textsecure.storage.put('signedKeyRotationRejected', rejections);
|
||||
window.log.error(
|
||||
'Signed key rotation rejected count:',
|
||||
rejections
|
||||
);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
queueTask(task) {
|
||||
const taskWithTimeout = textsecure.createTaskWithTimeout(task);
|
||||
this.pending = this.pending.then(taskWithTimeout, taskWithTimeout);
|
||||
|
||||
return this.pending;
|
||||
},
|
||||
cleanSignedPreKeys() {
|
||||
const MINIMUM_KEYS = 3;
|
||||
const store = textsecure.storage.protocol;
|
||||
return store.loadSignedPreKeys().then(allKeys => {
|
||||
allKeys.sort((a, b) => (a.created_at || 0) - (b.created_at || 0));
|
||||
allKeys.reverse(); // we want the most recent first
|
||||
let confirmed = allKeys.filter(key => key.confirmed);
|
||||
const unconfirmed = allKeys.filter(key => !key.confirmed);
|
||||
|
||||
const recent = allKeys[0] ? allKeys[0].keyId : 'none';
|
||||
const recentConfirmed = confirmed[0] ? confirmed[0].keyId : 'none';
|
||||
window.log.info(`Most recent signed key: ${recent}`);
|
||||
window.log.info(`Most recent confirmed signed key: ${recentConfirmed}`);
|
||||
window.log.info(
|
||||
'Total signed key count:',
|
||||
allKeys.length,
|
||||
'-',
|
||||
confirmed.length,
|
||||
'confirmed'
|
||||
);
|
||||
|
||||
let confirmedCount = confirmed.length;
|
||||
|
||||
// Keep MINIMUM_KEYS confirmed keys, then drop if older than a week
|
||||
confirmed = confirmed.forEach((key, index) => {
|
||||
if (index < MINIMUM_KEYS) {
|
||||
return;
|
||||
}
|
||||
const createdAt = key.created_at || 0;
|
||||
const age = Date.now() - createdAt;
|
||||
|
||||
if (age > ARCHIVE_AGE) {
|
||||
window.log.info(
|
||||
'Removing confirmed signed prekey:',
|
||||
key.keyId,
|
||||
'with timestamp:',
|
||||
createdAt
|
||||
);
|
||||
store.removeSignedPreKey(key.keyId);
|
||||
confirmedCount -= 1;
|
||||
}
|
||||
});
|
||||
|
||||
const stillNeeded = MINIMUM_KEYS - confirmedCount;
|
||||
|
||||
// If we still don't have enough total keys, we keep as many unconfirmed
|
||||
// keys as necessary. If not necessary, and over a week old, we drop.
|
||||
unconfirmed.forEach((key, index) => {
|
||||
if (index < stillNeeded) {
|
||||
return;
|
||||
}
|
||||
|
||||
const createdAt = key.created_at || 0;
|
||||
const age = Date.now() - createdAt;
|
||||
if (age > ARCHIVE_AGE) {
|
||||
window.log.info(
|
||||
'Removing unconfirmed signed prekey:',
|
||||
key.keyId,
|
||||
'with timestamp:',
|
||||
createdAt
|
||||
);
|
||||
store.removeSignedPreKey(key.keyId);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
async createAccount(identityKeyPair, userAgent, readReceipts) {
|
||||
const signalingKey = libsignal.crypto.getRandomBytes(32 + 20);
|
||||
let password = btoa(getString(libsignal.crypto.getRandomBytes(16)));
|
||||
password = password.substring(0, password.length - 2);
|
||||
const registrationId = libsignal.KeyHelper.generateRegistrationId();
|
||||
|
||||
await Promise.all([
|
||||
textsecure.storage.remove('identityKey'),
|
||||
|
@ -265,7 +114,6 @@
|
|||
publicKey: identityKeyPair.pubKey,
|
||||
firstUse: true,
|
||||
timestamp: Date.now(),
|
||||
verified: textsecure.storage.protocol.VerifiedStatus.VERIFIED,
|
||||
nonblockingApproval: true,
|
||||
}
|
||||
);
|
||||
|
@ -273,7 +121,6 @@
|
|||
await textsecure.storage.put('identityKey', identityKeyPair);
|
||||
await textsecure.storage.put('signaling_key', signalingKey);
|
||||
await textsecure.storage.put('password', password);
|
||||
await textsecure.storage.put('registrationId', registrationId);
|
||||
if (userAgent) {
|
||||
await textsecure.storage.put('userAgent', userAgent);
|
||||
}
|
||||
|
@ -292,12 +139,8 @@
|
|||
async clearSessionsAndPreKeys() {
|
||||
const store = textsecure.storage.protocol;
|
||||
|
||||
window.log.info('clearing all sessions, prekeys, and signed prekeys');
|
||||
await Promise.all([
|
||||
store.clearContactPreKeysStore(),
|
||||
store.clearContactSignedPreKeysStore(),
|
||||
store.clearSessionStore(),
|
||||
]);
|
||||
window.log.info('clearing all sessions');
|
||||
await Promise.all([store.clearSessionStore()]);
|
||||
// During secondary device registration we need to keep our prekeys sent
|
||||
// to other pubkeys
|
||||
if (textsecure.storage.get('secondaryDeviceStatus') !== 'ongoing') {
|
||||
|
@ -307,83 +150,6 @@
|
|||
]);
|
||||
}
|
||||
},
|
||||
// Takes the same object returned by generateKeys
|
||||
async confirmKeys(keys) {
|
||||
const store = textsecure.storage.protocol;
|
||||
const key = keys.signedPreKey;
|
||||
const confirmed = true;
|
||||
|
||||
window.log.info('confirmKeys: confirming key', key.keyId);
|
||||
await store.storeSignedPreKey(
|
||||
key.keyId,
|
||||
key.keyPair,
|
||||
confirmed,
|
||||
key.signature
|
||||
);
|
||||
},
|
||||
generateKeys(count, providedProgressCallback) {
|
||||
const progressCallback =
|
||||
typeof providedProgressCallback === 'function'
|
||||
? providedProgressCallback
|
||||
: null;
|
||||
const startId = textsecure.storage.get('maxPreKeyId', 1);
|
||||
const signedKeyId = textsecure.storage.get('signedKeyId', 1);
|
||||
|
||||
if (typeof startId !== 'number') {
|
||||
throw new Error('Invalid maxPreKeyId');
|
||||
}
|
||||
if (typeof signedKeyId !== 'number') {
|
||||
throw new Error('Invalid signedKeyId');
|
||||
}
|
||||
|
||||
const store = textsecure.storage.protocol;
|
||||
return store.getIdentityKeyPair().then(identityKey => {
|
||||
const result = { preKeys: [], identityKey: identityKey.pubKey };
|
||||
const promises = [];
|
||||
|
||||
for (let keyId = startId; keyId < startId + count; keyId += 1) {
|
||||
promises.push(
|
||||
libsignal.KeyHelper.generatePreKey(keyId).then(res => {
|
||||
store.storePreKey(res.keyId, res.keyPair);
|
||||
result.preKeys.push({
|
||||
keyId: res.keyId,
|
||||
publicKey: res.keyPair.pubKey,
|
||||
});
|
||||
if (progressCallback) {
|
||||
progressCallback();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
promises.push(
|
||||
libsignal.KeyHelper.generateSignedPreKey(
|
||||
identityKey,
|
||||
signedKeyId
|
||||
).then(res => {
|
||||
store.storeSignedPreKey(
|
||||
res.keyId,
|
||||
res.keyPair,
|
||||
undefined,
|
||||
res.signature
|
||||
);
|
||||
result.signedPreKey = {
|
||||
keyId: res.keyId,
|
||||
publicKey: res.keyPair.pubKey,
|
||||
signature: res.signature,
|
||||
keyPair: res.keyPair,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
textsecure.storage.put('maxPreKeyId', startId + count);
|
||||
textsecure.storage.put('signedKeyId', signedKeyId + 1);
|
||||
return Promise.all(promises).then(() =>
|
||||
// This is primarily for the signed prekey summary it logs out
|
||||
this.cleanSignedPreKeys().then(() => result)
|
||||
);
|
||||
});
|
||||
},
|
||||
async generateMnemonic(language = 'english') {
|
||||
// Note: 4 bytes are converted into 3 seed words, so length 12 seed words
|
||||
// (13 - 1 checksum) are generated using 12 * 4 / 3 = 16 bytes.
|
||||
|
@ -530,9 +296,13 @@
|
|||
const conversations = window
|
||||
.getConversationController()
|
||||
.getConversations();
|
||||
await textsecure.messaging.sendGroupSyncMessage(conversations);
|
||||
await textsecure.messaging.sendOpenGroupsSyncMessage(conversations);
|
||||
await textsecure.messaging.sendContactSyncMessage();
|
||||
await libsession.Utils.SyncMessageUtils.sendGroupSyncMessage(
|
||||
conversations
|
||||
);
|
||||
await libsession.Utils.SyncMessageUtils.sendOpenGroupsSyncMessage(
|
||||
conversations
|
||||
);
|
||||
await libsession.Utils.SyncMessageUtils.sendContactSyncMessage();
|
||||
}, 5000);
|
||||
},
|
||||
validatePubKeyHex(pubKey) {
|
||||
|
|
|
@ -30,42 +30,6 @@
|
|||
|
||||
window.textsecure = window.textsecure || {};
|
||||
window.textsecure.crypto = {
|
||||
// Decrypts message into a raw string
|
||||
decryptWebsocketMessage(message, signalingKey) {
|
||||
const decodedMessage = message.toArrayBuffer();
|
||||
|
||||
if (signalingKey.byteLength !== 52) {
|
||||
throw new Error('Got invalid length signalingKey');
|
||||
}
|
||||
if (decodedMessage.byteLength < 1 + 16 + 10) {
|
||||
throw new Error('Got invalid length message');
|
||||
}
|
||||
if (new Uint8Array(decodedMessage)[0] !== 1) {
|
||||
throw new Error(`Got bad version number: ${decodedMessage[0]}`);
|
||||
}
|
||||
|
||||
const aesKey = signalingKey.slice(0, 32);
|
||||
const macKey = signalingKey.slice(32, 32 + 20);
|
||||
|
||||
const iv = decodedMessage.slice(1, 1 + 16);
|
||||
const ciphertext = decodedMessage.slice(
|
||||
1 + 16,
|
||||
decodedMessage.byteLength - 10
|
||||
);
|
||||
const ivAndCiphertext = decodedMessage.slice(
|
||||
0,
|
||||
decodedMessage.byteLength - 10
|
||||
);
|
||||
const mac = decodedMessage.slice(
|
||||
decodedMessage.byteLength - 10,
|
||||
decodedMessage.byteLength
|
||||
);
|
||||
|
||||
return verifyMAC(ivAndCiphertext, macKey, mac, 10).then(() =>
|
||||
decrypt(aesKey, ciphertext, iv)
|
||||
);
|
||||
},
|
||||
|
||||
decryptAttachment(encryptedBin, keys, theirDigest) {
|
||||
if (keys.byteLength !== 64) {
|
||||
throw new Error('Got invalid length attachment keys');
|
||||
|
|
|
@ -72,14 +72,6 @@
|
|||
}
|
||||
inherit(ReplayableError, SendMessageNetworkError);
|
||||
|
||||
function SignedPreKeyRotationError() {
|
||||
ReplayableError.call(this, {
|
||||
name: 'SignedPreKeyRotationError',
|
||||
message: 'Too many signed prekey rotation failures',
|
||||
});
|
||||
}
|
||||
inherit(ReplayableError, SignedPreKeyRotationError);
|
||||
|
||||
function MessageError(message, httpError) {
|
||||
this.code = httpError.code;
|
||||
|
||||
|
@ -218,7 +210,6 @@
|
|||
window.textsecure.OutgoingIdentityKeyError = OutgoingIdentityKeyError;
|
||||
window.textsecure.ReplayableError = ReplayableError;
|
||||
window.textsecure.MessageError = MessageError;
|
||||
window.textsecure.SignedPreKeyRotationError = SignedPreKeyRotationError;
|
||||
window.textsecure.EmptySwarmError = EmptySwarmError;
|
||||
window.textsecure.SeedNodeError = SeedNodeError;
|
||||
window.textsecure.DNSResolutionError = DNSResolutionError;
|
||||
|
|
3
libtextsecure/index.d.ts
vendored
3
libtextsecure/index.d.ts
vendored
|
@ -1,7 +1,7 @@
|
|||
import { LibTextsecureCryptoInterface } from './crypto';
|
||||
|
||||
export interface LibTextsecure {
|
||||
messaging: any;
|
||||
messaging: boolean;
|
||||
crypto: LibTextsecureCryptoInterface;
|
||||
storage: any;
|
||||
SendMessageNetworkError: any;
|
||||
|
@ -9,7 +9,6 @@ export interface LibTextsecure {
|
|||
OutgoingIdentityKeyError: any;
|
||||
ReplayableError: any;
|
||||
MessageError: any;
|
||||
SignedPreKeyRotationError: any;
|
||||
EmptySwarmError: any;
|
||||
SeedNodeError: any;
|
||||
DNSResolutionError: any;
|
||||
|
|
|
@ -1,62 +0,0 @@
|
|||
/* global window, postMessage, textsecure, close */
|
||||
|
||||
/* eslint-disable more/no-then, no-global-assign, no-restricted-globals, no-unused-vars */
|
||||
|
||||
/*
|
||||
* Load this script in a Web Worker to generate new prekeys without
|
||||
* tying up the main thread.
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API
|
||||
*
|
||||
* Because workers don't have access to the window or localStorage, we
|
||||
* create our own version that proxies back to the caller for actual
|
||||
* storage.
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
var myWorker = new Worker('/js/key_worker.js');
|
||||
myWorker.onmessage = function(e) {
|
||||
switch(e.data.method) {
|
||||
case 'set':
|
||||
localStorage.setItem(e.data.key, e.data.value);
|
||||
break;
|
||||
case 'remove':
|
||||
localStorage.removeItem(e.data.key);
|
||||
break;
|
||||
case 'done':
|
||||
console.error(e.data.keys);
|
||||
}
|
||||
};
|
||||
*/
|
||||
let store = {};
|
||||
window.textsecure.storage.impl = {
|
||||
/** ***************************
|
||||
*** Override Storage Routines ***
|
||||
**************************** */
|
||||
put(key, value) {
|
||||
if (value === undefined) {
|
||||
throw new Error('Tried to store undefined');
|
||||
}
|
||||
store[key] = value;
|
||||
postMessage({ method: 'set', key, value });
|
||||
},
|
||||
|
||||
get(key, defaultValue) {
|
||||
if (key in store) {
|
||||
return store[key];
|
||||
}
|
||||
return defaultValue;
|
||||
},
|
||||
|
||||
remove(key) {
|
||||
delete store[key];
|
||||
postMessage({ method: 'remove', key });
|
||||
},
|
||||
};
|
||||
// eslint-disable-next-line no-undef
|
||||
onmessage = e => {
|
||||
store = e.data;
|
||||
textsecure.protocol_wrapper.generateKeys().then(keys => {
|
||||
postMessage({ method: 'done', keys });
|
||||
close();
|
||||
});
|
||||
};
|
31
libtextsecure/libsignal-protocol.d.ts
vendored
31
libtextsecure/libsignal-protocol.d.ts
vendored
|
@ -5,7 +5,6 @@ export type BinaryString = string;
|
|||
export type CipherTextObject = {
|
||||
type: SignalService.Envelope.Type;
|
||||
body: BinaryString;
|
||||
registrationId?: number;
|
||||
};
|
||||
export interface SignalProtocolAddressConstructor {
|
||||
new (hexEncodedPublicKey: string, deviceId: number): SignalProtocolAddress;
|
||||
|
@ -83,15 +82,6 @@ export interface CryptoInterface {
|
|||
|
||||
export interface KeyHelperInterface {
|
||||
generateIdentityKeyPair(): Promise<KeyPair>;
|
||||
generateRegistrationId(): number;
|
||||
generateSignedPreKey(
|
||||
identityKeyPair: KeyPair,
|
||||
signedKeyId: number
|
||||
): Promise<{
|
||||
keyId: number;
|
||||
keyPair: KeyPair;
|
||||
signature: ArrayBuffer;
|
||||
}>;
|
||||
generatePreKey(
|
||||
keyId: number
|
||||
): Promise<{
|
||||
|
@ -100,30 +90,9 @@ export interface KeyHelperInterface {
|
|||
}>;
|
||||
}
|
||||
|
||||
export type SessionCipherConstructor = new (
|
||||
storage: any,
|
||||
remoteAddress: SignalProtocolAddress
|
||||
) => SessionCipher;
|
||||
export interface SessionCipher {
|
||||
/**
|
||||
* @returns The envelope type, registration id and binary encoded encrypted body.
|
||||
*/
|
||||
encrypt(buffer: ArrayBuffer | Uint8Array): Promise<CipherTextObject>;
|
||||
decryptPreKeyWhisperMessage(
|
||||
buffer: ArrayBuffer | Uint8Array
|
||||
): Promise<ArrayBuffer>;
|
||||
decryptWhisperMessage(buffer: ArrayBuffer | Uint8Array): Promise<ArrayBuffer>;
|
||||
getRecord(encodedNumber: string): Promise<any | undefined>;
|
||||
getRemoteRegistrationId(): Promise<number>;
|
||||
hasOpenSession(): Promise<boolean>;
|
||||
closeOpenSessionForDevice(): Promise<void>;
|
||||
deleteAllSessionsForDevice(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface LibsignalProtocol {
|
||||
SignalProtocolAddress: SignalProtocolAddressConstructor;
|
||||
Curve: CurveInterface;
|
||||
crypto: CryptoInterface;
|
||||
KeyHelper: KeyHelperInterface;
|
||||
SessionCipher: SessionCipherConstructor;
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,24 +1,16 @@
|
|||
/* global window: false */
|
||||
/* global callWorker: false */
|
||||
/* global textsecure: false */
|
||||
/* global WebSocket: false */
|
||||
/* global Event: false */
|
||||
/* global dcodeIO: false */
|
||||
/* global lokiPublicChatAPI: false */
|
||||
/* global feeds: false */
|
||||
/* global WebAPI: false */
|
||||
|
||||
/* eslint-disable more/no-then */
|
||||
/* eslint-disable no-unreachable */
|
||||
|
||||
let openGroupBound = false;
|
||||
|
||||
function MessageReceiver(username, password, signalingKey) {
|
||||
this.count = 0;
|
||||
|
||||
this.signalingKey = signalingKey;
|
||||
this.server = WebAPI.connect();
|
||||
|
||||
function MessageReceiver() {
|
||||
this.pending = Promise.resolve();
|
||||
|
||||
// only do this once to prevent duplicates
|
||||
|
@ -58,7 +50,6 @@ MessageReceiver.prototype.extend({
|
|||
return;
|
||||
}
|
||||
|
||||
this.count = 0;
|
||||
if (this.hasConnected) {
|
||||
const ev = new Event('reconnect');
|
||||
this.dispatchEvent(ev);
|
||||
|
@ -71,10 +62,6 @@ MessageReceiver.prototype.extend({
|
|||
if (lokiPublicChatAPI) {
|
||||
lokiPublicChatAPI.open();
|
||||
}
|
||||
// set up pollers for any RSS feeds
|
||||
feeds.forEach(feed => {
|
||||
feed.on('rssMessage', window.NewReceiver.handleUnencryptedMessage);
|
||||
});
|
||||
|
||||
// Ensures that an immediate 'empty' event from the websocket will fire only after
|
||||
// all cached envelopes are processed.
|
||||
|
@ -94,60 +81,22 @@ MessageReceiver.prototype.extend({
|
|||
if (lokiPublicChatAPI) {
|
||||
await lokiPublicChatAPI.close();
|
||||
}
|
||||
|
||||
return this.drain();
|
||||
},
|
||||
onopen() {
|
||||
window.log.info('websocket open');
|
||||
},
|
||||
onerror() {
|
||||
window.log.error('websocket error');
|
||||
},
|
||||
onclose(ev) {
|
||||
window.log.info(
|
||||
'websocket closed',
|
||||
ev.code,
|
||||
ev.reason || '',
|
||||
'calledClose:',
|
||||
this.calledClose
|
||||
);
|
||||
},
|
||||
drain() {
|
||||
const { incoming } = this;
|
||||
this.incoming = [];
|
||||
|
||||
// This promise will resolve when there are no more messages to be processed.
|
||||
return Promise.all(incoming);
|
||||
},
|
||||
getStatus() {
|
||||
if (this.hasConnected) {
|
||||
return WebSocket.CLOSED;
|
||||
}
|
||||
return -1;
|
||||
},
|
||||
onopen() {},
|
||||
onerror() {},
|
||||
onclose() {},
|
||||
});
|
||||
|
||||
window.textsecure = window.textsecure || {};
|
||||
|
||||
textsecure.MessageReceiver = function MessageReceiverWrapper(
|
||||
username,
|
||||
password,
|
||||
signalingKey,
|
||||
options
|
||||
) {
|
||||
const messageReceiver = new MessageReceiver(
|
||||
username,
|
||||
password,
|
||||
signalingKey,
|
||||
options
|
||||
);
|
||||
textsecure.MessageReceiver = function MessageReceiverWrapper() {
|
||||
const messageReceiver = new MessageReceiver();
|
||||
this.addEventListener = messageReceiver.addEventListener.bind(
|
||||
messageReceiver
|
||||
);
|
||||
this.removeEventListener = messageReceiver.removeEventListener.bind(
|
||||
messageReceiver
|
||||
);
|
||||
this.getStatus = messageReceiver.getStatus.bind(messageReceiver);
|
||||
this.close = messageReceiver.close.bind(messageReceiver);
|
||||
|
||||
this.stopProcessing = messageReceiver.stopProcessing.bind(messageReceiver);
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
(function() {
|
||||
window.textsecure = window.textsecure || {};
|
||||
window.textsecure.storage = window.textsecure.storage || {};
|
||||
|
||||
textsecure.storage.protocol = new SignalProtocolStore();
|
||||
|
||||
textsecure.ProvisioningCipher = libsignal.ProvisioningCipher;
|
||||
|
|
|
@ -1,388 +0,0 @@
|
|||
/* global textsecure, WebAPI, window, libloki, _, libsession */
|
||||
|
||||
/* eslint-disable more/no-then, no-bitwise */
|
||||
|
||||
function stringToArrayBuffer(str) {
|
||||
if (typeof str !== 'string') {
|
||||
throw new Error('Passed non-string to stringToArrayBuffer');
|
||||
}
|
||||
const res = new ArrayBuffer(str.length);
|
||||
const uint = new Uint8Array(res);
|
||||
for (let i = 0; i < str.length; i += 1) {
|
||||
uint[i] = str.charCodeAt(i);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function Message(options) {
|
||||
this.body = options.body;
|
||||
this.attachments = options.attachments || [];
|
||||
this.quote = options.quote;
|
||||
this.preview = options.preview;
|
||||
this.group = options.group;
|
||||
this.flags = options.flags;
|
||||
this.recipients = options.recipients;
|
||||
this.timestamp = options.timestamp;
|
||||
this.needsSync = options.needsSync;
|
||||
this.expireTimer = options.expireTimer;
|
||||
this.profileKey = options.profileKey;
|
||||
this.profile = options.profile;
|
||||
this.groupInvitation = options.groupInvitation;
|
||||
this.sessionRestoration = options.sessionRestoration || false;
|
||||
|
||||
if (!(this.recipients instanceof Array)) {
|
||||
throw new Error('Invalid recipient list');
|
||||
}
|
||||
|
||||
if (!this.group && this.recipients.length !== 1) {
|
||||
throw new Error('Invalid recipient list for non-group');
|
||||
}
|
||||
|
||||
if (typeof this.timestamp !== 'number') {
|
||||
throw new Error('Invalid timestamp');
|
||||
}
|
||||
|
||||
if (this.expireTimer !== undefined && this.expireTimer !== null) {
|
||||
if (typeof this.expireTimer !== 'number' || !(this.expireTimer >= 0)) {
|
||||
throw new Error('Invalid expireTimer');
|
||||
}
|
||||
}
|
||||
|
||||
if (this.attachments) {
|
||||
if (!(this.attachments instanceof Array)) {
|
||||
throw new Error('Invalid message attachments');
|
||||
}
|
||||
}
|
||||
if (this.flags !== undefined) {
|
||||
if (typeof this.flags !== 'number') {
|
||||
throw new Error('Invalid message flags');
|
||||
}
|
||||
}
|
||||
if (this.isEndSession()) {
|
||||
if (
|
||||
this.body !== null ||
|
||||
this.group !== null ||
|
||||
this.attachments.length !== 0
|
||||
) {
|
||||
throw new Error('Invalid end session message');
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
typeof this.timestamp !== 'number' ||
|
||||
(this.body && typeof this.body !== 'string')
|
||||
) {
|
||||
throw new Error('Invalid message body');
|
||||
}
|
||||
if (this.group) {
|
||||
if (
|
||||
typeof this.group.id !== 'string' ||
|
||||
typeof this.group.type !== 'number'
|
||||
) {
|
||||
throw new Error('Invalid group context');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Message.prototype = {
|
||||
constructor: Message,
|
||||
isEndSession() {
|
||||
return this.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION;
|
||||
},
|
||||
toProto() {
|
||||
if (this.dataMessage instanceof textsecure.protobuf.DataMessage) {
|
||||
return this.dataMessage;
|
||||
}
|
||||
const proto = new textsecure.protobuf.DataMessage();
|
||||
if (this.body) {
|
||||
proto.body = this.body;
|
||||
}
|
||||
proto.attachments = this.attachmentPointers;
|
||||
if (this.flags) {
|
||||
proto.flags = this.flags;
|
||||
}
|
||||
if (this.group) {
|
||||
proto.group = new textsecure.protobuf.GroupContext();
|
||||
proto.group.id = stringToArrayBuffer(this.group.id);
|
||||
proto.group.type = this.group.type;
|
||||
}
|
||||
if (Array.isArray(this.preview)) {
|
||||
proto.preview = this.preview.map(preview => {
|
||||
const item = new textsecure.protobuf.DataMessage.Preview();
|
||||
item.title = preview.title;
|
||||
item.url = preview.url;
|
||||
item.image = preview.image || null;
|
||||
return item;
|
||||
});
|
||||
}
|
||||
if (this.quote) {
|
||||
const { QuotedAttachment } = textsecure.protobuf.DataMessage.Quote;
|
||||
const { Quote } = textsecure.protobuf.DataMessage;
|
||||
|
||||
proto.quote = new Quote();
|
||||
const { quote } = proto;
|
||||
|
||||
quote.id = this.quote.id;
|
||||
quote.author = this.quote.author;
|
||||
quote.text = this.quote.text;
|
||||
quote.attachments = (this.quote.attachments || []).map(attachment => {
|
||||
const quotedAttachment = new QuotedAttachment();
|
||||
|
||||
quotedAttachment.contentType = attachment.contentType;
|
||||
quotedAttachment.fileName = attachment.fileName;
|
||||
if (attachment.attachmentPointer) {
|
||||
quotedAttachment.thumbnail = attachment.attachmentPointer;
|
||||
}
|
||||
|
||||
return quotedAttachment;
|
||||
});
|
||||
}
|
||||
if (this.expireTimer) {
|
||||
proto.expireTimer = this.expireTimer;
|
||||
}
|
||||
|
||||
if (this.profileKey) {
|
||||
proto.profileKey = this.profileKey;
|
||||
}
|
||||
|
||||
// Set the loki profile
|
||||
if (this.profile) {
|
||||
const profile = new textsecure.protobuf.DataMessage.LokiProfile();
|
||||
if (this.profile.displayName) {
|
||||
profile.displayName = this.profile.displayName;
|
||||
}
|
||||
|
||||
const conversation = window
|
||||
.getConversationController()
|
||||
.get(textsecure.storage.user.getNumber());
|
||||
const avatarPointer = conversation.get('avatarPointer');
|
||||
if (avatarPointer) {
|
||||
profile.avatar = avatarPointer;
|
||||
}
|
||||
proto.profile = profile;
|
||||
}
|
||||
|
||||
if (this.groupInvitation) {
|
||||
proto.groupInvitation = new textsecure.protobuf.DataMessage.GroupInvitation(
|
||||
{
|
||||
serverAddress: this.groupInvitation.serverAddress,
|
||||
channelId: this.groupInvitation.channelId,
|
||||
serverName: this.groupInvitation.serverName,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (this.sessionRestoration) {
|
||||
proto.flags = textsecure.protobuf.DataMessage.Flags.SESSION_RESTORE;
|
||||
}
|
||||
|
||||
this.dataMessage = proto;
|
||||
return proto;
|
||||
},
|
||||
toArrayBuffer() {
|
||||
return this.toProto().toArrayBuffer();
|
||||
},
|
||||
};
|
||||
|
||||
function MessageSender() {
|
||||
// Currently only used for getProxiedSize() and makeProxiedRequest(), which are only used for fetching previews
|
||||
this.server = WebAPI.connect();
|
||||
}
|
||||
|
||||
MessageSender.prototype = {
|
||||
constructor: MessageSender,
|
||||
|
||||
async sendContactSyncMessage(convos) {
|
||||
let convosToSync;
|
||||
if (!convos) {
|
||||
convosToSync = await libsession.Utils.SyncMessageUtils.getSyncContacts();
|
||||
} else {
|
||||
convosToSync = convos;
|
||||
}
|
||||
|
||||
if (convosToSync.size === 0) {
|
||||
window.log.info('No contacts to sync.');
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
libloki.api.debug.logContactSync(
|
||||
'Triggering contact sync message with:',
|
||||
convosToSync
|
||||
);
|
||||
|
||||
// We need to sync across 3 contacts at a time
|
||||
// This is to avoid hitting storage server limit
|
||||
const chunked = _.chunk(convosToSync, 3);
|
||||
const syncMessages = await Promise.all(
|
||||
chunked.map(c => libloki.api.createContactSyncMessage(c))
|
||||
);
|
||||
|
||||
const syncPromises = syncMessages.map(syncMessage =>
|
||||
libsession.getMessageQueue().sendSyncMessage(syncMessage)
|
||||
);
|
||||
|
||||
return Promise.all(syncPromises);
|
||||
},
|
||||
|
||||
sendGroupSyncMessage(conversations) {
|
||||
// If we havn't got a primaryDeviceKey then we are in the middle of pairing
|
||||
// primaryDevicePubKey is set to our own number if we are the master device
|
||||
const primaryDeviceKey = window.storage.get('primaryDevicePubKey');
|
||||
if (!primaryDeviceKey) {
|
||||
window.log.debug('sendGroupSyncMessage: no primary device pubkey');
|
||||
return Promise.resolve();
|
||||
}
|
||||
// We only want to sync across closed groups that we haven't left
|
||||
const activeGroups = conversations.filter(
|
||||
c => c.isClosedGroup() && !c.get('left') && !c.get('isKickedFromGroup')
|
||||
);
|
||||
if (activeGroups.length === 0) {
|
||||
window.log.info('No closed group to sync.');
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const mediumGroups = activeGroups.filter(c => c.isMediumGroup());
|
||||
|
||||
window.libsession.ClosedGroupV2.syncMediumGroups(mediumGroups);
|
||||
|
||||
const legacyGroups = activeGroups.filter(c => !c.isMediumGroup());
|
||||
|
||||
// We need to sync across 1 group at a time
|
||||
// This is because we could hit the storage server limit with one group
|
||||
const syncPromises = legacyGroups
|
||||
.map(c => libloki.api.createGroupSyncMessage(c))
|
||||
.map(syncMessage =>
|
||||
libsession.getMessageQueue().sendSyncMessage(syncMessage)
|
||||
);
|
||||
|
||||
return Promise.all(syncPromises);
|
||||
},
|
||||
|
||||
async sendOpenGroupsSyncMessage(convos) {
|
||||
// If we havn't got a primaryDeviceKey then we are in the middle of pairing
|
||||
// primaryDevicePubKey is set to our own number if we are the master device
|
||||
const primaryDeviceKey = window.storage.get('primaryDevicePubKey');
|
||||
if (!primaryDeviceKey) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const conversations = Array.isArray(convos) ? convos : [convos];
|
||||
|
||||
const openGroupsConvos = await libsession.Utils.SyncMessageUtils.filterOpenGroupsConvos(
|
||||
conversations
|
||||
);
|
||||
|
||||
if (!openGroupsConvos.length) {
|
||||
window.log.info('No open groups to sync');
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// Send the whole list of open groups in a single message
|
||||
const openGroupsDetails = openGroupsConvos.map(conversation => ({
|
||||
url: conversation.id,
|
||||
channelId: conversation.get('channelId'),
|
||||
}));
|
||||
const openGroupsSyncParams = {
|
||||
timestamp: Date.now(),
|
||||
openGroupsDetails,
|
||||
};
|
||||
const openGroupsSyncMessage = new libsession.Messages.Outgoing.OpenGroupSyncMessage(
|
||||
openGroupsSyncParams
|
||||
);
|
||||
|
||||
return libsession.getMessageQueue().sendSyncMessage(openGroupsSyncMessage);
|
||||
},
|
||||
async sendBlockedListSyncMessage() {
|
||||
// If we havn't got a primaryDeviceKey then we are in the middle of pairing
|
||||
// primaryDevicePubKey is set to our own number if we are the master device
|
||||
const primaryDeviceKey = window.storage.get('primaryDevicePubKey');
|
||||
if (!primaryDeviceKey) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const currentlyBlockedNumbers = window.BlockedNumberController.getBlockedNumbers();
|
||||
|
||||
// currently we only sync user blocked, not groups
|
||||
const blockedSyncMessage = new libsession.Messages.Outgoing.BlockedListSyncMessage(
|
||||
{
|
||||
timestamp: Date.now(),
|
||||
numbers: currentlyBlockedNumbers,
|
||||
groups: [],
|
||||
}
|
||||
);
|
||||
return libsession.getMessageQueue().sendSyncMessage(blockedSyncMessage);
|
||||
},
|
||||
syncReadMessages(reads) {
|
||||
const myDevice = textsecure.storage.user.getDeviceId();
|
||||
// FIXME currently not in used
|
||||
if (myDevice !== 1 && myDevice !== '1') {
|
||||
const syncReadMessages = new libsession.Messages.Outgoing.SyncReadMessage(
|
||||
{
|
||||
timestamp: Date.now(),
|
||||
readMessages: reads,
|
||||
}
|
||||
);
|
||||
return libsession.getMessageQueue().sendSyncMessage(syncReadMessages);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
async syncVerification(destination, state, identityKey) {
|
||||
const myDevice = textsecure.storage.user.getDeviceId();
|
||||
// FIXME currently not in used
|
||||
if (myDevice === 1 || myDevice === '1') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
// send a session established message (used as a nullMessage)
|
||||
const destinationPubKey = new libsession.Types.PubKey(destination);
|
||||
|
||||
const sessionEstablished = new window.libsession.Messages.Outgoing.SessionEstablishedMessage(
|
||||
{ timestamp: Date.now() }
|
||||
);
|
||||
const { padding } = sessionEstablished;
|
||||
await libsession
|
||||
.getMessageQueue()
|
||||
.send(destinationPubKey, sessionEstablished);
|
||||
|
||||
const verifiedSyncParams = {
|
||||
state,
|
||||
destination: destinationPubKey,
|
||||
identityKey,
|
||||
padding,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
const verifiedSyncMessage = new window.libsession.Messages.Outgoing.VerifiedSyncMessage(
|
||||
verifiedSyncParams
|
||||
);
|
||||
|
||||
return libsession.getMessageQueue().sendSyncMessage(verifiedSyncMessage);
|
||||
},
|
||||
|
||||
makeProxiedRequest(url, options) {
|
||||
return this.server.makeProxiedRequest(url, options);
|
||||
},
|
||||
getProxiedSize(url) {
|
||||
return this.server.getProxiedSize(url);
|
||||
},
|
||||
};
|
||||
|
||||
window.textsecure = window.textsecure || {};
|
||||
|
||||
textsecure.MessageSender = function MessageSenderWrapper() {
|
||||
const sender = new MessageSender();
|
||||
this.sendContactSyncMessage = sender.sendContactSyncMessage.bind(sender);
|
||||
this.sendGroupSyncMessage = sender.sendGroupSyncMessage.bind(sender);
|
||||
this.sendOpenGroupsSyncMessage = sender.sendOpenGroupsSyncMessage.bind(
|
||||
sender
|
||||
);
|
||||
this.syncReadMessages = sender.syncReadMessages.bind(sender);
|
||||
this.syncVerification = sender.syncVerification.bind(sender);
|
||||
this.makeProxiedRequest = sender.makeProxiedRequest.bind(sender);
|
||||
this.getProxiedSize = sender.getProxiedSize.bind(sender);
|
||||
this.sendBlockedListSyncMessage = sender.sendBlockedListSyncMessage.bind(
|
||||
sender
|
||||
);
|
||||
};
|
||||
|
||||
textsecure.MessageSender.prototype = {
|
||||
constructor: textsecure.MessageSender,
|
||||
};
|
|
@ -35,17 +35,5 @@
|
|||
getDeviceName() {
|
||||
return textsecure.storage.get('device_name');
|
||||
},
|
||||
|
||||
setDeviceNameEncrypted() {
|
||||
return textsecure.storage.put('deviceNameEncrypted', true);
|
||||
},
|
||||
|
||||
getDeviceNameEncrypted() {
|
||||
return textsecure.storage.get('deviceNameEncrypted');
|
||||
},
|
||||
|
||||
getSignalingKey() {
|
||||
return textsecure.storage.get('signaling_key');
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
|
|
@ -1,88 +0,0 @@
|
|||
/* global Event, textsecure, window, libsession */
|
||||
|
||||
/* eslint-disable more/no-then */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
window.textsecure = window.textsecure || {};
|
||||
|
||||
async function SyncRequest() {
|
||||
// this.receiver = receiver;
|
||||
|
||||
window.log.info('SyncRequest created. Sending config sync request...');
|
||||
const { CONFIGURATION } = textsecure.protobuf.SyncMessage.Request.Type;
|
||||
const { RequestSyncMessage } = window.libsession.Messages.Outgoing;
|
||||
|
||||
const requestConfigurationSyncMessage = new RequestSyncMessage({
|
||||
timestamp: Date.now(),
|
||||
reqestType: CONFIGURATION,
|
||||
});
|
||||
|
||||
await libsession
|
||||
.getMessageQueue()
|
||||
.sendSyncMessage(requestConfigurationSyncMessage);
|
||||
|
||||
window.log.info('SyncRequest now sending contact sync message...');
|
||||
const { CONTACTS } = textsecure.protobuf.SyncMessage.Request.Type;
|
||||
const requestContactSyncMessage = new RequestSyncMessage({
|
||||
timestamp: Date.now(),
|
||||
reqestType: CONTACTS,
|
||||
});
|
||||
await libsession
|
||||
.getMessageQueue()
|
||||
.sendSyncMessage(requestContactSyncMessage);
|
||||
|
||||
window.log.info('SyncRequest now sending group sync messsage...');
|
||||
const { GROUPS } = textsecure.protobuf.SyncMessage.Request.Type;
|
||||
const requestGroupSyncMessage = new RequestSyncMessage({
|
||||
timestamp: Date.now(),
|
||||
reqestType: GROUPS,
|
||||
});
|
||||
await libsession.getMessageQueue().sendSyncMessage(requestGroupSyncMessage);
|
||||
|
||||
this.timeout = setTimeout(this.onTimeout.bind(this), 60000);
|
||||
}
|
||||
|
||||
SyncRequest.prototype = new textsecure.EventTarget();
|
||||
SyncRequest.prototype.extend({
|
||||
constructor: SyncRequest,
|
||||
onContactSyncComplete() {
|
||||
this.contactSync = true;
|
||||
this.update();
|
||||
},
|
||||
onGroupSyncComplete() {
|
||||
this.groupSync = true;
|
||||
this.update();
|
||||
},
|
||||
update() {
|
||||
if (this.contactSync && this.groupSync) {
|
||||
this.dispatchEvent(new Event('success'));
|
||||
this.cleanup();
|
||||
}
|
||||
},
|
||||
onTimeout() {
|
||||
if (this.contactSync || this.groupSync) {
|
||||
this.dispatchEvent(new Event('success'));
|
||||
} else {
|
||||
this.dispatchEvent(new Event('timeout'));
|
||||
}
|
||||
this.cleanup();
|
||||
},
|
||||
cleanup() {
|
||||
clearTimeout(this.timeout);
|
||||
delete this.listeners;
|
||||
},
|
||||
});
|
||||
|
||||
textsecure.SyncRequest = function SyncRequestWrapper() {
|
||||
const syncRequest = new SyncRequest();
|
||||
this.addEventListener = syncRequest.addEventListener.bind(syncRequest);
|
||||
this.removeEventListener = syncRequest.removeEventListener.bind(
|
||||
syncRequest
|
||||
);
|
||||
};
|
||||
|
||||
textsecure.SyncRequest.prototype = {
|
||||
constructor: textsecure.SyncRequest,
|
||||
};
|
||||
})();
|
|
@ -17,9 +17,7 @@
|
|||
"dcodeIO": true,
|
||||
"getString": true,
|
||||
"hexToArrayBuffer": true,
|
||||
"MockServer": true,
|
||||
"MockSocket": true,
|
||||
"PROTO_ROOT": true,
|
||||
"stringToArrayBuffer": true,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -59,5 +59,3 @@ window.hexToArrayBuffer = str => {
|
|||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
window.MockSocket.prototype.addEventListener = () => null;
|
||||
|
|
|
@ -1,169 +0,0 @@
|
|||
/* global libsignal */
|
||||
|
||||
describe('AccountManager', () => {
|
||||
let accountManager;
|
||||
|
||||
beforeEach(() => {
|
||||
accountManager = new window.textsecure.AccountManager();
|
||||
});
|
||||
|
||||
describe('#cleanSignedPreKeys', () => {
|
||||
let originalProtocolStorage;
|
||||
let signedPreKeys;
|
||||
const DAY = 1000 * 60 * 60 * 24;
|
||||
|
||||
beforeEach(async () => {
|
||||
const identityKey = await libsignal.KeyHelper.generateIdentityKeyPair();
|
||||
|
||||
originalProtocolStorage = window.textsecure.storage.protocol;
|
||||
window.textsecure.storage.protocol = {
|
||||
getIdentityKeyPair() {
|
||||
return identityKey;
|
||||
},
|
||||
loadSignedPreKeys() {
|
||||
return Promise.resolve(signedPreKeys);
|
||||
},
|
||||
};
|
||||
});
|
||||
afterEach(() => {
|
||||
window.textsecure.storage.protocol = originalProtocolStorage;
|
||||
});
|
||||
|
||||
it('keeps three confirmed keys even if over a week old', () => {
|
||||
const now = Date.now();
|
||||
signedPreKeys = [
|
||||
{
|
||||
keyId: 1,
|
||||
created_at: now - DAY * 21,
|
||||
confirmed: true,
|
||||
},
|
||||
{
|
||||
keyId: 2,
|
||||
created_at: now - DAY * 14,
|
||||
confirmed: true,
|
||||
},
|
||||
{
|
||||
keyId: 3,
|
||||
created_at: now - DAY * 18,
|
||||
confirmed: true,
|
||||
},
|
||||
];
|
||||
|
||||
// should be no calls to store.removeSignedPreKey, would cause crash
|
||||
return accountManager.cleanSignedPreKeys();
|
||||
});
|
||||
|
||||
it('eliminates confirmed keys over a week old, if more than three', async () => {
|
||||
const now = Date.now();
|
||||
signedPreKeys = [
|
||||
{
|
||||
keyId: 1,
|
||||
created_at: now - DAY * 21,
|
||||
confirmed: true,
|
||||
},
|
||||
{
|
||||
keyId: 2,
|
||||
created_at: now - DAY * 14,
|
||||
confirmed: true,
|
||||
},
|
||||
{
|
||||
keyId: 3,
|
||||
created_at: now - DAY * 4,
|
||||
confirmed: true,
|
||||
},
|
||||
{
|
||||
keyId: 4,
|
||||
created_at: now - DAY * 18,
|
||||
confirmed: true,
|
||||
},
|
||||
{
|
||||
keyId: 5,
|
||||
created_at: now - DAY,
|
||||
confirmed: true,
|
||||
},
|
||||
];
|
||||
|
||||
let count = 0;
|
||||
window.textsecure.storage.protocol.removeSignedPreKey = keyId => {
|
||||
if (keyId !== 1 && keyId !== 4) {
|
||||
throw new Error(`Wrong keys were eliminated! ${keyId}`);
|
||||
}
|
||||
|
||||
count += 1;
|
||||
};
|
||||
|
||||
await accountManager.cleanSignedPreKeys();
|
||||
assert.strictEqual(count, 2);
|
||||
});
|
||||
|
||||
it('keeps at least three unconfirmed keys if no confirmed', async () => {
|
||||
const now = Date.now();
|
||||
signedPreKeys = [
|
||||
{
|
||||
keyId: 1,
|
||||
created_at: now - DAY * 14,
|
||||
},
|
||||
{
|
||||
keyId: 2,
|
||||
created_at: now - DAY * 21,
|
||||
},
|
||||
{
|
||||
keyId: 3,
|
||||
created_at: now - DAY * 18,
|
||||
},
|
||||
{
|
||||
keyId: 4,
|
||||
created_at: now - DAY,
|
||||
},
|
||||
];
|
||||
|
||||
let count = 0;
|
||||
window.textsecure.storage.protocol.removeSignedPreKey = keyId => {
|
||||
if (keyId !== 2) {
|
||||
throw new Error(`Wrong keys were eliminated! ${keyId}`);
|
||||
}
|
||||
|
||||
count += 1;
|
||||
};
|
||||
|
||||
await accountManager.cleanSignedPreKeys();
|
||||
assert.strictEqual(count, 1);
|
||||
});
|
||||
|
||||
it('if some confirmed keys, keeps unconfirmed to addd up to three total', async () => {
|
||||
const now = Date.now();
|
||||
signedPreKeys = [
|
||||
{
|
||||
keyId: 1,
|
||||
created_at: now - DAY * 21,
|
||||
confirmed: true,
|
||||
},
|
||||
{
|
||||
keyId: 2,
|
||||
created_at: now - DAY * 14,
|
||||
confirmed: true,
|
||||
},
|
||||
{
|
||||
keyId: 3,
|
||||
created_at: now - DAY * 12,
|
||||
},
|
||||
{
|
||||
keyId: 4,
|
||||
created_at: now - DAY * 8,
|
||||
},
|
||||
];
|
||||
|
||||
let count = 0;
|
||||
window.textsecure.storage.protocol.removeSignedPreKey = keyId => {
|
||||
if (keyId !== 3) {
|
||||
throw new Error(`Wrong keys were eliminated! ${keyId}`);
|
||||
}
|
||||
|
||||
count += 1;
|
||||
};
|
||||
|
||||
await accountManager.cleanSignedPreKeys();
|
||||
assert.strictEqual(count, 1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,13 +0,0 @@
|
|||
window.setImmediate = window.nodeSetImmediate;
|
||||
|
||||
const fakeCall = () => Promise.resolve();
|
||||
|
||||
const fakeAPI = {
|
||||
getAttachment: fakeCall,
|
||||
putAttachment: fakeCall,
|
||||
putAvatar: fakeCall,
|
||||
};
|
||||
|
||||
window.WebAPI = {
|
||||
connect: () => fakeAPI,
|
||||
};
|
|
@ -1,156 +0,0 @@
|
|||
/* global libsignal, textsecure */
|
||||
|
||||
describe('Key generation', function thisNeeded() {
|
||||
const count = 10;
|
||||
this.timeout(count * 2000);
|
||||
|
||||
function validateStoredKeyPair(keyPair) {
|
||||
/* Ensure the keypair matches the format used internally by libsignal-protocol */
|
||||
assert.isObject(keyPair, 'Stored keyPair is not an object');
|
||||
assert.instanceOf(keyPair.pubKey, ArrayBuffer);
|
||||
assert.instanceOf(keyPair.privKey, ArrayBuffer);
|
||||
assert.strictEqual(keyPair.pubKey.byteLength, 33);
|
||||
assert.strictEqual(new Uint8Array(keyPair.pubKey)[0], 5);
|
||||
assert.strictEqual(keyPair.privKey.byteLength, 32);
|
||||
}
|
||||
function itStoresPreKey(keyId) {
|
||||
it(`prekey ${keyId} is valid`, () =>
|
||||
textsecure.storage.protocol.loadPreKey(keyId).then(keyPair => {
|
||||
validateStoredKeyPair(keyPair);
|
||||
}));
|
||||
}
|
||||
function itStoresSignedPreKey(keyId) {
|
||||
it(`signed prekey ${keyId} is valid`, () =>
|
||||
textsecure.storage.protocol.loadSignedPreKey(keyId).then(keyPair => {
|
||||
validateStoredKeyPair(keyPair);
|
||||
}));
|
||||
}
|
||||
function validateResultKey(resultKey) {
|
||||
return textsecure.storage.protocol
|
||||
.loadPreKey(resultKey.keyId)
|
||||
.then(keyPair => {
|
||||
assertEqualArrayBuffers(resultKey.publicKey, keyPair.pubKey);
|
||||
});
|
||||
}
|
||||
function validateResultSignedKey(resultSignedKey) {
|
||||
return textsecure.storage.protocol
|
||||
.loadSignedPreKey(resultSignedKey.keyId)
|
||||
.then(keyPair => {
|
||||
assertEqualArrayBuffers(resultSignedKey.publicKey, keyPair.pubKey);
|
||||
});
|
||||
}
|
||||
|
||||
before(() => {
|
||||
localStorage.clear();
|
||||
return libsignal.KeyHelper.generateIdentityKeyPair().then(keyPair =>
|
||||
textsecure.storage.protocol.put('identityKey', keyPair)
|
||||
);
|
||||
});
|
||||
|
||||
describe('the first time', () => {
|
||||
let result;
|
||||
/* result should have this format
|
||||
* {
|
||||
* preKeys: [ { keyId, publicKey }, ... ],
|
||||
* signedPreKey: { keyId, publicKey, signature },
|
||||
* identityKey: <ArrayBuffer>
|
||||
* }
|
||||
*/
|
||||
before(() => {
|
||||
const accountManager = new textsecure.AccountManager('');
|
||||
return accountManager.generateKeys(count).then(res => {
|
||||
result = res;
|
||||
});
|
||||
});
|
||||
for (let i = 1; i <= count; i += 1) {
|
||||
itStoresPreKey(i);
|
||||
}
|
||||
itStoresSignedPreKey(1);
|
||||
|
||||
it(`result contains ${count} preKeys`, () => {
|
||||
assert.isArray(result.preKeys);
|
||||
assert.lengthOf(result.preKeys, count);
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
assert.isObject(result.preKeys[i]);
|
||||
}
|
||||
});
|
||||
it('result contains the correct keyIds', () => {
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
assert.strictEqual(result.preKeys[i].keyId, i + 1);
|
||||
}
|
||||
});
|
||||
it('result contains the correct public keys', () =>
|
||||
Promise.all(result.preKeys.map(validateResultKey)));
|
||||
it('returns a signed prekey', () => {
|
||||
assert.strictEqual(result.signedPreKey.keyId, 1);
|
||||
assert.instanceOf(result.signedPreKey.signature, ArrayBuffer);
|
||||
return validateResultSignedKey(result.signedPreKey);
|
||||
});
|
||||
});
|
||||
describe('the second time', () => {
|
||||
let result;
|
||||
before(() => {
|
||||
const accountManager = new textsecure.AccountManager('');
|
||||
return accountManager.generateKeys(count).then(res => {
|
||||
result = res;
|
||||
});
|
||||
});
|
||||
for (let i = 1; i <= 2 * count; i += 1) {
|
||||
itStoresPreKey(i);
|
||||
}
|
||||
itStoresSignedPreKey(1);
|
||||
itStoresSignedPreKey(2);
|
||||
it(`result contains ${count} preKeys`, () => {
|
||||
assert.isArray(result.preKeys);
|
||||
assert.lengthOf(result.preKeys, count);
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
assert.isObject(result.preKeys[i]);
|
||||
}
|
||||
});
|
||||
it('result contains the correct keyIds', () => {
|
||||
for (let i = 1; i <= count; i += 1) {
|
||||
assert.strictEqual(result.preKeys[i - 1].keyId, i + count);
|
||||
}
|
||||
});
|
||||
it('result contains the correct public keys', () =>
|
||||
Promise.all(result.preKeys.map(validateResultKey)));
|
||||
it('returns a signed prekey', () => {
|
||||
assert.strictEqual(result.signedPreKey.keyId, 2);
|
||||
assert.instanceOf(result.signedPreKey.signature, ArrayBuffer);
|
||||
return validateResultSignedKey(result.signedPreKey);
|
||||
});
|
||||
});
|
||||
describe('the third time', () => {
|
||||
let result;
|
||||
before(() => {
|
||||
const accountManager = new textsecure.AccountManager('');
|
||||
return accountManager.generateKeys(count).then(res => {
|
||||
result = res;
|
||||
});
|
||||
});
|
||||
for (let i = 1; i <= 3 * count; i += 1) {
|
||||
itStoresPreKey(i);
|
||||
}
|
||||
itStoresSignedPreKey(2);
|
||||
itStoresSignedPreKey(3);
|
||||
it(`result contains ${count} preKeys`, () => {
|
||||
assert.isArray(result.preKeys);
|
||||
assert.lengthOf(result.preKeys, count);
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
assert.isObject(result.preKeys[i]);
|
||||
}
|
||||
});
|
||||
it('result contains the correct keyIds', () => {
|
||||
for (let i = 1; i <= count; i += 1) {
|
||||
assert.strictEqual(result.preKeys[i - 1].keyId, i + 2 * count);
|
||||
}
|
||||
});
|
||||
it('result contains the correct public keys', () =>
|
||||
Promise.all(result.preKeys.map(validateResultKey)));
|
||||
it('result contains a signed prekey', () => {
|
||||
assert.strictEqual(result.signedPreKey.keyId, 3);
|
||||
assert.instanceOf(result.signedPreKey.signature, ArrayBuffer);
|
||||
return validateResultSignedKey(result.signedPreKey);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,29 +0,0 @@
|
|||
describe('Helpers', () => {
|
||||
describe('ArrayBuffer->String conversion', () => {
|
||||
it('works', () => {
|
||||
const b = new ArrayBuffer(3);
|
||||
const a = new Uint8Array(b);
|
||||
a[0] = 0;
|
||||
a[1] = 255;
|
||||
a[2] = 128;
|
||||
assert.equal(getString(b), '\x00\xff\x80');
|
||||
});
|
||||
});
|
||||
|
||||
describe('stringToArrayBuffer', () => {
|
||||
it('returns ArrayBuffer when passed string', () => {
|
||||
const anArrayBuffer = new ArrayBuffer(1);
|
||||
const typedArray = new Uint8Array(anArrayBuffer);
|
||||
typedArray[0] = 'a'.charCodeAt(0);
|
||||
assertEqualArrayBuffers(stringToArrayBuffer('a'), anArrayBuffer);
|
||||
});
|
||||
it('throws an error when passed a non string', () => {
|
||||
const notStringable = [{}, undefined, null, new ArrayBuffer()];
|
||||
notStringable.forEach(notString => {
|
||||
assert.throw(() => {
|
||||
stringToArrayBuffer(notString);
|
||||
}, Error);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,212 +0,0 @@
|
|||
function SignalProtocolStore() {
|
||||
this.store = {};
|
||||
}
|
||||
|
||||
SignalProtocolStore.prototype = {
|
||||
Direction: { SENDING: 1, RECEIVING: 2 },
|
||||
getIdentityKeyPair() {
|
||||
return Promise.resolve(this.get('identityKey'));
|
||||
},
|
||||
getLocalRegistrationId() {
|
||||
return Promise.resolve(this.get('registrationId'));
|
||||
},
|
||||
put(key, value) {
|
||||
if (
|
||||
key === undefined ||
|
||||
value === undefined ||
|
||||
key === null ||
|
||||
value === null
|
||||
) {
|
||||
throw new Error('Tried to store undefined/null');
|
||||
}
|
||||
this.store[key] = value;
|
||||
},
|
||||
get(key, defaultValue) {
|
||||
if (key === null || key === undefined) {
|
||||
throw new Error('Tried to get value for undefined/null key');
|
||||
}
|
||||
if (key in this.store) {
|
||||
return this.store[key];
|
||||
}
|
||||
return defaultValue;
|
||||
},
|
||||
remove(key) {
|
||||
if (key === null || key === undefined) {
|
||||
throw new Error('Tried to remove value for undefined/null key');
|
||||
}
|
||||
delete this.store[key];
|
||||
},
|
||||
|
||||
isTrustedIdentity(identifier, identityKey) {
|
||||
if (identifier === null || identifier === undefined) {
|
||||
throw new Error('tried to check identity key for undefined/null key');
|
||||
}
|
||||
if (!(identityKey instanceof ArrayBuffer)) {
|
||||
throw new Error('Expected identityKey to be an ArrayBuffer');
|
||||
}
|
||||
const trusted = this.get(`identityKey${identifier}`);
|
||||
if (trusted === undefined) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
return Promise.resolve(identityKey === trusted);
|
||||
},
|
||||
loadIdentityKey(identifier) {
|
||||
if (identifier === null || identifier === undefined) {
|
||||
throw new Error('Tried to get identity key for undefined/null key');
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
resolve(this.get(`identityKey${identifier}`));
|
||||
});
|
||||
},
|
||||
saveIdentity(identifier, identityKey) {
|
||||
if (identifier === null || identifier === undefined) {
|
||||
throw new Error('Tried to put identity key for undefined/null key');
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
const existing = this.get(`identityKey${identifier}`);
|
||||
this.put(`identityKey${identifier}`, identityKey);
|
||||
if (existing && existing !== identityKey) {
|
||||
resolve(true);
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/* Returns a prekeypair object or undefined */
|
||||
loadPreKey(keyId) {
|
||||
return new Promise(resolve => {
|
||||
const res = this.get(`25519KeypreKey${keyId}`);
|
||||
resolve(res);
|
||||
});
|
||||
},
|
||||
storePreKey(keyId, keyPair, contactPubKey = null) {
|
||||
if (contactPubKey) {
|
||||
const data = {
|
||||
id: keyId,
|
||||
publicKey: keyPair.pubKey,
|
||||
privateKey: keyPair.privKey,
|
||||
recipient: contactPubKey,
|
||||
};
|
||||
return new Promise(resolve => {
|
||||
resolve(this.put(`25519KeypreKey${contactPubKey}`, data));
|
||||
});
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
resolve(this.put(`25519KeypreKey${keyId}`, keyPair));
|
||||
});
|
||||
},
|
||||
removePreKey(keyId) {
|
||||
return new Promise(resolve => {
|
||||
resolve(this.remove(`25519KeypreKey${keyId}`));
|
||||
});
|
||||
},
|
||||
|
||||
/* Returns a signed keypair object or undefined */
|
||||
loadSignedPreKey(keyId) {
|
||||
return new Promise(resolve => {
|
||||
const res = this.get(`25519KeysignedKey${keyId}`);
|
||||
resolve(res);
|
||||
});
|
||||
},
|
||||
loadSignedPreKeys() {
|
||||
return new Promise(resolve => {
|
||||
const res = [];
|
||||
const keys = Object.keys(this.store);
|
||||
for (let i = 0, max = keys.length; i < max; i += 1) {
|
||||
const key = keys[i];
|
||||
if (key.startsWith('25519KeysignedKey')) {
|
||||
res.push(this.store[key]);
|
||||
}
|
||||
}
|
||||
resolve(res);
|
||||
});
|
||||
},
|
||||
storeSignedPreKey(keyId, keyPair) {
|
||||
return new Promise(resolve => {
|
||||
resolve(this.put(`25519KeysignedKey${keyId}`, keyPair));
|
||||
});
|
||||
},
|
||||
removeSignedPreKey(keyId) {
|
||||
return new Promise(resolve => {
|
||||
resolve(this.remove(`25519KeysignedKey${keyId}`));
|
||||
});
|
||||
},
|
||||
|
||||
loadSession(identifier) {
|
||||
return new Promise(resolve => {
|
||||
resolve(this.get(`session${identifier}`));
|
||||
});
|
||||
},
|
||||
storeSession(identifier, record) {
|
||||
return new Promise(resolve => {
|
||||
resolve(this.put(`session${identifier}`, record));
|
||||
});
|
||||
},
|
||||
removeAllSessions(identifier) {
|
||||
return new Promise(resolve => {
|
||||
const keys = Object.keys(this.store);
|
||||
for (let i = 0, max = keys.length; i < max; i += 1) {
|
||||
const key = keys[i];
|
||||
if (key.match(RegExp(`^session${identifier.replace('+', '\\+')}.+`))) {
|
||||
delete this.store[key];
|
||||
}
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
},
|
||||
getDeviceIds(identifier) {
|
||||
return new Promise(resolve => {
|
||||
const deviceIds = [];
|
||||
const keys = Object.keys(this.store);
|
||||
for (let i = 0, max = keys.length; i < max; i += 1) {
|
||||
const key = keys[i];
|
||||
if (key.match(RegExp(`^session${identifier.replace('+', '\\+')}.+`))) {
|
||||
deviceIds.push(parseInt(key.split('.')[1], 10));
|
||||
}
|
||||
}
|
||||
resolve(deviceIds);
|
||||
});
|
||||
},
|
||||
async loadPreKeyForContact(contactPubKey) {
|
||||
return new Promise(resolve => {
|
||||
const key = this.get(`25519KeypreKey${contactPubKey}`);
|
||||
if (!key) {
|
||||
resolve(undefined);
|
||||
}
|
||||
resolve({
|
||||
pubKey: key.publicKey,
|
||||
privKey: key.privateKey,
|
||||
keyId: key.id,
|
||||
recipient: key.recipient,
|
||||
});
|
||||
});
|
||||
},
|
||||
async storeContactSignedPreKey(pubKey, signedPreKey) {
|
||||
const key = {
|
||||
identityKeyString: pubKey,
|
||||
keyId: signedPreKey.keyId,
|
||||
publicKey: signedPreKey.publicKey,
|
||||
signature: signedPreKey.signature,
|
||||
created_at: Date.now(),
|
||||
confirmed: false,
|
||||
};
|
||||
this.put(`contactSignedPreKey${pubKey}`, key);
|
||||
},
|
||||
async loadContactSignedPreKey(pubKey) {
|
||||
const preKey = this.get(`contactSignedPreKey${pubKey}`);
|
||||
if (preKey) {
|
||||
return {
|
||||
id: preKey.id,
|
||||
identityKeyString: preKey.identityKeyString,
|
||||
publicKey: preKey.publicKey,
|
||||
signature: preKey.signature,
|
||||
created_at: preKey.created_at,
|
||||
keyId: preKey.keyId,
|
||||
confirmed: preKey.confirmed,
|
||||
};
|
||||
}
|
||||
window.log.warn('Failed to fetch contact signed prekey:', pubKey);
|
||||
return undefined;
|
||||
},
|
||||
};
|
|
@ -12,10 +12,8 @@
|
|||
<div id="tests">
|
||||
</div>
|
||||
|
||||
<script type="text/javascript" src="fake_web_api.js"></script>
|
||||
|
||||
<script type="text/javascript" src="test.js"></script>
|
||||
<script type="text/javascript" src="in_memory_signal_protocol_store.js"></script>
|
||||
|
||||
<script type="text/javascript" src="../components.js"></script>
|
||||
<script type="text/javascript" src="../libsignal-protocol.js"></script>
|
||||
|
@ -26,23 +24,15 @@
|
|||
<script type="text/javascript" src="../protocol_wrapper.js" data-cover></script>
|
||||
|
||||
<script type="text/javascript" src="../event_target.js" data-cover></script>
|
||||
<script type="text/javascript" src="../websocket-resources.js" data-cover></script>
|
||||
<script type="text/javascript" src="../helpers.js" data-cover></script>
|
||||
<script type="text/javascript" src="../stringview.js" data-cover></script>
|
||||
<script type="text/javascript" src="../api.js"></script>
|
||||
<script type="text/javascript" src="../sendmessage.js" data-cover></script>
|
||||
<script type="text/javascript" src="../account_manager.js" data-cover></script>
|
||||
<script type="text/javascript" src="../contacts_parser.js" data-cover></script>
|
||||
<script type="text/javascript" src="../task_with_timeout.js" data-cover></script>
|
||||
|
||||
<script type="text/javascript" src="errors_test.js"></script>
|
||||
<script type="text/javascript" src="helpers_test.js"></script>
|
||||
<script type="text/javascript" src="storage_test.js"></script>
|
||||
<script type="text/javascript" src="crypto_test.js"></script>
|
||||
<script type="text/javascript" src="protocol_wrapper_test.js"></script>
|
||||
<script type="text/javascript" src="contacts_parser_test.js"></script>
|
||||
<script type="text/javascript" src="generate_keys_test.js"></script>
|
||||
<script type="text/javascript" src="websocket-resources_test.js"></script>
|
||||
<script type="text/javascript" src="task_with_timeout_test.js"></script>
|
||||
<script type="text/javascript" src="account_manager_test.js"></script>
|
||||
|
||||
|
|
|
@ -1,104 +0,0 @@
|
|||
/* global libsignal, textsecure, SignalProtocolStore */
|
||||
|
||||
describe('MessageReceiver', () => {
|
||||
textsecure.storage.impl = new SignalProtocolStore();
|
||||
const { WebSocket } = window;
|
||||
const number = '+19999999999';
|
||||
const deviceId = 1;
|
||||
const signalingKey = libsignal.crypto.getRandomBytes(32 + 20);
|
||||
|
||||
before(() => {
|
||||
window.WebSocket = MockSocket;
|
||||
textsecure.storage.user.setNumberAndDeviceId(number, deviceId, 'name');
|
||||
textsecure.storage.put('password', 'password');
|
||||
textsecure.storage.put('signaling_key', signalingKey);
|
||||
});
|
||||
after(() => {
|
||||
window.WebSocket = WebSocket;
|
||||
});
|
||||
|
||||
describe('connecting', () => {
|
||||
const attrs = {
|
||||
type: textsecure.protobuf.Envelope.Type.CIPHERTEXT,
|
||||
source: number,
|
||||
sourceDevice: deviceId,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
const websocketmessage = new textsecure.protobuf.WebSocketMessage({
|
||||
type: textsecure.protobuf.WebSocketMessage.Type.REQUEST,
|
||||
request: { verb: 'PUT', path: '/messages' },
|
||||
});
|
||||
|
||||
before(done => {
|
||||
const signal = new textsecure.protobuf.Envelope(attrs).toArrayBuffer();
|
||||
|
||||
const aesKey = signalingKey.slice(0, 32);
|
||||
const macKey = signalingKey.slice(32, 32 + 20);
|
||||
|
||||
window.crypto.subtle
|
||||
.importKey('raw', aesKey, { name: 'AES-CBC' }, false, ['encrypt'])
|
||||
.then(key => {
|
||||
const iv = libsignal.crypto.getRandomBytes(16);
|
||||
window.crypto.subtle
|
||||
.encrypt({ name: 'AES-CBC', iv: new Uint8Array(iv) }, key, signal)
|
||||
.then(ciphertext => {
|
||||
window.crypto.subtle
|
||||
.importKey(
|
||||
'raw',
|
||||
macKey,
|
||||
{ name: 'HMAC', hash: { name: 'SHA-256' } },
|
||||
false,
|
||||
['sign']
|
||||
)
|
||||
.then(innerKey => {
|
||||
window.crypto.subtle
|
||||
.sign({ name: 'HMAC', hash: 'SHA-256' }, innerKey, signal)
|
||||
.then(mac => {
|
||||
const version = new Uint8Array([1]);
|
||||
const message = dcodeIO.ByteBuffer.concat([
|
||||
version,
|
||||
iv,
|
||||
ciphertext,
|
||||
mac,
|
||||
]);
|
||||
websocketmessage.request.body = message.toArrayBuffer();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('connects', done => {
|
||||
const mockServer = new MockServer(
|
||||
`ws://localhost:8080/v1/websocket/?login=${encodeURIComponent(
|
||||
number
|
||||
)}.1&password=password`
|
||||
);
|
||||
|
||||
mockServer.on('connection', server => {
|
||||
server.send(new Blob([websocketmessage.toArrayBuffer()]));
|
||||
});
|
||||
|
||||
window.addEventListener('textsecure:message', ev => {
|
||||
const signal = ev.proto;
|
||||
const keys = Object.keys(attrs);
|
||||
|
||||
for (let i = 0, max = keys.length; i < max; i += 1) {
|
||||
const key = keys[i];
|
||||
assert.strictEqual(attrs[key], signal[key]);
|
||||
}
|
||||
assert.strictEqual(signal.message.body, 'hello');
|
||||
mockServer.close();
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
window.messageReceiver = new textsecure.MessageReceiver(
|
||||
'signalingKey'
|
||||
// 'ws://localhost:8080',
|
||||
// window,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,35 +0,0 @@
|
|||
/* global libsignal, textsecure */
|
||||
|
||||
describe('Protocol Wrapper', function thisNeeded() {
|
||||
const store = textsecure.storage.protocol;
|
||||
const identifier = '+5558675309';
|
||||
|
||||
this.timeout(5000);
|
||||
|
||||
before(done => {
|
||||
localStorage.clear();
|
||||
libsignal.KeyHelper.generateIdentityKeyPair()
|
||||
.then(key => textsecure.storage.protocol.saveIdentity(identifier, key))
|
||||
.then(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('processPreKey', () => {
|
||||
it('rejects if the identity key changes', () => {
|
||||
const address = new libsignal.SignalProtocolAddress(identifier, 1);
|
||||
const builder = new libsignal.SessionBuilder(store, address);
|
||||
return builder
|
||||
.processPreKey({
|
||||
identityKey: textsecure.crypto.getRandomBytes(33),
|
||||
encodedNumber: address.toString(),
|
||||
})
|
||||
.then(() => {
|
||||
throw new Error('Allowed to overwrite identity key');
|
||||
})
|
||||
.catch(e => {
|
||||
assert.strictEqual(e.message, 'Identity key changed');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,134 +0,0 @@
|
|||
/* global libsignal, textsecure */
|
||||
|
||||
describe('SignalProtocolStore', () => {
|
||||
before(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
const store = textsecure.storage.protocol;
|
||||
const identifier = '+5558675309';
|
||||
const identityKey = {
|
||||
pubKey: libsignal.crypto.getRandomBytes(33),
|
||||
privKey: libsignal.crypto.getRandomBytes(32),
|
||||
};
|
||||
const testKey = {
|
||||
pubKey: libsignal.crypto.getRandomBytes(33),
|
||||
privKey: libsignal.crypto.getRandomBytes(32),
|
||||
};
|
||||
it('retrieves my registration id', async () => {
|
||||
store.put('registrationId', 1337);
|
||||
|
||||
const reg = await store.getLocalRegistrationId();
|
||||
assert.strictEqual(reg, 1337);
|
||||
});
|
||||
it('retrieves my identity key', async () => {
|
||||
store.put('identityKey', identityKey);
|
||||
const key = await store.getIdentityKeyPair();
|
||||
assertEqualArrayBuffers(key.pubKey, identityKey.pubKey);
|
||||
assertEqualArrayBuffers(key.privKey, identityKey.privKey);
|
||||
});
|
||||
it('stores identity keys', async () => {
|
||||
await store.saveIdentity(identifier, testKey.pubKey);
|
||||
const key = await store.loadIdentityKey(identifier);
|
||||
assertEqualArrayBuffers(key, testKey.pubKey);
|
||||
});
|
||||
it('returns whether a key is trusted', async () => {
|
||||
const newIdentity = libsignal.crypto.getRandomBytes(33);
|
||||
await store.saveIdentity(identifier, testKey.pubKey);
|
||||
|
||||
const trusted = await store.isTrustedIdentity(identifier, newIdentity);
|
||||
if (trusted) {
|
||||
throw new Error('Allowed to overwrite identity key');
|
||||
}
|
||||
});
|
||||
it('returns whether a key is untrusted', async () => {
|
||||
await store.saveIdentity(identifier, testKey.pubKey);
|
||||
const trusted = await store.isTrustedIdentity(identifier, testKey.pubKey);
|
||||
|
||||
if (!trusted) {
|
||||
throw new Error('Allowed to overwrite identity key');
|
||||
}
|
||||
});
|
||||
it('stores prekeys', async () => {
|
||||
await store.storePreKey(1, testKey);
|
||||
|
||||
const key = await store.loadPreKey(1);
|
||||
assertEqualArrayBuffers(key.pubKey, testKey.pubKey);
|
||||
assertEqualArrayBuffers(key.privKey, testKey.privKey);
|
||||
});
|
||||
it('deletes prekeys', async () => {
|
||||
await store.storePreKey(2, testKey);
|
||||
await store.removePreKey(2, testKey);
|
||||
|
||||
const key = await store.loadPreKey(2);
|
||||
assert.isUndefined(key);
|
||||
});
|
||||
it('stores signed prekeys', async () => {
|
||||
await store.storeSignedPreKey(3, testKey);
|
||||
|
||||
const key = await store.loadSignedPreKey(3);
|
||||
assertEqualArrayBuffers(key.pubKey, testKey.pubKey);
|
||||
assertEqualArrayBuffers(key.privKey, testKey.privKey);
|
||||
});
|
||||
it('deletes signed prekeys', async () => {
|
||||
await store.storeSignedPreKey(4, testKey);
|
||||
await store.removeSignedPreKey(4, testKey);
|
||||
|
||||
const key = await store.loadSignedPreKey(4);
|
||||
assert.isUndefined(key);
|
||||
});
|
||||
it('stores sessions', async () => {
|
||||
const testRecord = 'an opaque string';
|
||||
const devices = [1, 2, 3].map(deviceId => [identifier, deviceId].join('.'));
|
||||
|
||||
await Promise.all(
|
||||
devices.map(async encodedNumber => {
|
||||
await store.storeSession(encodedNumber, testRecord + encodedNumber);
|
||||
})
|
||||
);
|
||||
|
||||
const records = await Promise.all(
|
||||
devices.map(store.loadSession.bind(store))
|
||||
);
|
||||
|
||||
for (let i = 0, max = records.length; i < max; i += 1) {
|
||||
assert.strictEqual(records[i], testRecord + devices[i]);
|
||||
}
|
||||
});
|
||||
it('removes all sessions for a number', async () => {
|
||||
const testRecord = 'an opaque string';
|
||||
const devices = [1, 2, 3].map(deviceId => [identifier, deviceId].join('.'));
|
||||
|
||||
await Promise.all(
|
||||
devices.map(async encodedNumber => {
|
||||
await store.storeSession(encodedNumber, testRecord + encodedNumber);
|
||||
})
|
||||
);
|
||||
|
||||
await store.removeAllSessions(identifier);
|
||||
|
||||
const records = await Promise.all(
|
||||
devices.map(store.loadSession.bind(store))
|
||||
);
|
||||
|
||||
for (let i = 0, max = records.length; i < max; i += 1) {
|
||||
assert.isUndefined(records[i]);
|
||||
}
|
||||
});
|
||||
it('returns deviceIds for a number', async () => {
|
||||
const testRecord = 'an opaque string';
|
||||
const devices = [1, 2, 3].map(deviceId => [identifier, deviceId].join('.'));
|
||||
|
||||
await Promise.all(
|
||||
devices.map(async encodedNumber => {
|
||||
await store.storeSession(encodedNumber, testRecord + encodedNumber);
|
||||
})
|
||||
);
|
||||
|
||||
const deviceIds = await store.getDeviceIds(identifier);
|
||||
assert.sameMembers(deviceIds, [1, 2, 3]);
|
||||
});
|
||||
it('returns empty array for a number with no device ids', async () => {
|
||||
const deviceIds = await store.getDeviceIds('foo');
|
||||
assert.sameMembers(deviceIds, []);
|
||||
});
|
||||
});
|
|
@ -1,214 +0,0 @@
|
|||
/* global textsecure, WebSocketResource */
|
||||
|
||||
describe('WebSocket-Resource', () => {
|
||||
describe('requests and responses', () => {
|
||||
it('receives requests and sends responses', done => {
|
||||
// mock socket
|
||||
const requestId = '1';
|
||||
const socket = {
|
||||
send(data) {
|
||||
const message = textsecure.protobuf.WebSocketMessage.decode(data);
|
||||
assert.strictEqual(
|
||||
message.type,
|
||||
textsecure.protobuf.WebSocketMessage.Type.RESPONSE
|
||||
);
|
||||
assert.strictEqual(message.response.message, 'OK');
|
||||
assert.strictEqual(message.response.status, 200);
|
||||
assert.strictEqual(message.response.id.toString(), requestId);
|
||||
done();
|
||||
},
|
||||
addEventListener() {},
|
||||
};
|
||||
|
||||
// actual test
|
||||
this.resource = new WebSocketResource(socket, {
|
||||
handleRequest(request) {
|
||||
assert.strictEqual(request.verb, 'PUT');
|
||||
assert.strictEqual(request.path, '/some/path');
|
||||
assertEqualArrayBuffers(
|
||||
request.body.toArrayBuffer(),
|
||||
new Uint8Array([1, 2, 3]).buffer
|
||||
);
|
||||
request.respond(200, 'OK');
|
||||
},
|
||||
});
|
||||
|
||||
// mock socket request
|
||||
socket.onmessage({
|
||||
data: new Blob([
|
||||
new textsecure.protobuf.WebSocketMessage({
|
||||
type: textsecure.protobuf.WebSocketMessage.Type.REQUEST,
|
||||
request: {
|
||||
id: requestId,
|
||||
verb: 'PUT',
|
||||
path: '/some/path',
|
||||
body: new Uint8Array([1, 2, 3]).buffer,
|
||||
},
|
||||
})
|
||||
.encode()
|
||||
.toArrayBuffer(),
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it('sends requests and receives responses', done => {
|
||||
// mock socket and request handler
|
||||
let requestId;
|
||||
const socket = {
|
||||
send(data) {
|
||||
const message = textsecure.protobuf.WebSocketMessage.decode(data);
|
||||
assert.strictEqual(
|
||||
message.type,
|
||||
textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
||||
);
|
||||
assert.strictEqual(message.request.verb, 'PUT');
|
||||
assert.strictEqual(message.request.path, '/some/path');
|
||||
assertEqualArrayBuffers(
|
||||
message.request.body.toArrayBuffer(),
|
||||
new Uint8Array([1, 2, 3]).buffer
|
||||
);
|
||||
requestId = message.request.id;
|
||||
},
|
||||
addEventListener() {},
|
||||
};
|
||||
|
||||
// actual test
|
||||
const resource = new WebSocketResource(socket);
|
||||
resource.sendRequest({
|
||||
verb: 'PUT',
|
||||
path: '/some/path',
|
||||
body: new Uint8Array([1, 2, 3]).buffer,
|
||||
error: done,
|
||||
success(message, status) {
|
||||
assert.strictEqual(message, 'OK');
|
||||
assert.strictEqual(status, 200);
|
||||
done();
|
||||
},
|
||||
});
|
||||
|
||||
// mock socket response
|
||||
socket.onmessage({
|
||||
data: new Blob([
|
||||
new textsecure.protobuf.WebSocketMessage({
|
||||
type: textsecure.protobuf.WebSocketMessage.Type.RESPONSE,
|
||||
response: { id: requestId, message: 'OK', status: 200 },
|
||||
})
|
||||
.encode()
|
||||
.toArrayBuffer(),
|
||||
]),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('close', () => {
|
||||
before(() => {
|
||||
window.WebSocket = MockSocket;
|
||||
});
|
||||
after(() => {
|
||||
window.WebSocket = WebSocket;
|
||||
});
|
||||
it('closes the connection', done => {
|
||||
const mockServer = new MockServer('ws://localhost:8081');
|
||||
mockServer.on('connection', server => {
|
||||
server.on('close', done);
|
||||
});
|
||||
const resource = new WebSocketResource(
|
||||
new WebSocket('ws://localhost:8081')
|
||||
);
|
||||
resource.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe.skip('with a keepalive config', function thisNeeded() {
|
||||
before(() => {
|
||||
window.WebSocket = MockSocket;
|
||||
});
|
||||
after(() => {
|
||||
window.WebSocket = WebSocket;
|
||||
});
|
||||
this.timeout(60000);
|
||||
it('sends keepalives once a minute', done => {
|
||||
const mockServer = new MockServer('ws://localhost:8081');
|
||||
mockServer.on('connection', server => {
|
||||
server.on('message', data => {
|
||||
const message = textsecure.protobuf.WebSocketMessage.decode(data);
|
||||
assert.strictEqual(
|
||||
message.type,
|
||||
textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
||||
);
|
||||
assert.strictEqual(message.request.verb, 'GET');
|
||||
assert.strictEqual(message.request.path, '/v1/keepalive');
|
||||
server.close();
|
||||
done();
|
||||
});
|
||||
});
|
||||
this.resource = new WebSocketResource(
|
||||
new WebSocket('ws://loc1alhost:8081'),
|
||||
{
|
||||
keepalive: { path: '/v1/keepalive' },
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('uses / as a default path', done => {
|
||||
const mockServer = new MockServer('ws://localhost:8081');
|
||||
mockServer.on('connection', server => {
|
||||
server.on('message', data => {
|
||||
const message = textsecure.protobuf.WebSocketMessage.decode(data);
|
||||
assert.strictEqual(
|
||||
message.type,
|
||||
textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
||||
);
|
||||
assert.strictEqual(message.request.verb, 'GET');
|
||||
assert.strictEqual(message.request.path, '/');
|
||||
server.close();
|
||||
done();
|
||||
});
|
||||
});
|
||||
this.resource = new WebSocketResource(
|
||||
new WebSocket('ws://localhost:8081'),
|
||||
{
|
||||
keepalive: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('optionally disconnects if no response', function thisNeeded1(done) {
|
||||
this.timeout(65000);
|
||||
const mockServer = new MockServer('ws://localhost:8081');
|
||||
const socket = new WebSocket('ws://localhost:8081');
|
||||
mockServer.on('connection', server => {
|
||||
server.on('close', done);
|
||||
});
|
||||
this.resource = new WebSocketResource(socket, { keepalive: true });
|
||||
});
|
||||
|
||||
it('allows resetting the keepalive timer', function thisNeeded2(done) {
|
||||
this.timeout(65000);
|
||||
const mockServer = new MockServer('ws://localhost:8081');
|
||||
const socket = new WebSocket('ws://localhost:8081');
|
||||
const startTime = Date.now();
|
||||
mockServer.on('connection', server => {
|
||||
server.on('message', data => {
|
||||
const message = textsecure.protobuf.WebSocketMessage.decode(data);
|
||||
assert.strictEqual(
|
||||
message.type,
|
||||
textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
||||
);
|
||||
assert.strictEqual(message.request.verb, 'GET');
|
||||
assert.strictEqual(message.request.path, '/');
|
||||
assert(
|
||||
Date.now() > startTime + 60000,
|
||||
'keepalive time should be longer than a minute'
|
||||
);
|
||||
server.close();
|
||||
done();
|
||||
});
|
||||
});
|
||||
const resource = new WebSocketResource(socket, { keepalive: true });
|
||||
setTimeout(() => {
|
||||
resource.resetKeepAliveTimer();
|
||||
}, 5000);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,66 +0,0 @@
|
|||
/* global TextSecureWebSocket */
|
||||
|
||||
describe('TextSecureWebSocket', () => {
|
||||
const RealWebSocket = window.WebSocket;
|
||||
before(() => {
|
||||
window.WebSocket = MockSocket;
|
||||
});
|
||||
after(() => {
|
||||
window.WebSocket = RealWebSocket;
|
||||
});
|
||||
it('connects and disconnects', done => {
|
||||
const mockServer = new MockServer('ws://localhost:8080');
|
||||
mockServer.on('connection', server => {
|
||||
socket.close();
|
||||
server.close();
|
||||
done();
|
||||
});
|
||||
const socket = new TextSecureWebSocket('ws://localhost:8080');
|
||||
});
|
||||
|
||||
it('sends and receives', done => {
|
||||
const mockServer = new MockServer('ws://localhost:8080');
|
||||
mockServer.on('connection', server => {
|
||||
server.on('message', () => {
|
||||
server.send('ack');
|
||||
server.close();
|
||||
});
|
||||
});
|
||||
const socket = new TextSecureWebSocket('ws://localhost:8080');
|
||||
socket.onmessage = response => {
|
||||
assert.strictEqual(response.data, 'ack');
|
||||
socket.close();
|
||||
done();
|
||||
};
|
||||
socket.send('syn');
|
||||
});
|
||||
|
||||
it('exposes the socket status', done => {
|
||||
const mockServer = new MockServer('ws://localhost:8082');
|
||||
mockServer.on('connection', server => {
|
||||
assert.strictEqual(socket.getStatus(), WebSocket.OPEN);
|
||||
server.close();
|
||||
socket.close();
|
||||
});
|
||||
const socket = new TextSecureWebSocket('ws://localhost:8082');
|
||||
socket.onclose = () => {
|
||||
assert.strictEqual(socket.getStatus(), WebSocket.CLOSING);
|
||||
done();
|
||||
};
|
||||
});
|
||||
|
||||
it('reconnects', function thisNeeded(done) {
|
||||
this.timeout(60000);
|
||||
const mockServer = new MockServer('ws://localhost:8082');
|
||||
const socket = new TextSecureWebSocket('ws://localhost:8082');
|
||||
socket.onclose = () => {
|
||||
const secondServer = new MockServer('ws://localhost:8082');
|
||||
secondServer.on('connection', server => {
|
||||
socket.close();
|
||||
server.close();
|
||||
done();
|
||||
});
|
||||
};
|
||||
mockServer.close();
|
||||
});
|
||||
});
|
|
@ -1,243 +0,0 @@
|
|||
/* global window, dcodeIO, Event, textsecure, FileReader, WebSocketResource */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
/*
|
||||
* 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/signalapp/WebSocket-Resources
|
||||
*
|
||||
*/
|
||||
|
||||
const Request = function Request(options) {
|
||||
this.verb = options.verb || options.type;
|
||||
this.path = options.path || options.url;
|
||||
this.headers = options.headers;
|
||||
this.body = options.body || options.data;
|
||||
this.success = options.success;
|
||||
this.error = options.error;
|
||||
this.id = options.id;
|
||||
|
||||
if (this.id === undefined) {
|
||||
const bits = new Uint32Array(2);
|
||||
window.crypto.getRandomValues(bits);
|
||||
this.id = dcodeIO.Long.fromBits(bits[0], bits[1], true);
|
||||
}
|
||||
|
||||
if (this.body === undefined) {
|
||||
this.body = null;
|
||||
}
|
||||
};
|
||||
|
||||
const IncomingWebSocketRequest = function IncomingWebSocketRequest(options) {
|
||||
const request = new Request(options);
|
||||
const { socket } = options;
|
||||
|
||||
this.verb = request.verb;
|
||||
this.path = request.path;
|
||||
this.body = request.body;
|
||||
this.headers = request.headers;
|
||||
|
||||
this.respond = (status, message) => {
|
||||
socket.send(
|
||||
new textsecure.protobuf.WebSocketMessage({
|
||||
type: textsecure.protobuf.WebSocketMessage.Type.RESPONSE,
|
||||
response: { id: request.id, message, status },
|
||||
})
|
||||
.encode()
|
||||
.toArrayBuffer()
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const outgoing = {};
|
||||
const OutgoingWebSocketRequest = function OutgoingWebSocketRequest(
|
||||
options,
|
||||
socket
|
||||
) {
|
||||
const 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,
|
||||
headers: request.headers,
|
||||
id: request.id,
|
||||
},
|
||||
})
|
||||
.encode()
|
||||
.toArrayBuffer()
|
||||
);
|
||||
};
|
||||
|
||||
window.WebSocketResource = function WebSocketResource(socket, opts = {}) {
|
||||
let { handleRequest } = opts;
|
||||
if (typeof handleRequest !== 'function') {
|
||||
handleRequest = request => request.respond(404, 'Not found');
|
||||
}
|
||||
this.sendRequest = options => new OutgoingWebSocketRequest(options, socket);
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
socket.onmessage = socketMessage => {
|
||||
const blob = socketMessage.data;
|
||||
const handleArrayBuffer = buffer => {
|
||||
const message = textsecure.protobuf.WebSocketMessage.decode(buffer);
|
||||
if (
|
||||
message.type === textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
||||
) {
|
||||
handleRequest(
|
||||
new IncomingWebSocketRequest({
|
||||
verb: message.request.verb,
|
||||
path: message.request.path,
|
||||
body: message.request.body,
|
||||
headers: message.request.headers,
|
||||
id: message.request.id,
|
||||
socket,
|
||||
})
|
||||
);
|
||||
} else if (
|
||||
message.type === textsecure.protobuf.WebSocketMessage.Type.RESPONSE
|
||||
) {
|
||||
const { response } = message;
|
||||
const request = outgoing[response.id];
|
||||
if (request) {
|
||||
request.response = response;
|
||||
let 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 new Error(
|
||||
`Received response for unknown request ${message.response.id}`
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (blob instanceof ArrayBuffer) {
|
||||
handleArrayBuffer(blob);
|
||||
} else {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => handleArrayBuffer(reader.result);
|
||||
reader.readAsArrayBuffer(blob);
|
||||
}
|
||||
};
|
||||
|
||||
if (opts.keepalive) {
|
||||
this.keepalive = new KeepAlive(this, {
|
||||
path: opts.keepalive.path,
|
||||
disconnect: opts.keepalive.disconnect,
|
||||
});
|
||||
const resetKeepAliveTimer = this.keepalive.reset.bind(this.keepalive);
|
||||
socket.addEventListener('open', resetKeepAliveTimer);
|
||||
socket.addEventListener('message', resetKeepAliveTimer);
|
||||
socket.addEventListener(
|
||||
'close',
|
||||
this.keepalive.stop.bind(this.keepalive)
|
||||
);
|
||||
}
|
||||
|
||||
socket.addEventListener('close', () => {
|
||||
this.closed = true;
|
||||
});
|
||||
|
||||
this.close = (code = 3000, reason) => {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.log.info('WebSocketResource.close()');
|
||||
if (this.keepalive) {
|
||||
this.keepalive.stop();
|
||||
}
|
||||
|
||||
socket.close(code, reason);
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
socket.onmessage = null;
|
||||
|
||||
// On linux the socket can wait a long time to emit its close event if we've
|
||||
// lost the internet connection. On the order of minutes. This speeds that
|
||||
// process up.
|
||||
setTimeout(() => {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
this.closed = true;
|
||||
|
||||
window.log.warn('Dispatching our own socket close event');
|
||||
const ev = new Event('close');
|
||||
ev.code = code;
|
||||
ev.reason = reason;
|
||||
this.dispatchEvent(ev);
|
||||
}, 1000);
|
||||
};
|
||||
};
|
||||
window.WebSocketResource.prototype = new textsecure.EventTarget();
|
||||
|
||||
function KeepAlive(websocketResource, opts = {}) {
|
||||
if (websocketResource instanceof WebSocketResource) {
|
||||
this.path = opts.path;
|
||||
if (this.path === undefined) {
|
||||
this.path = '/';
|
||||
}
|
||||
this.disconnect = opts.disconnect;
|
||||
if (this.disconnect === undefined) {
|
||||
this.disconnect = true;
|
||||
}
|
||||
this.wsr = websocketResource;
|
||||
} else {
|
||||
throw new TypeError('KeepAlive expected a WebSocketResource');
|
||||
}
|
||||
}
|
||||
|
||||
KeepAlive.prototype = {
|
||||
constructor: KeepAlive,
|
||||
stop() {
|
||||
clearTimeout(this.keepAliveTimer);
|
||||
clearTimeout(this.disconnectTimer);
|
||||
},
|
||||
reset() {
|
||||
clearTimeout(this.keepAliveTimer);
|
||||
clearTimeout(this.disconnectTimer);
|
||||
this.keepAliveTimer = setTimeout(() => {
|
||||
if (this.disconnect) {
|
||||
// automatically disconnect if server doesn't ack
|
||||
this.disconnectTimer = setTimeout(() => {
|
||||
clearTimeout(this.keepAliveTimer);
|
||||
this.wsr.close(3001, 'No response to keepalive request');
|
||||
}, 1000);
|
||||
} else {
|
||||
this.reset();
|
||||
}
|
||||
window.log.info('Sending a keepalive message');
|
||||
this.wsr.sendRequest({
|
||||
verb: 'GET',
|
||||
path: this.path,
|
||||
success: this.reset.bind(this),
|
||||
});
|
||||
}, 55000);
|
||||
},
|
||||
};
|
||||
})();
|
|
@ -1,62 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
import time
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
|
||||
# HTTPRequestHandler class
|
||||
class testHTTPServer_RequestHandler(BaseHTTPRequestHandler):
|
||||
def do_POST(self):
|
||||
print('got POST request to ' + self.path)
|
||||
# add some latency
|
||||
time.sleep(2)
|
||||
# Send response status code
|
||||
self.send_response(201)
|
||||
|
||||
# Send headers
|
||||
#self.send_header()
|
||||
self.end_headers()
|
||||
|
||||
#message = self.rfile.read(int(self.headers.get('Content-Length'))).decode('UTF-8')
|
||||
length = self.headers.get('Content-Length')
|
||||
for (k,v) in self.headers.items():
|
||||
print(k + ':' + v)
|
||||
if length:
|
||||
print ('length: ' + self.headers.get('Content-Length'))
|
||||
message = self.rfile.read(int(length))
|
||||
|
||||
array = []
|
||||
for k in message:
|
||||
array += [k]
|
||||
print(array)
|
||||
# Send message back to client
|
||||
#message = "ok"
|
||||
# Write content as utf-8 data
|
||||
#self.wfile.write(bytes(message, "utf8"))
|
||||
|
||||
# GET
|
||||
def do_GET(self):
|
||||
# Send response status code
|
||||
time.sleep(1)
|
||||
self.send_response(200)
|
||||
|
||||
# Send headers
|
||||
self.send_header('Content-type','text/html')
|
||||
self.end_headers()
|
||||
|
||||
# Send message back to client
|
||||
message = "Hello world!"
|
||||
# Write content as utf-8 data
|
||||
self.wfile.write(bytes(message, "utf8"))
|
||||
return
|
||||
|
||||
def run():
|
||||
print('starting server...')
|
||||
|
||||
# Server settings
|
||||
# Choose port 8080, for port 80, which is normally used for a http server, you need root access
|
||||
server_address = ('127.0.0.1', 80)
|
||||
httpd = HTTPServer(server_address, testHTTPServer_RequestHandler)
|
||||
print('running server...')
|
||||
httpd.serve_forever()
|
||||
|
||||
|
||||
run()
|
|
@ -1,6 +0,0 @@
|
|||
http server for mocking up sending message to the server and getting a response back.
|
||||
websocket server for mocking up opening a connection to receive messages from the server.
|
||||
|
||||
run either server with
|
||||
`sudo python3 <script>`
|
||||
(sudo is required for port 80) but both can't be run at the same time.
|
|
@ -1,53 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# WS server example
|
||||
import time
|
||||
import asyncio
|
||||
import websockets
|
||||
|
||||
async def hello(websocket, path):
|
||||
print(f"connection done {path}")
|
||||
|
||||
keep_alive_bytes = bytes([8, 1, 18, 28, 10, 3, 80, 85, 84, 18, 19, 47, 97, 112, 105, 47, 118, 49, 47, 113, 117, 101, 117, 101, 47, 101, 109, 112, 116, 121, 32, 99])
|
||||
# created by executing in js:
|
||||
# protomessage = new textsecure.protobuf.WebSocketMessage({type: textsecure.protobuf.WebSocketMessage.Type.REQUEST, request: {id:99, verb:'PUT', path:'/api/v1/queue/empty', body:null }})
|
||||
# new Uint8Array(protomessage.encode().toArrayBuffer())
|
||||
message = bytes(
|
||||
[
|
||||
# "hello world" - unencrypted
|
||||
#8,1,18,117,10,3,80,85,84,18,15,47,97,112,105,47,118,49,47,109,101,115,115,97,103,101,26,91,8,1,18,66,48,53,55,51,57,102,51,54,55,50,100,55,57,52,51,56,101,57,53,53,97,55,99,99,55,55,56,52,100,98,97,53,101,97,52,98,102,56,50,55,52,54,54,53,55,55,51,99,97,102,51,101,97,98,55,48,97,50,98,57,100,98,102,101,50,99,56,1,40,0,66,15,10,13,10,11,104,101,108,108,111,32,119,111,114,108,100,32,99
|
||||
# "test" - fall back encrypted
|
||||
8,1,18,140,1,10,3,80,85,84,18,15,47,97,112,105,47,118,49,47,109,101,115,115,97,103,101,26,113,8,6,18,66,48,53,51,102,48,101,57,56,54,53,97,100,101,54,97,100,57,48,48,97,54,99,101,51,98,98,54,101,102,97,99,102,102,102,97,98,99,50,56,49,101,53,97,50,102,100,102,54,101,97,49,51,57,98,51,48,51,50,49,55,57,57,97,97,50,99,56,1,40,181,202,171,141,229,44,66,32,147,127,63,203,38,142,133,120,28,115,7,150,230,26,166,28,182,199,199,182,11,101,80,48,252,232,108,164,8,236,98,50,32,150,1
|
||||
])
|
||||
# created by executing in js:
|
||||
# dataMessage = new textsecure.protobuf.DataMessage({body: "hello world", attachments:[], contact:[]})
|
||||
# content = new textsecure.protobuf.Content({ dataMessage })
|
||||
# contentBytes = content.encode().toArrayBuffer()
|
||||
# - skipped encryption -
|
||||
# messageEnvelope = new textsecure.protobuf.Envelope({ type:1, source:"0596395a7f0a6ca6379d49c5a584103a49274973cf57ab1b6301330cc33ea6f94c", sourceDevice:1, timestamp:0, content: contentBytes})
|
||||
# requestMessage = new textsecure.protobuf.WebSocketRequestMessage({id:99, verb:'PUT', path:'/api/v1/message', body: messageEnvelope.encode().toArrayBuffer()})
|
||||
# protomessage = new textsecure.protobuf.WebSocketMessage({type: textsecure.protobuf.WebSocketMessage.Type.REQUEST, request: requestMessage})
|
||||
# bytes = new Uint8Array(protomessage.encode().toArrayBuffer())
|
||||
# bytes.toString()
|
||||
signature = websocket.request_headers.get('signature')
|
||||
|
||||
if not signature:
|
||||
print("no signature provided")
|
||||
|
||||
counter = 0
|
||||
while(True):
|
||||
print("sending keepalive")
|
||||
await websocket.send(keep_alive_bytes)
|
||||
response = await websocket.recv()
|
||||
print(f"response: {response}")
|
||||
if counter % 5 == 0:
|
||||
await websocket.send(message)
|
||||
response = await websocket.recv()
|
||||
print(f"response: {response}")
|
||||
time.sleep(30)
|
||||
counter = counter + 1
|
||||
|
||||
start_server = websockets.serve(hello, 'localhost', 80)
|
||||
|
||||
asyncio.get_event_loop().run_until_complete(start_server)
|
||||
asyncio.get_event_loop().run_forever()
|
|
@ -2,7 +2,7 @@
|
|||
"name": "session-desktop",
|
||||
"productName": "Session",
|
||||
"description": "Private messaging from your desktop",
|
||||
"version": "1.4.5",
|
||||
"version": "1.4.6",
|
||||
"license": "GPL-3.0",
|
||||
"author": {
|
||||
"name": "Loki Project",
|
||||
|
|
|
@ -85,8 +85,7 @@ window.CONSTANTS = new (function() {
|
|||
this.DEFAULT_PUBLIC_CHAT_URL = appConfig.get('defaultPublicChatServer');
|
||||
this.MAX_LINKED_DEVICES = 1;
|
||||
this.MAX_CONNECTION_DURATION = 5000;
|
||||
// Limited due to the proof-of-work requirement
|
||||
this.MEDIUM_GROUP_SIZE_LIMIT = 20;
|
||||
this.CLOSED_GROUP_SIZE_LIMIT = 20;
|
||||
// Number of seconds to turn on notifications after reconnect/start of app
|
||||
this.NOTIFICATION_ENABLE_TIMEOUT_SECONDS = 10;
|
||||
this.SESSION_ID_LENGTH = 66;
|
||||
|
@ -340,10 +339,6 @@ if (config.proxyUrl) {
|
|||
|
||||
window.nodeSetImmediate = setImmediate;
|
||||
|
||||
const { initialize: initializeWebAPI } = require('./js/modules/web_api');
|
||||
|
||||
window.WebAPI = initializeWebAPI();
|
||||
|
||||
window.seedNodeList = JSON.parse(config.seedNodeList);
|
||||
|
||||
const { OnionAPI } = require('./ts/session/onions');
|
||||
|
@ -461,7 +456,6 @@ window.lokiFeatureFlags = {
|
|||
useFileOnionRequests: true,
|
||||
useFileOnionRequestsV2: true, // more compact encoding of files in response
|
||||
onionRequestHops: 3,
|
||||
debugMessageLogs: process.env.ENABLE_MESSAGE_LOGS,
|
||||
useMultiDevice: false,
|
||||
};
|
||||
|
||||
|
@ -497,7 +491,6 @@ if (config.environment.includes('test-integration')) {
|
|||
useOnionRequests: false,
|
||||
useFileOnionRequests: false,
|
||||
useOnionRequestsV2: false,
|
||||
debugMessageLogs: true,
|
||||
useMultiDevice: false,
|
||||
};
|
||||
/* eslint-disable global-require, import/no-extraneous-dependencies */
|
||||
|
|
|
@ -635,33 +635,6 @@
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
// Module: Safety Number Notification
|
||||
|
||||
.module-safety-number-notification {
|
||||
margin-top: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.module-safety-number-notification__icon {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: auto;
|
||||
margin-bottom: 7px;
|
||||
@include color-svg('../images/shield.svg', $color-gray-60);
|
||||
}
|
||||
|
||||
.module-safety-number-notification__text {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0.3px;
|
||||
color: $color-gray-60;
|
||||
}
|
||||
|
||||
.module-safety-number-notification__contact {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.module-verification-notification__button {
|
||||
margin-top: 5px;
|
||||
display: inline-block;
|
||||
|
@ -690,24 +663,6 @@
|
|||
font-weight: 300;
|
||||
}
|
||||
|
||||
.module-verification-notification__icon--mark-verified {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: auto;
|
||||
margin-bottom: 4px;
|
||||
@include color-svg('../images/verified-check.svg', $color-gray-60);
|
||||
}
|
||||
|
||||
.module-verification-notification__icon--mark-not-verified {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: auto;
|
||||
margin-bottom: 7px;
|
||||
@include color-svg('../images/shield.svg', $color-gray-60);
|
||||
}
|
||||
|
||||
// Module: Timer Notification
|
||||
|
||||
.module-timer-notification {
|
||||
|
@ -824,17 +779,6 @@
|
|||
font-size: 14px;
|
||||
}
|
||||
|
||||
.module-contact-list-item__text__verified-icon {
|
||||
@include color-svg('../images/verified-check.svg', $color-gray-60);
|
||||
display: inline-block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
vertical-align: text-bottom;
|
||||
|
||||
// Trying to account for the whitespace around the check mark
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
// Module: Conversation Header
|
||||
|
||||
.module-conversation-header {
|
||||
|
@ -904,14 +848,6 @@
|
|||
font-style: italic;
|
||||
}
|
||||
|
||||
.module-conversation-header__title__verified-icon {
|
||||
@include color-svg('../images/verified-check.svg', $color-gray-90);
|
||||
display: inline-block;
|
||||
width: 1.25em;
|
||||
height: 1.25em;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
.module-conversation-header__expiration {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
@ -1019,13 +955,6 @@
|
|||
text-align: right;
|
||||
}
|
||||
|
||||
.module-message-detail__contact__show-safety-number {
|
||||
@include button-reset;
|
||||
padding: 4px;
|
||||
color: $color-white;
|
||||
background-color: $color-light-35;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.module-message-detail__contact__send-anyway {
|
||||
@include button-reset;
|
||||
color: $color-white;
|
||||
|
|
|
@ -1356,70 +1356,3 @@ input {
|
|||
padding: $session-margin-sm $session-margin-lg;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* ************************************* */
|
||||
/* KEY VERIFICATION VIEW (SAFETY NUMBER) */
|
||||
/* ************************************* */
|
||||
|
||||
.key-verification {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-top: $session-margin-lg;
|
||||
text-align: center;
|
||||
@include themify($themes) {
|
||||
background: themed('inboxBackground');
|
||||
color: themed('textColor');
|
||||
}
|
||||
&__header {
|
||||
word-break: break-all;
|
||||
|
||||
h2 {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
small {
|
||||
margin-top: -25px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
&__key {
|
||||
font-family: $session-font-mono;
|
||||
margin: 30px 0;
|
||||
width: 250px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&__is-verified {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-size: $session-font-md;
|
||||
margin: 30px 0;
|
||||
|
||||
& > span {
|
||||
svg {
|
||||
min-width: 30px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
height: 50px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.session-button {
|
||||
margin: 20px 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.session-loader {
|
||||
margin-top: $session-margin-md;
|
||||
}
|
||||
}
|
||||
|
||||
/* ************************************* */
|
||||
/* ************************************* */
|
||||
/* ************************************* */
|
||||
|
|
|
@ -53,16 +53,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.contact-details {
|
||||
.number {
|
||||
color: $color-dark-30;
|
||||
}
|
||||
|
||||
.verified-icon {
|
||||
@include color-svg('../images/verified-check.svg', $grey);
|
||||
}
|
||||
}
|
||||
|
||||
.recipients-input {
|
||||
.recipients-container {
|
||||
background-color: white;
|
||||
|
@ -275,16 +265,6 @@
|
|||
color: $color-dark-30;
|
||||
}
|
||||
|
||||
// Module: Safety Number Notification
|
||||
|
||||
.module-safety-number-notification__icon {
|
||||
@include color-svg('../images/shield.svg', $color-dark-30);
|
||||
}
|
||||
|
||||
.module-safety-number-notification__text {
|
||||
color: $color-dark-30;
|
||||
}
|
||||
|
||||
.module-verification-notification__button {
|
||||
color: $color-loki-green;
|
||||
background-color: $color-gray-75;
|
||||
|
@ -296,14 +276,6 @@
|
|||
color: $color-dark-30;
|
||||
}
|
||||
|
||||
.module-verification-notification__icon--mark-verified {
|
||||
@include color-svg('../images/verified-check.svg', $color-dark-30);
|
||||
}
|
||||
|
||||
.module-verification-notification__icon--mark-not-verified {
|
||||
@include color-svg('../images/shield.svg', $color-dark-30);
|
||||
}
|
||||
|
||||
// Module: Timer Notification
|
||||
|
||||
.module-timer-notification {
|
||||
|
@ -324,10 +296,6 @@
|
|||
color: $color-dark-30;
|
||||
}
|
||||
|
||||
.module-contact-list-item__text__verified-icon {
|
||||
@include color-svg('../images/verified-check.svg', $color-dark-30);
|
||||
}
|
||||
|
||||
// Module: Message Detail
|
||||
|
||||
.module-message-detail__delete-button {
|
||||
|
@ -341,10 +309,6 @@
|
|||
color: $session-color-danger;
|
||||
}
|
||||
|
||||
.module-message-detail__contact__show-safety-number {
|
||||
color: $color-white;
|
||||
background-color: $color-light-35;
|
||||
}
|
||||
.module-message-detail__contact__send-anyway {
|
||||
color: $color-white;
|
||||
background-color: $session-color-danger;
|
||||
|
|
|
@ -13,8 +13,6 @@ module.exports = {
|
|||
dcodeIO: true,
|
||||
getString: true,
|
||||
hexToArrayBuffer: true,
|
||||
MockServer: true,
|
||||
MockSocket: true,
|
||||
PROTO_ROOT: true,
|
||||
stringToArrayBuffer: true,
|
||||
},
|
||||
|
|
|
@ -509,7 +509,6 @@ describe('Backup', () => {
|
|||
timestamp: 1524185933350,
|
||||
type: 'private',
|
||||
unreadCount: 0,
|
||||
verified: 0,
|
||||
sealedSender: 0,
|
||||
version: 2,
|
||||
};
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
describe('Fixtures', () => {
|
||||
before(async () => {
|
||||
// NetworkStatusView checks this method every five seconds while showing
|
||||
window.getSocketStatus = () => WebSocket.OPEN;
|
||||
|
||||
await clearDatabase();
|
||||
await textsecure.storage.user.setNumberAndDeviceId(
|
||||
|
|
|
@ -166,7 +166,6 @@
|
|||
{{/isError}}
|
||||
</script>
|
||||
|
||||
<script type="text/javascript" src="../libtextsecure/test/fake_web_api.js"></script>
|
||||
|
||||
<script type="text/javascript" src="../js/components.js"></script>
|
||||
<script type="text/javascript" src="../js/reliable_trigger.js" data-cover></script>
|
||||
|
@ -184,7 +183,6 @@
|
|||
|
||||
<script type="text/javascript" src="../js/models/messages.js" data-cover></script>
|
||||
<script type="text/javascript" src="../js/models/conversations.js" data-cover></script>
|
||||
<script type="text/javascript" src="../js/keychange_listener.js" data-cover></script>
|
||||
<script type="text/javascript" src="../js/expiring_messages.js" data-cover></script>
|
||||
<script type="text/javascript" src="../js/notifications.js" data-cover></script>
|
||||
<script type="text/javascript" src="../js/focus_listener.js"></script>
|
||||
|
@ -218,19 +216,14 @@
|
|||
<script type="text/javascript" src="../js/views/password_dialog_view.js"></script>
|
||||
<script type="text/javascript" src="../js/views/seed_dialog_view.js"></script>
|
||||
|
||||
<!-- <script type="text/javascript" src="metadata/SecretSessionCipher_test.js"></script> -->
|
||||
|
||||
<script type="text/javascript" src="views/whisper_view_test.js"></script>
|
||||
<script type="text/javascript" src="views/timestamp_view_test.js"></script>
|
||||
|
||||
<script type="text/javascript" src="models/conversations_test.js"></script>
|
||||
<script type="text/javascript" src="models/messages_test.js"></script>
|
||||
|
||||
<script type="text/javascript" src="storage_test.js"></script>
|
||||
<script type="text/javascript" src="keychange_listener_test.js"></script>
|
||||
<script type="text/javascript" src="reliable_trigger_test.js"></script>
|
||||
<script type="text/javascript" src="backup_test.js"></script>
|
||||
<script type="text/javascript" src="crypto_test.js"></script>
|
||||
<script type="text/javascript" src="database_test.js"></script>
|
||||
<script type="text/javascript" src="i18n_test.js"></script>
|
||||
|
||||
|
|
|
@ -1,417 +0,0 @@
|
|||
/* global libsignal, textsecure */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {
|
||||
SecretSessionCipher,
|
||||
createCertificateValidator,
|
||||
_createSenderCertificateFromBuffer,
|
||||
_createServerCertificateFromBuffer,
|
||||
} = window.Signal.Metadata;
|
||||
const {
|
||||
bytesFromString,
|
||||
stringFromBytes,
|
||||
arrayBufferToBase64,
|
||||
} = window.Signal.Crypto;
|
||||
|
||||
function InMemorySignalProtocolStore() {
|
||||
this.store = {};
|
||||
}
|
||||
|
||||
function toString(thing) {
|
||||
if (typeof thing === 'string') {
|
||||
return thing;
|
||||
}
|
||||
return arrayBufferToBase64(thing);
|
||||
}
|
||||
|
||||
InMemorySignalProtocolStore.prototype = {
|
||||
Direction: {
|
||||
SENDING: 1,
|
||||
RECEIVING: 2,
|
||||
},
|
||||
|
||||
getIdentityKeyPair() {
|
||||
return Promise.resolve(this.get('identityKey'));
|
||||
},
|
||||
getLocalRegistrationId() {
|
||||
return Promise.resolve(this.get('registrationId'));
|
||||
},
|
||||
put(key, value) {
|
||||
if (
|
||||
key === undefined ||
|
||||
value === undefined ||
|
||||
key === null ||
|
||||
value === null
|
||||
) {
|
||||
throw new Error('Tried to store undefined/null');
|
||||
}
|
||||
this.store[key] = value;
|
||||
},
|
||||
get(key, defaultValue) {
|
||||
if (key === null || key === undefined) {
|
||||
throw new Error('Tried to get value for undefined/null key');
|
||||
}
|
||||
if (key in this.store) {
|
||||
return this.store[key];
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
},
|
||||
remove(key) {
|
||||
if (key === null || key === undefined) {
|
||||
throw new Error('Tried to remove value for undefined/null key');
|
||||
}
|
||||
delete this.store[key];
|
||||
},
|
||||
|
||||
isTrustedIdentity(identifier, identityKey) {
|
||||
if (identifier === null || identifier === undefined) {
|
||||
throw new Error('tried to check identity key for undefined/null key');
|
||||
}
|
||||
if (!(identityKey instanceof ArrayBuffer)) {
|
||||
throw new Error('Expected identityKey to be an ArrayBuffer');
|
||||
}
|
||||
const trusted = this.get(`identityKey${identifier}`);
|
||||
if (trusted === undefined) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
return Promise.resolve(toString(identityKey) === toString(trusted));
|
||||
},
|
||||
loadIdentityKey(identifier) {
|
||||
if (identifier === null || identifier === undefined) {
|
||||
throw new Error('Tried to get identity key for undefined/null key');
|
||||
}
|
||||
return Promise.resolve(this.get(`identityKey${identifier}`));
|
||||
},
|
||||
saveIdentity(identifier, identityKey) {
|
||||
if (identifier === null || identifier === undefined) {
|
||||
throw new Error('Tried to put identity key for undefined/null key');
|
||||
}
|
||||
const address = libsignal.SignalProtocolAddress.fromString(identifier);
|
||||
|
||||
const existing = this.get(`identityKey${address.getName()}`);
|
||||
this.put(`identityKey${address.getName()}`, identityKey);
|
||||
|
||||
if (existing && toString(identityKey) !== toString(existing)) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
return Promise.resolve(false);
|
||||
},
|
||||
|
||||
/* Returns a prekeypair object or undefined */
|
||||
loadPreKey(keyId) {
|
||||
let res = this.get(`25519KeypreKey${keyId}`);
|
||||
if (res !== undefined) {
|
||||
res = { pubKey: res.pubKey, privKey: res.privKey };
|
||||
}
|
||||
return Promise.resolve(res);
|
||||
},
|
||||
storePreKey(keyId, keyPair) {
|
||||
return Promise.resolve(this.put(`25519KeypreKey${keyId}`, keyPair));
|
||||
},
|
||||
removePreKey(keyId) {
|
||||
return Promise.resolve(this.remove(`25519KeypreKey${keyId}`));
|
||||
},
|
||||
|
||||
/* Returns a signed keypair object or undefined */
|
||||
loadSignedPreKey(keyId) {
|
||||
let res = this.get(`25519KeysignedKey${keyId}`);
|
||||
if (res !== undefined) {
|
||||
res = { pubKey: res.pubKey, privKey: res.privKey };
|
||||
}
|
||||
return Promise.resolve(res);
|
||||
},
|
||||
storeSignedPreKey(keyId, keyPair) {
|
||||
return Promise.resolve(this.put(`25519KeysignedKey${keyId}`, keyPair));
|
||||
},
|
||||
removeSignedPreKey(keyId) {
|
||||
return Promise.resolve(this.remove(`25519KeysignedKey${keyId}`));
|
||||
},
|
||||
|
||||
loadSession(identifier) {
|
||||
return Promise.resolve(this.get(`session${identifier}`));
|
||||
},
|
||||
storeSession(identifier, record) {
|
||||
return Promise.resolve(this.put(`session${identifier}`, record));
|
||||
},
|
||||
removeSession(identifier) {
|
||||
return Promise.resolve(this.remove(`session${identifier}`));
|
||||
},
|
||||
removeAllSessions(identifier) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const id in this.store) {
|
||||
if (id.startsWith(`session${identifier}`)) {
|
||||
delete this.store[id];
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
};
|
||||
|
||||
describe('SecretSessionCipher', () => {
|
||||
it('successfully roundtrips', async function thisNeeded() {
|
||||
this.timeout(4000);
|
||||
|
||||
const aliceStore = new InMemorySignalProtocolStore();
|
||||
const bobStore = new InMemorySignalProtocolStore();
|
||||
|
||||
await _initializeSessions(aliceStore, bobStore);
|
||||
|
||||
const aliceIdentityKey = await aliceStore.getIdentityKeyPair();
|
||||
|
||||
const trustRoot = await libsignal.Curve.async.generateKeyPair();
|
||||
const senderCertificate = await _createSenderCertificateFor(
|
||||
trustRoot,
|
||||
'+14151111111',
|
||||
1,
|
||||
aliceIdentityKey.pubKey,
|
||||
31337
|
||||
);
|
||||
const aliceCipher = new SecretSessionCipher(aliceStore);
|
||||
|
||||
const ciphertext = await aliceCipher.encrypt(
|
||||
new libsignal.SignalProtocolAddress('+14152222222', 1),
|
||||
senderCertificate,
|
||||
bytesFromString('smert za smert')
|
||||
);
|
||||
|
||||
const bobCipher = new SecretSessionCipher(bobStore);
|
||||
|
||||
const decryptResult = await bobCipher.decrypt(
|
||||
createCertificateValidator(trustRoot.pubKey),
|
||||
ciphertext,
|
||||
31335
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
stringFromBytes(decryptResult.content),
|
||||
'smert za smert'
|
||||
);
|
||||
assert.strictEqual(decryptResult.sender.toString(), '+14151111111.1');
|
||||
});
|
||||
|
||||
it('fails when untrusted', async function thisNeeded() {
|
||||
this.timeout(4000);
|
||||
|
||||
const aliceStore = new InMemorySignalProtocolStore();
|
||||
const bobStore = new InMemorySignalProtocolStore();
|
||||
|
||||
await _initializeSessions(aliceStore, bobStore);
|
||||
|
||||
const aliceIdentityKey = await aliceStore.getIdentityKeyPair();
|
||||
|
||||
const trustRoot = await libsignal.Curve.async.generateKeyPair();
|
||||
const falseTrustRoot = await libsignal.Curve.async.generateKeyPair();
|
||||
const senderCertificate = await _createSenderCertificateFor(
|
||||
falseTrustRoot,
|
||||
'+14151111111',
|
||||
1,
|
||||
aliceIdentityKey.pubKey,
|
||||
31337
|
||||
);
|
||||
const aliceCipher = new SecretSessionCipher(aliceStore);
|
||||
|
||||
const ciphertext = await aliceCipher.encrypt(
|
||||
new libsignal.SignalProtocolAddress('+14152222222', 1),
|
||||
senderCertificate,
|
||||
bytesFromString('и вот я')
|
||||
);
|
||||
|
||||
const bobCipher = new SecretSessionCipher(bobStore);
|
||||
|
||||
try {
|
||||
await bobCipher.decrypt(
|
||||
createCertificateValidator(trustRoot.pubKey),
|
||||
ciphertext,
|
||||
31335
|
||||
);
|
||||
throw new Error('It did not fail!');
|
||||
} catch (error) {
|
||||
assert.strictEqual(error.message, 'Invalid signature');
|
||||
}
|
||||
});
|
||||
|
||||
it('fails when expired', async function thisNeeded() {
|
||||
this.timeout(4000);
|
||||
|
||||
const aliceStore = new InMemorySignalProtocolStore();
|
||||
const bobStore = new InMemorySignalProtocolStore();
|
||||
|
||||
await _initializeSessions(aliceStore, bobStore);
|
||||
|
||||
const aliceIdentityKey = await aliceStore.getIdentityKeyPair();
|
||||
|
||||
const trustRoot = await libsignal.Curve.async.generateKeyPair();
|
||||
const senderCertificate = await _createSenderCertificateFor(
|
||||
trustRoot,
|
||||
'+14151111111',
|
||||
1,
|
||||
aliceIdentityKey.pubKey,
|
||||
31337
|
||||
);
|
||||
const aliceCipher = new SecretSessionCipher(aliceStore);
|
||||
|
||||
const ciphertext = await aliceCipher.encrypt(
|
||||
new libsignal.SignalProtocolAddress('+14152222222', 1),
|
||||
senderCertificate,
|
||||
bytesFromString('и вот я')
|
||||
);
|
||||
|
||||
const bobCipher = new SecretSessionCipher(bobStore);
|
||||
|
||||
try {
|
||||
await bobCipher.decrypt(
|
||||
createCertificateValidator(trustRoot.pubKey),
|
||||
ciphertext,
|
||||
31338
|
||||
);
|
||||
throw new Error('It did not fail!');
|
||||
} catch (error) {
|
||||
assert.strictEqual(error.message, 'Certificate is expired');
|
||||
}
|
||||
});
|
||||
|
||||
it('fails when wrong identity', async function thisNeeded() {
|
||||
this.timeout(4000);
|
||||
|
||||
const aliceStore = new InMemorySignalProtocolStore();
|
||||
const bobStore = new InMemorySignalProtocolStore();
|
||||
|
||||
await _initializeSessions(aliceStore, bobStore);
|
||||
|
||||
const trustRoot = await libsignal.Curve.async.generateKeyPair();
|
||||
const randomKeyPair = await libsignal.Curve.async.generateKeyPair();
|
||||
const senderCertificate = await _createSenderCertificateFor(
|
||||
trustRoot,
|
||||
'+14151111111',
|
||||
1,
|
||||
randomKeyPair.pubKey,
|
||||
31337
|
||||
);
|
||||
const aliceCipher = new SecretSessionCipher(aliceStore);
|
||||
|
||||
const ciphertext = await aliceCipher.encrypt(
|
||||
new libsignal.SignalProtocolAddress('+14152222222', 1),
|
||||
senderCertificate,
|
||||
bytesFromString('smert za smert')
|
||||
);
|
||||
|
||||
const bobCipher = new SecretSessionCipher(bobStore);
|
||||
|
||||
try {
|
||||
await bobCipher.decrypt(
|
||||
createCertificateValidator(trustRoot.puKey),
|
||||
ciphertext,
|
||||
31335
|
||||
);
|
||||
throw new Error('It did not fail!');
|
||||
} catch (error) {
|
||||
assert.strictEqual(error.message, 'Invalid public key');
|
||||
}
|
||||
});
|
||||
|
||||
// private SenderCertificate _createCertificateFor(
|
||||
// ECKeyPair trustRoot
|
||||
// String sender
|
||||
// int deviceId
|
||||
// ECPublicKey identityKey
|
||||
// long expires
|
||||
// )
|
||||
async function _createSenderCertificateFor(
|
||||
trustRoot,
|
||||
sender,
|
||||
deviceId,
|
||||
identityKey,
|
||||
expires
|
||||
) {
|
||||
const serverKey = await libsignal.Curve.async.generateKeyPair();
|
||||
|
||||
const serverCertificateCertificateProto = new textsecure.protobuf.ServerCertificate.Certificate();
|
||||
serverCertificateCertificateProto.id = 1;
|
||||
serverCertificateCertificateProto.key = serverKey.pubKey;
|
||||
const serverCertificateCertificateBytes = serverCertificateCertificateProto
|
||||
.encode()
|
||||
.toArrayBuffer();
|
||||
|
||||
const serverCertificateSignature = await libsignal.Curve.async.calculateSignature(
|
||||
trustRoot.privKey,
|
||||
serverCertificateCertificateBytes
|
||||
);
|
||||
|
||||
const serverCertificateProto = new textsecure.protobuf.ServerCertificate();
|
||||
serverCertificateProto.certificate = serverCertificateCertificateBytes;
|
||||
serverCertificateProto.signature = serverCertificateSignature;
|
||||
const serverCertificate = _createServerCertificateFromBuffer(
|
||||
serverCertificateProto.encode().toArrayBuffer()
|
||||
);
|
||||
|
||||
const senderCertificateCertificateProto = new textsecure.protobuf.SenderCertificate.Certificate();
|
||||
senderCertificateCertificateProto.sender = sender;
|
||||
senderCertificateCertificateProto.senderDevice = deviceId;
|
||||
senderCertificateCertificateProto.identityKey = identityKey;
|
||||
senderCertificateCertificateProto.expires = expires;
|
||||
senderCertificateCertificateProto.signer = textsecure.protobuf.ServerCertificate.decode(
|
||||
serverCertificate.serialized
|
||||
);
|
||||
const senderCertificateBytes = senderCertificateCertificateProto
|
||||
.encode()
|
||||
.toArrayBuffer();
|
||||
|
||||
const senderCertificateSignature = await libsignal.Curve.async.calculateSignature(
|
||||
serverKey.privKey,
|
||||
senderCertificateBytes
|
||||
);
|
||||
|
||||
const senderCertificateProto = new textsecure.protobuf.SenderCertificate();
|
||||
senderCertificateProto.certificate = senderCertificateBytes;
|
||||
senderCertificateProto.signature = senderCertificateSignature;
|
||||
return _createSenderCertificateFromBuffer(
|
||||
senderCertificateProto.encode().toArrayBuffer()
|
||||
);
|
||||
}
|
||||
|
||||
// private void _initializeSessions(
|
||||
// SignalProtocolStore aliceStore, SignalProtocolStore bobStore)
|
||||
async function _initializeSessions(aliceStore, bobStore) {
|
||||
const aliceAddress = new libsignal.SignalProtocolAddress('+14152222222', 1);
|
||||
await aliceStore.put(
|
||||
'identityKey',
|
||||
await libsignal.Curve.generateKeyPair()
|
||||
);
|
||||
await bobStore.put('identityKey', await libsignal.Curve.generateKeyPair());
|
||||
|
||||
await aliceStore.put('registrationId', 57);
|
||||
await bobStore.put('registrationId', 58);
|
||||
|
||||
const bobPreKey = await libsignal.Curve.async.generateKeyPair();
|
||||
const bobIdentityKey = await bobStore.getIdentityKeyPair();
|
||||
const bobSignedPreKey = await libsignal.KeyHelper.generateSignedPreKey(
|
||||
bobIdentityKey,
|
||||
2
|
||||
);
|
||||
|
||||
const bobBundle = {
|
||||
identityKey: bobIdentityKey.pubKey,
|
||||
registrationId: 1,
|
||||
preKey: {
|
||||
keyId: 1,
|
||||
publicKey: bobPreKey.pubKey,
|
||||
},
|
||||
signedPreKey: {
|
||||
keyId: 2,
|
||||
publicKey: bobSignedPreKey.keyPair.pubKey,
|
||||
signature: bobSignedPreKey.signature,
|
||||
},
|
||||
};
|
||||
const aliceSessionBuilder = new libsignal.SessionBuilder(
|
||||
aliceStore,
|
||||
aliceAddress
|
||||
);
|
||||
await aliceSessionBuilder.processPreKey(bobBundle);
|
||||
|
||||
await bobStore.storeSignedPreKey(2, bobSignedPreKey.keyPair);
|
||||
await bobStore.storePreKey(1, bobPreKey);
|
||||
}
|
||||
});
|
|
@ -3,7 +3,7 @@
|
|||
// 'use strict';
|
||||
// FIXME audric enable back those test
|
||||
describe('ConversationCollection', () => {
|
||||
// textsecure.messaging = new textsecure.MessageSender();
|
||||
// textsecure.messaging = true;
|
||||
// before(clearDatabase);
|
||||
// after(clearDatabase);
|
||||
// it('should be ordered newest to oldest', () => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* global i18n, Whisper */
|
||||
/* global Whisper */
|
||||
|
||||
'use strict';
|
||||
|
||||
|
@ -129,17 +129,5 @@ describe('MessageCollection', () => {
|
|||
"Group name is now 'blerg'. Bob joined the group.",
|
||||
'Notes when there are multiple changes to group_updates properties.'
|
||||
);
|
||||
|
||||
message = messages.add({ flags: true });
|
||||
assert.equal(message.getDescription(), i18n('sessionEnded'));
|
||||
});
|
||||
|
||||
it('checks if it is end of the session', () => {
|
||||
const messages = new Whisper.MessageCollection();
|
||||
let message = messages.add(attributes);
|
||||
assert.notOk(message.isEndSession());
|
||||
|
||||
message = messages.add({ flags: true });
|
||||
assert.ok(message.isEndSession());
|
||||
});
|
||||
});
|
||||
|
|
1035
test/storage_test.js
1035
test/storage_test.js
File diff suppressed because it is too large
Load diff
|
@ -1,86 +0,0 @@
|
|||
// tslint:disable-next-line: no-implicit-dependencies
|
||||
import { assert } from 'chai';
|
||||
import { ConversationController } from '../../ts/session/conversations';
|
||||
|
||||
const { libsignal, Whisper } = window;
|
||||
|
||||
describe('KeyChangeListener', () => {
|
||||
const phoneNumberWithKeyChange = '+13016886524'; // nsa
|
||||
const address = new libsignal.SignalProtocolAddress(
|
||||
phoneNumberWithKeyChange,
|
||||
1
|
||||
);
|
||||
const oldKey = libsignal.crypto.getRandomBytes(33);
|
||||
const newKey = libsignal.crypto.getRandomBytes(33);
|
||||
let store: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
store = new window.SignalProtocolStore();
|
||||
await store.hydrateCaches();
|
||||
Whisper.KeyChangeListener.init(store);
|
||||
return store.saveIdentity(address.toString(), oldKey);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
return store.removeIdentityKey(phoneNumberWithKeyChange);
|
||||
});
|
||||
|
||||
describe('When we have a conversation with this contact', () => {
|
||||
// this.timeout(2000);
|
||||
|
||||
let convo: any;
|
||||
before(async () => {
|
||||
convo = ConversationController.getInstance().dangerouslyCreateAndAdd({
|
||||
id: phoneNumberWithKeyChange,
|
||||
type: 'private',
|
||||
} as any);
|
||||
await window.Signal.Data.saveConversation(convo.attributes, {
|
||||
Conversation: Whisper.Conversation,
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await convo.destroyMessages();
|
||||
await window.Signal.Data.saveConversation(convo.id);
|
||||
});
|
||||
|
||||
it('generates a key change notice in the private conversation with this contact', done => {
|
||||
convo.once('newmessage', async () => {
|
||||
await convo.fetchMessages();
|
||||
const message = convo.messageCollection.at(0);
|
||||
assert.strictEqual(message.get('type'), 'keychange');
|
||||
done();
|
||||
});
|
||||
store.saveIdentity(address.toString(), newKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When we have a group with this contact', () => {
|
||||
let convo: any;
|
||||
|
||||
before(async () => {
|
||||
convo = ConversationController.getInstance().dangerouslyCreateAndAdd({
|
||||
id: 'groupId',
|
||||
type: 'group',
|
||||
members: [phoneNumberWithKeyChange],
|
||||
} as any);
|
||||
await window.Signal.Data.saveConversation(convo.attributes, {
|
||||
Conversation: Whisper.Conversation,
|
||||
});
|
||||
});
|
||||
after(async () => {
|
||||
await convo.destroyMessages();
|
||||
await window.Signal.Data.saveConversation(convo.id);
|
||||
});
|
||||
|
||||
it('generates a key change notice in the group conversation with this contact', done => {
|
||||
convo.once('newmessage', async () => {
|
||||
await convo.fetchMessages();
|
||||
const message = convo.messageCollection.at(0);
|
||||
assert.strictEqual(message.get('type'), 'keychange');
|
||||
done();
|
||||
});
|
||||
store.saveIdentity(address.toString(), newKey);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -10,7 +10,6 @@ interface Props {
|
|||
phoneNumber: string;
|
||||
isMe?: boolean;
|
||||
name?: string;
|
||||
verified: boolean;
|
||||
profileName?: string;
|
||||
avatarPath?: string;
|
||||
i18n: LocalizerType;
|
||||
|
@ -34,15 +33,7 @@ export class ContactListItem extends React.Component<Props> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const {
|
||||
i18n,
|
||||
name,
|
||||
onClick,
|
||||
isMe,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
verified,
|
||||
} = this.props;
|
||||
const { i18n, name, onClick, isMe, phoneNumber, profileName } = this.props;
|
||||
|
||||
const title = name ? name : phoneNumber;
|
||||
const displayName = isMe ? i18n('me') : title;
|
||||
|
@ -60,7 +51,6 @@ export class ContactListItem extends React.Component<Props> {
|
|||
) : null;
|
||||
|
||||
const showNumber = isMe || name;
|
||||
const showVerified = !isMe && verified;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -76,14 +66,6 @@ export class ContactListItem extends React.Component<Props> {
|
|||
<div className="module-contact-list-item__text__name">
|
||||
<Emojify text={displayName} i18n={i18n} /> {profileElement}
|
||||
</div>
|
||||
<div className="module-contact-list-item__text__additional-data">
|
||||
{showVerified ? (
|
||||
<div className="module-contact-list-item__text__verified-icon" />
|
||||
) : null}
|
||||
{showVerified ? ` ${i18n('verified')}` : null}
|
||||
{showVerified && showNumber ? ' ∙ ' : null}
|
||||
{showNumber ? phoneNumber : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -69,7 +69,6 @@ export class LeftPane extends React.Component<Props> {
|
|||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
const ourPrimaryConversation = this.props.ourPrimaryConversation;
|
||||
return (
|
||||
<SessionTheme theme={this.props.theme}>
|
||||
<div className="module-left-pane-session">
|
||||
|
@ -77,8 +76,6 @@ export class LeftPane extends React.Component<Props> {
|
|||
{...this.props}
|
||||
selectedSection={this.props.focusedSection}
|
||||
onSectionSelected={this.handleSectionSelected}
|
||||
unreadMessageCount={this.props.unreadMessageCount}
|
||||
ourPrimaryConversation={ourPrimaryConversation}
|
||||
/>
|
||||
<div className="module-left-pane">{this.renderSection()}</div>
|
||||
</div>
|
||||
|
@ -162,22 +159,13 @@ export class LeftPane extends React.Component<Props> {
|
|||
}
|
||||
|
||||
private renderSettingSection() {
|
||||
const {
|
||||
isSecondaryDevice,
|
||||
showSessionSettingsCategory,
|
||||
settingsCategory,
|
||||
} = this.props;
|
||||
const { settingsCategory } = this.props;
|
||||
|
||||
const category = settingsCategory || SessionSettingCategory.Appearance;
|
||||
|
||||
return (
|
||||
<>
|
||||
<LeftPaneSettingSection
|
||||
{...this.props}
|
||||
isSecondaryDevice={isSecondaryDevice}
|
||||
showSessionSettingsCategory={showSessionSettingsCategory}
|
||||
settingsCategory={category}
|
||||
/>
|
||||
<LeftPaneSettingSection {...this.props} settingsCategory={category} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -62,7 +62,7 @@ async function createClosedGroup(
|
|||
window.i18n('pickClosedGroupMember')
|
||||
);
|
||||
return;
|
||||
} else if (groupMembers.length >= window.CONSTANTS.MEDIUM_GROUP_SIZE_LIMIT) {
|
||||
} else if (groupMembers.length >= window.CONSTANTS.CLOSED_GROUP_SIZE_LIMIT) {
|
||||
ToastUtils.pushToastError(
|
||||
'closedGroupMaxSize',
|
||||
window.i18n('closedGroupMaxSize')
|
||||
|
|
|
@ -37,7 +37,6 @@ interface Props {
|
|||
profileName?: string;
|
||||
avatarPath?: string;
|
||||
|
||||
isVerified: boolean;
|
||||
isMe: boolean;
|
||||
isClosable?: boolean;
|
||||
isGroup: boolean;
|
||||
|
@ -69,12 +68,10 @@ interface Props {
|
|||
onSetDisappearingMessages: (seconds: number) => void;
|
||||
onDeleteMessages: () => void;
|
||||
onDeleteContact: () => void;
|
||||
onResetSession: () => void;
|
||||
|
||||
onCloseOverlay: () => void;
|
||||
onDeleteSelectedMessages: () => void;
|
||||
|
||||
onShowSafetyNumber: () => void;
|
||||
onGoBack: () => void;
|
||||
|
||||
onBlockUser: () => void;
|
||||
|
|
|
@ -38,6 +38,7 @@ import uuid from 'uuid';
|
|||
import { InView } from 'react-intersection-observer';
|
||||
import { DefaultTheme, withTheme } from 'styled-components';
|
||||
import { MessageMetadata } from './message/MessageMetadata';
|
||||
import { MessageRegularProps } from '../../../js/models/messages';
|
||||
|
||||
// Same as MIN_WIDTH in ImageGrid.tsx
|
||||
const MINIMUM_LINK_PREVIEW_IMAGE_WIDTH = 200;
|
||||
|
@ -49,73 +50,6 @@ interface LinkPreviewType {
|
|||
image?: AttachmentType;
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
disableMenu?: boolean;
|
||||
isDeletable: boolean;
|
||||
isModerator?: boolean;
|
||||
text?: string;
|
||||
bodyPending?: boolean;
|
||||
id: string;
|
||||
collapseMetadata?: boolean;
|
||||
direction: 'incoming' | 'outgoing';
|
||||
timestamp: number;
|
||||
serverTimestamp?: number;
|
||||
status?: 'sending' | 'sent' | 'delivered' | 'read' | 'error' | 'pow';
|
||||
// What if changed this over to a single contact like quote, and put the events on it?
|
||||
contact?: Contact & {
|
||||
hasSignalAccount: boolean;
|
||||
onSendMessage?: () => void;
|
||||
onClick?: () => void;
|
||||
};
|
||||
authorName?: string;
|
||||
authorProfileName?: string;
|
||||
/** Note: this should be formatted for display */
|
||||
authorPhoneNumber: string;
|
||||
conversationType: 'group' | 'direct';
|
||||
attachments?: Array<AttachmentType>;
|
||||
quote?: {
|
||||
text: string;
|
||||
attachment?: QuotedAttachmentType;
|
||||
isFromMe: boolean;
|
||||
authorPhoneNumber: string;
|
||||
authorProfileName?: string;
|
||||
authorName?: string;
|
||||
messageId?: string;
|
||||
onClick: (data: any) => void;
|
||||
referencedMessageNotFound: boolean;
|
||||
};
|
||||
previews: Array<LinkPreviewType>;
|
||||
authorAvatarPath?: string;
|
||||
isExpired: boolean;
|
||||
expirationLength?: number;
|
||||
expirationTimestamp?: number;
|
||||
convoId: string;
|
||||
isPublic?: boolean;
|
||||
isRss?: boolean;
|
||||
selected: boolean;
|
||||
isKickedFromGroup: boolean;
|
||||
// whether or not to show check boxes
|
||||
multiSelectMode: boolean;
|
||||
firstMessageOfSeries: boolean;
|
||||
isUnread: boolean;
|
||||
isQuotedMessageToAnimate?: boolean;
|
||||
|
||||
onClickAttachment?: (attachment: AttachmentType) => void;
|
||||
onClickLinkPreview?: (url: string) => void;
|
||||
onCopyText?: () => void;
|
||||
onSelectMessage: (messageId: string) => void;
|
||||
onReply?: (messagId: number) => void;
|
||||
onRetrySend?: () => void;
|
||||
onDownload?: (attachment: AttachmentType) => void;
|
||||
onDeleteMessage: (messageId: string) => void;
|
||||
onCopyPubKey?: () => void;
|
||||
onBanUser?: () => void;
|
||||
onShowDetail: () => void;
|
||||
onShowUserDetails: (userPubKey: string) => void;
|
||||
markRead: (readAt: number) => Promise<void>;
|
||||
theme: DefaultTheme;
|
||||
}
|
||||
|
||||
interface State {
|
||||
expiring: boolean;
|
||||
expired: boolean;
|
||||
|
@ -125,14 +59,14 @@ interface State {
|
|||
const EXPIRATION_CHECK_MINIMUM = 2000;
|
||||
const EXPIRED_DELAY = 600;
|
||||
|
||||
class MessageInner extends React.PureComponent<Props, State> {
|
||||
class MessageInner extends React.PureComponent<MessageRegularProps, State> {
|
||||
public handleImageErrorBound: () => void;
|
||||
|
||||
public expirationCheckInterval: any;
|
||||
public expiredTimeout: any;
|
||||
public ctxMenuID: string;
|
||||
|
||||
public constructor(props: Props) {
|
||||
public constructor(props: MessageRegularProps) {
|
||||
super(props);
|
||||
|
||||
this.handleImageErrorBound = this.handleImageError.bind(this);
|
||||
|
@ -573,7 +507,7 @@ class MessageInner extends React.PureComponent<Props, State> {
|
|||
authorPhoneNumber,
|
||||
authorProfileName,
|
||||
collapseMetadata,
|
||||
isModerator,
|
||||
isAdmin,
|
||||
conversationType,
|
||||
direction,
|
||||
onShowUserDetails,
|
||||
|
@ -604,7 +538,7 @@ class MessageInner extends React.PureComponent<Props, State> {
|
|||
}}
|
||||
pubkey={authorPhoneNumber}
|
||||
/>
|
||||
{isModerator && (
|
||||
{isAdmin && (
|
||||
<div className="module-avatar__icon--crown-wrapper">
|
||||
<div className="module-avatar__icon--crown" />
|
||||
</div>
|
||||
|
@ -691,7 +625,7 @@ class MessageInner extends React.PureComponent<Props, State> {
|
|||
onRetrySend,
|
||||
onShowDetail,
|
||||
isPublic,
|
||||
isModerator,
|
||||
weAreAdmin,
|
||||
onBanUser,
|
||||
} = this.props;
|
||||
|
||||
|
@ -759,7 +693,7 @@ class MessageInner extends React.PureComponent<Props, State> {
|
|||
</Item>
|
||||
</>
|
||||
) : null}
|
||||
{isModerator && isPublic ? (
|
||||
{weAreAdmin && isPublic ? (
|
||||
<Item onClick={onBanUser}>{window.i18n('banUser')}</Item>
|
||||
) : null}
|
||||
</Menu>
|
||||
|
|
|
@ -4,7 +4,8 @@ import moment from 'moment';
|
|||
|
||||
import { Avatar } from '../Avatar';
|
||||
import { ContactName } from './ContactName';
|
||||
import { Message, Props as MessageProps } from './Message';
|
||||
import { Message } from './Message';
|
||||
import { MessageRegularProps } from '../../../js/models/messages';
|
||||
|
||||
interface Contact {
|
||||
status: string;
|
||||
|
@ -19,14 +20,13 @@ interface Contact {
|
|||
errors?: Array<Error>;
|
||||
|
||||
onSendAnyway: () => void;
|
||||
onShowSafetyNumber: () => void;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
sentAt: number;
|
||||
receivedAt: number;
|
||||
|
||||
message: MessageProps;
|
||||
message: MessageRegularProps;
|
||||
errors: Array<Error>;
|
||||
contacts: Array<Contact>;
|
||||
|
||||
|
@ -73,12 +73,6 @@ export class MessageDetail extends React.Component<Props> {
|
|||
|
||||
const errorComponent = contact.isOutgoingKeyError ? (
|
||||
<div className="module-message-detail__contact__error-buttons">
|
||||
<button
|
||||
className="module-message-detail__contact__show-safety-number"
|
||||
onClick={contact.onShowSafetyNumber}
|
||||
>
|
||||
{i18n('showSafetyNumber')}
|
||||
</button>
|
||||
<button
|
||||
className="module-message-detail__contact__send-anyway"
|
||||
onClick={contact.onSendAnyway}
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
sessionResetMessageKey: string;
|
||||
}
|
||||
|
||||
export class ResetSessionNotification extends React.Component<Props> {
|
||||
public render() {
|
||||
const { sessionResetMessageKey } = this.props;
|
||||
|
||||
return (
|
||||
<div className="module-reset-session-notification">
|
||||
{window.i18n(sessionResetMessageKey)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
import React from 'react';
|
||||
// import classNames from 'classnames';
|
||||
|
||||
import { ContactName } from './ContactName';
|
||||
import { Intl } from '../Intl';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
interface Contact {
|
||||
phoneNumber: string;
|
||||
profileName?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
isGroup: boolean;
|
||||
contact: Contact;
|
||||
i18n: LocalizerType;
|
||||
onVerify: () => void;
|
||||
}
|
||||
|
||||
export class SafetyNumberNotification extends React.Component<Props> {
|
||||
public render() {
|
||||
const { contact, isGroup, i18n, onVerify } = this.props;
|
||||
const changeKey = isGroup
|
||||
? 'safetyNumberChangedGroup'
|
||||
: 'safetyNumberChanged';
|
||||
|
||||
return (
|
||||
<div className="module-safety-number-notification">
|
||||
<div className="module-safety-number-notification__icon" />
|
||||
<div className="module-safety-number-notification__text">
|
||||
<Intl
|
||||
id={changeKey}
|
||||
components={[
|
||||
<span
|
||||
key="external-1"
|
||||
className="module-safety-number-notification__contact"
|
||||
>
|
||||
<ContactName
|
||||
i18n={i18n}
|
||||
name={contact.name}
|
||||
profileName={contact.profileName}
|
||||
phoneNumber={contact.phoneNumber}
|
||||
module="module-verification-notification__contact"
|
||||
shouldShowPubkey={true}
|
||||
/>
|
||||
</span>,
|
||||
]}
|
||||
i18n={i18n}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
role="button"
|
||||
onClick={onVerify}
|
||||
className="module-verification-notification__button"
|
||||
>
|
||||
{i18n('verifyNewNumber')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
import React from 'react';
|
||||
// import classNames from 'classnames';
|
||||
|
||||
import { ContactName } from './ContactName';
|
||||
import { Intl } from '../Intl';
|
||||
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
|
||||
interface Contact {
|
||||
phoneNumber: string;
|
||||
profileName?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
// tslint:disable: react-unused-props-and-state
|
||||
type: 'markVerified' | 'markNotVerified';
|
||||
isLocal: boolean;
|
||||
contact: Contact;
|
||||
};
|
||||
|
||||
export const VerificationNotification = (props: Props) => {
|
||||
const { type } = props;
|
||||
const suffix =
|
||||
type === 'markVerified' ? 'mark-verified' : 'mark-not-verified';
|
||||
|
||||
const getStringId = () => {
|
||||
const { isLocal } = props;
|
||||
|
||||
switch (type) {
|
||||
case 'markVerified':
|
||||
return isLocal
|
||||
? 'youMarkedAsVerified'
|
||||
: 'youMarkedAsVerifiedOtherDevice';
|
||||
case 'markNotVerified':
|
||||
return isLocal
|
||||
? 'youMarkedAsNotVerified'
|
||||
: 'youMarkedAsNotVerifiedOtherDevice';
|
||||
default:
|
||||
throw missingCaseError(type);
|
||||
}
|
||||
};
|
||||
|
||||
const renderContents = () => {
|
||||
const { contact } = props;
|
||||
const { i18n } = window;
|
||||
const id = getStringId();
|
||||
|
||||
return (
|
||||
<Intl
|
||||
id={id}
|
||||
components={[
|
||||
<ContactName
|
||||
i18n={i18n}
|
||||
key="external-1"
|
||||
name={contact.name}
|
||||
profileName={contact.profileName}
|
||||
phoneNumber={contact.phoneNumber}
|
||||
module="module-verification-notification__contact"
|
||||
shouldShowPubkey={true}
|
||||
/>,
|
||||
]}
|
||||
i18n={i18n}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="module-verification-notification">
|
||||
<div className={`module-verification-notification__icon--${suffix}`} />
|
||||
{renderContents()}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -14,7 +14,7 @@ import styled, { DefaultTheme } from 'styled-components';
|
|||
|
||||
type Props = {
|
||||
disableMenu?: boolean;
|
||||
isModerator?: boolean;
|
||||
isAdmin?: boolean;
|
||||
isDeletable: boolean;
|
||||
text?: string;
|
||||
bodyPending?: boolean;
|
||||
|
@ -74,7 +74,7 @@ export const MessageMetadata = (props: Props) => {
|
|||
serverTimestamp,
|
||||
isShowingImage,
|
||||
isPublic,
|
||||
isModerator,
|
||||
isAdmin,
|
||||
theme,
|
||||
} = props;
|
||||
|
||||
|
@ -109,7 +109,7 @@ export const MessageMetadata = (props: Props) => {
|
|||
<MetadataBadges
|
||||
direction={direction}
|
||||
isPublic={isPublic}
|
||||
isModerator={isModerator}
|
||||
isAdmin={isAdmin}
|
||||
id={id}
|
||||
withImageNoCaption={withImageNoCaption}
|
||||
/>
|
||||
|
|
|
@ -45,15 +45,15 @@ type BadgesProps = {
|
|||
id: string;
|
||||
direction: string;
|
||||
isPublic?: boolean;
|
||||
isModerator?: boolean;
|
||||
isAdmin?: boolean;
|
||||
withImageNoCaption: boolean;
|
||||
};
|
||||
|
||||
export const MetadataBadges = (props: BadgesProps): JSX.Element => {
|
||||
const { id, direction, isPublic, isModerator, withImageNoCaption } = props;
|
||||
const { id, direction, isPublic, isAdmin, withImageNoCaption } = props;
|
||||
const badges = [
|
||||
(isPublic && 'Public') || null,
|
||||
(isModerator && 'Mod') || null,
|
||||
(isAdmin && 'Mod') || null,
|
||||
].filter(nonNullish);
|
||||
|
||||
if (!badges || badges.length === 0) {
|
||||
|
|
|
@ -10,10 +10,11 @@ import { ConversationType } from '../../state/ducks/conversations';
|
|||
import { noop } from 'lodash';
|
||||
import { DefaultTheme } from 'styled-components';
|
||||
import { StateType } from '../../state/reducer';
|
||||
import { MessageEncrypter } from '../../session/crypto';
|
||||
import { PubKey } from '../../session/types';
|
||||
import { UserUtil } from '../../util';
|
||||
import { ConversationController } from '../../session/conversations';
|
||||
import { getFocusedSection } from '../../state/selectors/section';
|
||||
import { getTheme } from '../../state/selectors/theme';
|
||||
import { getPrimaryPubkey } from '../../state/selectors/user';
|
||||
// tslint:disable-next-line: no-import-side-effect no-submodule-imports
|
||||
|
||||
export enum SectionType {
|
||||
|
@ -30,6 +31,7 @@ interface Props {
|
|||
selectedSection: SectionType;
|
||||
unreadMessageCount: number;
|
||||
ourPrimaryConversation: ConversationType;
|
||||
ourPrimary: string;
|
||||
applyTheme?: any;
|
||||
theme: DefaultTheme;
|
||||
}
|
||||
|
@ -53,6 +55,9 @@ class ActionsPanelPrivate extends React.Component<Props> {
|
|||
this.props.applyTheme(newThemeObject);
|
||||
|
||||
void this.showResetSessionIDDialogIfNeeded();
|
||||
|
||||
// remove existing prekeys, sign prekeys and sessions
|
||||
void window.getAccountManager().clearSessionsAndPreKeys();
|
||||
}
|
||||
|
||||
public Section = ({
|
||||
|
@ -68,6 +73,7 @@ class ActionsPanelPrivate extends React.Component<Props> {
|
|||
avatarPath?: string;
|
||||
notificationCount?: number;
|
||||
}) => {
|
||||
const { ourPrimary } = this.props;
|
||||
const handleClick = onSelect
|
||||
? () => {
|
||||
/* tslint:disable:no-void-expression */
|
||||
|
@ -89,7 +95,6 @@ class ActionsPanelPrivate extends React.Component<Props> {
|
|||
: undefined;
|
||||
|
||||
if (type === SectionType.Profile) {
|
||||
const ourPrimary = window.storage.get('primaryDevicePubKey');
|
||||
const conversation = ConversationController.getInstance().get(ourPrimary);
|
||||
|
||||
const profile = conversation?.getLokiProfile();
|
||||
|
@ -202,11 +207,10 @@ class ActionsPanelPrivate extends React.Component<Props> {
|
|||
}
|
||||
|
||||
const mapStateToProps = (state: StateType) => {
|
||||
const { section, theme } = state;
|
||||
|
||||
return {
|
||||
section: section.focusedSection,
|
||||
theme,
|
||||
section: getFocusedSection(state),
|
||||
theme: getTheme(state),
|
||||
ourPrimary: getPrimaryPubkey(state),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ import { ToastUtils } from '../../session/utils';
|
|||
import { DefaultTheme } from 'styled-components';
|
||||
import { LeftPaneSectionHeader } from './LeftPaneSectionHeader';
|
||||
import { ConversationController } from '../../session/conversations';
|
||||
import { sendOpenGroupsSyncMessage } from '../../session/utils/SyncMessage';
|
||||
|
||||
export interface Props {
|
||||
searchTerm: string;
|
||||
|
@ -456,9 +457,7 @@ export class LeftPaneMessageSection extends React.Component<Props, State> {
|
|||
if (openGroupConversation) {
|
||||
// if no errors happened, trigger a sync with just this open group
|
||||
// so our other devices joins it
|
||||
await window.textsecure.messaging.sendOpenGroupsSyncMessage(
|
||||
openGroupConversation
|
||||
);
|
||||
await sendOpenGroupsSyncMessage([openGroupConversation]);
|
||||
} else {
|
||||
window.log.error(
|
||||
'Joined an opengroup but did not find ther corresponding conversation'
|
||||
|
|
|
@ -791,7 +791,6 @@ export class RegistrationTabs extends React.Component<any, State> {
|
|||
await window.storage.fetch();
|
||||
ConversationController.getInstance().reset();
|
||||
await ConversationController.getInstance().load();
|
||||
window.Whisper.RotateSignedPreKeyListener.stop(window.Whisper.events);
|
||||
|
||||
this.setState({
|
||||
loading: false,
|
||||
|
|
|
@ -200,6 +200,7 @@ export class SessionInboxView extends React.Component<Props, State> {
|
|||
},
|
||||
user: {
|
||||
regionCode: window.storage.get('regionCode'),
|
||||
ourPrimary: window.storage.get('primaryDevicePubKey'),
|
||||
ourNumber:
|
||||
window.storage.get('primaryDevicePubKey') ||
|
||||
window.textsecure.storage.user.getNumber(),
|
||||
|
|
|
@ -1,255 +0,0 @@
|
|||
import React from 'react';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import {
|
||||
SessionButton,
|
||||
SessionButtonColor,
|
||||
SessionButtonType,
|
||||
} from './SessionButton';
|
||||
import { UserUtil } from '../../util';
|
||||
import { MultiDeviceProtocol } from '../../session/protocols';
|
||||
import { PubKey } from '../../session/types';
|
||||
import { ConversationModel } from '../../../js/models/conversations';
|
||||
import { SessionSpinner } from './SessionSpinner';
|
||||
import classNames from 'classnames';
|
||||
import { SessionIcon, SessionIconSize, SessionIconType } from './icon';
|
||||
import { Constants } from '../../session';
|
||||
import { DefaultTheme, withTheme } from 'styled-components';
|
||||
|
||||
interface Props {
|
||||
conversation: ConversationModel;
|
||||
theme: DefaultTheme;
|
||||
}
|
||||
|
||||
interface State {
|
||||
loading: boolean;
|
||||
error?: 'verificationKeysLoadFail';
|
||||
securityNumber?: string;
|
||||
isVerified?: boolean;
|
||||
}
|
||||
|
||||
class SessionKeyVerificationInner extends React.Component<Props, State> {
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
loading: true,
|
||||
error: undefined,
|
||||
securityNumber: undefined,
|
||||
isVerified: this.props.conversation.isVerified(),
|
||||
};
|
||||
|
||||
this.toggleVerification = this.toggleVerification.bind(this);
|
||||
this.onSafetyNumberChanged = this.onSafetyNumberChanged.bind(this);
|
||||
}
|
||||
|
||||
public async componentWillMount() {
|
||||
const securityNumber = await this.generateSecurityNumber();
|
||||
|
||||
if (!securityNumber) {
|
||||
this.setState({
|
||||
error: 'verificationKeysLoadFail',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Finished loading
|
||||
this.setState({
|
||||
loading: false,
|
||||
securityNumber,
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
const theirName = this.props.conversation.attributes.profileName;
|
||||
const theirPubkey = this.props.conversation.id;
|
||||
const isVerified = this.props.conversation.isVerified();
|
||||
|
||||
if (this.state.loading) {
|
||||
return (
|
||||
<div className="key-verification">
|
||||
<SessionSpinner loading={this.state.loading} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const verificationIconColor = isVerified
|
||||
? Constants.UI.COLORS.GREEN
|
||||
: Constants.UI.COLORS.DANGER;
|
||||
const verificationButtonColor = isVerified
|
||||
? SessionButtonColor.Warning
|
||||
: SessionButtonColor.Success;
|
||||
const verificationButton = (
|
||||
<SessionButton
|
||||
buttonType={SessionButtonType.DefaultOutline}
|
||||
buttonColor={verificationButtonColor}
|
||||
onClick={this.toggleVerification}
|
||||
>
|
||||
{window.i18n(isVerified ? 'unverify' : 'verify')}
|
||||
</SessionButton>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="key-verification">
|
||||
{this.state.error ? (
|
||||
<h3>{window.i18n(this.state.error)}</h3>
|
||||
) : (
|
||||
<>
|
||||
<div className={classNames('key-verification__header')}>
|
||||
<h2>{window.i18n('safetyNumber')}</h2>
|
||||
<small>{theirPubkey}</small>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={classNames(
|
||||
'key-verification__key',
|
||||
'session-info-box'
|
||||
)}
|
||||
>
|
||||
{this.renderSecurityNumber()}
|
||||
</div>
|
||||
|
||||
<div className="key-verification__help">
|
||||
{window.i18n('verifyHelp', theirName)}
|
||||
</div>
|
||||
|
||||
<div className="key-verification__is-verified">
|
||||
<span>
|
||||
<SessionIcon
|
||||
iconType={SessionIconType.Lock}
|
||||
iconSize={SessionIconSize.Huge}
|
||||
iconColor={verificationIconColor}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
{window.i18n(
|
||||
isVerified ? 'isVerified' : 'isNotVerified',
|
||||
theirName
|
||||
)}
|
||||
</span>
|
||||
|
||||
{verificationButton}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public async onSafetyNumberChanged() {
|
||||
const conversationModel = this.props.conversation;
|
||||
await conversationModel.getProfiles();
|
||||
|
||||
const securityNumber = await this.generateSecurityNumber();
|
||||
this.setState({ securityNumber });
|
||||
|
||||
window.confirmationDialog({
|
||||
title: window.i18n('changedSinceVerifiedTitle'),
|
||||
message: window.i18n('changedRightAfterVerify', [
|
||||
conversationModel.attributes.profileName,
|
||||
conversationModel.attributes.profileName,
|
||||
]),
|
||||
hideCancel: true,
|
||||
});
|
||||
}
|
||||
|
||||
private async generateSecurityNumber(): Promise<string | undefined> {
|
||||
const ourDeviceKey = await UserUtil.getCurrentDevicePubKey();
|
||||
|
||||
if (!ourDeviceKey) {
|
||||
this.setState({
|
||||
error: 'verificationKeysLoadFail',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const conversationId = this.props.conversation.id;
|
||||
const ourPrimaryKey = (
|
||||
await MultiDeviceProtocol.getPrimaryDevice(PubKey.cast(ourDeviceKey))
|
||||
).key;
|
||||
|
||||
// Grab identity keys
|
||||
const ourIdentityKey = await window.textsecure.storage.protocol.loadIdentityKey(
|
||||
ourPrimaryKey
|
||||
);
|
||||
const theirIdentityKey = await window.textsecure.storage.protocol.loadIdentityKey(
|
||||
this.props.conversation.id
|
||||
);
|
||||
|
||||
if (!ourIdentityKey || !theirIdentityKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate security number
|
||||
const fingerprintGenerator = new window.libsignal.FingerprintGenerator(
|
||||
5200
|
||||
);
|
||||
return fingerprintGenerator.createFor(
|
||||
ourPrimaryKey,
|
||||
ourIdentityKey,
|
||||
conversationId,
|
||||
theirIdentityKey
|
||||
);
|
||||
}
|
||||
|
||||
private async toggleVerification() {
|
||||
const conversationModel = this.props.conversation;
|
||||
|
||||
try {
|
||||
await conversationModel.toggleVerified();
|
||||
this.setState({ isVerified: !this.state.isVerified });
|
||||
|
||||
await conversationModel.getProfiles();
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
if (e.name === 'OutgoingIdentityKeyError') {
|
||||
await this.onSafetyNumberChanged();
|
||||
} else {
|
||||
window.log.error(
|
||||
'failed to toggle verified:',
|
||||
e && e.stack ? e.stack : e
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const keyError = _.some(
|
||||
e.errors,
|
||||
error => error.name === 'OutgoingIdentityKeyError'
|
||||
);
|
||||
if (keyError) {
|
||||
await this.onSafetyNumberChanged();
|
||||
} else {
|
||||
_.forEach(e.errors, error => {
|
||||
window.log.error(
|
||||
'failed to toggle verified:',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private renderSecurityNumber(): Array<JSX.Element> | undefined {
|
||||
// Turns 32813902154726601686003948952478 ...
|
||||
// into 32813 90215 47266 ...
|
||||
const { loading, securityNumber } = this.state;
|
||||
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
const securityNumberChunks = _.chunk(
|
||||
Array.from(securityNumber ?? []),
|
||||
5
|
||||
).map(chunk => chunk.join(''));
|
||||
const securityNumberLines = _.chunk(securityNumberChunks, 4).map(chunk =>
|
||||
chunk.join(' ')
|
||||
);
|
||||
|
||||
const securityNumberElement = securityNumberLines.map(line => (
|
||||
<div key={line}>{line}</div>
|
||||
));
|
||||
return securityNumberElement;
|
||||
}
|
||||
}
|
||||
|
||||
export const SessionKeyVerification = withTheme(SessionKeyVerificationInner);
|
|
@ -29,6 +29,7 @@ import { MemberItem } from '../../conversation/MemberList';
|
|||
import { CaptionEditor } from '../../CaptionEditor';
|
||||
import { DefaultTheme } from 'styled-components';
|
||||
import { ConversationController } from '../../../session/conversations/ConversationController';
|
||||
import { ConversationType } from '../../../state/ducks/conversations';
|
||||
|
||||
export interface ReplyingToMessageProps {
|
||||
convoId: string;
|
||||
|
@ -64,7 +65,8 @@ interface Props {
|
|||
isPrivate: boolean;
|
||||
isKickedFromGroup: boolean;
|
||||
left: boolean;
|
||||
conversationKey: string;
|
||||
selectedConversationKey: string;
|
||||
selectedConversation: ConversationType | undefined;
|
||||
isPublic: boolean;
|
||||
|
||||
quotedMessageProps?: ReplyingToMessageProps;
|
||||
|
@ -186,7 +188,9 @@ export class SessionCompositionBox extends React.Component<Props, State> {
|
|||
}
|
||||
public componentDidUpdate(prevProps: Props, _prevState: State) {
|
||||
// reset the state on new conversation key
|
||||
if (prevProps.conversationKey !== this.props.conversationKey) {
|
||||
if (
|
||||
prevProps.selectedConversationKey !== this.props.selectedConversationKey
|
||||
) {
|
||||
this.setState(getDefaultState(), this.focusCompositionBox);
|
||||
this.lastBumpTypingMessageLength = 0;
|
||||
} else if (
|
||||
|
@ -452,13 +456,14 @@ export class SessionCompositionBox extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
private fetchUsersForClosedGroup(query: any, callback: any) {
|
||||
const conversationModel = ConversationController.getInstance().get(
|
||||
this.props.conversationKey
|
||||
);
|
||||
if (!conversationModel) {
|
||||
const { selectedConversation } = this.props;
|
||||
if (!selectedConversation) {
|
||||
return;
|
||||
}
|
||||
const allPubKeys = selectedConversation.members;
|
||||
if (!allPubKeys || allPubKeys.length === 0) {
|
||||
return;
|
||||
}
|
||||
const allPubKeys = conversationModel.get('members');
|
||||
|
||||
const allMembers = allPubKeys.map(pubKey => {
|
||||
const conv = ConversationController.getInstance().get(pubKey);
|
||||
|
@ -724,7 +729,7 @@ export class SessionCompositionBox extends React.Component<Props, State> {
|
|||
// catching ESC, tab, or whatever which is not typing
|
||||
if (message.length && message.length !== this.lastBumpTypingMessageLength) {
|
||||
const conversationModel = ConversationController.getInstance().get(
|
||||
this.props.conversationKey
|
||||
this.props.selectedConversationKey
|
||||
);
|
||||
if (!conversationModel) {
|
||||
return;
|
||||
|
|
|
@ -10,7 +10,6 @@ import {
|
|||
} from './SessionCompositionBox';
|
||||
|
||||
import { Constants } from '../../../session';
|
||||
import { SessionKeyVerification } from '../SessionKeyVerification';
|
||||
import _ from 'lodash';
|
||||
import { AttachmentUtil, GoogleChrome, UserUtil } from '../../../util';
|
||||
import { MultiDeviceProtocol } from '../../../session/protocols';
|
||||
|
@ -32,6 +31,7 @@ import { getMessageById } from '../../../../js/modules/data';
|
|||
import { pushUnblockToSend } from '../../../session/utils/Toast';
|
||||
import { MessageDetail } from '../../conversation/MessageDetail';
|
||||
import { ConversationController } from '../../../session/conversations';
|
||||
import { PubKey } from '../../../session/types';
|
||||
|
||||
interface State {
|
||||
// Message sending progress
|
||||
|
@ -52,9 +52,6 @@ interface State {
|
|||
showRecordingView: boolean;
|
||||
showOptionsPane: boolean;
|
||||
|
||||
// For displaying `Safety Number`, etc.
|
||||
infoViewState?: 'safetyNumber';
|
||||
|
||||
// if set, the `More Info` of a message screen is shown on top of the conversation.
|
||||
messageDetailShowProps?: any; // FIXME set the type for this
|
||||
|
||||
|
@ -70,8 +67,9 @@ interface State {
|
|||
}
|
||||
|
||||
interface Props {
|
||||
conversationKey: string;
|
||||
conversation: ConversationType;
|
||||
ourPrimary: string;
|
||||
selectedConversationKey: string;
|
||||
selectedConversation?: ConversationType;
|
||||
theme: DefaultTheme;
|
||||
messages: Array<any>;
|
||||
actions: any;
|
||||
|
@ -86,13 +84,7 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
const { conversationKey } = this.props;
|
||||
|
||||
const conversationModel = ConversationController.getInstance().get(
|
||||
conversationKey
|
||||
);
|
||||
|
||||
const unreadCount = conversationModel?.get('unreadCount') || 0;
|
||||
const unreadCount = this.props.selectedConversation?.unreadCount || 0;
|
||||
this.state = {
|
||||
messageProgressVisible: false,
|
||||
sendingProgress: 0,
|
||||
|
@ -104,7 +96,6 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
showOverlay: false,
|
||||
showRecordingView: false,
|
||||
showOptionsPane: false,
|
||||
infoViewState: undefined,
|
||||
stagedAttachments: [],
|
||||
isDraggingFile: false,
|
||||
};
|
||||
|
@ -161,10 +152,10 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
|
||||
public componentDidUpdate(prevProps: Props, prevState: State) {
|
||||
const {
|
||||
conversationKey: newConversationKey,
|
||||
conversation: newConversation,
|
||||
selectedConversationKey: newConversationKey,
|
||||
selectedConversation: newConversation,
|
||||
} = this.props;
|
||||
const { conversationKey: oldConversationKey } = prevProps;
|
||||
const { selectedConversationKey: oldConversationKey } = prevProps;
|
||||
|
||||
// if the convo is valid, and it changed, register for drag events
|
||||
if (
|
||||
|
@ -213,7 +204,6 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
displayScrollToBottomButton: false,
|
||||
showOverlay: false,
|
||||
showRecordingView: false,
|
||||
infoViewState: undefined,
|
||||
stagedAttachments: [],
|
||||
isDraggingFile: false,
|
||||
messageDetailShowProps: undefined,
|
||||
|
@ -248,23 +238,23 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
selectedMessages,
|
||||
isDraggingFile,
|
||||
stagedAttachments,
|
||||
infoViewState,
|
||||
messageDetailShowProps,
|
||||
} = this.state;
|
||||
const selectionMode = !!selectedMessages.length;
|
||||
|
||||
const { conversation, conversationKey, messages } = this.props;
|
||||
const conversationModel = ConversationController.getInstance().get(
|
||||
conversationKey
|
||||
);
|
||||
const {
|
||||
selectedConversation,
|
||||
selectedConversationKey,
|
||||
messages,
|
||||
} = this.props;
|
||||
|
||||
if (!conversationModel || !messages) {
|
||||
if (!selectedConversation || !messages) {
|
||||
// return an empty message view
|
||||
return <MessageView />;
|
||||
}
|
||||
|
||||
const { isRss } = conversation;
|
||||
|
||||
const conversationModel = ConversationController.getInstance().get(
|
||||
selectedConversationKey
|
||||
);
|
||||
// TODO VINCE: OPTIMISE FOR NEW SENDING???
|
||||
const sendMessageFn = (
|
||||
body: any,
|
||||
|
@ -274,6 +264,9 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
groupInvitation: any,
|
||||
otherOptions: any
|
||||
) => {
|
||||
if (!conversationModel) {
|
||||
return;
|
||||
}
|
||||
void conversationModel.sendMessage(
|
||||
body,
|
||||
attachments,
|
||||
|
@ -289,12 +282,11 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
.current as any).scrollTop = this.messageContainerRef.current?.scrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
const shouldRenderRightPanel = !conversationModel.isRss();
|
||||
|
||||
const showSafetyNumber = infoViewState === 'safetyNumber';
|
||||
const showMessageDetails = !!messageDetailShowProps;
|
||||
|
||||
const isPublic = selectedConversation.isPublic || false;
|
||||
|
||||
const isPrivate = selectedConversation.type === 'direct';
|
||||
return (
|
||||
<SessionTheme theme={this.props.theme}>
|
||||
<div className="conversation-header">{this.renderHeader()}</div>
|
||||
|
@ -320,12 +312,9 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
<div
|
||||
className={classNames(
|
||||
'conversation-info-panel',
|
||||
(infoViewState || showMessageDetails) && 'show'
|
||||
showMessageDetails && 'show'
|
||||
)}
|
||||
>
|
||||
{showSafetyNumber && (
|
||||
<SessionKeyVerification conversation={conversationModel} />
|
||||
)}
|
||||
{showMessageDetails && (
|
||||
<MessageDetail {...messageDetailShowProps} />
|
||||
)}
|
||||
|
@ -342,45 +331,41 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
{isDraggingFile && <SessionFileDropzone />}
|
||||
</div>
|
||||
|
||||
{!isRss && (
|
||||
// tslint:disable-next-line: use-simple-attributes
|
||||
<SessionCompositionBox
|
||||
isBlocked={conversation.isBlocked}
|
||||
left={conversation.left}
|
||||
isKickedFromGroup={conversation.isKickedFromGroup}
|
||||
isPrivate={conversation.type === 'direct'}
|
||||
isPublic={conversation.isPublic || false}
|
||||
conversationKey={conversationKey}
|
||||
sendMessage={sendMessageFn}
|
||||
stagedAttachments={stagedAttachments}
|
||||
onMessageSending={this.onMessageSending}
|
||||
onMessageSuccess={this.onMessageSuccess}
|
||||
onMessageFailure={this.onMessageFailure}
|
||||
onLoadVoiceNoteView={this.onLoadVoiceNoteView}
|
||||
onExitVoiceNoteView={this.onExitVoiceNoteView}
|
||||
quotedMessageProps={quotedMessageProps}
|
||||
removeQuotedMessage={() => {
|
||||
void this.replyToMessage(undefined);
|
||||
}}
|
||||
textarea={this.compositionBoxRef}
|
||||
clearAttachments={this.clearAttachments}
|
||||
removeAttachment={this.removeAttachment}
|
||||
onChoseAttachments={this.onChoseAttachments}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
)}
|
||||
<SessionCompositionBox
|
||||
isBlocked={selectedConversation.isBlocked}
|
||||
left={selectedConversation.left}
|
||||
isKickedFromGroup={selectedConversation.isKickedFromGroup}
|
||||
isPrivate={isPrivate}
|
||||
isPublic={isPublic}
|
||||
selectedConversationKey={selectedConversationKey}
|
||||
selectedConversation={selectedConversation}
|
||||
sendMessage={sendMessageFn}
|
||||
stagedAttachments={stagedAttachments}
|
||||
onMessageSending={this.onMessageSending}
|
||||
onMessageSuccess={this.onMessageSuccess}
|
||||
onMessageFailure={this.onMessageFailure}
|
||||
onLoadVoiceNoteView={this.onLoadVoiceNoteView}
|
||||
onExitVoiceNoteView={this.onExitVoiceNoteView}
|
||||
quotedMessageProps={quotedMessageProps}
|
||||
removeQuotedMessage={() => {
|
||||
void this.replyToMessage(undefined);
|
||||
}}
|
||||
textarea={this.compositionBoxRef}
|
||||
clearAttachments={this.clearAttachments}
|
||||
removeAttachment={this.removeAttachment}
|
||||
onChoseAttachments={this.onChoseAttachments}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{shouldRenderRightPanel && (
|
||||
<div
|
||||
className={classNames(
|
||||
'conversation-item__options-pane',
|
||||
showOptionsPane && 'show'
|
||||
)}
|
||||
>
|
||||
<SessionRightPanelWithDetails {...this.getRightPanelProps()} />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={classNames(
|
||||
'conversation-item__options-pane',
|
||||
showOptionsPane && 'show'
|
||||
)}
|
||||
>
|
||||
<SessionRightPanelWithDetails {...this.getRightPanelProps()} />
|
||||
</div>
|
||||
</SessionTheme>
|
||||
);
|
||||
}
|
||||
|
@ -395,33 +380,30 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
public async loadInitialMessages() {
|
||||
const { conversationKey } = this.props;
|
||||
const conversationModel = ConversationController.getInstance().get(
|
||||
conversationKey
|
||||
);
|
||||
if (!conversationModel) {
|
||||
const { selectedConversation, selectedConversationKey } = this.props;
|
||||
|
||||
if (!selectedConversation) {
|
||||
return;
|
||||
}
|
||||
const conversationModel = ConversationController.getInstance().get(
|
||||
selectedConversationKey
|
||||
);
|
||||
const unreadCount = await conversationModel.getUnreadCount();
|
||||
const messagesToFetch = Math.max(
|
||||
Constants.CONVERSATION.DEFAULT_MESSAGE_FETCH_COUNT,
|
||||
unreadCount
|
||||
);
|
||||
this.props.actions.fetchMessagesForConversation({
|
||||
conversationKey,
|
||||
conversationKey: selectedConversationKey,
|
||||
count: messagesToFetch,
|
||||
});
|
||||
}
|
||||
|
||||
public getHeaderProps() {
|
||||
const { conversationKey } = this.props;
|
||||
const {
|
||||
selectedMessages,
|
||||
infoViewState,
|
||||
messageDetailShowProps,
|
||||
} = this.state;
|
||||
const { selectedConversationKey, ourPrimary } = this.props;
|
||||
const { selectedMessages, messageDetailShowProps } = this.state;
|
||||
const conversation = ConversationController.getInstance().getOrThrow(
|
||||
conversationKey
|
||||
selectedConversationKey
|
||||
);
|
||||
const expireTimer = conversation.get('expireTimer');
|
||||
const expirationSettingName = expireTimer
|
||||
|
@ -436,7 +418,6 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
phoneNumber: conversation.getNumber(),
|
||||
profileName: conversation.getProfileName(),
|
||||
avatarPath: conversation.getAvatarPath(),
|
||||
isVerified: conversation.isVerified(),
|
||||
isMe: conversation.isMe(),
|
||||
isClosable: conversation.isClosable(),
|
||||
isBlocked: conversation.isBlocked(),
|
||||
|
@ -444,15 +425,13 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
isPrivate: conversation.isPrivate(),
|
||||
isPublic: conversation.isPublic(),
|
||||
isRss: conversation.isRss(),
|
||||
isAdmin: conversation.isModerator(
|
||||
window.storage.get('primaryDevicePubKey')
|
||||
),
|
||||
isAdmin: conversation.isAdmin(ourPrimary),
|
||||
members,
|
||||
subscriberCount: conversation.get('subscriberCount'),
|
||||
isKickedFromGroup: conversation.get('isKickedFromGroup'),
|
||||
left: conversation.get('left'),
|
||||
expirationSettingName,
|
||||
showBackButton: Boolean(infoViewState || messageDetailShowProps),
|
||||
showBackButton: Boolean(messageDetailShowProps),
|
||||
timerOptions: window.Whisper.ExpirationTimerOptions.map((item: any) => ({
|
||||
name: item.getName(),
|
||||
value: item.get('seconds'),
|
||||
|
@ -468,17 +447,9 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
this.setState({ selectedMessages: [] });
|
||||
},
|
||||
onDeleteContact: () => conversation.deleteContact(),
|
||||
onResetSession: () => {
|
||||
void conversation.endSession();
|
||||
},
|
||||
|
||||
onShowSafetyNumber: () => {
|
||||
this.setState({ infoViewState: 'safetyNumber' });
|
||||
},
|
||||
|
||||
onGoBack: () => {
|
||||
this.setState({
|
||||
infoViewState: undefined,
|
||||
messageDetailShowProps: undefined,
|
||||
});
|
||||
},
|
||||
|
@ -522,16 +493,23 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
public getMessagesListProps() {
|
||||
const { conversation, messages, actions } = this.props;
|
||||
const {
|
||||
selectedConversation,
|
||||
selectedConversationKey,
|
||||
ourPrimary,
|
||||
messages,
|
||||
actions,
|
||||
} = this.props;
|
||||
const { quotedMessageTimestamp, selectedMessages } = this.state;
|
||||
|
||||
return {
|
||||
selectedMessages,
|
||||
conversationKey: conversation.id,
|
||||
ourPrimary,
|
||||
conversationKey: selectedConversationKey,
|
||||
messages,
|
||||
resetSelection: this.resetSelection,
|
||||
quotedMessageTimestamp,
|
||||
conversation,
|
||||
conversation: selectedConversation as ConversationType,
|
||||
selectMessage: this.selectMessage,
|
||||
deleteMessage: this.deleteMessage,
|
||||
fetchMessagesForConversation: actions.fetchMessagesForConversation,
|
||||
|
@ -545,9 +523,9 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
public getRightPanelProps() {
|
||||
const { conversationKey } = this.props;
|
||||
const { selectedConversationKey } = this.props;
|
||||
const conversation = ConversationController.getInstance().getOrThrow(
|
||||
conversationKey
|
||||
selectedConversationKey
|
||||
);
|
||||
const ourPrimary = window.storage.get('primaryDevicePubKey');
|
||||
|
||||
|
@ -555,7 +533,7 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
const isAdmin = conversation.isMediumGroup()
|
||||
? true
|
||||
: conversation.isPublic()
|
||||
? conversation.isModerator(ourPrimary)
|
||||
? conversation.isAdmin(ourPrimary)
|
||||
: false;
|
||||
|
||||
return {
|
||||
|
@ -667,26 +645,32 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
askUserForConfirmation: boolean
|
||||
) {
|
||||
// Get message objects
|
||||
const { conversationKey, messages } = this.props;
|
||||
const {
|
||||
selectedConversationKey,
|
||||
selectedConversation,
|
||||
messages,
|
||||
} = this.props;
|
||||
|
||||
const conversationModel = ConversationController.getInstance().getOrThrow(
|
||||
conversationKey
|
||||
selectedConversationKey
|
||||
);
|
||||
if (!selectedConversation) {
|
||||
window.log.info('No valid selected conversation.');
|
||||
return;
|
||||
}
|
||||
const selectedMessages = messages.filter(message =>
|
||||
messageIds.find(selectedMessage => selectedMessage === message.id)
|
||||
);
|
||||
|
||||
const multiple = selectedMessages.length > 1;
|
||||
|
||||
const isPublic = conversationModel.isPublic();
|
||||
|
||||
// In future, we may be able to unsend private messages also
|
||||
// isServerDeletable also defined in ConversationHeader.tsx for
|
||||
// future reference
|
||||
const isServerDeletable = isPublic;
|
||||
const isServerDeletable = selectedConversation.isPublic;
|
||||
|
||||
const warningMessage = (() => {
|
||||
if (isPublic) {
|
||||
if (selectedConversation.isPublic) {
|
||||
return multiple
|
||||
? window.i18n('deleteMultiplePublicWarning')
|
||||
: window.i18n('deletePublicWarning');
|
||||
|
@ -701,7 +685,7 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
|
||||
// VINCE TODO: MARK TO-DELETE MESSAGES AS READ
|
||||
|
||||
if (isPublic) {
|
||||
if (selectedConversation.isPublic) {
|
||||
// Get our Moderator status
|
||||
const ourDevicePubkey = await UserUtil.getCurrentDevicePubKey();
|
||||
if (!ourDevicePubkey) {
|
||||
|
@ -710,7 +694,7 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
const ourPrimaryPubkey = (
|
||||
await MultiDeviceProtocol.getPrimaryDevice(ourDevicePubkey)
|
||||
).key;
|
||||
const isModerator = conversationModel.isModerator(ourPrimaryPubkey);
|
||||
const isAdmin = conversationModel.isAdmin(ourPrimaryPubkey);
|
||||
const ourNumbers = (await MultiDeviceProtocol.getOurDevices()).map(
|
||||
m => m.key
|
||||
);
|
||||
|
@ -718,7 +702,7 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
ourNumbers.includes(message.attributes.source)
|
||||
);
|
||||
|
||||
if (!isAllOurs && !isModerator) {
|
||||
if (!isAllOurs && !isAdmin) {
|
||||
ToastUtils.pushMessageDeleteForbidden();
|
||||
|
||||
this.setState({ selectedMessages: [] });
|
||||
|
@ -817,14 +801,14 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
// ~~~~~~~~~~~~~~ MESSAGE QUOTE ~~~~~~~~~~~~~~~
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
private async replyToMessage(quotedMessageTimestamp?: number) {
|
||||
if (this.props.conversation.isBlocked) {
|
||||
if (this.props.selectedConversation?.isBlocked) {
|
||||
pushUnblockToSend();
|
||||
return;
|
||||
}
|
||||
if (!_.isEqual(this.state.quotedMessageTimestamp, quotedMessageTimestamp)) {
|
||||
const { messages, conversationKey } = this.props;
|
||||
const { messages, selectedConversationKey } = this.props;
|
||||
const conversationModel = ConversationController.getInstance().getOrThrow(
|
||||
conversationKey
|
||||
selectedConversationKey
|
||||
);
|
||||
|
||||
let quotedMessageProps = null;
|
||||
|
@ -1226,7 +1210,7 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
|
||||
private async updateMemberList() {
|
||||
const allPubKeys = await window.Signal.Data.getPubkeysInPublicConversation(
|
||||
this.props.conversationKey
|
||||
this.props.selectedConversationKey
|
||||
);
|
||||
|
||||
const allMembers = allPubKeys.map((pubKey: string) => {
|
||||
|
|
|
@ -4,7 +4,6 @@ import { Message } from '../../conversation/Message';
|
|||
import { TimerNotification } from '../../conversation/TimerNotification';
|
||||
|
||||
import { SessionScrollButton } from '../SessionScrollButton';
|
||||
import { ResetSessionNotification } from '../../conversation/ResetSessionNotification';
|
||||
import { Constants } from '../../../session';
|
||||
import _ from 'lodash';
|
||||
import { contextMenu } from 'react-contexify';
|
||||
|
@ -12,12 +11,15 @@ import { AttachmentType } from '../../../types/Attachment';
|
|||
import { GroupNotification } from '../../conversation/GroupNotification';
|
||||
import { GroupInvitation } from '../../conversation/GroupInvitation';
|
||||
import { ConversationType } from '../../../state/ducks/conversations';
|
||||
import { MessageModel } from '../../../../js/models/messages';
|
||||
import {
|
||||
MessageModel,
|
||||
MessageRegularProps,
|
||||
} from '../../../../js/models/messages';
|
||||
import { SessionLastSeenIndicator } from './SessionLastSeedIndicator';
|
||||
import { VerificationNotification } from '../../conversation/VerificationNotification';
|
||||
import { ToastUtils } from '../../../session/utils';
|
||||
import { TypingBubble } from '../../conversation/TypingBubble';
|
||||
import { ConversationController } from '../../../session/conversations';
|
||||
import { PubKey } from '../../../session/types';
|
||||
|
||||
interface State {
|
||||
showScrollButton: boolean;
|
||||
|
@ -29,6 +31,7 @@ interface Props {
|
|||
conversationKey: string;
|
||||
messages: Array<MessageModel>;
|
||||
conversation: ConversationType;
|
||||
ourPrimary: string;
|
||||
messageContainerRef: React.RefObject<any>;
|
||||
selectMessage: (messageId: string) => void;
|
||||
deleteMessage: (messageId: string) => void;
|
||||
|
@ -202,7 +205,8 @@ export class SessionMessagesList extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
private renderMessages(messages: Array<MessageModel>) {
|
||||
const multiSelectMode = Boolean(this.props.selectedMessages.length);
|
||||
const { conversation, ourPrimary, selectedMessages } = this.props;
|
||||
const multiSelectMode = Boolean(selectedMessages.length);
|
||||
let currentMessageIndex = 0;
|
||||
const displayUnreadBannerIndex = this.displayUnreadBannerIndex(messages);
|
||||
|
||||
|
@ -212,9 +216,6 @@ export class SessionMessagesList extends React.Component<Props, State> {
|
|||
const messageProps = message.propsForMessage;
|
||||
|
||||
const timerProps = message.propsForTimerNotification;
|
||||
const resetSessionProps = message.propsForResetSessionNotification;
|
||||
const verificationSessionProps =
|
||||
message.propsForVerificationNotification;
|
||||
const propsForGroupInvitation = message.propsForGroupInvitation;
|
||||
|
||||
const groupNotificationProps = message.propsForGroupNotification;
|
||||
|
@ -261,30 +262,6 @@ export class SessionMessagesList extends React.Component<Props, State> {
|
|||
);
|
||||
}
|
||||
|
||||
if (verificationSessionProps) {
|
||||
return (
|
||||
<>
|
||||
<VerificationNotification
|
||||
{...verificationSessionProps}
|
||||
key={message.id}
|
||||
/>
|
||||
{unreadIndicator}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (resetSessionProps) {
|
||||
return (
|
||||
<>
|
||||
<ResetSessionNotification
|
||||
{...resetSessionProps}
|
||||
key={message.id}
|
||||
/>
|
||||
{unreadIndicator}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (timerProps) {
|
||||
return (
|
||||
<>
|
||||
|
@ -293,6 +270,25 @@ export class SessionMessagesList extends React.Component<Props, State> {
|
|||
</>
|
||||
);
|
||||
}
|
||||
if (!messageProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (messageProps.conversationType === 'group') {
|
||||
messageProps.weAreAdmin = conversation.groupAdmins?.includes(
|
||||
ourPrimary
|
||||
);
|
||||
}
|
||||
// a message is deletable if
|
||||
// either we sent it,
|
||||
// or the convo is not a public one (in this case, we will only be able to delete for us)
|
||||
// or the convo is public and we are an admin
|
||||
const isDeletable =
|
||||
messageProps.authorPhoneNumber === this.props.ourPrimary ||
|
||||
!conversation.isPublic ||
|
||||
(conversation.isPublic && !!messageProps.weAreAdmin);
|
||||
|
||||
messageProps.isDeletable = isDeletable;
|
||||
|
||||
// firstMessageOfSeries tells us to render the avatar only for the first message
|
||||
// in a series of messages from the same user
|
||||
|
@ -313,7 +309,7 @@ export class SessionMessagesList extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
private renderMessage(
|
||||
messageProps: any,
|
||||
messageProps: MessageRegularProps,
|
||||
firstMessageOfSeries: boolean,
|
||||
multiSelectMode: boolean,
|
||||
message: MessageModel
|
||||
|
@ -322,7 +318,6 @@ export class SessionMessagesList extends React.Component<Props, State> {
|
|||
!!messageProps?.id &&
|
||||
this.props.selectedMessages.includes(messageProps.id);
|
||||
|
||||
messageProps.i18n = window.i18n;
|
||||
messageProps.selected = selected;
|
||||
messageProps.firstMessageOfSeries = firstMessageOfSeries;
|
||||
|
||||
|
@ -335,7 +330,7 @@ export class SessionMessagesList extends React.Component<Props, State> {
|
|||
void this.props.showMessageDetails(messageDetailsProps);
|
||||
};
|
||||
|
||||
messageProps.onClickAttachment = (attachment: any) => {
|
||||
messageProps.onClickAttachment = (attachment: AttachmentType) => {
|
||||
this.props.onClickAttachment(attachment, messageProps);
|
||||
};
|
||||
messageProps.onDownload = (attachment: AttachmentType) => {
|
||||
|
@ -581,15 +576,6 @@ export class SessionMessagesList extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
const databaseId = targetMessage.id;
|
||||
// const el = this.$(`#${databaseId}`);
|
||||
// if (!el || el.length === 0) {
|
||||
// ToastUtils.pushOriginalNoLongerAvailable();
|
||||
// window.log.info(
|
||||
// `Error: had target message ${id} in messageCollection, but it was not in DOM`
|
||||
// );
|
||||
// return;
|
||||
// }
|
||||
// this probably does not work for us as we need to call getMessages before
|
||||
this.scrollToMessage(databaseId, true);
|
||||
}
|
||||
|
||||
|
@ -608,28 +594,6 @@ export class SessionMessagesList extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
private async onSendAnyway({ contact, message }: any) {
|
||||
const { i18n } = window;
|
||||
window.confirmationDialog({
|
||||
message: i18n('identityKeyErrorOnSend', [
|
||||
contact.getTitle(),
|
||||
contact.getTitle(),
|
||||
]),
|
||||
messageSub: i18n('youMayWishToVerifyContact'),
|
||||
okText: i18n('sendAnyway'),
|
||||
resolve: async () => {
|
||||
await contact.updateVerified();
|
||||
|
||||
if (contact.isUnverified()) {
|
||||
await contact.setVerifiedDefault();
|
||||
}
|
||||
|
||||
const untrusted = await contact.isUntrusted();
|
||||
if (untrusted) {
|
||||
await contact.setApproved();
|
||||
}
|
||||
|
||||
message.resend(contact.id);
|
||||
},
|
||||
});
|
||||
message.resend(contact.id);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,8 +10,6 @@ import {
|
|||
getInviteContactMenuItem,
|
||||
getLeaveGroupMenuItem,
|
||||
getRemoveModeratorsMenuItem,
|
||||
getResetSessionMenuItem,
|
||||
getShowSafetyNumberMenuItem,
|
||||
getUpdateGroupNameMenuItem,
|
||||
} from './Menu';
|
||||
import { TimerOption } from '../../conversation/ConversationHeader';
|
||||
|
@ -40,9 +38,7 @@ export type PropsConversationHeaderMenu = {
|
|||
onUpdateGroupName: () => void;
|
||||
onBlockUser: () => void;
|
||||
onUnblockUser: () => void;
|
||||
onShowSafetyNumber: () => void;
|
||||
onSetDisappearingMessages: (seconds: number) => void;
|
||||
onResetSession: () => void;
|
||||
};
|
||||
|
||||
export const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => {
|
||||
|
@ -70,8 +66,6 @@ export const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => {
|
|||
onUpdateGroupName,
|
||||
onBlockUser,
|
||||
onUnblockUser,
|
||||
onShowSafetyNumber,
|
||||
onResetSession,
|
||||
onSetDisappearingMessages,
|
||||
} = props;
|
||||
|
||||
|
@ -87,22 +81,6 @@ export const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => {
|
|||
onSetDisappearingMessages,
|
||||
window.i18n
|
||||
)}
|
||||
{getShowSafetyNumberMenuItem(
|
||||
isPublic,
|
||||
isRss,
|
||||
isGroup,
|
||||
isMe,
|
||||
onShowSafetyNumber,
|
||||
window.i18n
|
||||
)}
|
||||
{getResetSessionMenuItem(
|
||||
isPublic,
|
||||
isRss,
|
||||
isGroup,
|
||||
isBlocked,
|
||||
onResetSession,
|
||||
window.i18n
|
||||
)}
|
||||
{getBlockMenuItem(
|
||||
isMe,
|
||||
isPrivate,
|
||||
|
|
|
@ -21,24 +21,6 @@ function showMemberMenu(
|
|||
return !isPublic && !isRss && isGroup;
|
||||
}
|
||||
|
||||
function showSafetyNumber(
|
||||
isPublic: boolean,
|
||||
isRss: boolean,
|
||||
isGroup: boolean,
|
||||
isMe: boolean
|
||||
): boolean {
|
||||
return !isPublic && !isRss && !isGroup && !isMe;
|
||||
}
|
||||
|
||||
function showResetSession(
|
||||
isPublic: boolean,
|
||||
isRss: boolean,
|
||||
isGroup: boolean,
|
||||
isBlocked: boolean
|
||||
): boolean {
|
||||
return !isPublic && !isRss && !isGroup && !isBlocked;
|
||||
}
|
||||
|
||||
function showBlock(isMe: boolean, isPrivate: boolean): boolean {
|
||||
return !isMe && isPrivate;
|
||||
}
|
||||
|
@ -289,48 +271,6 @@ export function getShowMemberMenuItem(
|
|||
return null;
|
||||
}
|
||||
|
||||
export function getShowSafetyNumberMenuItem(
|
||||
isPublic: boolean | undefined,
|
||||
isRss: boolean | undefined,
|
||||
isGroup: boolean | undefined,
|
||||
isMe: boolean | undefined,
|
||||
action: any,
|
||||
i18n: LocalizerType
|
||||
): JSX.Element | null {
|
||||
if (
|
||||
showSafetyNumber(
|
||||
Boolean(isPublic),
|
||||
Boolean(isRss),
|
||||
Boolean(isGroup),
|
||||
Boolean(isMe)
|
||||
)
|
||||
) {
|
||||
return <Item onClick={action}>{i18n('showSafetyNumber')}</Item>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getResetSessionMenuItem(
|
||||
isPublic: boolean | undefined,
|
||||
isRss: boolean | undefined,
|
||||
isGroup: boolean | undefined,
|
||||
isBlocked: boolean | undefined,
|
||||
action: any,
|
||||
i18n: LocalizerType
|
||||
): JSX.Element | null {
|
||||
if (
|
||||
showResetSession(
|
||||
Boolean(isPublic),
|
||||
Boolean(isRss),
|
||||
Boolean(isGroup),
|
||||
Boolean(isBlocked)
|
||||
)
|
||||
) {
|
||||
return <Item onClick={action}>{i18n('resetSession')}</Item>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getBlockMenuItem(
|
||||
isMe: boolean | undefined,
|
||||
isPrivate: boolean | undefined,
|
||||
|
|
|
@ -19,6 +19,10 @@ import { mapDispatchToProps } from '../../../state/actions';
|
|||
import { connect } from 'react-redux';
|
||||
import { StateType } from '../../../state/reducer';
|
||||
import { ConversationController } from '../../../session/conversations';
|
||||
import {
|
||||
getConversationLookup,
|
||||
getConversations,
|
||||
} from '../../../state/selectors/conversations';
|
||||
|
||||
export enum SessionSettingCategory {
|
||||
Appearance = 'appearance',
|
||||
|
@ -746,10 +750,8 @@ class SettingsViewInner extends React.Component<SettingsViewProps, State> {
|
|||
}
|
||||
|
||||
const mapStateToProps = (state: StateType) => {
|
||||
const { conversations } = state;
|
||||
|
||||
return {
|
||||
conversations: conversations.conversationLookup,
|
||||
conversations: getConversationLookup(state),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -366,10 +366,6 @@ async function handleUpdateClosedGroupV2(
|
|||
diff.newName
|
||||
) {
|
||||
await ClosedGroupV2.addUpdateMessage(convo, diff, 'incoming');
|
||||
if (diff.joiningMembers?.length) {
|
||||
// send a session request for all the members we do not have a session with
|
||||
await window.libloki.api.sendSessionRequestsToMembers(members);
|
||||
}
|
||||
}
|
||||
|
||||
convo.set('name', name);
|
||||
|
|
|
@ -5,16 +5,13 @@ import { getEnvelopeId } from './common';
|
|||
import { removeFromCache, updateCache } from './cache';
|
||||
import { SignalService } from '../protobuf';
|
||||
import * as Lodash from 'lodash';
|
||||
import * as libsession from '../session';
|
||||
import { handlePairingAuthorisationMessage } from './multidevice';
|
||||
import { MultiDeviceProtocol, SessionProtocol } from '../session/protocols';
|
||||
import { MultiDeviceProtocol } from '../session/protocols';
|
||||
import { PubKey } from '../session/types';
|
||||
|
||||
import { handleSyncMessage } from './syncMessages';
|
||||
import { onError } from './errors';
|
||||
import ByteBuffer from 'bytebuffer';
|
||||
import { BlockedNumberController } from '../util/blockedNumberController';
|
||||
import { GroupUtils, StringUtils } from '../session/utils';
|
||||
import { GroupUtils } from '../session/utils';
|
||||
import { UserUtil } from '../util';
|
||||
import { fromHexToArray, toHex } from '../session/utils/String';
|
||||
import { concatUInt8Array, getSodium } from '../session/crypto';
|
||||
|
@ -221,30 +218,6 @@ export async function isBlocked(number: string) {
|
|||
return BlockedNumberController.isBlockedAsync(number);
|
||||
}
|
||||
|
||||
async function decryptPreKeyWhisperMessage(
|
||||
ciphertext: any,
|
||||
sessionCipher: any,
|
||||
address: any
|
||||
): Promise<ArrayBuffer> {
|
||||
const padded = await sessionCipher.decryptPreKeyWhisperMessage(ciphertext);
|
||||
|
||||
try {
|
||||
return unpad(padded);
|
||||
} catch (e) {
|
||||
if (e.message === 'Unknown identity key') {
|
||||
// create an error that the UI will pick up and ask the
|
||||
// user if they want to re-negotiate
|
||||
const buffer = ByteBuffer.wrap(ciphertext);
|
||||
throw new window.textsecure.IncomingIdentityKeyError(
|
||||
address.toString(),
|
||||
buffer.toArrayBuffer(),
|
||||
e.identityKey
|
||||
);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async function decryptUnidentifiedSender(
|
||||
envelope: EnvelopePlus,
|
||||
ciphertext: ArrayBuffer
|
||||
|
@ -277,46 +250,36 @@ async function decryptUnidentifiedSender(
|
|||
|
||||
async function doDecrypt(
|
||||
envelope: EnvelopePlus,
|
||||
ciphertext: ArrayBuffer,
|
||||
address: any
|
||||
ciphertext: ArrayBuffer
|
||||
): Promise<ArrayBuffer | null> {
|
||||
const { textsecure, libloki } = window;
|
||||
|
||||
const lokiSessionCipher = new libloki.crypto.LokiSessionCipher(
|
||||
textsecure.storage.protocol,
|
||||
address
|
||||
);
|
||||
if (ciphertext.byteLength === 0) {
|
||||
throw new Error('Received an empty envelope.'); // Error.noData
|
||||
}
|
||||
|
||||
switch (envelope.type) {
|
||||
case SignalService.Envelope.Type.CIPHERTEXT:
|
||||
window.log.info('message from', getEnvelopeId(envelope));
|
||||
return lokiSessionCipher.decryptWhisperMessage(ciphertext).then(unpad);
|
||||
// Only UNIDENTIFIED_SENDER and CLOSED_GROUP_CIPHERTEXT are supported
|
||||
case SignalService.Envelope.Type.CLOSED_GROUP_CIPHERTEXT:
|
||||
return decryptForClosedGroupV2(envelope, ciphertext);
|
||||
case SignalService.Envelope.Type.FALLBACK_MESSAGE: {
|
||||
window.log.info('Fallback message from ', envelope.source);
|
||||
|
||||
const fallBackSessionCipher = new libloki.crypto.FallBackSessionCipher(
|
||||
address
|
||||
);
|
||||
|
||||
return fallBackSessionCipher.decrypt(ciphertext).then(unpad);
|
||||
}
|
||||
case SignalService.Envelope.Type.PREKEY_BUNDLE:
|
||||
window.log.info('prekey message from', getEnvelopeId(envelope));
|
||||
return decryptPreKeyWhisperMessage(
|
||||
ciphertext,
|
||||
lokiSessionCipher,
|
||||
address
|
||||
);
|
||||
case SignalService.Envelope.Type.UNIDENTIFIED_SENDER: {
|
||||
return decryptUnidentifiedSender(envelope, ciphertext);
|
||||
}
|
||||
case SignalService.Envelope.Type.PREKEY_BUNDLE: {
|
||||
window.log.info('prekey message from', getEnvelopeId(envelope));
|
||||
throw new Error('Envelope.Type.PREKEY_BUNDLE cannot happen anymore');
|
||||
}
|
||||
case SignalService.Envelope.Type.CIPHERTEXT: {
|
||||
window.log.info('CIPHERTEXT envelope from', getEnvelopeId(envelope));
|
||||
throw new Error('Envelope.Type.CIPHERTEXT cannot happen anymore');
|
||||
}
|
||||
case SignalService.Envelope.Type.FALLBACK_MESSAGE: {
|
||||
window.log.info(
|
||||
'FALLBACK_MESSAGE envelope from',
|
||||
getEnvelopeId(envelope)
|
||||
);
|
||||
throw new Error('Envelope.Type.FALLBACK_MESSAGE cannot happen anymore');
|
||||
}
|
||||
default:
|
||||
throw new Error('Unknown message type');
|
||||
throw new Error(`Unknown message type:${envelope.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -325,17 +288,10 @@ async function decrypt(
|
|||
envelope: EnvelopePlus,
|
||||
ciphertext: ArrayBuffer
|
||||
): Promise<any> {
|
||||
const { textsecure, libsignal, log } = window;
|
||||
|
||||
// Envelope.source will be null on UNIDENTIFIED_SENDER
|
||||
// Don't use it there!
|
||||
const address = new libsignal.SignalProtocolAddress(
|
||||
envelope.source,
|
||||
envelope.sourceDevice
|
||||
);
|
||||
const { textsecure } = window;
|
||||
|
||||
try {
|
||||
const plaintext = await doDecrypt(envelope, ciphertext, address);
|
||||
const plaintext = await doDecrypt(envelope, ciphertext);
|
||||
|
||||
if (!plaintext) {
|
||||
await removeFromCache(envelope);
|
||||
|
@ -351,34 +307,7 @@ async function decrypt(
|
|||
|
||||
return plaintext;
|
||||
} catch (error) {
|
||||
let errorToThrow = error;
|
||||
|
||||
const noSession =
|
||||
error &&
|
||||
(error.message.indexOf('No record for device') === 0 ||
|
||||
error.message.indexOf('decryptWithSessionList: list is empty') === 0);
|
||||
|
||||
if (error && error.message === 'Unknown identity key') {
|
||||
// create an error that the UI will pick up and ask the
|
||||
// user if they want to re-negotiate
|
||||
const buffer = ByteBuffer.wrap(ciphertext);
|
||||
errorToThrow = new textsecure.IncomingIdentityKeyError(
|
||||
address.toString(),
|
||||
buffer.toArrayBuffer(),
|
||||
error.identityKey
|
||||
);
|
||||
} else if (!noSession) {
|
||||
// We want to handle "no-session" error, not re-throw it
|
||||
throw error;
|
||||
}
|
||||
const ev: any = new Event('error');
|
||||
ev.error = errorToThrow;
|
||||
ev.proto = envelope;
|
||||
ev.confirm = removeFromCache.bind(null, envelope);
|
||||
|
||||
const returnError = async () => Promise.reject(errorToThrow);
|
||||
|
||||
onError(ev).then(returnError, returnError);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -445,20 +374,12 @@ export async function innerHandleContentMessage(
|
|||
);
|
||||
}
|
||||
}
|
||||
const { FALLBACK_MESSAGE } = SignalService.Envelope.Type;
|
||||
|
||||
await ConversationController.getInstance().getOrCreateAndWait(
|
||||
envelope.source,
|
||||
'private'
|
||||
);
|
||||
|
||||
if (envelope.type !== FALLBACK_MESSAGE) {
|
||||
const device = new PubKey(envelope.source);
|
||||
|
||||
await SessionProtocol.onSessionEstablished(device);
|
||||
await libsession.getMessageQueue().processPending(device);
|
||||
}
|
||||
|
||||
if (content.pairingAuthorisation) {
|
||||
await handlePairingAuthorisationMessage(
|
||||
envelope,
|
||||
|
@ -590,7 +511,7 @@ async function handleTypingMessage(
|
|||
|
||||
const typingMessage = iTypingMessage as SignalService.TypingMessage;
|
||||
|
||||
const { timestamp, groupId, action } = typingMessage;
|
||||
const { timestamp, action } = typingMessage;
|
||||
const { source } = envelope;
|
||||
|
||||
await removeFromCache(envelope);
|
||||
|
@ -627,11 +548,9 @@ async function handleTypingMessage(
|
|||
const started = action === SignalService.TypingMessage.Action.STARTED;
|
||||
|
||||
if (conversation) {
|
||||
const senderDevice = 1;
|
||||
conversation.notifyTyping({
|
||||
isTyping: started,
|
||||
sender: source,
|
||||
senderDevice,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue