diff --git a/.eslintignore b/.eslintignore index 6ef379a1c..3335487c3 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,6 +2,7 @@ build/** components/** coverage/** dist/** +mnemonic_languages/** # these aren't ready yet, pulling files in one-by-one libtextsecure/test/*.js diff --git a/.prettierignore b/.prettierignore index e6db8458c..c75a6c5ce 100644 --- a/.prettierignore +++ b/.prettierignore @@ -28,6 +28,7 @@ js/libsignal-protocol-worker.js libtextsecure/libsignal-protocol.js libtextsecure/test/blanket_mocha.js test/blanket_mocha.js +mnemonic_languages/** # Test fixtures test/fixtures.js diff --git a/Gruntfile.js b/Gruntfile.js index b05512dac..a1ccc8082 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -87,9 +87,7 @@ module.exports = grunt => { dest: 'js/libtextsecure.js', }, libloki: { - src: [ - 'libloki/libloki-protocol.js', - ], + src: ['libloki/libloki-protocol.js'], dest: 'js/libloki.js', }, libtextsecuretest: { diff --git a/LOKI-NOTES.md b/LOKI-NOTES.md index 65ba4f873..18d4aefad 100644 --- a/LOKI-NOTES.md +++ b/LOKI-NOTES.md @@ -3,4 +3,4 @@ Run `window.getAccountManager().addMockContact()` in the debugger console. This will print the fake contact public key. Behind the scenes, this also emulates that we're already received the contact's prekeys by generating them and saving them in our db. Copy/paste that public key in the search bar to start chatting. -The messages should have a "v" tick to mark that the message was correctly sent (you need to run the httpserver from /mockup_servers) \ No newline at end of file +The messages should have a "v" tick to mark that the message was correctly sent (you need to run the httpserver from /mockup_servers) diff --git a/README.md b/README.md index 63e9f5082..861976c41 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ -Loki Messenger -========================== +# Loki Messenger + Loki Messenger allows for truly decentralized and end to end and private encrypted chats, Loki Messenger is built to handle both online and fully Asynchronous offline messages , Loki messenger implements the Signal protocol for message encryption, Our Client interface is a fork of [Signal Messenger](https://signal.org/). All communication that passes through Loki messenger is routed through [Lokinet](https://github.com/loki-project/loki-network). ## Summary Loki messenger integrates directly with Loki Service Nodes, which are a set of distributed, decentralized and Sybil resistant nodes. Service Nodes act as both federated servers which store messages offline, and a set of nodes which allow for mixnet functionality obfuscating users IP Addresses. For a full understanding of how Loki messenger works, read the [Loki whitepaper](https://loki.network/wp-content/uploads/2018/08/LokiWhitepaperV3_1.pdf) -**Online Messages** +**Online Messages** If Alice and Bob are both online they can simply resolve each others public keys, to introduction sets, this functionality is handled by interfacing with Lokinet. With the appropriate introduction sets Alice and Bob can create a path and using onion routing pass messages through the Loki network without giving away personally identifiable information like their IP address. @@ -18,7 +18,7 @@ Offline messaging uses Swarms, given any users public key the user can resolve a Spam protections for Loki Messenger are based on a Proof of Work which is attached to any message that exceeds a default size or Time To Live, this process is discussed further in the [Loki whitepaper](https://loki.network/wp-content/uploads/2018/08/LokiWhitepaperV3_1.pdf). -## Want to Contribute? Found a Bug or Have a feature request? +## Want to Contribute? Found a Bug or Have a feature request? Please search for any [existing issues](https://github.com/loki-project/loki-messenger/issues) that describe your bugs in order to avoid duplicate submissions. Submissions can be made by making a pull request to our development branch, if you don't know where to start contributing , try reading the Github issues page for ideas. diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 4b070aa71..5f03599fa 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -823,11 +823,13 @@ }, "sendMessageDisabled": { "message": "Waiting for friend request approval", - "description": "Placeholder text in the message entry field when it is disabled while we are waiting for a friend request approval" + "description": + "Placeholder text in the message entry field when it is disabled while we are waiting for a friend request approval" }, "sendMessageFriendRequest": { "message": "Hi there! This is !", - "description": "Placeholder text in the message entry field when it is the first message sent to that contact" + "description": + "Placeholder text in the message entry field when it is the first message sent to that contact" }, "groupMembers": { "message": "Group members" diff --git a/js/models/conversations.js b/js/models/conversations.js index 099568192..c618baa5e 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -57,7 +57,7 @@ unreadCount: 0, verified: textsecure.storage.protocol.VerifiedStatus.DEFAULT, keyExchangeCompleted: false, - friendRequestStatus: { allowSending: true, unlockTimestamp: null } + friendRequestStatus: { allowSending: true, unlockTimestamp: null }, }; }, @@ -352,9 +352,7 @@ }, isKeyExchangeCompleted() { if (!this.isPrivate()) { - throw new Error( - 'isKeyExchangeCompleted not implemented for groups' - ); + throw new Error('isKeyExchangeCompleted not implemented for groups'); } if (this.isMe()) { @@ -365,11 +363,9 @@ }, setKeyExchangeCompleted(completed) { if (typeof completed !== 'boolean') { - throw new Error( - 'setKeyExchangeCompleted expects a bool' - ); + throw new Error('setKeyExchangeCompleted expects a bool'); } - + this.set({ keyExchangeCompleted: completed }); }, getFriendRequestStatus() { @@ -386,7 +382,10 @@ const friendRequestStatus = this.getFriendRequestStatus(); if (friendRequestStatus) { if (!friendRequestStatus.allowSending) { - const delay = Math.max(friendRequestStatus.unlockTimestamp - Date.now(), 0); + const delay = Math.max( + friendRequestStatus.unlockTimestamp - Date.now(), + 0 + ); setTimeout(() => { this.onFriendRequestTimedOut(); }, delay); @@ -394,14 +393,14 @@ } }, onFriendRequestAccepted() { - this.save({ friendRequestStatus: null }) + this.save({ friendRequestStatus: null }); this.trigger('disable:input', false); this.trigger('change:placeholder', 'chat'); }, onFriendRequestTimedOut() { - let friendRequestStatus = this.getFriendRequestStatus(); + const friendRequestStatus = this.getFriendRequestStatus(); friendRequestStatus.allowSending = true; - this.save({ friendRequestStatus }) + this.save({ friendRequestStatus }); this.trigger('disable:input', false); this.trigger('change:placeholder', 'friend-request'); }, @@ -414,14 +413,16 @@ } friendRequestStatus.allowSending = false; - const delayMs = 100 * 1000 ;//(60 * 60 * 1000 * friendRequestLockDuration); + const delayMs = 60 * 60 * 1000 * friendRequestLockDuration; friendRequestStatus.unlockTimestamp = Date.now() + delayMs; this.trigger('disable:input', true); this.trigger('change:placeholder', 'disabled'); - setTimeout(() => { this.onFriendRequestTimedOut() }, delayMs); + setTimeout(() => { + this.onFriendRequestTimedOut(); + }, delayMs); - this.save({ friendRequestStatus }) + this.save({ friendRequestStatus }); }, isUnverified() { if (this.isPrivate()) { @@ -672,8 +673,8 @@ validateNumber() { if (this.isPrivate()) { - if (this.id.length == (33 * 2)) // 33 bytes in hex - { + if (this.id.length === 33 * 2) { + // 33 bytes in hex this.set({ id: this.id }); return null; } diff --git a/js/modules/loki_message_api.js b/js/modules/loki_message_api.js index 283f86bae..5506f482a 100644 --- a/js/modules/loki_message_api.js +++ b/js/modules/loki_message_api.js @@ -1,3 +1,5 @@ +/* global log */ + const fetch = require('node-fetch'); const is = require('@sindresorhus/is'); const { fork } = require('child_process'); @@ -34,12 +36,12 @@ function initialize({ url }) { }); // Handle child process error (should never happen) - child.on('error', (err) => { + child.on('error', err => { reject(err); }); // Callback to receive PoW result - child.on('message', (msg) => { + child.on('message', msg => { if (msg.err) { reject(msg.err); } else { @@ -47,7 +49,6 @@ function initialize({ url }) { resolve(msg.nonce); } }); - }); } @@ -57,11 +58,11 @@ function initialize({ url }) { let nonce; try { nonce = await getPoWNonce(timestamp, ttl, pubKey, data); - } catch(err) { + } catch (err) { // Something went horribly wrong // TODO: Handle gracefully - console.log("Error computing PoW"); - }; + log.error('Error computing PoW'); + } const options = { url: `${url}/send_message`, @@ -69,7 +70,7 @@ function initialize({ url }) { responseType: undefined, timeout: undefined, }; - + log.info(options.type, options.url); const fetchOptions = { @@ -88,8 +89,7 @@ function initialize({ url }) { let response; try { response = await fetch(options.url, fetchOptions); - } - catch(e) { + } catch (e) { log.error(options.type, options.url, 0, 'Error'); throw HTTPError('fetch error', 0, e.toString()); } @@ -109,15 +109,10 @@ function initialize({ url }) { if (response.status >= 0 && response.status < 400) { log.info(options.type, options.url, response.status, 'Success'); return [result, response.status]; - } else { - log.error(options.type, options.url, response.status, 'Error'); - throw HTTPError( - 'sendMessage: error response', - response.status, - result - ); } - }; + log.error(options.type, options.url, response.status, 'Error'); + throw HTTPError('sendMessage: error response', response.status, result); + } } } diff --git a/js/modules/migrations/migrations_0_database_with_attachment_data.js b/js/modules/migrations/migrations_0_database_with_attachment_data.js index ab8e98157..a39146d68 100644 --- a/js/modules/migrations/migrations_0_database_with_attachment_data.js +++ b/js/modules/migrations/migrations_0_database_with_attachment_data.js @@ -44,18 +44,32 @@ const migrations = [ transaction.db.createObjectStore('sessions'); transaction.db.createObjectStore('identityKeys'); - const preKeys = transaction.db.createObjectStore('preKeys', { keyPath: 'id'}); + const preKeys = transaction.db.createObjectStore('preKeys', { + keyPath: 'id', + }); preKeys.createIndex('recipient', 'recipient', { unique: true }); transaction.db.createObjectStore('signedPreKeys'); transaction.db.createObjectStore('items'); - - const contactPreKeys = transaction.db.createObjectStore('contactPreKeys', { keyPath: 'id', autoIncrement : true }); - contactPreKeys.createIndex('identityKeyString', 'identityKeyString', { unique: false }); + + const contactPreKeys = transaction.db.createObjectStore( + 'contactPreKeys', + { keyPath: 'id', autoIncrement: true } + ); + contactPreKeys.createIndex('identityKeyString', 'identityKeyString', { + unique: false, + }); contactPreKeys.createIndex('keyId', 'keyId', { unique: false }); - - const contactSignedPreKeys = transaction.db.createObjectStore('contactSignedPreKeys', { keyPath: 'id', autoIncrement : true }); - contactSignedPreKeys.createIndex('identityKeyString', 'identityKeyString', { unique: false }); + + const contactSignedPreKeys = transaction.db.createObjectStore( + 'contactSignedPreKeys', + { keyPath: 'id', autoIncrement: true } + ); + contactSignedPreKeys.createIndex( + 'identityKeyString', + 'identityKeyString', + { unique: false } + ); contactSignedPreKeys.createIndex('keyId', 'keyId', { unique: false }); window.log.info('creating debug log'); diff --git a/js/modules/web_api.js b/js/modules/web_api.js index 69ee799b4..84a9cdaba 100644 --- a/js/modules/web_api.js +++ b/js/modules/web_api.js @@ -156,7 +156,7 @@ function _createSocket(url, { certificateAuthority, proxyUrl, signature }) { let headers; if (signature) { headers = { - signature + signature, }; } diff --git a/js/signal_protocol_store.js b/js/signal_protocol_store.js index b0b8df694..427781622 100644 --- a/js/signal_protocol_store.js +++ b/js/signal_protocol_store.js @@ -184,16 +184,18 @@ database: Whisper.Database, model: ContactPreKey, fetchBy(filter) { - return this.fetch({ conditions: filter, }); + return this.fetch({ conditions: filter }); }, }); - const ContactSignedPreKey = Model.extend({ storeName: 'contactSignedPreKeys' }); + const ContactSignedPreKey = Model.extend({ + storeName: 'contactSignedPreKeys', + }); const ContactSignedPreKeyCollection = Backbone.Collection.extend({ storeName: 'contactSignedPreKeys', database: Whisper.Database, model: ContactSignedPreKey, fetchBy(filter) { - return this.fetch({ conditions: filter, }); + return this.fetch({ conditions: filter }); }, }); @@ -217,7 +219,7 @@ item.fetch().then(() => { resolve(item.get('value')); }, reject); - });*/ + }); */ }, /* Returns a prekeypair object or undefined */ @@ -244,7 +246,10 @@ return new Promise(resolve => { prekey.fetch().then( () => { - window.log.info('Successfully fetched prekey for recipient :', contactIdentityKeyString); + window.log.info( + 'Successfully fetched prekey for recipient :', + contactIdentityKeyString + ); resolve({ pubKey: prekey.get('publicKey'), privKey: prekey.get('privateKey'), @@ -280,19 +285,25 @@ loadContactPreKeys(filters) { const contactPreKeys = new ContactPreKeyCollection(); return new Promise((resolve, reject) => { - contactPreKeys.fetchBy(filters).then(() => { - resolve( - contactPreKeys.map(prekey => ({ - id: prekey.get('id'), - keyId: prekey.get('keyId'), - publicKey: prekey.get('publicKey'), - identityKeyString: prekey.get('identityKeyString'), - })) - ); - }).fail(e => { - window.log.error('Failed to fetch signed prekey with filters', filters); - reject(e); - }); + contactPreKeys + .fetchBy(filters) + .then(() => { + resolve( + contactPreKeys.map(prekey => ({ + id: prekey.get('id'), + keyId: prekey.get('keyId'), + publicKey: prekey.get('publicKey'), + identityKeyString: prekey.get('identityKeyString'), + })) + ); + }) + .fail(e => { + window.log.error( + 'Failed to fetch signed prekey with filters', + filters + ); + reject(e); + }); }); }, storeContactPreKey(pubKey, preKey) { @@ -377,22 +388,28 @@ loadContactSignedPreKeys(filters) { const contactSignedPreKeys = new ContactSignedPreKeyCollection(); return new Promise((resolve, reject) => { - contactSignedPreKeys.fetchBy(filters).then(() => { - resolve( - contactSignedPreKeys.map(prekey => ({ - id: prekey.get('id'), - identityKeyString: prekey.get('identityKeyString'), - publicKey: prekey.get('publicKey'), - signature: prekey.get('signature'), - created_at: prekey.get('created_at'), - keyId: prekey.get('keyId'), - confirmed: prekey.get('confirmed'), - })) - ); - }).fail(e => { - window.log.error('Failed to fetch signed prekey with filters', filters); - reject(e); - }); + contactSignedPreKeys + .fetchBy(filters) + .then(() => { + resolve( + contactSignedPreKeys.map(prekey => ({ + id: prekey.get('id'), + identityKeyString: prekey.get('identityKeyString'), + publicKey: prekey.get('publicKey'), + signature: prekey.get('signature'), + created_at: prekey.get('created_at'), + keyId: prekey.get('keyId'), + confirmed: prekey.get('confirmed'), + })) + ); + }) + .fail(e => { + window.log.error( + 'Failed to fetch signed prekey with filters', + filters + ); + reject(e); + }); }); }, loadContactSignedPreKey(pubKey) { diff --git a/js/views/app_view.js b/js/views/app_view.js index 6304698ec..36320cae4 100644 --- a/js/views/app_view.js +++ b/js/views/app_view.js @@ -181,7 +181,7 @@ showFriendRequest({ pubKey, message, accept, decline }) { const dialog = new Whisper.ConfirmationDialogView({ title: `${pubKey} sent you a friend request:`, - message: message, + message, okText: 'Accept', cancelText: 'Decline', resolve: accept, diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index abf8fa2dd..35099e78b 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -136,7 +136,7 @@ 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 @@ -168,7 +168,7 @@ color: this.model.getColor(), avatarPath: this.model.getAvatarPath(), isVerified: this.model.isVerified(), - isKeysPending: this.model.isKeyExchangeCompleted() == false, + isKeysPending: this.model.isKeyExchangeCompleted() === false, isMe: this.model.isMe(), isGroup: !this.model.isPrivate(), expirationSettingName, @@ -286,7 +286,9 @@ }, onDisableInput(disable) { - this.$('button.emoji, button.microphone, button.paperclip, .send-message').attr('disabled', disable); + this.$( + 'button.emoji, button.microphone, button.paperclip, .send-message' + ).attr('disabled', disable); }, onChangePlaceholder(type) { diff --git a/js/views/inbox_view.js b/js/views/inbox_view.js index fdac68103..72abb36f0 100644 --- a/js/views/inbox_view.js +++ b/js/views/inbox_view.js @@ -5,6 +5,7 @@ /* global Whisper: false */ /* global textsecure: false */ /* global Signal: false */ +/* global StringView: false */ // eslint-disable-next-line func-names (function() { @@ -182,7 +183,7 @@ selectAContact: i18n('selectAContact'), searchForPeopleOrGroups: i18n('searchForPeopleOrGroups'), settings: i18n('settings'), - identityKey: StringView.arrayBufferToHex(identityKey) + identityKey: StringView.arrayBufferToHex(identityKey), }; }, events: { diff --git a/js/views/standalone_registration_view.js b/js/views/standalone_registration_view.js index e97da6a5b..d10bec1b2 100644 --- a/js/views/standalone_registration_view.js +++ b/js/views/standalone_registration_view.js @@ -26,12 +26,13 @@ this.$('#error').hide(); window.mnemonic.get_languages().forEach(language => { - this.$('#mnemonic-language').append($('