Merge pull request #1442 from oxen-io/clearnet

Bump to v1.4.6
This commit is contained in:
Audric Ackermann 2021-01-20 15:15:42 +11:00 committed by GitHub
commit 1d0b4c9572
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
149 changed files with 35633 additions and 46306 deletions

View file

@ -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',

View file

@ -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"

View file

@ -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'>

View file

@ -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'>

View file

@ -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]);

View file

@ -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();

View file

@ -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);
});
});
},
};
})();

View file

@ -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;

View file

@ -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');

View file

@ -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;
}

View file

@ -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;
},

View file

@ -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();
}
}

View file

@ -8,7 +8,6 @@ export type IdentityKey = {
id: string;
publicKey: ArrayBuffer;
firstUse: boolean;
verified: number;
nonblockingApproval: boolean;
secretKey?: string; // found in medium groups
};

View file

@ -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

View file

@ -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;
}

View file

@ -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,
};

View file

@ -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;
}>;
}

View file

@ -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,
};

View file

@ -1,9 +0,0 @@
import { SecretSessionCipherConstructor } from './metadata/SecretSessionCipher';
interface Metadata {
SecretSessionCipher: SecretSessionCipherConstructor;
}
export interface SignalInterface {
Metadata: Metadata;
}

View file

@ -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,

View file

@ -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,
});
}
}
}

View file

@ -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);
},
};
})();

View file

@ -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;
})();

View file

@ -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();

View file

@ -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

View file

@ -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;

View file

@ -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
View file

@ -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;

View file

@ -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,

View file

@ -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();
};
})();

View file

@ -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);
});
});
});

View file

@ -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. -->

View file

@ -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);
});
});
});

View file

@ -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) {

View file

@ -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');

View file

@ -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;

View file

@ -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;

View file

@ -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();
});
};

View file

@ -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

View file

@ -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);

View file

@ -4,7 +4,6 @@
(function() {
window.textsecure = window.textsecure || {};
window.textsecure.storage = window.textsecure.storage || {};
textsecure.storage.protocol = new SignalProtocolStore();
textsecure.ProvisioningCipher = libsignal.ProvisioningCipher;

View file

@ -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,
};

View file

@ -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');
},
};
})();

View file

@ -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,
};
})();

View file

@ -17,9 +17,7 @@
"dcodeIO": true,
"getString": true,
"hexToArrayBuffer": true,
"MockServer": true,
"MockSocket": true,
"PROTO_ROOT": true,
"stringToArrayBuffer": true,
}
}
}

View file

@ -59,5 +59,3 @@ window.hexToArrayBuffer = str => {
}
return ret;
};
window.MockSocket.prototype.addEventListener = () => null;

View file

@ -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);
});
});
});

View file

@ -1,13 +0,0 @@
window.setImmediate = window.nodeSetImmediate;
const fakeCall = () => Promise.resolve();
const fakeAPI = {
getAttachment: fakeCall,
putAttachment: fakeCall,
putAvatar: fakeCall,
};
window.WebAPI = {
connect: () => fakeAPI,
};

View file

@ -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);
});
});
});

View file

@ -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);
});
});
});
});

View file

@ -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;
},
};

View file

@ -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>

View file

@ -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,
);
});
});
});

View file

@ -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');
});
});
});
});

View file

@ -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, []);
});
});

View file

@ -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);
});
});
});

View file

@ -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();
});
});

View file

@ -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);
},
};
})();

View file

@ -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()

View file

@ -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.

View file

@ -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()

View file

@ -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",

View file

@ -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 */

View file

@ -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;

View file

@ -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;
}
}
/* ************************************* */
/* ************************************* */
/* ************************************* */

View file

@ -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;

View file

@ -13,8 +13,6 @@ module.exports = {
dcodeIO: true,
getString: true,
hexToArrayBuffer: true,
MockServer: true,
MockSocket: true,
PROTO_ROOT: true,
stringToArrayBuffer: true,
},

View file

@ -509,7 +509,6 @@ describe('Backup', () => {
timestamp: 1524185933350,
type: 'private',
unreadCount: 0,
verified: 0,
sealedSender: 0,
version: 2,
};

View file

@ -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(

View file

@ -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>

View file

@ -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);
}
});

View file

@ -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', () => {

View file

@ -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());
});
});

File diff suppressed because it is too large Load diff

View file

@ -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);
});
});
});

View file

@ -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>
);

View file

@ -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} />
</>
);
}

View file

@ -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')

View file

@ -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;

View file

@ -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>

View file

@ -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}

View file

@ -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>
);
}
}

View file

@ -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>
);
}
}

View file

@ -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>
);
};

View file

@ -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}
/>

View file

@ -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) {

View file

@ -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),
};
};

View file

@ -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'

View file

@ -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,

View file

@ -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(),

View file

@ -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);

View file

@ -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;

View file

@ -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) => {

View file

@ -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);
}
}

View file

@ -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,

View file

@ -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,

View file

@ -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),
};
};

View file

@ -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);

View file

@ -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