diff --git a/js/modules/loki_message_api.js b/js/modules/loki_message_api.js index ebc4917a5..0caaf8130 100644 --- a/js/modules/loki_message_api.js +++ b/js/modules/loki_message_api.js @@ -4,163 +4,152 @@ const fetch = require('node-fetch'); const is = require('@sindresorhus/is'); const { fork } = require('child_process'); -module.exports = { - initialize, -}; +function getPoWNonce(timestamp, ttl, pubKey, data) { + return new Promise((resolve, reject) => { + // Create forked node process to calculate PoW without blocking main process + const child = fork('./libloki/proof-of-work.js'); + + // Send data required for PoW to child process + child.send({ + timestamp, + ttl, + pubKey, + data, + }); + + // Handle child process error (should never happen) + child.on('error', err => { + reject(err); + }); + + // Callback to receive PoW result + child.on('message', msg => { + if (msg.err) { + reject(msg.err); + } else { + child.kill(); + resolve(msg.nonce); + } + }); + }); +} + +class LokiServer { -function initialize({ url }) { - if (!is.string(url)) { - throw new Error('WebAPI.initialize: Invalid server url'); + constructor({ url }) { + if (!is.string(url)) { + throw new Error('WebAPI.initialize: Invalid server url'); + } + this.url = url; } - return { - connect, - }; + async sendMessage(pubKey, data, ttl) { + const data64 = dcodeIO.ByteBuffer.wrap(data).toString('base64'); + + const timestamp = Math.floor(Date.now() / 1000); + // Nonce is returned as a base64 string to include in header + let nonce; + try { + nonce = await getPoWNonce(timestamp, ttl, pubKey, data64); + } catch (err) { + // Something went horribly wrong + // TODO: Handle gracefully + log.error('Error computing PoW'); + } + + const options = { + url: `${this.url}/store`, + type: 'POST', + responseType: undefined, + timeout: undefined, + }; - function connect() { - return { - sendMessage, - retrieveMessages, + log.info(options.type, options.url); + + const fetchOptions = { + method: options.type, + body: data64, + headers: { + 'X-Loki-pow-nonce': nonce, + 'X-Loki-timestamp': timestamp.toString(), + 'X-Loki-ttl': ttl.toString(), + 'X-Loki-recipient': pubKey, + }, + timeout: options.timeout, }; - function getPoWNonce(timestamp, ttl, pubKey, data) { - return new Promise((resolve, reject) => { - // Create forked node process to calculate PoW without blocking main process - const child = fork('./libloki/proof-of-work.js'); - - // Send data required for PoW to child process - child.send({ - timestamp, - ttl, - pubKey, - data, - }); - - // Handle child process error (should never happen) - child.on('error', err => { - reject(err); - }); - - // Callback to receive PoW result - child.on('message', msg => { - if (msg.err) { - reject(msg.err); - } else { - child.kill(); - resolve(msg.nonce); - } - }); - }); + let response; + try { + response = await fetch(options.url, fetchOptions); + } catch (e) { + log.error(options.type, options.url, 0, 'Error'); + throw HTTPError('fetch error', 0, e.toString()); } - async function retrieveMessages(pubKey) { - const options = { - url: `${url}/retrieve`, - type: 'GET', - responseType: 'json', - timeout: undefined, - }; - - log.info(options.type, options.url); - - const fetchOptions = { - method: options.type, - headers: { - 'X-Loki-recipient': pubKey, - }, - timeout: options.timeout, - }; - - let response; - try { - response = await fetch(options.url, fetchOptions); - } catch (e) { - log.error(options.type, options.url, 0, 'Error'); - throw HTTPError('fetch error', 0, e.toString()); - } - - let result; - if ( - options.responseType === 'json' && - response.headers.get('Content-Type') === 'application/json' - ) { - result = await response.json(); - } else if (options.responseType === 'arraybuffer') { - result = await response.buffer(); - } else { - result = await response.text(); - } + let result; + if ( + options.responseType === 'json' && + response.headers.get('Content-Type') === 'application/json' + ) { + result = await response.json(); + } else if (options.responseType === 'arraybuffer') { + result = await response.buffer(); + } else { + result = await response.text(); + } - if (response.status >= 0 && response.status < 400) { - log.info(options.type, options.url, response.status, 'Success'); - return result; - } - log.error(options.type, options.url, response.status, 'Error'); - throw HTTPError('retrieveMessages: error response', response.status, result); + if (response.status >= 0 && response.status < 400) { + log.info(options.type, options.url, response.status, 'Success'); + return result; } + log.error(options.type, options.url, response.status, 'Error'); + throw HTTPError('sendMessage: error response', response.status, result); + } - async function sendMessage(pubKey, data, ttl) { - const data64 = dcodeIO.ByteBuffer.wrap(data).toString('base64'); - - const timestamp = Math.floor(Date.now() / 1000); - // Nonce is returned as a base64 string to include in header - let nonce; - try { - nonce = await getPoWNonce(timestamp, ttl, pubKey, data64); - } catch (err) { - // Something went horribly wrong - // TODO: Handle gracefully - log.error('Error computing PoW'); - } + async retrieveMessages(pubKey) { + const options = { + url: `${this.url}/retrieve`, + type: 'GET', + responseType: 'json', + timeout: undefined, + }; - const options = { - url: `${url}/store`, - type: 'POST', - responseType: undefined, - timeout: undefined, - }; - - log.info(options.type, options.url); - - const fetchOptions = { - method: options.type, - body: data64, - headers: { - 'X-Loki-pow-nonce': nonce, - 'X-Loki-timestamp': timestamp.toString(), - 'X-Loki-ttl': ttl.toString(), - 'X-Loki-recipient': pubKey, - }, - timeout: options.timeout, - }; - - let response; - try { - response = await fetch(options.url, fetchOptions); - } catch (e) { - log.error(options.type, options.url, 0, 'Error'); - throw HTTPError('fetch error', 0, e.toString()); - } + log.info(options.type, options.url); - let result; - if ( - options.responseType === 'json' && - response.headers.get('Content-Type') === 'application/json' - ) { - result = await response.json(); - } else if (options.responseType === 'arraybuffer') { - result = await response.buffer(); - } else { - result = await response.text(); - } + const fetchOptions = { + method: options.type, + headers: { + 'X-Loki-recipient': pubKey, + }, + timeout: options.timeout, + }; - if (response.status >= 0 && response.status < 400) { - log.info(options.type, options.url, response.status, 'Success'); - return result; - } - log.error(options.type, options.url, response.status, 'Error'); - throw HTTPError('sendMessage: error response', response.status, result); + let response; + try { + response = await fetch(options.url, fetchOptions); + } catch (e) { + log.error(options.type, options.url, 0, 'Error'); + throw HTTPError('fetch error', 0, e.toString()); } + + let result; + if ( + options.responseType === 'json' && + response.headers.get('Content-Type') === 'application/json' + ) { + result = await response.json(); + } else if (options.responseType === 'arraybuffer') { + result = await response.buffer(); + } else { + result = await response.text(); + } + + if (response.status >= 0 && response.status < 400) { + log.info(options.type, options.url, response.status, 'Success'); + return result; + } + log.error(options.type, options.url, response.status, 'Error'); + throw HTTPError('retrieveMessages: error response', response.status, result); } } @@ -177,3 +166,7 @@ function HTTPError(message, providedCode, response, stack) { } return e; } + +module.exports = { + LokiServer, +}; diff --git a/libloki/libloki-protocol.js b/libloki/libloki-protocol.js index 5b4109735..147529019 100644 --- a/libloki/libloki-protocol.js +++ b/libloki/libloki-protocol.js @@ -1,61 +1,49 @@ /* global window, libsignal, textsecure, StringView, log */ // eslint-disable-next-line func-names -(function() { +(function () { window.libloki = window.libloki || {}; - class FallBackDecryptionError extends Error {} + class FallBackDecryptionError extends Error { } const IV_LENGTH = 16; - function FallBackSessionCipher(address) { - this.identityKeyString = address.getName(); - this.pubKey = StringView.hexToArrayBuffer(address.getName()); + class FallBackSessionCipher { - this.encrypt = async plaintext => { + constructor(address) { + this.identityKeyString = address.getName(); + this.pubKey = StringView.hexToArrayBuffer(address.getName()); + } + + async encrypt(plaintext) { const myKeyPair = await textsecure.storage.protocol.getIdentityKeyPair(); const myPrivateKey = myKeyPair.privKey; - const symmetricKey = libsignal.Curve.calculateAgreement( - this.pubKey, - myPrivateKey - ); + const symmetricKey = libsignal.Curve.calculateAgreement(this.pubKey, myPrivateKey); const iv = libsignal.crypto.getRandomBytes(IV_LENGTH); - const ciphertext = await libsignal.crypto.encrypt( - symmetricKey, - plaintext, - iv - ); - const ivAndCiphertext = new Uint8Array( - iv.byteLength + ciphertext.byteLength - ); + const ciphertext = await libsignal.crypto.encrypt(symmetricKey, plaintext, iv); + const ivAndCiphertext = new Uint8Array(iv.byteLength + ciphertext.byteLength); ivAndCiphertext.set(new Uint8Array(iv)); ivAndCiphertext.set(new Uint8Array(ciphertext), iv.byteLength); - return { - type: textsecure.protobuf.Envelope.Type.FRIEND_REQUEST, // friend request + type: textsecure.protobuf.Envelope.Type.FRIEND_REQUEST, body: ivAndCiphertext, registrationId: null, }; - }; - this.decrypt = async ivAndCiphertext => { + } + + async decrypt(ivAndCiphertext) { const iv = ivAndCiphertext.slice(0, IV_LENGTH); const cipherText = ivAndCiphertext.slice(IV_LENGTH); const myKeyPair = await textsecure.storage.protocol.getIdentityKeyPair(); const myPrivateKey = myKeyPair.privKey; - const symmetricKey = libsignal.Curve.calculateAgreement( - this.pubKey, - myPrivateKey - ); + const symmetricKey = libsignal.Curve.calculateAgreement(this.pubKey, myPrivateKey); try { return await libsignal.crypto.decrypt(symmetricKey, cipherText, iv); - } catch (e) { - throw new FallBackDecryptionError( - `Could not decrypt message from ${ - this.identityKeyString - } using FallBack encryption.` - ); } - }; + catch (e) { + throw new FallBackDecryptionError(`Could not decrypt message from ${this.identityKeyString} using FallBack encryption.`); + } + } } async function getPreKeyBundleForNumber(pubKey) { @@ -78,13 +66,13 @@ // generate and store new prekey const preKeyId = textsecure.storage.get('maxPreKeyId', 1); textsecure.storage.put('maxPreKeyId', preKeyId + 1); - const preKey = await libsignal.KeyHelper.generatePreKey(preKeyId); + const newPreKey = await libsignal.KeyHelper.generatePreKey(preKeyId); await textsecure.storage.protocol.storePreKey( - preKey.keyId, - preKey.keyPair, + newPreKey.keyId, + newPreKey.keyPair, pubKey ); - resolve({ pubKey: preKey.keyPair.pubKey, keyId: preKeyId }); + resolve({ pubKey: newPreKey.keyPair.pubKey, keyId: preKeyId }); } }), ]); diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index fc13ecba5..f79f368d1 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -121,7 +121,7 @@ function MessageReceiver(username, password, signalingKey, options = {}) { this.signalingKey = signalingKey; this.username = username; this.password = password; - this.lokiserver = window.LokiAPI.connect(); + this.lokiserver = window.LokiAPI; if (!options.serverTrustRoot) { throw new Error('Server trust root is required!'); @@ -456,7 +456,7 @@ MessageReceiver.prototype.extend({ if (envelope.source) { return `${envelope.source}.${ envelope.sourceDevice - } ${envelope.timestamp.toNumber()} (${envelope.id})`; + } ${envelope.timestamp.toNumber()} (${envelope.id})`; } return envelope.id; @@ -844,7 +844,7 @@ MessageReceiver.prototype.extend({ const isMe = envelope.source === textsecure.storage.user.getNumber(); const isLeavingGroup = Boolean( message.group && - message.group.type === textsecure.protobuf.GroupContext.Type.QUIT + message.group.type === textsecure.protobuf.GroupContext.Type.QUIT ); if (groupId && isBlocked && !(isMe && isLeavingGroup)) { @@ -886,7 +886,7 @@ MessageReceiver.prototype.extend({ const isMe = envelope.source === textsecure.storage.user.getNumber(); const isLeavingGroup = Boolean( message.group && - message.group.type === textsecure.protobuf.GroupContext.Type.QUIT + message.group.type === textsecure.protobuf.GroupContext.Type.QUIT ); if (groupId && isBlocked && !(isMe && isLeavingGroup)) { @@ -958,7 +958,7 @@ MessageReceiver.prototype.extend({ let conversation; try { conversation = ConversationController.get(envelope.source); - } catch (e) {} + } catch (e) { } if (!conversation) { const accepted = await this.promptUserToAcceptFriendRequest( envelope.source, diff --git a/libtextsecure/outgoing_message.js b/libtextsecure/outgoing_message.js index 6c3f033ad..c1fb2b615 100644 --- a/libtextsecure/outgoing_message.js +++ b/libtextsecure/outgoing_message.js @@ -35,7 +35,7 @@ function OutgoingMessage( this.callback = callback; this.silent = silent; - this.lokiserver = window.LokiAPI.connect(); + this.lokiserver = window.LokiAPI; this.numbersCompleted = 0; this.errors = []; diff --git a/preload.js b/preload.js index 310b26ba5..d2383891f 100644 --- a/preload.js +++ b/preload.js @@ -201,11 +201,9 @@ window.WebAPI = initializeWebAPI({ proxyUrl: config.proxyUrl, }); -const { - initialize: initializeLokiAPI, -} = require('./js/modules/loki_message_api'); +const { LokiServer } = require('./js/modules/loki_message_api'); -window.LokiAPI = initializeLokiAPI({ +window.LokiAPI = new LokiServer({ url: config.serverUrl, }); @@ -213,7 +211,7 @@ window.mnemonic = require('./libloki/mnemonic'); // Linux seems to periodically let the event loop stop, so this is a global workaround setInterval(() => { - window.nodeSetImmediate(() => {}); + window.nodeSetImmediate(() => { }); }, 1000); const { autoOrientImage } = require('./js/modules/auto_orient_image');