diff --git a/.prettierignore b/.prettierignore index c64fb9324a4f..7b7efc2ae2db 100644 --- a/.prettierignore +++ b/.prettierignore @@ -14,5 +14,10 @@ js/jquery.js js/Mp3LameEncoder.min.js js/WebAudioRecorderMp3.js +ts/**/*.js +components/* +dist/* +libtextsecure/libsignal-protocol.js + /**/*.json /**/*.css diff --git a/app/attachments.js b/app/attachments.js index 8ce961b49128..9ef5b794942c 100644 --- a/app/attachments.js +++ b/app/attachments.js @@ -5,11 +5,10 @@ const fse = require('fs-extra'); const toArrayBuffer = require('to-arraybuffer'); const { isArrayBuffer, isString } = require('lodash'); - const PATH = 'attachments.noindex'; // getPath :: AbsolutePath -> AbsolutePath -exports.getPath = (userDataPath) => { +exports.getPath = userDataPath => { if (!isString(userDataPath)) { throw new TypeError("'userDataPath' must be a string"); } @@ -17,7 +16,7 @@ exports.getPath = (userDataPath) => { }; // ensureDirectory :: AbsolutePath -> IO Unit -exports.ensureDirectory = async (userDataPath) => { +exports.ensureDirectory = async userDataPath => { if (!isString(userDataPath)) { throw new TypeError("'userDataPath' must be a string"); } @@ -27,12 +26,12 @@ exports.ensureDirectory = async (userDataPath) => { // createReader :: AttachmentsPath -> // RelativePath -> // IO (Promise ArrayBuffer) -exports.createReader = (root) => { +exports.createReader = root => { if (!isString(root)) { throw new TypeError("'root' must be a path"); } - return async (relativePath) => { + return async relativePath => { if (!isString(relativePath)) { throw new TypeError("'relativePath' must be a string"); } @@ -46,12 +45,12 @@ exports.createReader = (root) => { // createWriterForNew :: AttachmentsPath -> // ArrayBuffer -> // IO (Promise RelativePath) -exports.createWriterForNew = (root) => { +exports.createWriterForNew = root => { if (!isString(root)) { throw new TypeError("'root' must be a path"); } - return async (arrayBuffer) => { + return async arrayBuffer => { if (!isArrayBuffer(arrayBuffer)) { throw new TypeError("'arrayBuffer' must be an array buffer"); } @@ -68,7 +67,7 @@ exports.createWriterForNew = (root) => { // createWriter :: AttachmentsPath -> // { data: ArrayBuffer, path: RelativePath } -> // IO (Promise RelativePath) -exports.createWriterForExisting = (root) => { +exports.createWriterForExisting = root => { if (!isString(root)) { throw new TypeError("'root' must be a path"); } @@ -93,12 +92,12 @@ exports.createWriterForExisting = (root) => { // createDeleter :: AttachmentsPath -> // RelativePath -> // IO Unit -exports.createDeleter = (root) => { +exports.createDeleter = root => { if (!isString(root)) { throw new TypeError("'root' must be a path"); } - return async (relativePath) => { + return async relativePath => { if (!isString(relativePath)) { throw new TypeError("'relativePath' must be a string"); } @@ -115,7 +114,7 @@ exports.createName = () => { }; // getRelativePath :: String -> Path -exports.getRelativePath = (name) => { +exports.getRelativePath = name => { if (!isString(name)) { throw new TypeError("'name' must be a string"); } diff --git a/app/auto_update.js b/app/auto_update.js index 9badffb65a30..959826b92b39 100644 --- a/app/auto_update.js +++ b/app/auto_update.js @@ -11,7 +11,11 @@ const RESTART_BUTTON = 0; const LATER_BUTTON = 1; function autoUpdateDisabled() { - return process.platform === 'linux' || process.mas || config.get('disableAutoUpdate'); + return ( + process.platform === 'linux' || + process.mas || + config.get('disableAutoUpdate') + ); } function checkForUpdates() { @@ -38,7 +42,7 @@ function showUpdateDialog(mainWindow, messages) { cancelId: RESTART_BUTTON, }; - dialog.showMessageBox(mainWindow, options, (response) => { + dialog.showMessageBox(mainWindow, options, response => { if (response === RESTART_BUTTON) { // We delay these update calls because they don't seem to work in this // callback - but only if the message box has a parent window. diff --git a/app/config.js b/app/config.js index d0eff76d76ad..b5ef81500318 100644 --- a/app/config.js +++ b/app/config.js @@ -39,7 +39,7 @@ config.environment = environment; 'HOSTNAME', 'NODE_APP_INSTANCE', 'SUPPRESS_NO_CONFIG_WARNING', -].forEach((s) => { +].forEach(s => { console.log(`${s} ${config.util.getEnv(s)}`); }); diff --git a/app/locale.js b/app/locale.js index b2df2fe00def..ddf80159e8ef 100644 --- a/app/locale.js +++ b/app/locale.js @@ -2,7 +2,6 @@ const path = require('path'); const fs = require('fs'); const _ = require('lodash'); - function normalizeLocaleName(locale) { if (/^en-/.test(locale)) { return 'en'; @@ -50,7 +49,9 @@ function load({ appLocale, logger } = {}) { // We start with english, then overwrite that with anything present in locale messages = _.merge(english, messages); } catch (e) { - logger.error(`Problem loading messages for locale ${localeName} ${e.stack}`); + logger.error( + `Problem loading messages for locale ${localeName} ${e.stack}` + ); logger.error('Falling back to en locale'); localeName = 'en'; diff --git a/app/logging.js b/app/logging.js index 29c95607f409..a9492be27c84 100644 --- a/app/logging.js +++ b/app/logging.js @@ -12,14 +12,10 @@ const readFirstLine = require('firstline'); const readLastLines = require('read-last-lines').read; const rimraf = require('rimraf'); -const { - app, - ipcMain: ipc, -} = electron; +const { app, ipcMain: ipc } = electron; const LEVELS = ['fatal', 'error', 'warn', 'info', 'debug', 'trace']; let logger; - module.exports = { initialize, getLogger, @@ -45,32 +41,38 @@ function initialize() { logger = bunyan.createLogger({ name: 'log', - streams: [{ - level: 'debug', - stream: process.stdout, - }, { - type: 'rotating-file', - path: logFile, - period: '1d', - count: 3, - }], + streams: [ + { + level: 'debug', + stream: process.stdout, + }, + { + type: 'rotating-file', + path: logFile, + period: '1d', + count: 3, + }, + ], }); - LEVELS.forEach((level) => { + LEVELS.forEach(level => { ipc.on(`log-${level}`, (first, ...rest) => { logger[level](...rest); }); }); - ipc.on('fetch-log', (event) => { - fetch(logPath).then((data) => { - event.sender.send('fetched-log', data); - }, (error) => { - logger.error(`Problem loading log from disk: ${error.stack}`); - }); + ipc.on('fetch-log', event => { + fetch(logPath).then( + data => { + event.sender.send('fetched-log', data); + }, + error => { + logger.error(`Problem loading log from disk: ${error.stack}`); + } + ); }); - ipc.on('delete-all-logs', async (event) => { + ipc.on('delete-all-logs', async event => { try { await deleteAllLogs(logPath); } catch (error) { @@ -84,27 +86,29 @@ function initialize() { async function deleteAllLogs(logPath) { return new Promise((resolve, reject) => { - rimraf(logPath, { - disableGlob: true, - }, (error) => { - if (error) { - return reject(error); - } + rimraf( + logPath, + { + disableGlob: true, + }, + error => { + if (error) { + return reject(error); + } - return resolve(); - }); + return resolve(); + } + ); }); } function cleanupLogs(logPath) { const now = new Date(); - const earliestDate = new Date(Date.UTC( - now.getUTCFullYear(), - now.getUTCMonth(), - now.getUTCDate() - 3 - )); + const earliestDate = new Date( + Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() - 3) + ); - return eliminateOutOfDateFiles(logPath, earliestDate).then((remaining) => { + return eliminateOutOfDateFiles(logPath, earliestDate).then(remaining => { const files = _.filter(remaining, file => !file.start && file.end); if (!files.length) { @@ -122,7 +126,7 @@ function isLineAfterDate(line, date) { try { const data = JSON.parse(line); - return (new Date(data.time)).getTime() > date.getTime(); + return new Date(data.time).getTime() > date.getTime(); } catch (e) { console.log('error parsing log line', e.stack, line); return false; @@ -133,48 +137,53 @@ function eliminateOutOfDateFiles(logPath, date) { const files = fs.readdirSync(logPath); const paths = files.map(file => path.join(logPath, file)); - return Promise.all(_.map( - paths, - target => Promise.all([ - readFirstLine(target), - readLastLines(target, 2), - ]).then((results) => { - const start = results[0]; - const end = results[1].split('\n'); + return Promise.all( + _.map(paths, target => + Promise.all([readFirstLine(target), readLastLines(target, 2)]).then( + results => { + const start = results[0]; + const end = results[1].split('\n'); - const file = { - path: target, - start: isLineAfterDate(start, date), - end: isLineAfterDate(end[end.length - 1], date) || - isLineAfterDate(end[end.length - 2], date), - }; + const file = { + path: target, + start: isLineAfterDate(start, date), + end: + isLineAfterDate(end[end.length - 1], date) || + isLineAfterDate(end[end.length - 2], date), + }; - if (!file.start && !file.end) { - fs.unlinkSync(file.path); - } + if (!file.start && !file.end) { + fs.unlinkSync(file.path); + } - return file; - }) - )); + return file; + } + ) + ) + ); } function eliminateOldEntries(files, date) { const earliest = date.getTime(); - return Promise.all(_.map( - files, - file => fetchLog(file.path).then((lines) => { - const recent = _.filter(lines, line => (new Date(line.time)).getTime() >= earliest); - const text = _.map(recent, line => JSON.stringify(line)).join('\n'); + return Promise.all( + _.map(files, file => + fetchLog(file.path).then(lines => { + const recent = _.filter( + lines, + line => new Date(line.time).getTime() >= earliest + ); + const text = _.map(recent, line => JSON.stringify(line)).join('\n'); - return fs.writeFileSync(file.path, `${text}\n`); - }) - )); + return fs.writeFileSync(file.path, `${text}\n`); + }) + ) + ); } function getLogger() { if (!logger) { - throw new Error('Logger hasn\'t been initialized yet!'); + throw new Error("Logger hasn't been initialized yet!"); } return logger; @@ -188,13 +197,15 @@ function fetchLog(logFile) { } const lines = _.compact(text.split('\n')); - const data = _.compact(lines.map((line) => { - try { - return _.pick(JSON.parse(line), ['level', 'time', 'msg']); - } catch (e) { - return null; - } - })); + const data = _.compact( + lines.map(line => { + try { + return _.pick(JSON.parse(line), ['level', 'time', 'msg']); + } catch (e) { + return null; + } + }) + ); return resolve(data); }); @@ -213,7 +224,7 @@ function fetch(logPath) { msg: `Loaded this list of log files from logPath: ${files.join(', ')}`, }; - return Promise.all(paths.map(fetchLog)).then((results) => { + return Promise.all(paths.map(fetchLog)).then(results => { const data = _.flatten(results); data.push(fileListEntry); @@ -222,11 +233,10 @@ function fetch(logPath) { }); } - function logAtLevel(level, ...args) { if (logger) { // To avoid [Object object] in our log since console.log handles non-strings smoothly - const str = args.map((item) => { + const str = args.map(item => { if (typeof item !== 'string') { try { return JSON.stringify(item); diff --git a/app/menu.js b/app/menu.js index ecc1f654b992..962276273694 100644 --- a/app/menu.js +++ b/app/menu.js @@ -1,6 +1,5 @@ const { isString } = require('lodash'); - exports.createTemplate = (options, messages) => { if (!isString(options.platform)) { throw new TypeError('`options.platform` must be a string'); @@ -21,127 +20,129 @@ exports.createTemplate = (options, messages) => { showSettings, } = options; - const template = [{ - label: messages.mainMenuFile.message, - submenu: [ - { - label: messages.mainMenuSettings.message, - click: showSettings, - }, - { - type: 'separator', - }, - { - role: 'quit', - }, - ], - }, - { - label: messages.mainMenuEdit.message, - submenu: [ - { - role: 'undo', - }, - { - role: 'redo', - }, - { - type: 'separator', - }, - { - role: 'cut', - }, - { - role: 'copy', - }, - { - role: 'paste', - }, - { - role: 'pasteandmatchstyle', - }, - { - role: 'delete', - }, - { - role: 'selectall', - }, - ], - }, - { - label: messages.mainMenuView.message, - submenu: [ - { - role: 'resetzoom', - }, - { - role: 'zoomin', - }, - { - role: 'zoomout', - }, - { - type: 'separator', - }, - { - role: 'togglefullscreen', - }, - { - type: 'separator', - }, - { - label: messages.debugLog.message, - click: showDebugLog, - }, - { - type: 'separator', - }, - { - role: 'toggledevtools', - }, - ], - }, - { - label: messages.mainMenuWindow.message, - role: 'window', - submenu: [ - { - role: 'minimize', - }, - ], - }, - { - label: messages.mainMenuHelp.message, - role: 'help', - submenu: [ - { - label: messages.goToReleaseNotes.message, - click: openReleaseNotes, - }, - { - type: 'separator', - }, - { - label: messages.goToForums.message, - click: openForums, - }, - { - label: messages.goToSupportPage.message, - click: openSupportPage, - }, - { - label: messages.menuReportIssue.message, - click: openNewBugForm, - }, - { - type: 'separator', - }, - { - label: messages.aboutSignalDesktop.message, - click: showAbout, - }, - ], - }]; + const template = [ + { + label: messages.mainMenuFile.message, + submenu: [ + { + label: messages.mainMenuSettings.message, + click: showSettings, + }, + { + type: 'separator', + }, + { + role: 'quit', + }, + ], + }, + { + label: messages.mainMenuEdit.message, + submenu: [ + { + role: 'undo', + }, + { + role: 'redo', + }, + { + type: 'separator', + }, + { + role: 'cut', + }, + { + role: 'copy', + }, + { + role: 'paste', + }, + { + role: 'pasteandmatchstyle', + }, + { + role: 'delete', + }, + { + role: 'selectall', + }, + ], + }, + { + label: messages.mainMenuView.message, + submenu: [ + { + role: 'resetzoom', + }, + { + role: 'zoomin', + }, + { + role: 'zoomout', + }, + { + type: 'separator', + }, + { + role: 'togglefullscreen', + }, + { + type: 'separator', + }, + { + label: messages.debugLog.message, + click: showDebugLog, + }, + { + type: 'separator', + }, + { + role: 'toggledevtools', + }, + ], + }, + { + label: messages.mainMenuWindow.message, + role: 'window', + submenu: [ + { + role: 'minimize', + }, + ], + }, + { + label: messages.mainMenuHelp.message, + role: 'help', + submenu: [ + { + label: messages.goToReleaseNotes.message, + click: openReleaseNotes, + }, + { + type: 'separator', + }, + { + label: messages.goToForums.message, + click: openForums, + }, + { + label: messages.goToSupportPage.message, + click: openSupportPage, + }, + { + label: messages.menuReportIssue.message, + click: openNewBugForm, + }, + { + type: 'separator', + }, + { + label: messages.aboutSignalDesktop.message, + click: showAbout, + }, + ], + }, + ]; if (includeSetup) { const fileMenu = template[0]; diff --git a/app/tray_icon.js b/app/tray_icon.js index b3fe18cf0b1d..ab3f34ac7f34 100644 --- a/app/tray_icon.js +++ b/app/tray_icon.js @@ -1,10 +1,6 @@ const path = require('path'); -const { - app, - Menu, - Tray, -} = require('electron'); +const { app, Menu, Tray } = require('electron'); let trayContextMenu = null; let tray = null; @@ -12,7 +8,12 @@ let tray = null; function createTrayIcon(getMainWindow, messages) { // A smaller icon is needed on macOS const iconSize = process.platform === 'darwin' ? '16' : '256'; - const iconNoNewMessages = path.join(__dirname, '..', 'images', `icon_${iconSize}.png`); + const iconNoNewMessages = path.join( + __dirname, + '..', + 'images', + `icon_${iconSize}.png` + ); tray = new Tray(iconNoNewMessages); @@ -42,24 +43,28 @@ function createTrayIcon(getMainWindow, messages) { // context menu, since the 'click' event may not work on all platforms. // For details please refer to: // https://github.com/electron/electron/blob/master/docs/api/tray.md. - trayContextMenu = Menu.buildFromTemplate([{ - id: 'toggleWindowVisibility', - label: messages[mainWindow.isVisible() ? 'hide' : 'show'].message, - click: tray.toggleWindowVisibility, - }, - { - id: 'quit', - label: messages.quit.message, - click: app.quit.bind(app), - }]); + trayContextMenu = Menu.buildFromTemplate([ + { + id: 'toggleWindowVisibility', + label: messages[mainWindow.isVisible() ? 'hide' : 'show'].message, + click: tray.toggleWindowVisibility, + }, + { + id: 'quit', + label: messages.quit.message, + click: app.quit.bind(app), + }, + ]); tray.setContextMenu(trayContextMenu); }; - tray.updateIcon = (unreadCount) => { + tray.updateIcon = unreadCount => { if (unreadCount > 0) { const filename = `${String(unreadCount >= 10 ? 10 : unreadCount)}.png`; - tray.setImage(path.join(__dirname, '..', 'images', 'alert', iconSize, filename)); + tray.setImage( + path.join(__dirname, '..', 'images', 'alert', iconSize, filename) + ); } else { tray.setImage(iconNoNewMessages); } diff --git a/app/user_config.js b/app/user_config.js index c0f0bb539613..5413e89ed3ef 100644 --- a/app/user_config.js +++ b/app/user_config.js @@ -5,7 +5,6 @@ const ElectronConfig = require('electron-config'); const config = require('./config'); - // use a separate data directory for development if (config.has('storageProfile')) { const userData = path.join( diff --git a/js/modules/deferred_to_promise.d.ts b/js/modules/deferred_to_promise.d.ts index b4e87b0121c2..67f9ff212af6 100644 --- a/js/modules/deferred_to_promise.d.ts +++ b/js/modules/deferred_to_promise.d.ts @@ -1 +1,3 @@ -export function deferredToPromise(deferred: JQuery.Deferred): Promise; +export function deferredToPromise( + deferred: JQuery.Deferred +): Promise; diff --git a/js/modules/link_text.d.ts b/js/modules/link_text.d.ts index 72e015249e5f..25425ee18c80 100644 --- a/js/modules/link_text.d.ts +++ b/js/modules/link_text.d.ts @@ -1,9 +1,12 @@ declare namespace LinkText { type Attributes = { [key: string]: string; - } + }; } -declare function linkText(value: string, attributes: LinkText.Attributes): string; +declare function linkText( + value: string, + attributes: LinkText.Attributes +): string; export = linkText; diff --git a/libtextsecure/account_manager.js b/libtextsecure/account_manager.js index 351a40ec2434..9498c673dc9c 100644 --- a/libtextsecure/account_manager.js +++ b/libtextsecure/account_manager.js @@ -1,426 +1,522 @@ -;(function () { - 'use strict'; - window.textsecure = window.textsecure || {}; +(function() { + 'use strict'; + window.textsecure = window.textsecure || {}; - var ARCHIVE_AGE = 7 * 24 * 60 * 60 * 1000; + var ARCHIVE_AGE = 7 * 24 * 60 * 60 * 1000; - function AccountManager(url, username, password) { - this.server = new TextSecureServer(url, username, password); - this.pending = Promise.resolve(); + function AccountManager(url, username, password) { + this.server = new TextSecureServer(url, username, password); + this.pending = Promise.resolve(); + } + + function getNumber(numberId) { + if (!numberId || !numberId.length) { + return numberId; } - function getNumber(numberId) { - if (!numberId || !numberId.length) { - return numberId; - } - - var parts = numberId.split('.'); - if (!parts.length) { - return numberId; - } - - return parts[0]; + var parts = numberId.split('.'); + if (!parts.length) { + return numberId; } - AccountManager.prototype = new textsecure.EventTarget(); - AccountManager.prototype.extend({ - constructor: AccountManager, - requestVoiceVerification: function(number) { - return this.server.requestVerificationVoice(number); - }, - requestSMSVerification: function(number) { - return this.server.requestVerificationSMS(number); - }, - registerSingleDevice: function(number, verificationCode) { - var registerKeys = this.server.registerKeys.bind(this.server); - var createAccount = this.createAccount.bind(this); - var clearSessionsAndPreKeys = this.clearSessionsAndPreKeys.bind(this); - var generateKeys = this.generateKeys.bind(this, 100); - var confirmKeys = this.confirmKeys.bind(this); - var registrationDone = this.registrationDone.bind(this); - return this.queueTask(function() { - return libsignal.KeyHelper.generateIdentityKeyPair().then(function(identityKeyPair) { - var profileKey = textsecure.crypto.getRandomBytes(32); - return createAccount(number, verificationCode, identityKeyPair, profileKey) - .then(clearSessionsAndPreKeys) - .then(generateKeys) - .then(function(keys) { - return registerKeys(keys).then(function() { - return confirmKeys(keys); - }); - }) - .then(registrationDone); - }); - }); - }, - registerSecondDevice: function(setProvisioningUrl, confirmNumber, progressCallback) { - var createAccount = this.createAccount.bind(this); - var clearSessionsAndPreKeys = this.clearSessionsAndPreKeys.bind(this); - var generateKeys = this.generateKeys.bind(this, 100, progressCallback); - var confirmKeys = this.confirmKeys.bind(this); - var registrationDone = this.registrationDone.bind(this); - var registerKeys = this.server.registerKeys.bind(this.server); - var getSocket = this.server.getProvisioningSocket.bind(this.server); - var queueTask = this.queueTask.bind(this); - var provisioningCipher = new libsignal.ProvisioningCipher(); - var gotProvisionEnvelope = false; - return provisioningCipher.getPublicKey().then(function(pubKey) { - return new Promise(function(resolve, reject) { - var socket = getSocket(); - socket.onclose = function(e) { - console.log('provisioning socket closed', e.code); - if (!gotProvisionEnvelope) { - reject(new Error('websocket closed')); - } - }; - socket.onopen = function(e) { - console.log('provisioning socket open'); - }; - var wsr = new WebSocketResource(socket, { - keepalive: { path: '/v1/keepalive/provisioning' }, - handleRequest: function(request) { - if (request.path === "/v1/address" && request.verb === "PUT") { - var proto = textsecure.protobuf.ProvisioningUuid.decode(request.body); - setProvisioningUrl([ - 'tsdevice:/?uuid=', proto.uuid, '&pub_key=', - encodeURIComponent(btoa(getString(pubKey))) - ].join('')); - request.respond(200, 'OK'); - } else if (request.path === "/v1/message" && request.verb === "PUT") { - var envelope = textsecure.protobuf.ProvisionEnvelope.decode(request.body, 'binary'); - request.respond(200, 'OK'); - gotProvisionEnvelope = true; - wsr.close(); - resolve(provisioningCipher.decrypt(envelope).then(function(provisionMessage) { - return queueTask(function() { - return confirmNumber(provisionMessage.number).then(function(deviceName) { - if (typeof deviceName !== 'string' || deviceName.length === 0) { - throw new Error('Invalid device name'); - } - return createAccount( - provisionMessage.number, - provisionMessage.provisioningCode, - provisionMessage.identityKeyPair, - provisionMessage.profileKey, - deviceName, - provisionMessage.userAgent, - provisionMessage.readReceipts - ) - .then(clearSessionsAndPreKeys) - .then(generateKeys) - .then(function(keys) { - return registerKeys(keys).then(function() { - return confirmKeys(keys); - }); - }) - .then(registrationDone); - }); - }); - })); - } else { - console.log('Unknown websocket message', request.path); - } - } - }); - }); - }); - }, - refreshPreKeys: function() { - var generateKeys = this.generateKeys.bind(this, 100); - var registerKeys = this.server.registerKeys.bind(this.server); + return parts[0]; + } - return this.queueTask(function() { - return this.server.getMyKeys().then(function(preKeyCount) { - console.log('prekey count ' + preKeyCount); - if (preKeyCount < 10) { - return generateKeys().then(registerKeys); - } - }); - }.bind(this)); - }, - rotateSignedPreKey: function() { - return this.queueTask(function() { - var signedKeyId = textsecure.storage.get('signedKeyId', 1); - if (typeof signedKeyId != 'number') { - throw new Error('Invalid signedKeyId'); - } - - var store = textsecure.storage.protocol; - var server = this.server; - var cleanSignedPreKeys = this.cleanSignedPreKeys; - - // TODO: harden this against missing identity key? Otherwise, we get - // retries every five seconds. - return store.getIdentityKeyPair().then(function(identityKey) { - return libsignal.KeyHelper.generateSignedPreKey(identityKey, signedKeyId); - }, function(error) { - console.log('Failed to get identity key. Canceling key rotation.'); - }).then(function(res) { - if (!res) { - return; - } - console.log('Saving new signed prekey', res.keyId); - return Promise.all([ - textsecure.storage.put('signedKeyId', signedKeyId + 1), - store.storeSignedPreKey(res.keyId, res.keyPair), - server.setSignedPreKey({ - keyId : res.keyId, - publicKey : res.keyPair.pubKey, - signature : res.signature - }), - ]).then(function() { - var confirmed = true; - console.log('Confirming new signed prekey', res.keyId); - return Promise.all([ - textsecure.storage.remove('signedKeyRotationRejected'), - store.storeSignedPreKey(res.keyId, res.keyPair, confirmed), - ]); - }).then(function() { - return cleanSignedPreKeys(); - }); - }).catch(function(e) { - console.log( - 'rotateSignedPrekey error:', - e && e.stack ? e.stack : e - ); - - if (e instanceof Error && e.name == 'HTTPError' && e.code >= 400 && e.code <= 599) { - var rejections = 1 + textsecure.storage.get('signedKeyRotationRejected', 0); - textsecure.storage.put('signedKeyRotationRejected', rejections); - console.log('Signed key rotation rejected count:', rejections); - } else { - throw e; - } - }); - }.bind(this)); - }, - queueTask: function(task) { - var taskWithTimeout = textsecure.createTaskWithTimeout(task); - return this.pending = this.pending.then(taskWithTimeout, taskWithTimeout); - }, - cleanSignedPreKeys: function() { - var MINIMUM_KEYS = 3; - var store = textsecure.storage.protocol; - return store.loadSignedPreKeys().then(function(allKeys) { - allKeys.sort(function(a, b) { - return (a.created_at || 0) - (b.created_at || 0); - }); - allKeys.reverse(); // we want the most recent first - var confirmed = allKeys.filter(function(key) { - return key.confirmed; - }); - var unconfirmed = allKeys.filter(function(key) { - return !key.confirmed; - }); - - var recent = allKeys[0] ? allKeys[0].keyId : 'none'; - var recentConfirmed = confirmed[0] ? confirmed[0].keyId : 'none'; - console.log('Most recent signed key: ' + recent); - console.log('Most recent confirmed signed key: ' + recentConfirmed); - console.log( - 'Total signed key count:', - allKeys.length, - '-', - confirmed.length, - 'confirmed' + AccountManager.prototype = new textsecure.EventTarget(); + AccountManager.prototype.extend({ + constructor: AccountManager, + requestVoiceVerification: function(number) { + return this.server.requestVerificationVoice(number); + }, + requestSMSVerification: function(number) { + return this.server.requestVerificationSMS(number); + }, + registerSingleDevice: function(number, verificationCode) { + var registerKeys = this.server.registerKeys.bind(this.server); + var createAccount = this.createAccount.bind(this); + var clearSessionsAndPreKeys = this.clearSessionsAndPreKeys.bind(this); + var generateKeys = this.generateKeys.bind(this, 100); + var confirmKeys = this.confirmKeys.bind(this); + var registrationDone = this.registrationDone.bind(this); + return this.queueTask(function() { + return libsignal.KeyHelper.generateIdentityKeyPair().then(function( + identityKeyPair + ) { + var profileKey = textsecure.crypto.getRandomBytes(32); + return createAccount( + number, + verificationCode, + identityKeyPair, + profileKey + ) + .then(clearSessionsAndPreKeys) + .then(generateKeys) + .then(function(keys) { + return registerKeys(keys).then(function() { + return confirmKeys(keys); + }); + }) + .then(registrationDone); + }); + }); + }, + registerSecondDevice: function( + setProvisioningUrl, + confirmNumber, + progressCallback + ) { + var createAccount = this.createAccount.bind(this); + var clearSessionsAndPreKeys = this.clearSessionsAndPreKeys.bind(this); + var generateKeys = this.generateKeys.bind(this, 100, progressCallback); + var confirmKeys = this.confirmKeys.bind(this); + var registrationDone = this.registrationDone.bind(this); + var registerKeys = this.server.registerKeys.bind(this.server); + var getSocket = this.server.getProvisioningSocket.bind(this.server); + var queueTask = this.queueTask.bind(this); + var provisioningCipher = new libsignal.ProvisioningCipher(); + var gotProvisionEnvelope = false; + return provisioningCipher.getPublicKey().then(function(pubKey) { + return new Promise(function(resolve, reject) { + var socket = getSocket(); + socket.onclose = function(e) { + console.log('provisioning socket closed', e.code); + if (!gotProvisionEnvelope) { + reject(new Error('websocket closed')); + } + }; + socket.onopen = function(e) { + console.log('provisioning socket open'); + }; + var wsr = new WebSocketResource(socket, { + keepalive: { path: '/v1/keepalive/provisioning' }, + handleRequest: function(request) { + if (request.path === '/v1/address' && request.verb === 'PUT') { + var proto = textsecure.protobuf.ProvisioningUuid.decode( + request.body ); - - var confirmedCount = confirmed.length; - - // Keep MINIMUM_KEYS confirmed keys, then drop if older than a week - confirmed = confirmed.forEach(function(key, index) { - if (index < MINIMUM_KEYS) { - return; - } - var created_at = key.created_at || 0; - var age = Date.now() - created_at; - if (age > ARCHIVE_AGE) { - console.log( - 'Removing confirmed signed prekey:', - key.keyId, - 'with timestamp:', - created_at + setProvisioningUrl( + [ + 'tsdevice:/?uuid=', + proto.uuid, + '&pub_key=', + encodeURIComponent(btoa(getString(pubKey))), + ].join('') + ); + request.respond(200, 'OK'); + } else if ( + request.path === '/v1/message' && + request.verb === 'PUT' + ) { + var envelope = textsecure.protobuf.ProvisionEnvelope.decode( + request.body, + 'binary' + ); + request.respond(200, 'OK'); + gotProvisionEnvelope = true; + wsr.close(); + resolve( + provisioningCipher + .decrypt(envelope) + .then(function(provisionMessage) { + return queueTask(function() { + return confirmNumber(provisionMessage.number).then( + function(deviceName) { + if ( + typeof deviceName !== 'string' || + deviceName.length === 0 + ) { + throw new Error('Invalid device name'); + } + return createAccount( + provisionMessage.number, + provisionMessage.provisioningCode, + provisionMessage.identityKeyPair, + provisionMessage.profileKey, + deviceName, + provisionMessage.userAgent, + provisionMessage.readReceipts + ) + .then(clearSessionsAndPreKeys) + .then(generateKeys) + .then(function(keys) { + return registerKeys(keys).then(function() { + return confirmKeys(keys); + }); + }) + .then(registrationDone); + } ); - store.removeSignedPreKey(key.keyId); - confirmedCount--; - } - }); - - var 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(function(key, index) { - if (index < stillNeeded) { - return; - } - - var created_at = key.created_at || 0; - var age = Date.now() - created_at; - if (age > ARCHIVE_AGE) { - console.log( - 'Removing unconfirmed signed prekey:', - key.keyId, - 'with timestamp:', - created_at - ); - store.removeSignedPreKey(key.keyId); - } - }); - }); - }, - createAccount: function(number, verificationCode, identityKeyPair, - profileKey, deviceName, userAgent, readReceipts) { - var signalingKey = libsignal.crypto.getRandomBytes(32 + 20); - var password = btoa(getString(libsignal.crypto.getRandomBytes(16))); - password = password.substring(0, password.length - 2); - var registrationId = libsignal.KeyHelper.generateRegistrationId(); - - var previousNumber = getNumber(textsecure.storage.get('number_id')); - - return this.server.confirmCode( - number, verificationCode, password, signalingKey, registrationId, deviceName - ).then(function(response) { - if (previousNumber && previousNumber !== number) { - console.log('New number is different from old number; deleting all previous data'); - - return textsecure.storage.protocol.removeAllData().then(function() { - console.log('Successfully deleted previous data'); - return response; - }, function(error) { - console.log( - 'Something went wrong deleting data from previous number', - error && error.stack ? error.stack : error - ); - - return response; - }); - } - - return response; - }).then(function(response) { - textsecure.storage.remove('identityKey'); - textsecure.storage.remove('signaling_key'); - textsecure.storage.remove('password'); - textsecure.storage.remove('registrationId'); - textsecure.storage.remove('number_id'); - textsecure.storage.remove('device_name'); - textsecure.storage.remove('regionCode'); - textsecure.storage.remove('userAgent'); - textsecure.storage.remove('profileKey'); - textsecure.storage.remove('read-receipts-setting'); - - // update our own identity key, which may have changed - // if we're relinking after a reinstall on the master device - textsecure.storage.protocol.saveIdentityWithAttributes(number, { - id : number, - publicKey : identityKeyPair.pubKey, - firstUse : true, - timestamp : Date.now(), - verified : textsecure.storage.protocol.VerifiedStatus.VERIFIED, - nonblockingApproval : true - }); - - textsecure.storage.put('identityKey', identityKeyPair); - textsecure.storage.put('signaling_key', signalingKey); - textsecure.storage.put('password', password); - textsecure.storage.put('registrationId', registrationId); - if (profileKey) { - textsecure.storage.put('profileKey', profileKey); - } - if (userAgent) { - textsecure.storage.put('userAgent', userAgent); - } - if (readReceipts) { - textsecure.storage.put('read-receipt-setting', true); - } else { - textsecure.storage.put('read-receipt-setting', false); - } - - textsecure.storage.user.setNumberAndDeviceId(number, response.deviceId || 1, deviceName); - textsecure.storage.put('regionCode', libphonenumber.util.getRegionCodeForNumber(number)); - this.server.username = textsecure.storage.get('number_id'); - }.bind(this)); - }, - clearSessionsAndPreKeys: function() { - var store = textsecure.storage.protocol; - - console.log('clearing all sessions, prekeys, and signed prekeys'); - return Promise.all([ - store.clearPreKeyStore(), - store.clearSignedPreKeysStore(), - store.clearSessionStore(), - ]); - }, - // Takes the same object returned by generateKeys - confirmKeys: function(keys) { - var store = textsecure.storage.protocol; - var key = keys.signedPreKey; - var confirmed = true; - - console.log('confirmKeys: confirming key', key.keyId); - return store.storeSignedPreKey(key.keyId, key.keyPair, confirmed); - }, - generateKeys: function (count, progressCallback) { - if (typeof progressCallback !== 'function') { - progressCallback = undefined; - } - var startId = textsecure.storage.get('maxPreKeyId', 1); - var signedKeyId = textsecure.storage.get('signedKeyId', 1); - - if (typeof startId != 'number') { - throw new Error('Invalid maxPreKeyId'); - } - if (typeof signedKeyId != 'number') { - throw new Error('Invalid signedKeyId'); - } - - var store = textsecure.storage.protocol; - return store.getIdentityKeyPair().then(function(identityKey) { - var result = { preKeys: [], identityKey: identityKey.pubKey }; - var promises = []; - - for (var keyId = startId; keyId < startId+count; ++keyId) { - promises.push( - libsignal.KeyHelper.generatePreKey(keyId).then(function(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(function(res) { - store.storeSignedPreKey(res.keyId, res.keyPair); - result.signedPreKey = { - keyId : res.keyId, - publicKey : res.keyPair.pubKey, - signature : res.signature, - // server.registerKeys doesn't use keyPair, confirmKeys does - keyPair : res.keyPair, - }; + }); }) ); + } else { + console.log('Unknown websocket message', request.path); + } + }, + }); + }); + }); + }, + refreshPreKeys: function() { + var generateKeys = this.generateKeys.bind(this, 100); + var registerKeys = this.server.registerKeys.bind(this.server); - textsecure.storage.put('maxPreKeyId', startId + count); - textsecure.storage.put('signedKeyId', signedKeyId + 1); - return Promise.all(promises).then(function() { - // This is primarily for the signed prekey summary it logs out - return this.cleanSignedPreKeys().then(function() { - return result; - }); - }.bind(this)); - }.bind(this)); - }, - registrationDone: function() { - console.log('registration done'); - this.dispatchEvent(new Event('registration')); - } - }); - textsecure.AccountManager = AccountManager; + return this.queueTask( + function() { + return this.server.getMyKeys().then(function(preKeyCount) { + console.log('prekey count ' + preKeyCount); + if (preKeyCount < 10) { + return generateKeys().then(registerKeys); + } + }); + }.bind(this) + ); + }, + rotateSignedPreKey: function() { + return this.queueTask( + function() { + var signedKeyId = textsecure.storage.get('signedKeyId', 1); + if (typeof signedKeyId != 'number') { + throw new Error('Invalid signedKeyId'); + } -}()); + var store = textsecure.storage.protocol; + var server = this.server; + var cleanSignedPreKeys = this.cleanSignedPreKeys; + + // TODO: harden this against missing identity key? Otherwise, we get + // retries every five seconds. + return store + .getIdentityKeyPair() + .then( + function(identityKey) { + return libsignal.KeyHelper.generateSignedPreKey( + identityKey, + signedKeyId + ); + }, + function(error) { + console.log( + 'Failed to get identity key. Canceling key rotation.' + ); + } + ) + .then(function(res) { + if (!res) { + return; + } + console.log('Saving new signed prekey', res.keyId); + return Promise.all([ + textsecure.storage.put('signedKeyId', signedKeyId + 1), + store.storeSignedPreKey(res.keyId, res.keyPair), + server.setSignedPreKey({ + keyId: res.keyId, + publicKey: res.keyPair.pubKey, + signature: res.signature, + }), + ]) + .then(function() { + var confirmed = true; + console.log('Confirming new signed prekey', res.keyId); + return Promise.all([ + textsecure.storage.remove('signedKeyRotationRejected'), + store.storeSignedPreKey(res.keyId, res.keyPair, confirmed), + ]); + }) + .then(function() { + return cleanSignedPreKeys(); + }); + }) + .catch(function(e) { + console.log( + 'rotateSignedPrekey error:', + e && e.stack ? e.stack : e + ); + + if ( + e instanceof Error && + e.name == 'HTTPError' && + e.code >= 400 && + e.code <= 599 + ) { + var rejections = + 1 + textsecure.storage.get('signedKeyRotationRejected', 0); + textsecure.storage.put('signedKeyRotationRejected', rejections); + console.log('Signed key rotation rejected count:', rejections); + } else { + throw e; + } + }); + }.bind(this) + ); + }, + queueTask: function(task) { + var taskWithTimeout = textsecure.createTaskWithTimeout(task); + return (this.pending = this.pending.then( + taskWithTimeout, + taskWithTimeout + )); + }, + cleanSignedPreKeys: function() { + var MINIMUM_KEYS = 3; + var store = textsecure.storage.protocol; + return store.loadSignedPreKeys().then(function(allKeys) { + allKeys.sort(function(a, b) { + return (a.created_at || 0) - (b.created_at || 0); + }); + allKeys.reverse(); // we want the most recent first + var confirmed = allKeys.filter(function(key) { + return key.confirmed; + }); + var unconfirmed = allKeys.filter(function(key) { + return !key.confirmed; + }); + + var recent = allKeys[0] ? allKeys[0].keyId : 'none'; + var recentConfirmed = confirmed[0] ? confirmed[0].keyId : 'none'; + console.log('Most recent signed key: ' + recent); + console.log('Most recent confirmed signed key: ' + recentConfirmed); + console.log( + 'Total signed key count:', + allKeys.length, + '-', + confirmed.length, + 'confirmed' + ); + + var confirmedCount = confirmed.length; + + // Keep MINIMUM_KEYS confirmed keys, then drop if older than a week + confirmed = confirmed.forEach(function(key, index) { + if (index < MINIMUM_KEYS) { + return; + } + var created_at = key.created_at || 0; + var age = Date.now() - created_at; + if (age > ARCHIVE_AGE) { + console.log( + 'Removing confirmed signed prekey:', + key.keyId, + 'with timestamp:', + created_at + ); + store.removeSignedPreKey(key.keyId); + confirmedCount--; + } + }); + + var 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(function(key, index) { + if (index < stillNeeded) { + return; + } + + var created_at = key.created_at || 0; + var age = Date.now() - created_at; + if (age > ARCHIVE_AGE) { + console.log( + 'Removing unconfirmed signed prekey:', + key.keyId, + 'with timestamp:', + created_at + ); + store.removeSignedPreKey(key.keyId); + } + }); + }); + }, + createAccount: function( + number, + verificationCode, + identityKeyPair, + profileKey, + deviceName, + userAgent, + readReceipts + ) { + var signalingKey = libsignal.crypto.getRandomBytes(32 + 20); + var password = btoa(getString(libsignal.crypto.getRandomBytes(16))); + password = password.substring(0, password.length - 2); + var registrationId = libsignal.KeyHelper.generateRegistrationId(); + + var previousNumber = getNumber(textsecure.storage.get('number_id')); + + return this.server + .confirmCode( + number, + verificationCode, + password, + signalingKey, + registrationId, + deviceName + ) + .then(function(response) { + if (previousNumber && previousNumber !== number) { + console.log( + 'New number is different from old number; deleting all previous data' + ); + + return textsecure.storage.protocol.removeAllData().then( + function() { + console.log('Successfully deleted previous data'); + return response; + }, + function(error) { + console.log( + 'Something went wrong deleting data from previous number', + error && error.stack ? error.stack : error + ); + + return response; + } + ); + } + + return response; + }) + .then( + function(response) { + textsecure.storage.remove('identityKey'); + textsecure.storage.remove('signaling_key'); + textsecure.storage.remove('password'); + textsecure.storage.remove('registrationId'); + textsecure.storage.remove('number_id'); + textsecure.storage.remove('device_name'); + textsecure.storage.remove('regionCode'); + textsecure.storage.remove('userAgent'); + textsecure.storage.remove('profileKey'); + textsecure.storage.remove('read-receipts-setting'); + + // update our own identity key, which may have changed + // if we're relinking after a reinstall on the master device + textsecure.storage.protocol.saveIdentityWithAttributes(number, { + id: number, + publicKey: identityKeyPair.pubKey, + firstUse: true, + timestamp: Date.now(), + verified: textsecure.storage.protocol.VerifiedStatus.VERIFIED, + nonblockingApproval: true, + }); + + textsecure.storage.put('identityKey', identityKeyPair); + textsecure.storage.put('signaling_key', signalingKey); + textsecure.storage.put('password', password); + textsecure.storage.put('registrationId', registrationId); + if (profileKey) { + textsecure.storage.put('profileKey', profileKey); + } + if (userAgent) { + textsecure.storage.put('userAgent', userAgent); + } + if (readReceipts) { + textsecure.storage.put('read-receipt-setting', true); + } else { + textsecure.storage.put('read-receipt-setting', false); + } + + textsecure.storage.user.setNumberAndDeviceId( + number, + response.deviceId || 1, + deviceName + ); + textsecure.storage.put( + 'regionCode', + libphonenumber.util.getRegionCodeForNumber(number) + ); + this.server.username = textsecure.storage.get('number_id'); + }.bind(this) + ); + }, + clearSessionsAndPreKeys: function() { + var store = textsecure.storage.protocol; + + console.log('clearing all sessions, prekeys, and signed prekeys'); + return Promise.all([ + store.clearPreKeyStore(), + store.clearSignedPreKeysStore(), + store.clearSessionStore(), + ]); + }, + // Takes the same object returned by generateKeys + confirmKeys: function(keys) { + var store = textsecure.storage.protocol; + var key = keys.signedPreKey; + var confirmed = true; + + console.log('confirmKeys: confirming key', key.keyId); + return store.storeSignedPreKey(key.keyId, key.keyPair, confirmed); + }, + generateKeys: function(count, progressCallback) { + if (typeof progressCallback !== 'function') { + progressCallback = undefined; + } + var startId = textsecure.storage.get('maxPreKeyId', 1); + var signedKeyId = textsecure.storage.get('signedKeyId', 1); + + if (typeof startId != 'number') { + throw new Error('Invalid maxPreKeyId'); + } + if (typeof signedKeyId != 'number') { + throw new Error('Invalid signedKeyId'); + } + + var store = textsecure.storage.protocol; + return store.getIdentityKeyPair().then( + function(identityKey) { + var result = { preKeys: [], identityKey: identityKey.pubKey }; + var promises = []; + + for (var keyId = startId; keyId < startId + count; ++keyId) { + promises.push( + libsignal.KeyHelper.generatePreKey(keyId).then(function(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(function(res) { + store.storeSignedPreKey(res.keyId, res.keyPair); + result.signedPreKey = { + keyId: res.keyId, + publicKey: res.keyPair.pubKey, + signature: res.signature, + // server.registerKeys doesn't use keyPair, confirmKeys does + keyPair: res.keyPair, + }; + }) + ); + + textsecure.storage.put('maxPreKeyId', startId + count); + textsecure.storage.put('signedKeyId', signedKeyId + 1); + return Promise.all(promises).then( + function() { + // This is primarily for the signed prekey summary it logs out + return this.cleanSignedPreKeys().then(function() { + return result; + }); + }.bind(this) + ); + }.bind(this) + ); + }, + registrationDone: function() { + console.log('registration done'); + this.dispatchEvent(new Event('registration')); + }, + }); + textsecure.AccountManager = AccountManager; +})(); diff --git a/libtextsecure/api.js b/libtextsecure/api.js index e89e5d07256d..71d6f4f137b2 100644 --- a/libtextsecure/api.js +++ b/libtextsecure/api.js @@ -1,87 +1,93 @@ var TextSecureServer = (function() { - 'use strict'; + 'use strict'; - function validateResponse(response, schema) { - try { - for (var i in schema) { - switch (schema[i]) { - case 'object': - case 'string': - case 'number': - if (typeof response[i] !== schema[i]) { - return false; - } - break; - } + function validateResponse(response, schema) { + try { + for (var i in schema) { + switch (schema[i]) { + case 'object': + case 'string': + case 'number': + if (typeof response[i] !== schema[i]) { + return false; } - } catch(ex) { - return false; + break; } - return true; + } + } catch (ex) { + return false; + } + return true; + } + + function createSocket(url) { + var proxyUrl = window.config.proxyUrl; + var requestOptions; + if (proxyUrl) { + requestOptions = { + ca: window.config.certificateAuthorities, + agent: new ProxyAgent(proxyUrl), + }; + } else { + requestOptions = { + ca: window.config.certificateAuthorities, + }; } - function createSocket(url) { + return new nodeWebSocket(url, null, null, null, requestOptions); + } + + window.setImmediate = nodeSetImmediate; + + function promise_ajax(url, options) { + return new Promise(function(resolve, reject) { + if (!url) { + url = options.host + '/' + options.path; + } + console.log(options.type, url); + var timeout = + typeof options.timeout !== 'undefined' ? options.timeout : 10000; + var proxyUrl = window.config.proxyUrl; - var requestOptions; + var agent; if (proxyUrl) { - requestOptions = { - ca: window.config.certificateAuthorities, - agent: new ProxyAgent(proxyUrl), - }; - } else { - requestOptions = { - ca: window.config.certificateAuthorities, - }; + agent = new ProxyAgent(proxyUrl); } - return new nodeWebSocket(url, null, null, null, requestOptions); - } + var fetchOptions = { + method: options.type, + body: options.data || null, + headers: { 'X-Signal-Agent': 'OWD' }, + agent: agent, + ca: options.certificateAuthorities, + timeout: timeout, + }; - window.setImmediate = nodeSetImmediate; + if (fetchOptions.body instanceof ArrayBuffer) { + // node-fetch doesn't support ArrayBuffer, only node Buffer + var contentLength = fetchOptions.body.byteLength; + fetchOptions.body = nodeBuffer.from(fetchOptions.body); - function promise_ajax(url, options) { - return new Promise(function (resolve, reject) { - if (!url) { - url = options.host + '/' + options.path; - } - console.log(options.type, url); - var timeout = typeof options.timeout !== 'undefined' ? options.timeout : 10000; + // node-fetch doesn't set content-length like S3 requires + fetchOptions.headers['Content-Length'] = contentLength; + } - var proxyUrl = window.config.proxyUrl; - var agent; - if (proxyUrl) { - agent = new ProxyAgent(proxyUrl); - } - - var fetchOptions = { - method: options.type, - body: options.data || null, - headers: { 'X-Signal-Agent': 'OWD' }, - agent: agent, - ca: options.certificateAuthorities, - timeout: timeout, - }; - - if (fetchOptions.body instanceof ArrayBuffer) { - // node-fetch doesn't support ArrayBuffer, only node Buffer - var contentLength = fetchOptions.body.byteLength; - fetchOptions.body = nodeBuffer.from(fetchOptions.body); - - // node-fetch doesn't set content-length like S3 requires - fetchOptions.headers["Content-Length"] = contentLength; - } - - if (options.user && options.password) { - fetchOptions.headers["Authorization"] = - "Basic " + btoa(getString(options.user) + ":" + getString(options.password)); - } - if (options.contentType) { - fetchOptions.headers["Content-Type"] = options.contentType; - } - window.nodeFetch(url, fetchOptions).then(function(response) { + if (options.user && options.password) { + fetchOptions.headers['Authorization'] = + 'Basic ' + + btoa(getString(options.user) + ':' + getString(options.password)); + } + if (options.contentType) { + fetchOptions.headers['Content-Type'] = options.contentType; + } + window + .nodeFetch(url, fetchOptions) + .then(function(response) { var resultPromise; - if (options.responseType === 'json' - && response.headers.get('Content-Type') === 'application/json') { + if ( + options.responseType === 'json' && + response.headers.get('Content-Type') === 'application/json' + ) { resultPromise = response.json(); } else if (options.responseType === 'arraybuffer') { resultPromise = response.buffer(); @@ -99,12 +105,14 @@ var TextSecureServer = (function() { if (options.validateResponse) { if (!validateResponse(result, options.validateResponse)) { console.log(options.type, url, response.status, 'Error'); - reject(HTTPError( - 'promise_ajax: invalid response', - response.status, - result, - options.stack - )); + reject( + HTTPError( + 'promise_ajax: invalid response', + response.status, + result, + options.stack + ) + ); } } } @@ -113,342 +121,379 @@ var TextSecureServer = (function() { resolve(result, response.status); } else { console.log(options.type, url, response.status, 'Error'); - reject(HTTPError( - 'promise_ajax: error response', - response.status, - result, - options.stack - )); + reject( + HTTPError( + 'promise_ajax: error response', + response.status, + result, + options.stack + ) + ); } }); - }).catch(function(e) { + }) + .catch(function(e) { console.log(options.type, url, 0, 'Error'); var stack = e.stack + '\nInitial stack:\n' + options.stack; reject(HTTPError('promise_ajax catch', 0, e.toString(), stack)); }); - }); - } + }); + } - function retry_ajax(url, options, limit, count) { - count = count || 0; - limit = limit || 3; - count++; - return promise_ajax(url, options).catch(function(e) { - if (e.name === 'HTTPError' && e.code === -1 && count < limit) { - return new Promise(function(resolve) { - setTimeout(function() { - resolve(retry_ajax(url, options, limit, count)); - }, 1000); - }); - } else { - throw e; - } + function retry_ajax(url, options, limit, count) { + count = count || 0; + limit = limit || 3; + count++; + return promise_ajax(url, options).catch(function(e) { + if (e.name === 'HTTPError' && e.code === -1 && count < limit) { + return new Promise(function(resolve) { + setTimeout(function() { + resolve(retry_ajax(url, options, limit, count)); + }, 1000); }); + } else { + throw e; + } + }); + } + + function ajax(url, options) { + options.stack = new Error().stack; // just in case, save stack here. + return retry_ajax(url, options); + } + + function HTTPError(message, code, response, stack) { + if (code > 999 || code < 100) { + code = -1; } - - function ajax(url, options) { - options.stack = new Error().stack; // just in case, save stack here. - return retry_ajax(url, options); + var 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; + } - function HTTPError(message, code, response, stack) { - if (code > 999 || code < 100) { - code = -1; - } - var 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; + var URL_CALLS = { + accounts: 'v1/accounts', + devices: 'v1/devices', + keys: 'v2/keys', + signed: 'v2/keys/signed', + messages: 'v1/messages', + attachment: 'v1/attachments', + profile: 'v1/profile', + }; + + function TextSecureServer(url, username, password, cdn_url) { + if (typeof url !== 'string') { + throw new Error('Invalid server url'); } + this.url = url; + this.cdn_url = cdn_url; + this.username = username; + this.password = password; + } - var URL_CALLS = { - accounts : "v1/accounts", - devices : "v1/devices", - keys : "v2/keys", - signed : "v2/keys/signed", - messages : "v1/messages", - attachment : "v1/attachments", - profile : "v1/profile" - }; - - function TextSecureServer(url, username, password, cdn_url) { - if (typeof url !== 'string') { - throw new Error('Invalid server url'); + TextSecureServer.prototype = { + constructor: TextSecureServer, + ajax: function(param) { + if (!param.urlParameters) { + param.urlParameters = ''; + } + return ajax(null, { + host: this.url, + path: URL_CALLS[param.call] + param.urlParameters, + type: param.httpType, + data: param.jsonData && textsecure.utils.jsonThing(param.jsonData), + contentType: 'application/json; charset=utf-8', + responseType: param.responseType, + user: this.username, + password: this.password, + validateResponse: param.validateResponse, + certificateAuthorities: window.config.certificateAuthorities, + timeout: param.timeout, + }).catch(function(e) { + var code = e.code; + if (code === 200) { + // happens sometimes when we get no response + // (TODO: Fix server to return 204? instead) + return null; } - this.url = url; - this.cdn_url = cdn_url; - this.username = username; - this.password = password; - } - - TextSecureServer.prototype = { - constructor: TextSecureServer, - ajax: function(param) { - if (!param.urlParameters) { - param.urlParameters = ''; - } - return ajax(null, { - host : this.url, - path : URL_CALLS[param.call] + param.urlParameters, - type : param.httpType, - data : param.jsonData && textsecure.utils.jsonThing(param.jsonData), - contentType : 'application/json; charset=utf-8', - responseType : param.responseType, - user : this.username, - password : this.password, - validateResponse: param.validateResponse, - certificateAuthorities: window.config.certificateAuthorities, - timeout : param.timeout - }).catch(function(e) { - var code = e.code; - if (code === 200) { - // happens sometimes when we get no response - // (TODO: Fix server to return 204? instead) - return null; - } - var message; - switch (code) { - case -1: - message = "Failed to connect to the server, please check your network connection."; - break; - case 413: - message = "Rate limit exceeded, please try again later."; - break; - case 403: - message = "Invalid code, please try again."; - break; - case 417: - // TODO: This shouldn't be a thing?, but its in the API doc? - message = "Number already registered."; - break; - case 401: - message = "Invalid authentication, most likely someone re-registered and invalidated our registration."; - break; - case 404: - message = "Number is not registered."; - break; - default: - message = "The server rejected our query, please file a bug report."; - } - e.message = message - throw e; - }); - }, - getProfile: function(number) { - return this.ajax({ - call : 'profile', - httpType : 'GET', - urlParameters : '/' + number, - responseType : 'json', - }); - }, - getAvatar: function(path) { - return ajax(this.cdn_url + '/' + path, { - type : "GET", - responseType: "arraybuffer", - contentType : "application/octet-stream", - certificateAuthorities: window.config.certificateAuthorities, - timeout: 0 - }); - }, - requestVerificationSMS: function(number) { - return this.ajax({ - call : 'accounts', - httpType : 'GET', - urlParameters : '/sms/code/' + number, - }); - }, - requestVerificationVoice: function(number) { - return this.ajax({ - call : 'accounts', - httpType : 'GET', - urlParameters : '/voice/code/' + number, - }); - }, - confirmCode: function(number, code, password, signaling_key, registrationId, deviceName) { - var jsonData = { - signalingKey : btoa(getString(signaling_key)), - supportsSms : false, - fetchesMessages : true, - registrationId : registrationId, - }; - - var call, urlPrefix, schema, responseType; - if (deviceName) { - jsonData.name = deviceName; - call = 'devices'; - urlPrefix = '/'; - schema = { deviceId: 'number' }; - responseType = 'json' - } else { - call = 'accounts'; - urlPrefix = '/code/'; - } - - this.username = number; - this.password = password; - return this.ajax({ - call : call, - httpType : 'PUT', - urlParameters : urlPrefix + code, - jsonData : jsonData, - responseType : responseType, - validateResponse : schema - }); - }, - getDevices: function(number) { - return this.ajax({ - call : 'devices', - httpType : 'GET', - }); - }, - registerKeys: function(genKeys) { - var keys = {}; - keys.identityKey = btoa(getString(genKeys.identityKey)); - keys.signedPreKey = { - keyId: genKeys.signedPreKey.keyId, - publicKey: btoa(getString(genKeys.signedPreKey.publicKey)), - signature: btoa(getString(genKeys.signedPreKey.signature)) - }; - - keys.preKeys = []; - var j = 0; - for (var i in genKeys.preKeys) { - keys.preKeys[j++] = { - keyId: genKeys.preKeys[i].keyId, - publicKey: btoa(getString(genKeys.preKeys[i].publicKey)) - }; - } - - // This is just to make the server happy - // (v2 clients should choke on publicKey) - keys.lastResortKey = {keyId: 0x7fffFFFF, publicKey: btoa("42")}; - - return this.ajax({ - call : 'keys', - httpType : 'PUT', - jsonData : keys, - }); - }, - setSignedPreKey: function(signedPreKey) { - return this.ajax({ - call : 'signed', - httpType : 'PUT', - jsonData : { - keyId: signedPreKey.keyId, - publicKey: btoa(getString(signedPreKey.publicKey)), - signature: btoa(getString(signedPreKey.signature)) - } - }); - }, - getMyKeys: function(number, deviceId) { - return this.ajax({ - call : 'keys', - httpType : 'GET', - responseType : 'json', - validateResponse : {count: 'number'} - }).then(function(res) { - return res.count; - }); - }, - getKeysForNumber: function(number, deviceId) { - if (deviceId === undefined) - deviceId = "*"; - - return this.ajax({ - call : 'keys', - httpType : 'GET', - urlParameters : "/" + number + "/" + deviceId, - responseType : 'json', - validateResponse : {identityKey: 'string', devices: 'object'} - }).then(function(res) { - if (res.devices.constructor !== Array) { - throw new Error("Invalid response"); - } - res.identityKey = StringView.base64ToBytes(res.identityKey); - res.devices.forEach(function(device) { - if ( !validateResponse(device, {signedPreKey: 'object'}) || - !validateResponse(device.signedPreKey, {publicKey: 'string', signature: 'string'}) ) { - throw new Error("Invalid signedPreKey"); - } - if ( device.preKey ) { - if ( !validateResponse(device, {preKey: 'object'}) || - !validateResponse(device.preKey, {publicKey: 'string'})) { - throw new Error("Invalid preKey"); - } - device.preKey.publicKey = StringView.base64ToBytes(device.preKey.publicKey); - } - device.signedPreKey.publicKey = StringView.base64ToBytes(device.signedPreKey.publicKey); - device.signedPreKey.signature = StringView.base64ToBytes(device.signedPreKey.signature); - }); - return res; - }); - }, - sendMessages: function(destination, messageArray, timestamp, silent) { - var jsonData = { messages: messageArray, timestamp: timestamp}; - - if (silent) { - jsonData.silent = true; - } - - return this.ajax({ - call : 'messages', - httpType : 'PUT', - urlParameters : '/' + destination, - jsonData : jsonData, - responseType : 'json', - }); - }, - getAttachment: function(id) { - return this.ajax({ - call : 'attachment', - httpType : 'GET', - urlParameters : '/' + id, - responseType : 'json', - validateResponse : {location: 'string'}, - }).then(function(response) { - return ajax(response.location, { - timeout : 0, - type : "GET", - responseType: "arraybuffer", - contentType : "application/octet-stream" - }); - }.bind(this)); - }, - putAttachment: function(encryptedBin) { - return this.ajax({ - call : 'attachment', - httpType : 'GET', - responseType : 'json', - }).then(function(response) { - return ajax(response.location, { - timeout : 0, - type : "PUT", - contentType : "application/octet-stream", - data : encryptedBin, - processData : false, - }).then(function() { - return response.idString; - }.bind(this)); - }.bind(this)); - }, - getMessageSocket: function() { - console.log('opening message socket', this.url); - return createSocket(this.url.replace('https://', 'wss://').replace('http://', 'ws://') - + '/v1/websocket/?login=' + encodeURIComponent(this.username) - + '&password=' + encodeURIComponent(this.password) - + '&agent=OWD'); - }, - getProvisioningSocket: function () { - console.log('opening provisioning socket', this.url); - return createSocket(this.url.replace('https://', 'wss://').replace('http://', 'ws://') - + '/v1/websocket/provisioning/?agent=OWD'); + var message; + switch (code) { + case -1: + message = + 'Failed to connect to the server, please check your network connection.'; + break; + case 413: + message = 'Rate limit exceeded, please try again later.'; + break; + case 403: + message = 'Invalid code, please try again.'; + break; + case 417: + // TODO: This shouldn't be a thing?, but its in the API doc? + message = 'Number already registered.'; + break; + case 401: + message = + 'Invalid authentication, most likely someone re-registered and invalidated our registration.'; + break; + case 404: + message = 'Number is not registered.'; + break; + default: + message = + 'The server rejected our query, please file a bug report.'; } - }; + e.message = message; + throw e; + }); + }, + getProfile: function(number) { + return this.ajax({ + call: 'profile', + httpType: 'GET', + urlParameters: '/' + number, + responseType: 'json', + }); + }, + getAvatar: function(path) { + return ajax(this.cdn_url + '/' + path, { + type: 'GET', + responseType: 'arraybuffer', + contentType: 'application/octet-stream', + certificateAuthorities: window.config.certificateAuthorities, + timeout: 0, + }); + }, + requestVerificationSMS: function(number) { + return this.ajax({ + call: 'accounts', + httpType: 'GET', + urlParameters: '/sms/code/' + number, + }); + }, + requestVerificationVoice: function(number) { + return this.ajax({ + call: 'accounts', + httpType: 'GET', + urlParameters: '/voice/code/' + number, + }); + }, + confirmCode: function( + number, + code, + password, + signaling_key, + registrationId, + deviceName + ) { + var jsonData = { + signalingKey: btoa(getString(signaling_key)), + supportsSms: false, + fetchesMessages: true, + registrationId: registrationId, + }; - return TextSecureServer; + var call, urlPrefix, schema, responseType; + if (deviceName) { + jsonData.name = deviceName; + call = 'devices'; + urlPrefix = '/'; + schema = { deviceId: 'number' }; + responseType = 'json'; + } else { + call = 'accounts'; + urlPrefix = '/code/'; + } + + this.username = number; + this.password = password; + return this.ajax({ + call: call, + httpType: 'PUT', + urlParameters: urlPrefix + code, + jsonData: jsonData, + responseType: responseType, + validateResponse: schema, + }); + }, + getDevices: function(number) { + return this.ajax({ + call: 'devices', + httpType: 'GET', + }); + }, + registerKeys: function(genKeys) { + var keys = {}; + keys.identityKey = btoa(getString(genKeys.identityKey)); + keys.signedPreKey = { + keyId: genKeys.signedPreKey.keyId, + publicKey: btoa(getString(genKeys.signedPreKey.publicKey)), + signature: btoa(getString(genKeys.signedPreKey.signature)), + }; + + keys.preKeys = []; + var j = 0; + for (var i in genKeys.preKeys) { + keys.preKeys[j++] = { + keyId: genKeys.preKeys[i].keyId, + publicKey: btoa(getString(genKeys.preKeys[i].publicKey)), + }; + } + + // This is just to make the server happy + // (v2 clients should choke on publicKey) + keys.lastResortKey = { keyId: 0x7fffffff, publicKey: btoa('42') }; + + return this.ajax({ + call: 'keys', + httpType: 'PUT', + jsonData: keys, + }); + }, + setSignedPreKey: function(signedPreKey) { + return this.ajax({ + call: 'signed', + httpType: 'PUT', + jsonData: { + keyId: signedPreKey.keyId, + publicKey: btoa(getString(signedPreKey.publicKey)), + signature: btoa(getString(signedPreKey.signature)), + }, + }); + }, + getMyKeys: function(number, deviceId) { + return this.ajax({ + call: 'keys', + httpType: 'GET', + responseType: 'json', + validateResponse: { count: 'number' }, + }).then(function(res) { + return res.count; + }); + }, + getKeysForNumber: function(number, deviceId) { + if (deviceId === undefined) deviceId = '*'; + + return this.ajax({ + call: 'keys', + httpType: 'GET', + urlParameters: '/' + number + '/' + deviceId, + responseType: 'json', + validateResponse: { identityKey: 'string', devices: 'object' }, + }).then(function(res) { + if (res.devices.constructor !== Array) { + throw new Error('Invalid response'); + } + res.identityKey = StringView.base64ToBytes(res.identityKey); + res.devices.forEach(function(device) { + if ( + !validateResponse(device, { signedPreKey: 'object' }) || + !validateResponse(device.signedPreKey, { + publicKey: 'string', + signature: 'string', + }) + ) { + throw new Error('Invalid signedPreKey'); + } + if (device.preKey) { + if ( + !validateResponse(device, { preKey: 'object' }) || + !validateResponse(device.preKey, { publicKey: 'string' }) + ) { + throw new Error('Invalid preKey'); + } + device.preKey.publicKey = StringView.base64ToBytes( + device.preKey.publicKey + ); + } + device.signedPreKey.publicKey = StringView.base64ToBytes( + device.signedPreKey.publicKey + ); + device.signedPreKey.signature = StringView.base64ToBytes( + device.signedPreKey.signature + ); + }); + return res; + }); + }, + sendMessages: function(destination, messageArray, timestamp, silent) { + var jsonData = { messages: messageArray, timestamp: timestamp }; + + if (silent) { + jsonData.silent = true; + } + + return this.ajax({ + call: 'messages', + httpType: 'PUT', + urlParameters: '/' + destination, + jsonData: jsonData, + responseType: 'json', + }); + }, + getAttachment: function(id) { + return this.ajax({ + call: 'attachment', + httpType: 'GET', + urlParameters: '/' + id, + responseType: 'json', + validateResponse: { location: 'string' }, + }).then( + function(response) { + return ajax(response.location, { + timeout: 0, + type: 'GET', + responseType: 'arraybuffer', + contentType: 'application/octet-stream', + }); + }.bind(this) + ); + }, + putAttachment: function(encryptedBin) { + return this.ajax({ + call: 'attachment', + httpType: 'GET', + responseType: 'json', + }).then( + function(response) { + return ajax(response.location, { + timeout: 0, + type: 'PUT', + contentType: 'application/octet-stream', + data: encryptedBin, + processData: false, + }).then( + function() { + return response.idString; + }.bind(this) + ); + }.bind(this) + ); + }, + getMessageSocket: function() { + console.log('opening message socket', this.url); + return createSocket( + this.url.replace('https://', 'wss://').replace('http://', 'ws://') + + '/v1/websocket/?login=' + + encodeURIComponent(this.username) + + '&password=' + + encodeURIComponent(this.password) + + '&agent=OWD' + ); + }, + getProvisioningSocket: function() { + console.log('opening provisioning socket', this.url); + return createSocket( + this.url.replace('https://', 'wss://').replace('http://', 'ws://') + + '/v1/websocket/provisioning/?agent=OWD' + ); + }, + }; + + return TextSecureServer; })(); diff --git a/libtextsecure/contacts_parser.js b/libtextsecure/contacts_parser.js index 678ffeb7f56d..27e8c25b93d8 100644 --- a/libtextsecure/contacts_parser.js +++ b/libtextsecure/contacts_parser.js @@ -1,52 +1,52 @@ function ProtoParser(arrayBuffer, protobuf) { - this.protobuf = protobuf; - this.buffer = new dcodeIO.ByteBuffer(); - this.buffer.append(arrayBuffer); - this.buffer.offset = 0; - this.buffer.limit = arrayBuffer.byteLength; + this.protobuf = protobuf; + this.buffer = new dcodeIO.ByteBuffer(); + this.buffer.append(arrayBuffer); + this.buffer.offset = 0; + this.buffer.limit = arrayBuffer.byteLength; } ProtoParser.prototype = { - constructor: ProtoParser, - next: function() { - try { - if (this.buffer.limit === this.buffer.offset) { - return undefined; // eof - } - var len = this.buffer.readVarint32(); - var nextBuffer = this.buffer.slice( - this.buffer.offset, this.buffer.offset+len - ).toArrayBuffer(); - // TODO: de-dupe ByteBuffer.js includes in libaxo/libts - // then remove this toArrayBuffer call. + constructor: ProtoParser, + next: function() { + try { + if (this.buffer.limit === this.buffer.offset) { + return undefined; // eof + } + var len = this.buffer.readVarint32(); + var nextBuffer = this.buffer + .slice(this.buffer.offset, this.buffer.offset + len) + .toArrayBuffer(); + // TODO: de-dupe ByteBuffer.js includes in libaxo/libts + // then remove this toArrayBuffer call. - var proto = this.protobuf.decode(nextBuffer); - this.buffer.skip(len); + var proto = this.protobuf.decode(nextBuffer); + this.buffer.skip(len); - if (proto.avatar) { - var attachmentLen = proto.avatar.length; - proto.avatar.data = this.buffer.slice( - this.buffer.offset, this.buffer.offset + attachmentLen - ).toArrayBuffer(); - this.buffer.skip(attachmentLen); - } + if (proto.avatar) { + var attachmentLen = proto.avatar.length; + proto.avatar.data = this.buffer + .slice(this.buffer.offset, this.buffer.offset + attachmentLen) + .toArrayBuffer(); + this.buffer.skip(attachmentLen); + } - if (proto.profileKey) { - proto.profileKey = proto.profileKey.toArrayBuffer(); - } + if (proto.profileKey) { + proto.profileKey = proto.profileKey.toArrayBuffer(); + } - return proto; - } catch(e) { - console.log(e); - } + return proto; + } catch (e) { + console.log(e); } + }, }; var GroupBuffer = function(arrayBuffer) { - ProtoParser.call(this, arrayBuffer, textsecure.protobuf.GroupDetails); + ProtoParser.call(this, arrayBuffer, textsecure.protobuf.GroupDetails); }; GroupBuffer.prototype = Object.create(ProtoParser.prototype); GroupBuffer.prototype.constructor = GroupBuffer; var ContactBuffer = function(arrayBuffer) { - ProtoParser.call(this, arrayBuffer, textsecure.protobuf.ContactDetails); + ProtoParser.call(this, arrayBuffer, textsecure.protobuf.ContactDetails); }; ContactBuffer.prototype = Object.create(ProtoParser.prototype); ContactBuffer.prototype.constructor = ContactBuffer; diff --git a/libtextsecure/crypto.js b/libtextsecure/crypto.js index 7455e4cc0e0c..a30954811f9a 100644 --- a/libtextsecure/crypto.js +++ b/libtextsecure/crypto.js @@ -1,184 +1,233 @@ -;(function(){ - 'use strict'; +(function() { + 'use strict'; - var encrypt = libsignal.crypto.encrypt; - var decrypt = libsignal.crypto.decrypt; - var calculateMAC = libsignal.crypto.calculateMAC; - var verifyMAC = libsignal.crypto.verifyMAC; + var encrypt = libsignal.crypto.encrypt; + var decrypt = libsignal.crypto.decrypt; + var calculateMAC = libsignal.crypto.calculateMAC; + var verifyMAC = libsignal.crypto.verifyMAC; - var PROFILE_IV_LENGTH = 12; // bytes - var PROFILE_KEY_LENGTH = 32; // bytes - var PROFILE_TAG_LENGTH = 128; // bits - var PROFILE_NAME_PADDED_LENGTH = 26; // bytes + var PROFILE_IV_LENGTH = 12; // bytes + var PROFILE_KEY_LENGTH = 32; // bytes + var PROFILE_TAG_LENGTH = 128; // bits + var PROFILE_NAME_PADDED_LENGTH = 26; // bytes - function verifyDigest(data, theirDigest) { - return crypto.subtle.digest({name: 'SHA-256'}, data).then(function(ourDigest) { - var a = new Uint8Array(ourDigest); - var b = new Uint8Array(theirDigest); - var result = 0; - for (var i=0; i < theirDigest.byteLength; ++i) { - result = result | (a[i] ^ b[i]); - } - if (result !== 0) { - throw new Error('Bad digest'); - } + function verifyDigest(data, theirDigest) { + return crypto.subtle + .digest({ name: 'SHA-256' }, data) + .then(function(ourDigest) { + var a = new Uint8Array(ourDigest); + var b = new Uint8Array(theirDigest); + var result = 0; + for (var i = 0; i < theirDigest.byteLength; ++i) { + result = result | (a[i] ^ b[i]); + } + if (result !== 0) { + throw new Error('Bad digest'); + } + }); + } + function calculateDigest(data) { + return crypto.subtle.digest({ name: 'SHA-256' }, data); + } + + window.textsecure = window.textsecure || {}; + window.textsecure.crypto = { + // Decrypts message into a raw string + decryptWebsocketMessage: function(message, signaling_key) { + var decodedMessage = message.toArrayBuffer(); + + if (signaling_key.byteLength != 52) { + throw new Error('Got invalid length signaling_key'); + } + 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]); + } + + var aes_key = signaling_key.slice(0, 32); + var mac_key = signaling_key.slice(32, 32 + 20); + + var iv = decodedMessage.slice(1, 1 + 16); + var ciphertext = decodedMessage.slice( + 1 + 16, + decodedMessage.byteLength - 10 + ); + var ivAndCiphertext = decodedMessage.slice( + 0, + decodedMessage.byteLength - 10 + ); + var mac = decodedMessage.slice( + decodedMessage.byteLength - 10, + decodedMessage.byteLength + ); + + return verifyMAC(ivAndCiphertext, mac_key, mac, 10).then(function() { + return decrypt(aes_key, ciphertext, iv); + }); + }, + + decryptAttachment: function(encryptedBin, keys, theirDigest) { + if (keys.byteLength != 64) { + throw new Error('Got invalid length attachment keys'); + } + if (encryptedBin.byteLength < 16 + 32) { + throw new Error('Got invalid length attachment'); + } + + var aes_key = keys.slice(0, 32); + var mac_key = keys.slice(32, 64); + + var iv = encryptedBin.slice(0, 16); + var ciphertext = encryptedBin.slice(16, encryptedBin.byteLength - 32); + var ivAndCiphertext = encryptedBin.slice(0, encryptedBin.byteLength - 32); + var mac = encryptedBin.slice( + encryptedBin.byteLength - 32, + encryptedBin.byteLength + ); + + return verifyMAC(ivAndCiphertext, mac_key, mac, 32) + .then(function() { + if (theirDigest !== null) { + return verifyDigest(encryptedBin, theirDigest); + } + }) + .then(function() { + return decrypt(aes_key, ciphertext, iv); }); - } - function calculateDigest(data) { - return crypto.subtle.digest({name: 'SHA-256'}, data); - } + }, - window.textsecure = window.textsecure || {}; - window.textsecure.crypto = { - // Decrypts message into a raw string - decryptWebsocketMessage: function(message, signaling_key) { - var decodedMessage = message.toArrayBuffer(); + encryptAttachment: function(plaintext, keys, iv) { + if ( + !(plaintext instanceof ArrayBuffer) && + !ArrayBuffer.isView(plaintext) + ) { + throw new TypeError( + '`plaintext` must be an `ArrayBuffer` or `ArrayBufferView`; got: ' + + typeof plaintext + ); + } - if (signaling_key.byteLength != 52) { - throw new Error("Got invalid length signaling_key"); - } - 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]); - } + if (keys.byteLength != 64) { + throw new Error('Got invalid length attachment keys'); + } + if (iv.byteLength != 16) { + throw new Error('Got invalid length attachment iv'); + } + var aes_key = keys.slice(0, 32); + var mac_key = keys.slice(32, 64); - var aes_key = signaling_key.slice(0, 32); - var mac_key = signaling_key.slice(32, 32 + 20); + return encrypt(aes_key, plaintext, iv).then(function(ciphertext) { + var ivAndCiphertext = new Uint8Array(16 + ciphertext.byteLength); + ivAndCiphertext.set(new Uint8Array(iv)); + ivAndCiphertext.set(new Uint8Array(ciphertext), 16); - var iv = decodedMessage.slice(1, 1 + 16); - var ciphertext = decodedMessage.slice(1 + 16, decodedMessage.byteLength - 10); - var ivAndCiphertext = decodedMessage.slice(0, decodedMessage.byteLength - 10); - var mac = decodedMessage.slice(decodedMessage.byteLength - 10, decodedMessage.byteLength); - - return verifyMAC(ivAndCiphertext, mac_key, mac, 10).then(function() { - return decrypt(aes_key, ciphertext, iv); - }); - }, - - decryptAttachment: function(encryptedBin, keys, theirDigest) { - if (keys.byteLength != 64) { - throw new Error("Got invalid length attachment keys"); - } - if (encryptedBin.byteLength < 16 + 32) { - throw new Error("Got invalid length attachment"); - } - - var aes_key = keys.slice(0, 32); - var mac_key = keys.slice(32, 64); - - var iv = encryptedBin.slice(0, 16); - var ciphertext = encryptedBin.slice(16, encryptedBin.byteLength - 32); - var ivAndCiphertext = encryptedBin.slice(0, encryptedBin.byteLength - 32); - var mac = encryptedBin.slice(encryptedBin.byteLength - 32, encryptedBin.byteLength); - - return verifyMAC(ivAndCiphertext, mac_key, mac, 32).then(function() { - if (theirDigest !== null) { - return verifyDigest(encryptedBin, theirDigest); - } - }).then(function() { - return decrypt(aes_key, ciphertext, iv); - }); - }, - - encryptAttachment: function(plaintext, keys, iv) { - if (!(plaintext instanceof ArrayBuffer) && !ArrayBuffer.isView(plaintext)) { - throw new TypeError( - '`plaintext` must be an `ArrayBuffer` or `ArrayBufferView`; got: ' + - typeof plaintext + return calculateMAC(mac_key, ivAndCiphertext.buffer).then(function( + mac + ) { + var encryptedBin = new Uint8Array(16 + ciphertext.byteLength + 32); + encryptedBin.set(ivAndCiphertext); + encryptedBin.set(new Uint8Array(mac), 16 + ciphertext.byteLength); + return calculateDigest(encryptedBin.buffer).then(function(digest) { + return { ciphertext: encryptedBin.buffer, digest: digest }; + }); + }); + }); + }, + encryptProfile: function(data, key) { + var iv = libsignal.crypto.getRandomBytes(PROFILE_IV_LENGTH); + if (key.byteLength != PROFILE_KEY_LENGTH) { + throw new Error('Got invalid length profile key'); + } + if (iv.byteLength != PROFILE_IV_LENGTH) { + throw new Error('Got invalid length profile iv'); + } + return crypto.subtle + .importKey('raw', key, { name: 'AES-GCM' }, false, ['encrypt']) + .then(function(key) { + return crypto.subtle + .encrypt( + { name: 'AES-GCM', iv: iv, tagLength: PROFILE_TAG_LENGTH }, + key, + data + ) + .then(function(ciphertext) { + var ivAndCiphertext = new Uint8Array( + PROFILE_IV_LENGTH + ciphertext.byteLength ); - } - - if (keys.byteLength != 64) { - throw new Error("Got invalid length attachment keys"); - } - if (iv.byteLength != 16) { - throw new Error("Got invalid length attachment iv"); - } - var aes_key = keys.slice(0, 32); - var mac_key = keys.slice(32, 64); - - return encrypt(aes_key, plaintext, iv).then(function(ciphertext) { - var ivAndCiphertext = new Uint8Array(16 + ciphertext.byteLength); - ivAndCiphertext.set(new Uint8Array(iv)); - ivAndCiphertext.set(new Uint8Array(ciphertext), 16); - - return calculateMAC(mac_key, ivAndCiphertext.buffer).then(function(mac) { - var encryptedBin = new Uint8Array(16 + ciphertext.byteLength + 32); - encryptedBin.set(ivAndCiphertext); - encryptedBin.set(new Uint8Array(mac), 16 + ciphertext.byteLength); - return calculateDigest(encryptedBin.buffer).then(function(digest) { - return { ciphertext: encryptedBin.buffer, digest: digest }; - }); - }); - }); - }, - encryptProfile: function(data, key) { - var iv = libsignal.crypto.getRandomBytes(PROFILE_IV_LENGTH); - if (key.byteLength != PROFILE_KEY_LENGTH) { - throw new Error("Got invalid length profile key"); - } - if (iv.byteLength != PROFILE_IV_LENGTH) { - throw new Error("Got invalid length profile iv"); - } - return crypto.subtle.importKey('raw', key, {name: 'AES-GCM'}, false, ['encrypt']).then(function(key) { - return crypto.subtle.encrypt({name: 'AES-GCM', iv: iv, tagLength: PROFILE_TAG_LENGTH}, key, data).then(function(ciphertext) { - var ivAndCiphertext = new Uint8Array(PROFILE_IV_LENGTH + ciphertext.byteLength); ivAndCiphertext.set(new Uint8Array(iv)); - ivAndCiphertext.set(new Uint8Array(ciphertext), PROFILE_IV_LENGTH); + ivAndCiphertext.set( + new Uint8Array(ciphertext), + PROFILE_IV_LENGTH + ); return ivAndCiphertext.buffer; }); - }); - }, - decryptProfile: function(data, key) { - if (data.byteLength < 12 + 16 + 1) { - throw new Error("Got too short input: " + data.byteLength); - } - var iv = data.slice(0, PROFILE_IV_LENGTH); - var ciphertext = data.slice(PROFILE_IV_LENGTH, data.byteLength); - if (key.byteLength != PROFILE_KEY_LENGTH) { - throw new Error("Got invalid length profile key"); - } - if (iv.byteLength != PROFILE_IV_LENGTH) { - throw new Error("Got invalid length profile iv"); - } - var error = new Error(); // save stack - return crypto.subtle.importKey('raw', key, {name: 'AES-GCM'}, false, ['decrypt']).then(function(key) { - return crypto.subtle.decrypt({name: 'AES-GCM', iv: iv, tagLength: PROFILE_TAG_LENGTH}, key, ciphertext).catch(function(e) { + }); + }, + decryptProfile: function(data, key) { + if (data.byteLength < 12 + 16 + 1) { + throw new Error('Got too short input: ' + data.byteLength); + } + var iv = data.slice(0, PROFILE_IV_LENGTH); + var ciphertext = data.slice(PROFILE_IV_LENGTH, data.byteLength); + if (key.byteLength != PROFILE_KEY_LENGTH) { + throw new Error('Got invalid length profile key'); + } + if (iv.byteLength != PROFILE_IV_LENGTH) { + throw new Error('Got invalid length profile iv'); + } + var error = new Error(); // save stack + return crypto.subtle + .importKey('raw', key, { name: 'AES-GCM' }, false, ['decrypt']) + .then(function(key) { + return crypto.subtle + .decrypt( + { name: 'AES-GCM', iv: iv, tagLength: PROFILE_TAG_LENGTH }, + key, + ciphertext + ) + .catch(function(e) { if (e.name === 'OperationError') { // bad mac, basically. - error.message = 'Failed to decrypt profile data. Most likely the profile key has changed.'; + error.message = + 'Failed to decrypt profile data. Most likely the profile key has changed.'; error.name = 'ProfileDecryptError'; throw error; } }); - }); - }, - encryptProfileName: function(name, key) { - var padded = new Uint8Array(PROFILE_NAME_PADDED_LENGTH); - padded.set(new Uint8Array(name)); - return textsecure.crypto.encryptProfile(padded.buffer, key); - }, - decryptProfileName: function(encryptedProfileName, key) { - var data = dcodeIO.ByteBuffer.wrap(encryptedProfileName, 'base64').toArrayBuffer(); - return textsecure.crypto.decryptProfile(data, key).then(function(decrypted) { - // unpad - var name = ''; - var padded = new Uint8Array(decrypted); - for (var i = padded.length; i > 0; i--) { - if (padded[i-1] !== 0x00) { - break; - } + }); + }, + encryptProfileName: function(name, key) { + var padded = new Uint8Array(PROFILE_NAME_PADDED_LENGTH); + padded.set(new Uint8Array(name)); + return textsecure.crypto.encryptProfile(padded.buffer, key); + }, + decryptProfileName: function(encryptedProfileName, key) { + var data = dcodeIO.ByteBuffer.wrap( + encryptedProfileName, + 'base64' + ).toArrayBuffer(); + return textsecure.crypto + .decryptProfile(data, key) + .then(function(decrypted) { + // unpad + var name = ''; + var padded = new Uint8Array(decrypted); + for (var i = padded.length; i > 0; i--) { + if (padded[i - 1] !== 0x00) { + break; } + } - return dcodeIO.ByteBuffer.wrap(padded).slice(0, i).toArrayBuffer(); - }); - }, + return dcodeIO.ByteBuffer.wrap(padded) + .slice(0, i) + .toArrayBuffer(); + }); + }, - - getRandomBytes: function(size) { - return libsignal.crypto.getRandomBytes(size); - } - }; + getRandomBytes: function(size) { + return libsignal.crypto.getRandomBytes(size); + }, + }; })(); diff --git a/libtextsecure/errors.js b/libtextsecure/errors.js index 123603067570..0e2d97a52c06 100644 --- a/libtextsecure/errors.js +++ b/libtextsecure/errors.js @@ -1,165 +1,164 @@ -;(function() { - 'use strict'; +(function() { + 'use strict'; - var registeredFunctions = {}; - var Type = { - ENCRYPT_MESSAGE: 1, - INIT_SESSION: 2, - TRANSMIT_MESSAGE: 3, - REBUILD_MESSAGE: 4, - RETRY_SEND_MESSAGE_PROTO: 5 - }; - window.textsecure = window.textsecure || {}; - window.textsecure.replay = { - Type: Type, - registerFunction: function(func, functionCode) { - registeredFunctions[functionCode] = func; - } - }; + var registeredFunctions = {}; + var Type = { + ENCRYPT_MESSAGE: 1, + INIT_SESSION: 2, + TRANSMIT_MESSAGE: 3, + REBUILD_MESSAGE: 4, + RETRY_SEND_MESSAGE_PROTO: 5, + }; + window.textsecure = window.textsecure || {}; + window.textsecure.replay = { + Type: Type, + registerFunction: function(func, functionCode) { + registeredFunctions[functionCode] = func; + }, + }; - function inherit(Parent, Child) { - Child.prototype = Object.create(Parent.prototype, { - constructor: { - value: Child, - writable: true, - configurable: true - } - }); - } - function appendStack(newError, originalError) { - newError.stack += '\nOriginal stack:\n' + originalError.stack; + function inherit(Parent, Child) { + Child.prototype = Object.create(Parent.prototype, { + constructor: { + value: Child, + writable: true, + configurable: true, + }, + }); + } + function appendStack(newError, originalError) { + newError.stack += '\nOriginal stack:\n' + originalError.stack; + } + + function ReplayableError(options) { + options = options || {}; + this.name = options.name || 'ReplayableError'; + this.message = options.message; + + Error.call(this, options.message); + + // Maintains proper stack trace, where our error was thrown (only available on V8) + // via https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error + if (Error.captureStackTrace) { + Error.captureStackTrace(this); } - function ReplayableError(options) { - options = options || {}; - this.name = options.name || 'ReplayableError'; - this.message = options.message; + this.functionCode = options.functionCode; + this.args = options.args; + } + inherit(Error, ReplayableError); - Error.call(this, options.message); + ReplayableError.prototype.replay = function() { + var argumentsAsArray = Array.prototype.slice.call(arguments, 0); + var args = this.args.concat(argumentsAsArray); + return registeredFunctions[this.functionCode].apply(window, args); + }; - // Maintains proper stack trace, where our error was thrown (only available on V8) - // via https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error - if (Error.captureStackTrace) { - Error.captureStackTrace(this); - } + function IncomingIdentityKeyError(number, message, key) { + this.number = number.split('.')[0]; + this.identityKey = key; - this.functionCode = options.functionCode; - this.args = options.args; + ReplayableError.call(this, { + functionCode: Type.INIT_SESSION, + args: [number, message], + name: 'IncomingIdentityKeyError', + message: 'The identity of ' + this.number + ' has changed.', + }); + } + inherit(ReplayableError, IncomingIdentityKeyError); + + function OutgoingIdentityKeyError(number, message, timestamp, identityKey) { + this.number = number.split('.')[0]; + this.identityKey = identityKey; + + ReplayableError.call(this, { + functionCode: Type.ENCRYPT_MESSAGE, + args: [number, message, timestamp], + name: 'OutgoingIdentityKeyError', + message: 'The identity of ' + this.number + ' has changed.', + }); + } + inherit(ReplayableError, OutgoingIdentityKeyError); + + function OutgoingMessageError(number, message, timestamp, httpError) { + ReplayableError.call(this, { + functionCode: Type.ENCRYPT_MESSAGE, + args: [number, message, timestamp], + name: 'OutgoingMessageError', + message: httpError ? httpError.message : 'no http error', + }); + + if (httpError) { + this.code = httpError.code; + appendStack(this, httpError); } - inherit(Error, ReplayableError); + } + inherit(ReplayableError, OutgoingMessageError); - ReplayableError.prototype.replay = function() { - var argumentsAsArray = Array.prototype.slice.call(arguments, 0); - var args = this.args.concat(argumentsAsArray); - return registeredFunctions[this.functionCode].apply(window, args); - }; + function SendMessageNetworkError(number, jsonData, httpError, timestamp) { + this.number = number; + this.code = httpError.code; - function IncomingIdentityKeyError(number, message, key) { - this.number = number.split('.')[0]; - this.identityKey = key; + ReplayableError.call(this, { + functionCode: Type.TRANSMIT_MESSAGE, + args: [number, jsonData, timestamp], + name: 'SendMessageNetworkError', + message: httpError.message, + }); - ReplayableError.call(this, { - functionCode : Type.INIT_SESSION, - args : [number, message], - name : 'IncomingIdentityKeyError', - message : "The identity of " + this.number + " has changed." - }); + appendStack(this, httpError); + } + inherit(ReplayableError, SendMessageNetworkError); + + function SignedPreKeyRotationError(numbers, message, timestamp) { + ReplayableError.call(this, { + functionCode: Type.RETRY_SEND_MESSAGE_PROTO, + args: [numbers, message, timestamp], + name: 'SignedPreKeyRotationError', + message: 'Too many signed prekey rotation failures', + }); + } + inherit(ReplayableError, SignedPreKeyRotationError); + + function MessageError(message, httpError) { + this.code = httpError.code; + + ReplayableError.call(this, { + functionCode: Type.REBUILD_MESSAGE, + args: [message], + name: 'MessageError', + message: httpError.message, + }); + + appendStack(this, httpError); + } + inherit(ReplayableError, MessageError); + + function UnregisteredUserError(number, httpError) { + this.message = httpError.message; + this.name = 'UnregisteredUserError'; + + Error.call(this, this.message); + + // Maintains proper stack trace, where our error was thrown (only available on V8) + // via https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error + if (Error.captureStackTrace) { + Error.captureStackTrace(this); } - inherit(ReplayableError, IncomingIdentityKeyError); - function OutgoingIdentityKeyError(number, message, timestamp, identityKey) { - this.number = number.split('.')[0]; - this.identityKey = identityKey; + this.number = number; + this.code = httpError.code; - ReplayableError.call(this, { - functionCode : Type.ENCRYPT_MESSAGE, - args : [number, message, timestamp], - name : 'OutgoingIdentityKeyError', - message : "The identity of " + this.number + " has changed." - }); - } - inherit(ReplayableError, OutgoingIdentityKeyError); - - function OutgoingMessageError(number, message, timestamp, httpError) { - ReplayableError.call(this, { - functionCode : Type.ENCRYPT_MESSAGE, - args : [number, message, timestamp], - name : 'OutgoingMessageError', - message : httpError ? httpError.message : 'no http error' - }); - - if (httpError) { - this.code = httpError.code; - appendStack(this, httpError); - } - } - inherit(ReplayableError, OutgoingMessageError); - - function SendMessageNetworkError(number, jsonData, httpError, timestamp) { - this.number = number; - this.code = httpError.code; - - ReplayableError.call(this, { - functionCode : Type.TRANSMIT_MESSAGE, - args : [number, jsonData, timestamp], - name : 'SendMessageNetworkError', - message : httpError.message - }); - - appendStack(this, httpError); - } - inherit(ReplayableError, SendMessageNetworkError); - - function SignedPreKeyRotationError(numbers, message, timestamp) { - ReplayableError.call(this, { - functionCode : Type.RETRY_SEND_MESSAGE_PROTO, - args : [numbers, message, timestamp], - name : 'SignedPreKeyRotationError', - message : "Too many signed prekey rotation failures" - }); - } - inherit(ReplayableError, SignedPreKeyRotationError); - - function MessageError(message, httpError) { - this.code = httpError.code; - - ReplayableError.call(this, { - functionCode : Type.REBUILD_MESSAGE, - args : [message], - name : 'MessageError', - message : httpError.message - }); - - appendStack(this, httpError); - } - inherit(ReplayableError, MessageError); - - function UnregisteredUserError(number, httpError) { - this.message = httpError.message; - this.name = 'UnregisteredUserError'; - - Error.call(this, this.message); - - // Maintains proper stack trace, where our error was thrown (only available on V8) - // via https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error - if (Error.captureStackTrace) { - Error.captureStackTrace(this); - } - - this.number = number; - this.code = httpError.code; - - appendStack(this, httpError); - } - inherit(Error, UnregisteredUserError); - - window.textsecure.UnregisteredUserError = UnregisteredUserError; - window.textsecure.SendMessageNetworkError = SendMessageNetworkError; - window.textsecure.IncomingIdentityKeyError = IncomingIdentityKeyError; - window.textsecure.OutgoingIdentityKeyError = OutgoingIdentityKeyError; - window.textsecure.ReplayableError = ReplayableError; - window.textsecure.OutgoingMessageError = OutgoingMessageError; - window.textsecure.MessageError = MessageError; - window.textsecure.SignedPreKeyRotationError = SignedPreKeyRotationError; + appendStack(this, httpError); + } + inherit(Error, UnregisteredUserError); + window.textsecure.UnregisteredUserError = UnregisteredUserError; + window.textsecure.SendMessageNetworkError = SendMessageNetworkError; + window.textsecure.IncomingIdentityKeyError = IncomingIdentityKeyError; + window.textsecure.OutgoingIdentityKeyError = OutgoingIdentityKeyError; + window.textsecure.ReplayableError = ReplayableError; + window.textsecure.OutgoingMessageError = OutgoingMessageError; + window.textsecure.MessageError = MessageError; + window.textsecure.SignedPreKeyRotationError = SignedPreKeyRotationError; })(); diff --git a/libtextsecure/event_target.js b/libtextsecure/event_target.js index 3bfce9cd0632..6594a5b52d34 100644 --- a/libtextsecure/event_target.js +++ b/libtextsecure/event_target.js @@ -2,79 +2,78 @@ * Implements EventTarget * https://developer.mozilla.org/en-US/docs/Web/API/EventTarget */ -;(function () { - 'use strict'; - window.textsecure = window.textsecure || {}; +(function() { + 'use strict'; + window.textsecure = window.textsecure || {}; - function EventTarget() { - } + function EventTarget() {} - EventTarget.prototype = { - constructor: EventTarget, - dispatchEvent: function(ev) { - if (!(ev instanceof Event)) { - throw new Error('Expects an event'); - } - if (this.listeners === null || typeof this.listeners !== 'object') { - this.listeners = {}; - } - var listeners = this.listeners[ev.type]; - var results = []; - if (typeof listeners === 'object') { - for (var i = 0, max = listeners.length; i < max; i += 1) { - var listener = listeners[i]; - if (typeof listener === 'function') { - results.push(listener.call(null, ev)); - } - } - } - return results; - }, - addEventListener: function(eventName, callback) { - if (typeof eventName !== 'string') { - throw new Error('First argument expects a string'); - } - if (typeof callback !== 'function') { - throw new Error('Second argument expects a function'); - } - if (this.listeners === null || typeof this.listeners !== 'object') { - this.listeners = {}; - } - var listeners = this.listeners[eventName]; - if (typeof listeners !== 'object') { - listeners = []; - } - listeners.push(callback); - this.listeners[eventName] = listeners; - }, - removeEventListener: function(eventName, callback) { - if (typeof eventName !== 'string') { - throw new Error('First argument expects a string'); - } - if (typeof callback !== 'function') { - throw new Error('Second argument expects a function'); - } - if (this.listeners === null || typeof this.listeners !== 'object') { - this.listeners = {}; - } - var listeners = this.listeners[eventName]; - if (typeof listeners === 'object') { - for (var i=0; i < listeners.length; ++ i) { - if (listeners[i] === callback) { - listeners.splice(i, 1); - return; - } - } - } - this.listeners[eventName] = listeners; - }, - extend: function(obj) { - for (var prop in obj) { - this[prop] = obj[prop]; + EventTarget.prototype = { + constructor: EventTarget, + dispatchEvent: function(ev) { + if (!(ev instanceof Event)) { + throw new Error('Expects an event'); + } + if (this.listeners === null || typeof this.listeners !== 'object') { + this.listeners = {}; + } + var listeners = this.listeners[ev.type]; + var results = []; + if (typeof listeners === 'object') { + for (var i = 0, max = listeners.length; i < max; i += 1) { + var listener = listeners[i]; + if (typeof listener === 'function') { + results.push(listener.call(null, ev)); } - return this; } - }; + } + return results; + }, + addEventListener: function(eventName, callback) { + if (typeof eventName !== 'string') { + throw new Error('First argument expects a string'); + } + if (typeof callback !== 'function') { + throw new Error('Second argument expects a function'); + } + if (this.listeners === null || typeof this.listeners !== 'object') { + this.listeners = {}; + } + var listeners = this.listeners[eventName]; + if (typeof listeners !== 'object') { + listeners = []; + } + listeners.push(callback); + this.listeners[eventName] = listeners; + }, + removeEventListener: function(eventName, callback) { + if (typeof eventName !== 'string') { + throw new Error('First argument expects a string'); + } + if (typeof callback !== 'function') { + throw new Error('Second argument expects a function'); + } + if (this.listeners === null || typeof this.listeners !== 'object') { + this.listeners = {}; + } + var listeners = this.listeners[eventName]; + if (typeof listeners === 'object') { + for (var i = 0; i < listeners.length; ++i) { + if (listeners[i] === callback) { + listeners.splice(i, 1); + return; + } + } + } + this.listeners[eventName] = listeners; + }, + extend: function(obj) { + for (var prop in obj) { + this[prop] = obj[prop]; + } + return this; + }, + }; - textsecure.EventTarget = EventTarget; -}()); + textsecure.EventTarget = EventTarget; +})(); diff --git a/libtextsecure/helpers.js b/libtextsecure/helpers.js index 9dd5caafbd11..bd56e958b7c0 100644 --- a/libtextsecure/helpers.js +++ b/libtextsecure/helpers.js @@ -10,64 +10,62 @@ var StaticByteBufferProto = new dcodeIO.ByteBuffer().__proto__; var StaticArrayBufferProto = new ArrayBuffer().__proto__; var StaticUint8ArrayProto = new Uint8Array().__proto__; function getString(thing) { - if (thing === Object(thing)) { - if (thing.__proto__ == StaticUint8ArrayProto) - return String.fromCharCode.apply(null, thing); - if (thing.__proto__ == StaticArrayBufferProto) - return getString(new Uint8Array(thing)); - if (thing.__proto__ == StaticByteBufferProto) - return thing.toString("binary"); - } - return thing; + if (thing === Object(thing)) { + if (thing.__proto__ == StaticUint8ArrayProto) + return String.fromCharCode.apply(null, thing); + if (thing.__proto__ == StaticArrayBufferProto) + return getString(new Uint8Array(thing)); + if (thing.__proto__ == StaticByteBufferProto) + return thing.toString('binary'); + } + return thing; } function getStringable(thing) { - return (typeof thing == "string" || typeof thing == "number" || typeof thing == "boolean" || - (thing === Object(thing) && - (thing.__proto__ == StaticArrayBufferProto || - thing.__proto__ == StaticUint8ArrayProto || - thing.__proto__ == StaticByteBufferProto))); + return ( + typeof thing == 'string' || + typeof thing == 'number' || + typeof thing == 'boolean' || + (thing === Object(thing) && + (thing.__proto__ == StaticArrayBufferProto || + thing.__proto__ == StaticUint8ArrayProto || + thing.__proto__ == StaticByteBufferProto)) + ); } // Number formatting utils -window.textsecure.utils = function() { - var self = {}; - self.unencodeNumber = function(number) { - return number.split("."); - }; +window.textsecure.utils = (function() { + var self = {}; + self.unencodeNumber = function(number) { + return number.split('.'); + }; - self.isNumberSane = function(number) { - return number[0] == "+" && - /^[0-9]+$/.test(number.substring(1)); + self.isNumberSane = function(number) { + return number[0] == '+' && /^[0-9]+$/.test(number.substring(1)); + }; + + /************************** + *** JSON'ing Utilities *** + **************************/ + function ensureStringed(thing) { + if (getStringable(thing)) return getString(thing); + else if (thing instanceof Array) { + var res = []; + for (var i = 0; i < thing.length; i++) res[i] = ensureStringed(thing[i]); + return res; + } else if (thing === Object(thing)) { + var res = {}; + for (var key in thing) res[key] = ensureStringed(thing[key]); + return res; + } else if (thing === null) { + return null; } + throw new Error('unsure of how to jsonify object of type ' + typeof thing); + } - /************************** - *** JSON'ing Utilities *** - **************************/ - function ensureStringed(thing) { - if (getStringable(thing)) - return getString(thing); - else if (thing instanceof Array) { - var res = []; - for (var i = 0; i < thing.length; i++) - res[i] = ensureStringed(thing[i]); - return res; - } else if (thing === Object(thing)) { - var res = {}; - for (var key in thing) - res[key] = ensureStringed(thing[key]); - return res; - } else if (thing === null) { - return null; - } - throw new Error("unsure of how to jsonify object of type " + typeof thing); - - } - - self.jsonThing = function(thing) { - return JSON.stringify(ensureStringed(thing)); - } - - return self; -}(); + self.jsonThing = function(thing) { + return JSON.stringify(ensureStringed(thing)); + }; + return self; +})(); diff --git a/libtextsecure/key_worker.js b/libtextsecure/key_worker.js index d32e420bebd4..8a249fd8ebf6 100644 --- a/libtextsecure/key_worker.js +++ b/libtextsecure/key_worker.js @@ -27,33 +27,32 @@ */ var store = {}; window.textsecure.storage.impl = { - /***************************** - *** Override Storage Routines *** - *****************************/ - put: function(key, value) { - if (value === undefined) - throw new Error("Tried to store undefined"); - store[key] = value; - postMessage({method: 'set', key: key, value: value}); - }, + /***************************** + *** Override Storage Routines *** + *****************************/ + put: function(key, value) { + if (value === undefined) throw new Error('Tried to store undefined'); + store[key] = value; + postMessage({ method: 'set', key: key, value: value }); + }, - get: function(key, defaultValue) { - if (key in store) { - return store[key]; - } else { - return defaultValue; - } - }, + get: function(key, defaultValue) { + if (key in store) { + return store[key]; + } else { + return defaultValue; + } + }, - remove: function(key) { - delete store[key]; - postMessage({method: 'remove', key: key}); - }, + remove: function(key) { + delete store[key]; + postMessage({ method: 'remove', key: key }); + }, }; onmessage = function(e) { - store = e.data; - textsecure.protocol_wrapper.generateKeys().then(function(keys) { - postMessage({method: 'done', keys: keys}); - close(); - }); -} + store = e.data; + textsecure.protocol_wrapper.generateKeys().then(function(keys) { + postMessage({ method: 'done', keys: keys }); + close(); + }); +}; diff --git a/package.json b/package.json index bec36c58a739..fd447c78fadd 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "lint": "yarn format --list-different && yarn lint-windows", "lint-windows": "yarn eslint && yarn grunt lint && yarn tslint", "tslint": "tslint --config tslint.json --format stylish --project .", - "format": "prettier --write \"*.js\" \"js/**/*.js\" \"ts/**/*.{ts,tsx}\" \"test/**/*.js\" \"*.md\" \"./**/*.md\"", + "format": "prettier --write \"*.{js,ts,tsx,md}\" \"./**/*.{js,ts,tsx,md}\"", "transpile": "tsc", "clean-transpile": "rimraf ts/**/*.js ts/*.js", "open-coverage": "open coverage/lcov-report/index.html",