Let's make it all pretty, shall we?

We missed a couple directories with previous attempts to turn this on
globally: app/ and libtextsecure/

Not to mention files in places we didn't expect: ts files that weren't
in the ts directory!

This turns prettier on for every file we care about (js, ts, tsx, md)
everywhere in the project but for a few key parts.
This commit is contained in:
Scott Nonnenberg 2018-05-01 18:54:43 -07:00
parent df9c4d5629
commit 754d65ae2e
20 changed files with 1756 additions and 1542 deletions

View file

@ -14,5 +14,10 @@ js/jquery.js
js/Mp3LameEncoder.min.js js/Mp3LameEncoder.min.js
js/WebAudioRecorderMp3.js js/WebAudioRecorderMp3.js
ts/**/*.js
components/*
dist/*
libtextsecure/libsignal-protocol.js
/**/*.json /**/*.json
/**/*.css /**/*.css

View file

@ -5,11 +5,10 @@ const fse = require('fs-extra');
const toArrayBuffer = require('to-arraybuffer'); const toArrayBuffer = require('to-arraybuffer');
const { isArrayBuffer, isString } = require('lodash'); const { isArrayBuffer, isString } = require('lodash');
const PATH = 'attachments.noindex'; const PATH = 'attachments.noindex';
// getPath :: AbsolutePath -> AbsolutePath // getPath :: AbsolutePath -> AbsolutePath
exports.getPath = (userDataPath) => { exports.getPath = userDataPath => {
if (!isString(userDataPath)) { if (!isString(userDataPath)) {
throw new TypeError("'userDataPath' must be a string"); throw new TypeError("'userDataPath' must be a string");
} }
@ -17,7 +16,7 @@ exports.getPath = (userDataPath) => {
}; };
// ensureDirectory :: AbsolutePath -> IO Unit // ensureDirectory :: AbsolutePath -> IO Unit
exports.ensureDirectory = async (userDataPath) => { exports.ensureDirectory = async userDataPath => {
if (!isString(userDataPath)) { if (!isString(userDataPath)) {
throw new TypeError("'userDataPath' must be a string"); throw new TypeError("'userDataPath' must be a string");
} }
@ -27,12 +26,12 @@ exports.ensureDirectory = async (userDataPath) => {
// createReader :: AttachmentsPath -> // createReader :: AttachmentsPath ->
// RelativePath -> // RelativePath ->
// IO (Promise ArrayBuffer) // IO (Promise ArrayBuffer)
exports.createReader = (root) => { exports.createReader = root => {
if (!isString(root)) { if (!isString(root)) {
throw new TypeError("'root' must be a path"); throw new TypeError("'root' must be a path");
} }
return async (relativePath) => { return async relativePath => {
if (!isString(relativePath)) { if (!isString(relativePath)) {
throw new TypeError("'relativePath' must be a string"); throw new TypeError("'relativePath' must be a string");
} }
@ -46,12 +45,12 @@ exports.createReader = (root) => {
// createWriterForNew :: AttachmentsPath -> // createWriterForNew :: AttachmentsPath ->
// ArrayBuffer -> // ArrayBuffer ->
// IO (Promise RelativePath) // IO (Promise RelativePath)
exports.createWriterForNew = (root) => { exports.createWriterForNew = root => {
if (!isString(root)) { if (!isString(root)) {
throw new TypeError("'root' must be a path"); throw new TypeError("'root' must be a path");
} }
return async (arrayBuffer) => { return async arrayBuffer => {
if (!isArrayBuffer(arrayBuffer)) { if (!isArrayBuffer(arrayBuffer)) {
throw new TypeError("'arrayBuffer' must be an array buffer"); throw new TypeError("'arrayBuffer' must be an array buffer");
} }
@ -68,7 +67,7 @@ exports.createWriterForNew = (root) => {
// createWriter :: AttachmentsPath -> // createWriter :: AttachmentsPath ->
// { data: ArrayBuffer, path: RelativePath } -> // { data: ArrayBuffer, path: RelativePath } ->
// IO (Promise RelativePath) // IO (Promise RelativePath)
exports.createWriterForExisting = (root) => { exports.createWriterForExisting = root => {
if (!isString(root)) { if (!isString(root)) {
throw new TypeError("'root' must be a path"); throw new TypeError("'root' must be a path");
} }
@ -93,12 +92,12 @@ exports.createWriterForExisting = (root) => {
// createDeleter :: AttachmentsPath -> // createDeleter :: AttachmentsPath ->
// RelativePath -> // RelativePath ->
// IO Unit // IO Unit
exports.createDeleter = (root) => { exports.createDeleter = root => {
if (!isString(root)) { if (!isString(root)) {
throw new TypeError("'root' must be a path"); throw new TypeError("'root' must be a path");
} }
return async (relativePath) => { return async relativePath => {
if (!isString(relativePath)) { if (!isString(relativePath)) {
throw new TypeError("'relativePath' must be a string"); throw new TypeError("'relativePath' must be a string");
} }
@ -115,7 +114,7 @@ exports.createName = () => {
}; };
// getRelativePath :: String -> Path // getRelativePath :: String -> Path
exports.getRelativePath = (name) => { exports.getRelativePath = name => {
if (!isString(name)) { if (!isString(name)) {
throw new TypeError("'name' must be a string"); throw new TypeError("'name' must be a string");
} }

View file

@ -11,7 +11,11 @@ const RESTART_BUTTON = 0;
const LATER_BUTTON = 1; const LATER_BUTTON = 1;
function autoUpdateDisabled() { function autoUpdateDisabled() {
return process.platform === 'linux' || process.mas || config.get('disableAutoUpdate'); return (
process.platform === 'linux' ||
process.mas ||
config.get('disableAutoUpdate')
);
} }
function checkForUpdates() { function checkForUpdates() {
@ -38,7 +42,7 @@ function showUpdateDialog(mainWindow, messages) {
cancelId: RESTART_BUTTON, cancelId: RESTART_BUTTON,
}; };
dialog.showMessageBox(mainWindow, options, (response) => { dialog.showMessageBox(mainWindow, options, response => {
if (response === RESTART_BUTTON) { if (response === RESTART_BUTTON) {
// We delay these update calls because they don't seem to work in this // 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. // callback - but only if the message box has a parent window.

View file

@ -39,7 +39,7 @@ config.environment = environment;
'HOSTNAME', 'HOSTNAME',
'NODE_APP_INSTANCE', 'NODE_APP_INSTANCE',
'SUPPRESS_NO_CONFIG_WARNING', 'SUPPRESS_NO_CONFIG_WARNING',
].forEach((s) => { ].forEach(s => {
console.log(`${s} ${config.util.getEnv(s)}`); console.log(`${s} ${config.util.getEnv(s)}`);
}); });

View file

@ -2,7 +2,6 @@ const path = require('path');
const fs = require('fs'); const fs = require('fs');
const _ = require('lodash'); const _ = require('lodash');
function normalizeLocaleName(locale) { function normalizeLocaleName(locale) {
if (/^en-/.test(locale)) { if (/^en-/.test(locale)) {
return 'en'; return 'en';
@ -50,7 +49,9 @@ function load({ appLocale, logger } = {}) {
// We start with english, then overwrite that with anything present in locale // We start with english, then overwrite that with anything present in locale
messages = _.merge(english, messages); messages = _.merge(english, messages);
} catch (e) { } 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'); logger.error('Falling back to en locale');
localeName = 'en'; localeName = 'en';

View file

@ -12,14 +12,10 @@ const readFirstLine = require('firstline');
const readLastLines = require('read-last-lines').read; const readLastLines = require('read-last-lines').read;
const rimraf = require('rimraf'); const rimraf = require('rimraf');
const { const { app, ipcMain: ipc } = electron;
app,
ipcMain: ipc,
} = electron;
const LEVELS = ['fatal', 'error', 'warn', 'info', 'debug', 'trace']; const LEVELS = ['fatal', 'error', 'warn', 'info', 'debug', 'trace'];
let logger; let logger;
module.exports = { module.exports = {
initialize, initialize,
getLogger, getLogger,
@ -45,32 +41,38 @@ function initialize() {
logger = bunyan.createLogger({ logger = bunyan.createLogger({
name: 'log', name: 'log',
streams: [{ streams: [
{
level: 'debug', level: 'debug',
stream: process.stdout, stream: process.stdout,
}, { },
{
type: 'rotating-file', type: 'rotating-file',
path: logFile, path: logFile,
period: '1d', period: '1d',
count: 3, count: 3,
}], },
],
}); });
LEVELS.forEach((level) => { LEVELS.forEach(level => {
ipc.on(`log-${level}`, (first, ...rest) => { ipc.on(`log-${level}`, (first, ...rest) => {
logger[level](...rest); logger[level](...rest);
}); });
}); });
ipc.on('fetch-log', (event) => { ipc.on('fetch-log', event => {
fetch(logPath).then((data) => { fetch(logPath).then(
data => {
event.sender.send('fetched-log', data); event.sender.send('fetched-log', data);
}, (error) => { },
error => {
logger.error(`Problem loading log from disk: ${error.stack}`); logger.error(`Problem loading log from disk: ${error.stack}`);
}); }
);
}); });
ipc.on('delete-all-logs', async (event) => { ipc.on('delete-all-logs', async event => {
try { try {
await deleteAllLogs(logPath); await deleteAllLogs(logPath);
} catch (error) { } catch (error) {
@ -84,27 +86,29 @@ function initialize() {
async function deleteAllLogs(logPath) { async function deleteAllLogs(logPath) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
rimraf(logPath, { rimraf(
logPath,
{
disableGlob: true, disableGlob: true,
}, (error) => { },
error => {
if (error) { if (error) {
return reject(error); return reject(error);
} }
return resolve(); return resolve();
}); }
);
}); });
} }
function cleanupLogs(logPath) { function cleanupLogs(logPath) {
const now = new Date(); const now = new Date();
const earliestDate = new Date(Date.UTC( const earliestDate = new Date(
now.getUTCFullYear(), Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() - 3)
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); const files = _.filter(remaining, file => !file.start && file.end);
if (!files.length) { if (!files.length) {
@ -122,7 +126,7 @@ function isLineAfterDate(line, date) {
try { try {
const data = JSON.parse(line); const data = JSON.parse(line);
return (new Date(data.time)).getTime() > date.getTime(); return new Date(data.time).getTime() > date.getTime();
} catch (e) { } catch (e) {
console.log('error parsing log line', e.stack, line); console.log('error parsing log line', e.stack, line);
return false; return false;
@ -133,19 +137,18 @@ function eliminateOutOfDateFiles(logPath, date) {
const files = fs.readdirSync(logPath); const files = fs.readdirSync(logPath);
const paths = files.map(file => path.join(logPath, file)); const paths = files.map(file => path.join(logPath, file));
return Promise.all(_.map( return Promise.all(
paths, _.map(paths, target =>
target => Promise.all([ Promise.all([readFirstLine(target), readLastLines(target, 2)]).then(
readFirstLine(target), results => {
readLastLines(target, 2),
]).then((results) => {
const start = results[0]; const start = results[0];
const end = results[1].split('\n'); const end = results[1].split('\n');
const file = { const file = {
path: target, path: target,
start: isLineAfterDate(start, date), start: isLineAfterDate(start, date),
end: isLineAfterDate(end[end.length - 1], date) || end:
isLineAfterDate(end[end.length - 1], date) ||
isLineAfterDate(end[end.length - 2], date), isLineAfterDate(end[end.length - 2], date),
}; };
@ -154,27 +157,33 @@ function eliminateOutOfDateFiles(logPath, date) {
} }
return file; return file;
}) }
)); )
)
);
} }
function eliminateOldEntries(files, date) { function eliminateOldEntries(files, date) {
const earliest = date.getTime(); const earliest = date.getTime();
return Promise.all(_.map( return Promise.all(
files, _.map(files, file =>
file => fetchLog(file.path).then((lines) => { fetchLog(file.path).then(lines => {
const recent = _.filter(lines, line => (new Date(line.time)).getTime() >= earliest); const recent = _.filter(
lines,
line => new Date(line.time).getTime() >= earliest
);
const text = _.map(recent, line => JSON.stringify(line)).join('\n'); 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() { function getLogger() {
if (!logger) { if (!logger) {
throw new Error('Logger hasn\'t been initialized yet!'); throw new Error("Logger hasn't been initialized yet!");
} }
return logger; return logger;
@ -188,13 +197,15 @@ function fetchLog(logFile) {
} }
const lines = _.compact(text.split('\n')); const lines = _.compact(text.split('\n'));
const data = _.compact(lines.map((line) => { const data = _.compact(
lines.map(line => {
try { try {
return _.pick(JSON.parse(line), ['level', 'time', 'msg']); return _.pick(JSON.parse(line), ['level', 'time', 'msg']);
} catch (e) { } catch (e) {
return null; return null;
} }
})); })
);
return resolve(data); return resolve(data);
}); });
@ -213,7 +224,7 @@ function fetch(logPath) {
msg: `Loaded this list of log files from logPath: ${files.join(', ')}`, 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); const data = _.flatten(results);
data.push(fileListEntry); data.push(fileListEntry);
@ -222,11 +233,10 @@ function fetch(logPath) {
}); });
} }
function logAtLevel(level, ...args) { function logAtLevel(level, ...args) {
if (logger) { if (logger) {
// To avoid [Object object] in our log since console.log handles non-strings smoothly // 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') { if (typeof item !== 'string') {
try { try {
return JSON.stringify(item); return JSON.stringify(item);

View file

@ -1,6 +1,5 @@
const { isString } = require('lodash'); const { isString } = require('lodash');
exports.createTemplate = (options, messages) => { exports.createTemplate = (options, messages) => {
if (!isString(options.platform)) { if (!isString(options.platform)) {
throw new TypeError('`options.platform` must be a string'); throw new TypeError('`options.platform` must be a string');
@ -21,7 +20,8 @@ exports.createTemplate = (options, messages) => {
showSettings, showSettings,
} = options; } = options;
const template = [{ const template = [
{
label: messages.mainMenuFile.message, label: messages.mainMenuFile.message,
submenu: [ submenu: [
{ {
@ -141,7 +141,8 @@ exports.createTemplate = (options, messages) => {
click: showAbout, click: showAbout,
}, },
], ],
}]; },
];
if (includeSetup) { if (includeSetup) {
const fileMenu = template[0]; const fileMenu = template[0];

View file

@ -1,10 +1,6 @@
const path = require('path'); const path = require('path');
const { const { app, Menu, Tray } = require('electron');
app,
Menu,
Tray,
} = require('electron');
let trayContextMenu = null; let trayContextMenu = null;
let tray = null; let tray = null;
@ -12,7 +8,12 @@ let tray = null;
function createTrayIcon(getMainWindow, messages) { function createTrayIcon(getMainWindow, messages) {
// A smaller icon is needed on macOS // A smaller icon is needed on macOS
const iconSize = process.platform === 'darwin' ? '16' : '256'; 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); tray = new Tray(iconNoNewMessages);
@ -42,7 +43,8 @@ function createTrayIcon(getMainWindow, messages) {
// context menu, since the 'click' event may not work on all platforms. // context menu, since the 'click' event may not work on all platforms.
// For details please refer to: // For details please refer to:
// https://github.com/electron/electron/blob/master/docs/api/tray.md. // https://github.com/electron/electron/blob/master/docs/api/tray.md.
trayContextMenu = Menu.buildFromTemplate([{ trayContextMenu = Menu.buildFromTemplate([
{
id: 'toggleWindowVisibility', id: 'toggleWindowVisibility',
label: messages[mainWindow.isVisible() ? 'hide' : 'show'].message, label: messages[mainWindow.isVisible() ? 'hide' : 'show'].message,
click: tray.toggleWindowVisibility, click: tray.toggleWindowVisibility,
@ -51,15 +53,18 @@ function createTrayIcon(getMainWindow, messages) {
id: 'quit', id: 'quit',
label: messages.quit.message, label: messages.quit.message,
click: app.quit.bind(app), click: app.quit.bind(app),
}]); },
]);
tray.setContextMenu(trayContextMenu); tray.setContextMenu(trayContextMenu);
}; };
tray.updateIcon = (unreadCount) => { tray.updateIcon = unreadCount => {
if (unreadCount > 0) { if (unreadCount > 0) {
const filename = `${String(unreadCount >= 10 ? 10 : unreadCount)}.png`; 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 { } else {
tray.setImage(iconNoNewMessages); tray.setImage(iconNoNewMessages);
} }

View file

@ -5,7 +5,6 @@ const ElectronConfig = require('electron-config');
const config = require('./config'); const config = require('./config');
// use a separate data directory for development // use a separate data directory for development
if (config.has('storageProfile')) { if (config.has('storageProfile')) {
const userData = path.join( const userData = path.join(

View file

@ -1 +1,3 @@
export function deferredToPromise<T>(deferred: JQuery.Deferred<any, any, any>): Promise<T>; export function deferredToPromise<T>(
deferred: JQuery.Deferred<any, any, any>
): Promise<T>;

View file

@ -1,9 +1,12 @@
declare namespace LinkText { declare namespace LinkText {
type Attributes = { type Attributes = {
[key: string]: string; [key: string]: string;
} };
} }
declare function linkText(value: string, attributes: LinkText.Attributes): string; declare function linkText(
value: string,
attributes: LinkText.Attributes
): string;
export = linkText; export = linkText;

View file

@ -1,4 +1,4 @@
;(function () { (function() {
'use strict'; 'use strict';
window.textsecure = window.textsecure || {}; window.textsecure = window.textsecure || {};
@ -39,9 +39,16 @@
var confirmKeys = this.confirmKeys.bind(this); var confirmKeys = this.confirmKeys.bind(this);
var registrationDone = this.registrationDone.bind(this); var registrationDone = this.registrationDone.bind(this);
return this.queueTask(function() { return this.queueTask(function() {
return libsignal.KeyHelper.generateIdentityKeyPair().then(function(identityKeyPair) { return libsignal.KeyHelper.generateIdentityKeyPair().then(function(
identityKeyPair
) {
var profileKey = textsecure.crypto.getRandomBytes(32); var profileKey = textsecure.crypto.getRandomBytes(32);
return createAccount(number, verificationCode, identityKeyPair, profileKey) return createAccount(
number,
verificationCode,
identityKeyPair,
profileKey
)
.then(clearSessionsAndPreKeys) .then(clearSessionsAndPreKeys)
.then(generateKeys) .then(generateKeys)
.then(function(keys) { .then(function(keys) {
@ -53,7 +60,11 @@
}); });
}); });
}, },
registerSecondDevice: function(setProvisioningUrl, confirmNumber, progressCallback) { registerSecondDevice: function(
setProvisioningUrl,
confirmNumber,
progressCallback
) {
var createAccount = this.createAccount.bind(this); var createAccount = this.createAccount.bind(this);
var clearSessionsAndPreKeys = this.clearSessionsAndPreKeys.bind(this); var clearSessionsAndPreKeys = this.clearSessionsAndPreKeys.bind(this);
var generateKeys = this.generateKeys.bind(this, 100, progressCallback); var generateKeys = this.generateKeys.bind(this, 100, progressCallback);
@ -79,22 +90,41 @@
var wsr = new WebSocketResource(socket, { var wsr = new WebSocketResource(socket, {
keepalive: { path: '/v1/keepalive/provisioning' }, keepalive: { path: '/v1/keepalive/provisioning' },
handleRequest: function(request) { handleRequest: function(request) {
if (request.path === "/v1/address" && request.verb === "PUT") { if (request.path === '/v1/address' && request.verb === 'PUT') {
var proto = textsecure.protobuf.ProvisioningUuid.decode(request.body); var proto = textsecure.protobuf.ProvisioningUuid.decode(
setProvisioningUrl([ request.body
'tsdevice:/?uuid=', proto.uuid, '&pub_key=', );
encodeURIComponent(btoa(getString(pubKey))) setProvisioningUrl(
].join('')); [
'tsdevice:/?uuid=',
proto.uuid,
'&pub_key=',
encodeURIComponent(btoa(getString(pubKey))),
].join('')
);
request.respond(200, 'OK'); request.respond(200, 'OK');
} else if (request.path === "/v1/message" && request.verb === "PUT") { } else if (
var envelope = textsecure.protobuf.ProvisionEnvelope.decode(request.body, 'binary'); request.path === '/v1/message' &&
request.verb === 'PUT'
) {
var envelope = textsecure.protobuf.ProvisionEnvelope.decode(
request.body,
'binary'
);
request.respond(200, 'OK'); request.respond(200, 'OK');
gotProvisionEnvelope = true; gotProvisionEnvelope = true;
wsr.close(); wsr.close();
resolve(provisioningCipher.decrypt(envelope).then(function(provisionMessage) { resolve(
provisioningCipher
.decrypt(envelope)
.then(function(provisionMessage) {
return queueTask(function() { return queueTask(function() {
return confirmNumber(provisionMessage.number).then(function(deviceName) { return confirmNumber(provisionMessage.number).then(
if (typeof deviceName !== 'string' || deviceName.length === 0) { function(deviceName) {
if (
typeof deviceName !== 'string' ||
deviceName.length === 0
) {
throw new Error('Invalid device name'); throw new Error('Invalid device name');
} }
return createAccount( return createAccount(
@ -114,13 +144,15 @@
}); });
}) })
.then(registrationDone); .then(registrationDone);
}
);
}); });
}); })
})); );
} else { } else {
console.log('Unknown websocket message', request.path); console.log('Unknown websocket message', request.path);
} }
} },
}); });
}); });
}); });
@ -129,17 +161,20 @@
var generateKeys = this.generateKeys.bind(this, 100); var generateKeys = this.generateKeys.bind(this, 100);
var registerKeys = this.server.registerKeys.bind(this.server); var registerKeys = this.server.registerKeys.bind(this.server);
return this.queueTask(function() { return this.queueTask(
function() {
return this.server.getMyKeys().then(function(preKeyCount) { return this.server.getMyKeys().then(function(preKeyCount) {
console.log('prekey count ' + preKeyCount); console.log('prekey count ' + preKeyCount);
if (preKeyCount < 10) { if (preKeyCount < 10) {
return generateKeys().then(registerKeys); return generateKeys().then(registerKeys);
} }
}); });
}.bind(this)); }.bind(this)
);
}, },
rotateSignedPreKey: function() { rotateSignedPreKey: function() {
return this.queueTask(function() { return this.queueTask(
function() {
var signedKeyId = textsecure.storage.get('signedKeyId', 1); var signedKeyId = textsecure.storage.get('signedKeyId', 1);
if (typeof signedKeyId != 'number') { if (typeof signedKeyId != 'number') {
throw new Error('Invalid signedKeyId'); throw new Error('Invalid signedKeyId');
@ -151,11 +186,22 @@
// TODO: harden this against missing identity key? Otherwise, we get // TODO: harden this against missing identity key? Otherwise, we get
// retries every five seconds. // retries every five seconds.
return store.getIdentityKeyPair().then(function(identityKey) { return store
return libsignal.KeyHelper.generateSignedPreKey(identityKey, signedKeyId); .getIdentityKeyPair()
}, function(error) { .then(
console.log('Failed to get identity key. Canceling key rotation.'); function(identityKey) {
}).then(function(res) { return libsignal.KeyHelper.generateSignedPreKey(
identityKey,
signedKeyId
);
},
function(error) {
console.log(
'Failed to get identity key. Canceling key rotation.'
);
}
)
.then(function(res) {
if (!res) { if (!res) {
return; return;
} }
@ -166,37 +212,50 @@
server.setSignedPreKey({ server.setSignedPreKey({
keyId: res.keyId, keyId: res.keyId,
publicKey: res.keyPair.pubKey, publicKey: res.keyPair.pubKey,
signature : res.signature signature: res.signature,
}), }),
]).then(function() { ])
.then(function() {
var confirmed = true; var confirmed = true;
console.log('Confirming new signed prekey', res.keyId); console.log('Confirming new signed prekey', res.keyId);
return Promise.all([ return Promise.all([
textsecure.storage.remove('signedKeyRotationRejected'), textsecure.storage.remove('signedKeyRotationRejected'),
store.storeSignedPreKey(res.keyId, res.keyPair, confirmed), store.storeSignedPreKey(res.keyId, res.keyPair, confirmed),
]); ]);
}).then(function() { })
.then(function() {
return cleanSignedPreKeys(); return cleanSignedPreKeys();
}); });
}).catch(function(e) { })
.catch(function(e) {
console.log( console.log(
'rotateSignedPrekey error:', 'rotateSignedPrekey error:',
e && e.stack ? e.stack : e e && e.stack ? e.stack : e
); );
if (e instanceof Error && e.name == 'HTTPError' && e.code >= 400 && e.code <= 599) { if (
var rejections = 1 + textsecure.storage.get('signedKeyRotationRejected', 0); 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); textsecure.storage.put('signedKeyRotationRejected', rejections);
console.log('Signed key rotation rejected count:', rejections); console.log('Signed key rotation rejected count:', rejections);
} else { } else {
throw e; throw e;
} }
}); });
}.bind(this)); }.bind(this)
);
}, },
queueTask: function(task) { queueTask: function(task) {
var taskWithTimeout = textsecure.createTaskWithTimeout(task); var taskWithTimeout = textsecure.createTaskWithTimeout(task);
return this.pending = this.pending.then(taskWithTimeout, taskWithTimeout); return (this.pending = this.pending.then(
taskWithTimeout,
taskWithTimeout
));
}, },
cleanSignedPreKeys: function() { cleanSignedPreKeys: function() {
var MINIMUM_KEYS = 3; var MINIMUM_KEYS = 3;
@ -269,8 +328,15 @@
}); });
}); });
}, },
createAccount: function(number, verificationCode, identityKeyPair, createAccount: function(
profileKey, deviceName, userAgent, readReceipts) { number,
verificationCode,
identityKeyPair,
profileKey,
deviceName,
userAgent,
readReceipts
) {
var signalingKey = libsignal.crypto.getRandomBytes(32 + 20); var signalingKey = libsignal.crypto.getRandomBytes(32 + 20);
var password = btoa(getString(libsignal.crypto.getRandomBytes(16))); var password = btoa(getString(libsignal.crypto.getRandomBytes(16)));
password = password.substring(0, password.length - 2); password = password.substring(0, password.length - 2);
@ -278,27 +344,41 @@
var previousNumber = getNumber(textsecure.storage.get('number_id')); var previousNumber = getNumber(textsecure.storage.get('number_id'));
return this.server.confirmCode( return this.server
number, verificationCode, password, signalingKey, registrationId, deviceName .confirmCode(
).then(function(response) { number,
verificationCode,
password,
signalingKey,
registrationId,
deviceName
)
.then(function(response) {
if (previousNumber && previousNumber !== number) { if (previousNumber && previousNumber !== number) {
console.log('New number is different from old number; deleting all previous data'); console.log(
'New number is different from old number; deleting all previous data'
);
return textsecure.storage.protocol.removeAllData().then(function() { return textsecure.storage.protocol.removeAllData().then(
function() {
console.log('Successfully deleted previous data'); console.log('Successfully deleted previous data');
return response; return response;
}, function(error) { },
function(error) {
console.log( console.log(
'Something went wrong deleting data from previous number', 'Something went wrong deleting data from previous number',
error && error.stack ? error.stack : error error && error.stack ? error.stack : error
); );
return response; return response;
}); }
);
} }
return response; return response;
}).then(function(response) { })
.then(
function(response) {
textsecure.storage.remove('identityKey'); textsecure.storage.remove('identityKey');
textsecure.storage.remove('signaling_key'); textsecure.storage.remove('signaling_key');
textsecure.storage.remove('password'); textsecure.storage.remove('password');
@ -318,7 +398,7 @@
firstUse: true, firstUse: true,
timestamp: Date.now(), timestamp: Date.now(),
verified: textsecure.storage.protocol.VerifiedStatus.VERIFIED, verified: textsecure.storage.protocol.VerifiedStatus.VERIFIED,
nonblockingApproval : true nonblockingApproval: true,
}); });
textsecure.storage.put('identityKey', identityKeyPair); textsecure.storage.put('identityKey', identityKeyPair);
@ -337,10 +417,18 @@
textsecure.storage.put('read-receipt-setting', false); textsecure.storage.put('read-receipt-setting', false);
} }
textsecure.storage.user.setNumberAndDeviceId(number, response.deviceId || 1, deviceName); textsecure.storage.user.setNumberAndDeviceId(
textsecure.storage.put('regionCode', libphonenumber.util.getRegionCodeForNumber(number)); number,
response.deviceId || 1,
deviceName
);
textsecure.storage.put(
'regionCode',
libphonenumber.util.getRegionCodeForNumber(number)
);
this.server.username = textsecure.storage.get('number_id'); this.server.username = textsecure.storage.get('number_id');
}.bind(this)); }.bind(this)
);
}, },
clearSessionsAndPreKeys: function() { clearSessionsAndPreKeys: function() {
var store = textsecure.storage.protocol; var store = textsecure.storage.protocol;
@ -376,7 +464,8 @@
} }
var store = textsecure.storage.protocol; var store = textsecure.storage.protocol;
return store.getIdentityKeyPair().then(function(identityKey) { return store.getIdentityKeyPair().then(
function(identityKey) {
var result = { preKeys: [], identityKey: identityKey.pubKey }; var result = { preKeys: [], identityKey: identityKey.pubKey };
var promises = []; var promises = [];
@ -386,15 +475,20 @@
store.storePreKey(res.keyId, res.keyPair); store.storePreKey(res.keyId, res.keyPair);
result.preKeys.push({ result.preKeys.push({
keyId: res.keyId, keyId: res.keyId,
publicKey : res.keyPair.pubKey publicKey: res.keyPair.pubKey,
}); });
if (progressCallback) { progressCallback(); } if (progressCallback) {
progressCallback();
}
}) })
); );
} }
promises.push( promises.push(
libsignal.KeyHelper.generateSignedPreKey(identityKey, signedKeyId).then(function(res) { libsignal.KeyHelper.generateSignedPreKey(
identityKey,
signedKeyId
).then(function(res) {
store.storeSignedPreKey(res.keyId, res.keyPair); store.storeSignedPreKey(res.keyId, res.keyPair);
result.signedPreKey = { result.signedPreKey = {
keyId: res.keyId, keyId: res.keyId,
@ -408,19 +502,21 @@
textsecure.storage.put('maxPreKeyId', startId + count); textsecure.storage.put('maxPreKeyId', startId + count);
textsecure.storage.put('signedKeyId', signedKeyId + 1); textsecure.storage.put('signedKeyId', signedKeyId + 1);
return Promise.all(promises).then(function() { return Promise.all(promises).then(
function() {
// This is primarily for the signed prekey summary it logs out // This is primarily for the signed prekey summary it logs out
return this.cleanSignedPreKeys().then(function() { return this.cleanSignedPreKeys().then(function() {
return result; return result;
}); });
}.bind(this)); }.bind(this)
}.bind(this)); );
}.bind(this)
);
}, },
registrationDone: function() { registrationDone: function() {
console.log('registration done'); console.log('registration done');
this.dispatchEvent(new Event('registration')); this.dispatchEvent(new Event('registration'));
} },
}); });
textsecure.AccountManager = AccountManager; textsecure.AccountManager = AccountManager;
})();
}());

View file

@ -45,7 +45,8 @@ var TextSecureServer = (function() {
url = options.host + '/' + options.path; url = options.host + '/' + options.path;
} }
console.log(options.type, url); console.log(options.type, url);
var timeout = typeof options.timeout !== 'undefined' ? options.timeout : 10000; var timeout =
typeof options.timeout !== 'undefined' ? options.timeout : 10000;
var proxyUrl = window.config.proxyUrl; var proxyUrl = window.config.proxyUrl;
var agent; var agent;
@ -68,20 +69,25 @@ var TextSecureServer = (function() {
fetchOptions.body = nodeBuffer.from(fetchOptions.body); fetchOptions.body = nodeBuffer.from(fetchOptions.body);
// node-fetch doesn't set content-length like S3 requires // node-fetch doesn't set content-length like S3 requires
fetchOptions.headers["Content-Length"] = contentLength; fetchOptions.headers['Content-Length'] = contentLength;
} }
if (options.user && options.password) { if (options.user && options.password) {
fetchOptions.headers["Authorization"] = fetchOptions.headers['Authorization'] =
"Basic " + btoa(getString(options.user) + ":" + getString(options.password)); 'Basic ' +
btoa(getString(options.user) + ':' + getString(options.password));
} }
if (options.contentType) { if (options.contentType) {
fetchOptions.headers["Content-Type"] = options.contentType; fetchOptions.headers['Content-Type'] = options.contentType;
} }
window.nodeFetch(url, fetchOptions).then(function(response) { window
.nodeFetch(url, fetchOptions)
.then(function(response) {
var resultPromise; var resultPromise;
if (options.responseType === 'json' if (
&& response.headers.get('Content-Type') === 'application/json') { options.responseType === 'json' &&
response.headers.get('Content-Type') === 'application/json'
) {
resultPromise = response.json(); resultPromise = response.json();
} else if (options.responseType === 'arraybuffer') { } else if (options.responseType === 'arraybuffer') {
resultPromise = response.buffer(); resultPromise = response.buffer();
@ -99,12 +105,14 @@ var TextSecureServer = (function() {
if (options.validateResponse) { if (options.validateResponse) {
if (!validateResponse(result, options.validateResponse)) { if (!validateResponse(result, options.validateResponse)) {
console.log(options.type, url, response.status, 'Error'); console.log(options.type, url, response.status, 'Error');
reject(HTTPError( reject(
HTTPError(
'promise_ajax: invalid response', 'promise_ajax: invalid response',
response.status, response.status,
result, result,
options.stack options.stack
)); )
);
} }
} }
} }
@ -113,15 +121,18 @@ var TextSecureServer = (function() {
resolve(result, response.status); resolve(result, response.status);
} else { } else {
console.log(options.type, url, response.status, 'Error'); console.log(options.type, url, response.status, 'Error');
reject(HTTPError( reject(
HTTPError(
'promise_ajax: error response', 'promise_ajax: error response',
response.status, response.status,
result, result,
options.stack options.stack
)); )
);
} }
}); });
}).catch(function(e) { })
.catch(function(e) {
console.log(options.type, url, 0, 'Error'); console.log(options.type, url, 0, 'Error');
var stack = e.stack + '\nInitial stack:\n' + options.stack; var stack = e.stack + '\nInitial stack:\n' + options.stack;
reject(HTTPError('promise_ajax catch', 0, e.toString(), stack)); reject(HTTPError('promise_ajax catch', 0, e.toString(), stack));
@ -166,13 +177,13 @@ var TextSecureServer = (function() {
} }
var URL_CALLS = { var URL_CALLS = {
accounts : "v1/accounts", accounts: 'v1/accounts',
devices : "v1/devices", devices: 'v1/devices',
keys : "v2/keys", keys: 'v2/keys',
signed : "v2/keys/signed", signed: 'v2/keys/signed',
messages : "v1/messages", messages: 'v1/messages',
attachment : "v1/attachments", attachment: 'v1/attachments',
profile : "v1/profile" profile: 'v1/profile',
}; };
function TextSecureServer(url, username, password, cdn_url) { function TextSecureServer(url, username, password, cdn_url) {
@ -202,7 +213,7 @@ var TextSecureServer = (function() {
password: this.password, password: this.password,
validateResponse: param.validateResponse, validateResponse: param.validateResponse,
certificateAuthorities: window.config.certificateAuthorities, certificateAuthorities: window.config.certificateAuthorities,
timeout : param.timeout timeout: param.timeout,
}).catch(function(e) { }).catch(function(e) {
var code = e.code; var code = e.code;
if (code === 200) { if (code === 200) {
@ -213,28 +224,31 @@ var TextSecureServer = (function() {
var message; var message;
switch (code) { switch (code) {
case -1: case -1:
message = "Failed to connect to the server, please check your network connection."; message =
'Failed to connect to the server, please check your network connection.';
break; break;
case 413: case 413:
message = "Rate limit exceeded, please try again later."; message = 'Rate limit exceeded, please try again later.';
break; break;
case 403: case 403:
message = "Invalid code, please try again."; message = 'Invalid code, please try again.';
break; break;
case 417: case 417:
// TODO: This shouldn't be a thing?, but its in the API doc? // TODO: This shouldn't be a thing?, but its in the API doc?
message = "Number already registered."; message = 'Number already registered.';
break; break;
case 401: case 401:
message = "Invalid authentication, most likely someone re-registered and invalidated our registration."; message =
'Invalid authentication, most likely someone re-registered and invalidated our registration.';
break; break;
case 404: case 404:
message = "Number is not registered."; message = 'Number is not registered.';
break; break;
default: default:
message = "The server rejected our query, please file a bug report."; message =
'The server rejected our query, please file a bug report.';
} }
e.message = message e.message = message;
throw e; throw e;
}); });
}, },
@ -248,11 +262,11 @@ var TextSecureServer = (function() {
}, },
getAvatar: function(path) { getAvatar: function(path) {
return ajax(this.cdn_url + '/' + path, { return ajax(this.cdn_url + '/' + path, {
type : "GET", type: 'GET',
responseType: "arraybuffer", responseType: 'arraybuffer',
contentType : "application/octet-stream", contentType: 'application/octet-stream',
certificateAuthorities: window.config.certificateAuthorities, certificateAuthorities: window.config.certificateAuthorities,
timeout: 0 timeout: 0,
}); });
}, },
requestVerificationSMS: function(number) { requestVerificationSMS: function(number) {
@ -269,7 +283,14 @@ var TextSecureServer = (function() {
urlParameters: '/voice/code/' + number, urlParameters: '/voice/code/' + number,
}); });
}, },
confirmCode: function(number, code, password, signaling_key, registrationId, deviceName) { confirmCode: function(
number,
code,
password,
signaling_key,
registrationId,
deviceName
) {
var jsonData = { var jsonData = {
signalingKey: btoa(getString(signaling_key)), signalingKey: btoa(getString(signaling_key)),
supportsSms: false, supportsSms: false,
@ -283,7 +304,7 @@ var TextSecureServer = (function() {
call = 'devices'; call = 'devices';
urlPrefix = '/'; urlPrefix = '/';
schema = { deviceId: 'number' }; schema = { deviceId: 'number' };
responseType = 'json' responseType = 'json';
} else { } else {
call = 'accounts'; call = 'accounts';
urlPrefix = '/code/'; urlPrefix = '/code/';
@ -297,7 +318,7 @@ var TextSecureServer = (function() {
urlParameters: urlPrefix + code, urlParameters: urlPrefix + code,
jsonData: jsonData, jsonData: jsonData,
responseType: responseType, responseType: responseType,
validateResponse : schema validateResponse: schema,
}); });
}, },
getDevices: function(number) { getDevices: function(number) {
@ -312,7 +333,7 @@ var TextSecureServer = (function() {
keys.signedPreKey = { keys.signedPreKey = {
keyId: genKeys.signedPreKey.keyId, keyId: genKeys.signedPreKey.keyId,
publicKey: btoa(getString(genKeys.signedPreKey.publicKey)), publicKey: btoa(getString(genKeys.signedPreKey.publicKey)),
signature: btoa(getString(genKeys.signedPreKey.signature)) signature: btoa(getString(genKeys.signedPreKey.signature)),
}; };
keys.preKeys = []; keys.preKeys = [];
@ -320,13 +341,13 @@ var TextSecureServer = (function() {
for (var i in genKeys.preKeys) { for (var i in genKeys.preKeys) {
keys.preKeys[j++] = { keys.preKeys[j++] = {
keyId: genKeys.preKeys[i].keyId, keyId: genKeys.preKeys[i].keyId,
publicKey: btoa(getString(genKeys.preKeys[i].publicKey)) publicKey: btoa(getString(genKeys.preKeys[i].publicKey)),
}; };
} }
// This is just to make the server happy // This is just to make the server happy
// (v2 clients should choke on publicKey) // (v2 clients should choke on publicKey)
keys.lastResortKey = {keyId: 0x7fffFFFF, publicKey: btoa("42")}; keys.lastResortKey = { keyId: 0x7fffffff, publicKey: btoa('42') };
return this.ajax({ return this.ajax({
call: 'keys', call: 'keys',
@ -341,8 +362,8 @@ var TextSecureServer = (function() {
jsonData: { jsonData: {
keyId: signedPreKey.keyId, keyId: signedPreKey.keyId,
publicKey: btoa(getString(signedPreKey.publicKey)), publicKey: btoa(getString(signedPreKey.publicKey)),
signature: btoa(getString(signedPreKey.signature)) signature: btoa(getString(signedPreKey.signature)),
} },
}); });
}, },
getMyKeys: function(number, deviceId) { getMyKeys: function(number, deviceId) {
@ -350,40 +371,52 @@ var TextSecureServer = (function() {
call: 'keys', call: 'keys',
httpType: 'GET', httpType: 'GET',
responseType: 'json', responseType: 'json',
validateResponse : {count: 'number'} validateResponse: { count: 'number' },
}).then(function(res) { }).then(function(res) {
return res.count; return res.count;
}); });
}, },
getKeysForNumber: function(number, deviceId) { getKeysForNumber: function(number, deviceId) {
if (deviceId === undefined) if (deviceId === undefined) deviceId = '*';
deviceId = "*";
return this.ajax({ return this.ajax({
call: 'keys', call: 'keys',
httpType: 'GET', httpType: 'GET',
urlParameters : "/" + number + "/" + deviceId, urlParameters: '/' + number + '/' + deviceId,
responseType: 'json', responseType: 'json',
validateResponse : {identityKey: 'string', devices: 'object'} validateResponse: { identityKey: 'string', devices: 'object' },
}).then(function(res) { }).then(function(res) {
if (res.devices.constructor !== Array) { if (res.devices.constructor !== Array) {
throw new Error("Invalid response"); throw new Error('Invalid response');
} }
res.identityKey = StringView.base64ToBytes(res.identityKey); res.identityKey = StringView.base64ToBytes(res.identityKey);
res.devices.forEach(function(device) { res.devices.forEach(function(device) {
if ( !validateResponse(device, {signedPreKey: 'object'}) || if (
!validateResponse(device.signedPreKey, {publicKey: 'string', signature: 'string'}) ) { !validateResponse(device, { signedPreKey: 'object' }) ||
throw new Error("Invalid signedPreKey"); !validateResponse(device.signedPreKey, {
publicKey: 'string',
signature: 'string',
})
) {
throw new Error('Invalid signedPreKey');
} }
if (device.preKey) { if (device.preKey) {
if ( !validateResponse(device, {preKey: 'object'}) || if (
!validateResponse(device.preKey, {publicKey: 'string'})) { !validateResponse(device, { preKey: 'object' }) ||
throw new Error("Invalid preKey"); !validateResponse(device.preKey, { publicKey: 'string' })
) {
throw new Error('Invalid preKey');
} }
device.preKey.publicKey = StringView.base64ToBytes(device.preKey.publicKey); device.preKey.publicKey = StringView.base64ToBytes(
device.preKey.publicKey
);
} }
device.signedPreKey.publicKey = StringView.base64ToBytes(device.signedPreKey.publicKey); device.signedPreKey.publicKey = StringView.base64ToBytes(
device.signedPreKey.signature = StringView.base64ToBytes(device.signedPreKey.signature); device.signedPreKey.publicKey
);
device.signedPreKey.signature = StringView.base64ToBytes(
device.signedPreKey.signature
);
}); });
return res; return res;
}); });
@ -410,44 +443,56 @@ var TextSecureServer = (function() {
urlParameters: '/' + id, urlParameters: '/' + id,
responseType: 'json', responseType: 'json',
validateResponse: { location: 'string' }, validateResponse: { location: 'string' },
}).then(function(response) { }).then(
function(response) {
return ajax(response.location, { return ajax(response.location, {
timeout: 0, timeout: 0,
type : "GET", type: 'GET',
responseType: "arraybuffer", responseType: 'arraybuffer',
contentType : "application/octet-stream" contentType: 'application/octet-stream',
}); });
}.bind(this)); }.bind(this)
);
}, },
putAttachment: function(encryptedBin) { putAttachment: function(encryptedBin) {
return this.ajax({ return this.ajax({
call: 'attachment', call: 'attachment',
httpType: 'GET', httpType: 'GET',
responseType: 'json', responseType: 'json',
}).then(function(response) { }).then(
function(response) {
return ajax(response.location, { return ajax(response.location, {
timeout: 0, timeout: 0,
type : "PUT", type: 'PUT',
contentType : "application/octet-stream", contentType: 'application/octet-stream',
data: encryptedBin, data: encryptedBin,
processData: false, processData: false,
}).then(function() { }).then(
function() {
return response.idString; return response.idString;
}.bind(this)); }.bind(this)
}.bind(this)); );
}.bind(this)
);
}, },
getMessageSocket: function() { getMessageSocket: function() {
console.log('opening message socket', this.url); console.log('opening message socket', this.url);
return createSocket(this.url.replace('https://', 'wss://').replace('http://', 'ws://') return createSocket(
+ '/v1/websocket/?login=' + encodeURIComponent(this.username) this.url.replace('https://', 'wss://').replace('http://', 'ws://') +
+ '&password=' + encodeURIComponent(this.password) '/v1/websocket/?login=' +
+ '&agent=OWD'); encodeURIComponent(this.username) +
'&password=' +
encodeURIComponent(this.password) +
'&agent=OWD'
);
}, },
getProvisioningSocket: function() { getProvisioningSocket: function() {
console.log('opening provisioning socket', this.url); console.log('opening provisioning socket', this.url);
return createSocket(this.url.replace('https://', 'wss://').replace('http://', 'ws://') return createSocket(
+ '/v1/websocket/provisioning/?agent=OWD'); this.url.replace('https://', 'wss://').replace('http://', 'ws://') +
} '/v1/websocket/provisioning/?agent=OWD'
);
},
}; };
return TextSecureServer; return TextSecureServer;

View file

@ -13,9 +13,9 @@ ProtoParser.prototype = {
return undefined; // eof return undefined; // eof
} }
var len = this.buffer.readVarint32(); var len = this.buffer.readVarint32();
var nextBuffer = this.buffer.slice( var nextBuffer = this.buffer
this.buffer.offset, this.buffer.offset+len .slice(this.buffer.offset, this.buffer.offset + len)
).toArrayBuffer(); .toArrayBuffer();
// TODO: de-dupe ByteBuffer.js includes in libaxo/libts // TODO: de-dupe ByteBuffer.js includes in libaxo/libts
// then remove this toArrayBuffer call. // then remove this toArrayBuffer call.
@ -24,9 +24,9 @@ ProtoParser.prototype = {
if (proto.avatar) { if (proto.avatar) {
var attachmentLen = proto.avatar.length; var attachmentLen = proto.avatar.length;
proto.avatar.data = this.buffer.slice( proto.avatar.data = this.buffer
this.buffer.offset, this.buffer.offset + attachmentLen .slice(this.buffer.offset, this.buffer.offset + attachmentLen)
).toArrayBuffer(); .toArrayBuffer();
this.buffer.skip(attachmentLen); this.buffer.skip(attachmentLen);
} }
@ -38,7 +38,7 @@ ProtoParser.prototype = {
} catch (e) { } catch (e) {
console.log(e); console.log(e);
} }
} },
}; };
var GroupBuffer = function(arrayBuffer) { var GroupBuffer = function(arrayBuffer) {
ProtoParser.call(this, arrayBuffer, textsecure.protobuf.GroupDetails); ProtoParser.call(this, arrayBuffer, textsecure.protobuf.GroupDetails);

View file

@ -1,4 +1,4 @@
;(function(){ (function() {
'use strict'; 'use strict';
var encrypt = libsignal.crypto.encrypt; var encrypt = libsignal.crypto.encrypt;
@ -12,7 +12,9 @@
var PROFILE_NAME_PADDED_LENGTH = 26; // bytes var PROFILE_NAME_PADDED_LENGTH = 26; // bytes
function verifyDigest(data, theirDigest) { function verifyDigest(data, theirDigest) {
return crypto.subtle.digest({name: 'SHA-256'}, data).then(function(ourDigest) { return crypto.subtle
.digest({ name: 'SHA-256' }, data)
.then(function(ourDigest) {
var a = new Uint8Array(ourDigest); var a = new Uint8Array(ourDigest);
var b = new Uint8Array(theirDigest); var b = new Uint8Array(theirDigest);
var result = 0; var result = 0;
@ -35,22 +37,31 @@
var decodedMessage = message.toArrayBuffer(); var decodedMessage = message.toArrayBuffer();
if (signaling_key.byteLength != 52) { if (signaling_key.byteLength != 52) {
throw new Error("Got invalid length signaling_key"); throw new Error('Got invalid length signaling_key');
} }
if (decodedMessage.byteLength < 1 + 16 + 10) { if (decodedMessage.byteLength < 1 + 16 + 10) {
throw new Error("Got invalid length message"); throw new Error('Got invalid length message');
} }
if (new Uint8Array(decodedMessage)[0] != 1) { if (new Uint8Array(decodedMessage)[0] != 1) {
throw new Error("Got bad version number: " + decodedMessage[0]); throw new Error('Got bad version number: ' + decodedMessage[0]);
} }
var aes_key = signaling_key.slice(0, 32); var aes_key = signaling_key.slice(0, 32);
var mac_key = signaling_key.slice(32, 32 + 20); var mac_key = signaling_key.slice(32, 32 + 20);
var iv = decodedMessage.slice(1, 1 + 16); var iv = decodedMessage.slice(1, 1 + 16);
var ciphertext = decodedMessage.slice(1 + 16, decodedMessage.byteLength - 10); var ciphertext = decodedMessage.slice(
var ivAndCiphertext = decodedMessage.slice(0, decodedMessage.byteLength - 10); 1 + 16,
var mac = decodedMessage.slice(decodedMessage.byteLength - 10, decodedMessage.byteLength); 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 verifyMAC(ivAndCiphertext, mac_key, mac, 10).then(function() {
return decrypt(aes_key, ciphertext, iv); return decrypt(aes_key, ciphertext, iv);
@ -59,10 +70,10 @@
decryptAttachment: function(encryptedBin, keys, theirDigest) { decryptAttachment: function(encryptedBin, keys, theirDigest) {
if (keys.byteLength != 64) { if (keys.byteLength != 64) {
throw new Error("Got invalid length attachment keys"); throw new Error('Got invalid length attachment keys');
} }
if (encryptedBin.byteLength < 16 + 32) { if (encryptedBin.byteLength < 16 + 32) {
throw new Error("Got invalid length attachment"); throw new Error('Got invalid length attachment');
} }
var aes_key = keys.slice(0, 32); var aes_key = keys.slice(0, 32);
@ -71,19 +82,27 @@
var iv = encryptedBin.slice(0, 16); var iv = encryptedBin.slice(0, 16);
var ciphertext = encryptedBin.slice(16, encryptedBin.byteLength - 32); var ciphertext = encryptedBin.slice(16, encryptedBin.byteLength - 32);
var ivAndCiphertext = encryptedBin.slice(0, encryptedBin.byteLength - 32); var ivAndCiphertext = encryptedBin.slice(0, encryptedBin.byteLength - 32);
var mac = encryptedBin.slice(encryptedBin.byteLength - 32, encryptedBin.byteLength); var mac = encryptedBin.slice(
encryptedBin.byteLength - 32,
encryptedBin.byteLength
);
return verifyMAC(ivAndCiphertext, mac_key, mac, 32).then(function() { return verifyMAC(ivAndCiphertext, mac_key, mac, 32)
.then(function() {
if (theirDigest !== null) { if (theirDigest !== null) {
return verifyDigest(encryptedBin, theirDigest); return verifyDigest(encryptedBin, theirDigest);
} }
}).then(function() { })
.then(function() {
return decrypt(aes_key, ciphertext, iv); return decrypt(aes_key, ciphertext, iv);
}); });
}, },
encryptAttachment: function(plaintext, keys, iv) { encryptAttachment: function(plaintext, keys, iv) {
if (!(plaintext instanceof ArrayBuffer) && !ArrayBuffer.isView(plaintext)) { if (
!(plaintext instanceof ArrayBuffer) &&
!ArrayBuffer.isView(plaintext)
) {
throw new TypeError( throw new TypeError(
'`plaintext` must be an `ArrayBuffer` or `ArrayBufferView`; got: ' + '`plaintext` must be an `ArrayBuffer` or `ArrayBufferView`; got: ' +
typeof plaintext typeof plaintext
@ -91,10 +110,10 @@
} }
if (keys.byteLength != 64) { if (keys.byteLength != 64) {
throw new Error("Got invalid length attachment keys"); throw new Error('Got invalid length attachment keys');
} }
if (iv.byteLength != 16) { if (iv.byteLength != 16) {
throw new Error("Got invalid length attachment iv"); throw new Error('Got invalid length attachment iv');
} }
var aes_key = keys.slice(0, 32); var aes_key = keys.slice(0, 32);
var mac_key = keys.slice(32, 64); var mac_key = keys.slice(32, 64);
@ -104,7 +123,9 @@
ivAndCiphertext.set(new Uint8Array(iv)); ivAndCiphertext.set(new Uint8Array(iv));
ivAndCiphertext.set(new Uint8Array(ciphertext), 16); ivAndCiphertext.set(new Uint8Array(ciphertext), 16);
return calculateMAC(mac_key, ivAndCiphertext.buffer).then(function(mac) { return calculateMAC(mac_key, ivAndCiphertext.buffer).then(function(
mac
) {
var encryptedBin = new Uint8Array(16 + ciphertext.byteLength + 32); var encryptedBin = new Uint8Array(16 + ciphertext.byteLength + 32);
encryptedBin.set(ivAndCiphertext); encryptedBin.set(ivAndCiphertext);
encryptedBin.set(new Uint8Array(mac), 16 + ciphertext.byteLength); encryptedBin.set(new Uint8Array(mac), 16 + ciphertext.byteLength);
@ -117,38 +138,60 @@
encryptProfile: function(data, key) { encryptProfile: function(data, key) {
var iv = libsignal.crypto.getRandomBytes(PROFILE_IV_LENGTH); var iv = libsignal.crypto.getRandomBytes(PROFILE_IV_LENGTH);
if (key.byteLength != PROFILE_KEY_LENGTH) { if (key.byteLength != PROFILE_KEY_LENGTH) {
throw new Error("Got invalid length profile key"); throw new Error('Got invalid length profile key');
} }
if (iv.byteLength != PROFILE_IV_LENGTH) { if (iv.byteLength != PROFILE_IV_LENGTH) {
throw new Error("Got invalid length profile iv"); 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
return crypto.subtle.encrypt({name: 'AES-GCM', iv: iv, tagLength: PROFILE_TAG_LENGTH}, key, data).then(function(ciphertext) { .importKey('raw', key, { name: 'AES-GCM' }, false, ['encrypt'])
var ivAndCiphertext = new Uint8Array(PROFILE_IV_LENGTH + ciphertext.byteLength); .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(iv));
ivAndCiphertext.set(new Uint8Array(ciphertext), PROFILE_IV_LENGTH); ivAndCiphertext.set(
new Uint8Array(ciphertext),
PROFILE_IV_LENGTH
);
return ivAndCiphertext.buffer; return ivAndCiphertext.buffer;
}); });
}); });
}, },
decryptProfile: function(data, key) { decryptProfile: function(data, key) {
if (data.byteLength < 12 + 16 + 1) { if (data.byteLength < 12 + 16 + 1) {
throw new Error("Got too short input: " + data.byteLength); throw new Error('Got too short input: ' + data.byteLength);
} }
var iv = data.slice(0, PROFILE_IV_LENGTH); var iv = data.slice(0, PROFILE_IV_LENGTH);
var ciphertext = data.slice(PROFILE_IV_LENGTH, data.byteLength); var ciphertext = data.slice(PROFILE_IV_LENGTH, data.byteLength);
if (key.byteLength != PROFILE_KEY_LENGTH) { if (key.byteLength != PROFILE_KEY_LENGTH) {
throw new Error("Got invalid length profile key"); throw new Error('Got invalid length profile key');
} }
if (iv.byteLength != PROFILE_IV_LENGTH) { if (iv.byteLength != PROFILE_IV_LENGTH) {
throw new Error("Got invalid length profile iv"); throw new Error('Got invalid length profile iv');
} }
var error = new Error(); // save stack var error = new Error(); // save stack
return crypto.subtle.importKey('raw', key, {name: 'AES-GCM'}, false, ['decrypt']).then(function(key) { return crypto.subtle
return crypto.subtle.decrypt({name: 'AES-GCM', iv: iv, tagLength: PROFILE_TAG_LENGTH}, key, ciphertext).catch(function(e) { .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') { if (e.name === 'OperationError') {
// bad mac, basically. // 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'; error.name = 'ProfileDecryptError';
throw error; throw error;
} }
@ -161,8 +204,13 @@
return textsecure.crypto.encryptProfile(padded.buffer, key); return textsecure.crypto.encryptProfile(padded.buffer, key);
}, },
decryptProfileName: function(encryptedProfileName, key) { decryptProfileName: function(encryptedProfileName, key) {
var data = dcodeIO.ByteBuffer.wrap(encryptedProfileName, 'base64').toArrayBuffer(); var data = dcodeIO.ByteBuffer.wrap(
return textsecure.crypto.decryptProfile(data, key).then(function(decrypted) { encryptedProfileName,
'base64'
).toArrayBuffer();
return textsecure.crypto
.decryptProfile(data, key)
.then(function(decrypted) {
// unpad // unpad
var name = ''; var name = '';
var padded = new Uint8Array(decrypted); var padded = new Uint8Array(decrypted);
@ -172,13 +220,14 @@
} }
} }
return dcodeIO.ByteBuffer.wrap(padded).slice(0, i).toArrayBuffer(); return dcodeIO.ByteBuffer.wrap(padded)
.slice(0, i)
.toArrayBuffer();
}); });
}, },
getRandomBytes: function(size) { getRandomBytes: function(size) {
return libsignal.crypto.getRandomBytes(size); return libsignal.crypto.getRandomBytes(size);
} },
}; };
})(); })();

View file

@ -1,4 +1,4 @@
;(function() { (function() {
'use strict'; 'use strict';
var registeredFunctions = {}; var registeredFunctions = {};
@ -7,14 +7,14 @@
INIT_SESSION: 2, INIT_SESSION: 2,
TRANSMIT_MESSAGE: 3, TRANSMIT_MESSAGE: 3,
REBUILD_MESSAGE: 4, REBUILD_MESSAGE: 4,
RETRY_SEND_MESSAGE_PROTO: 5 RETRY_SEND_MESSAGE_PROTO: 5,
}; };
window.textsecure = window.textsecure || {}; window.textsecure = window.textsecure || {};
window.textsecure.replay = { window.textsecure.replay = {
Type: Type, Type: Type,
registerFunction: function(func, functionCode) { registerFunction: function(func, functionCode) {
registeredFunctions[functionCode] = func; registeredFunctions[functionCode] = func;
} },
}; };
function inherit(Parent, Child) { function inherit(Parent, Child) {
@ -22,8 +22,8 @@
constructor: { constructor: {
value: Child, value: Child,
writable: true, writable: true,
configurable: true configurable: true,
} },
}); });
} }
function appendStack(newError, originalError) { function appendStack(newError, originalError) {
@ -62,7 +62,7 @@
functionCode: Type.INIT_SESSION, functionCode: Type.INIT_SESSION,
args: [number, message], args: [number, message],
name: 'IncomingIdentityKeyError', name: 'IncomingIdentityKeyError',
message : "The identity of " + this.number + " has changed." message: 'The identity of ' + this.number + ' has changed.',
}); });
} }
inherit(ReplayableError, IncomingIdentityKeyError); inherit(ReplayableError, IncomingIdentityKeyError);
@ -75,7 +75,7 @@
functionCode: Type.ENCRYPT_MESSAGE, functionCode: Type.ENCRYPT_MESSAGE,
args: [number, message, timestamp], args: [number, message, timestamp],
name: 'OutgoingIdentityKeyError', name: 'OutgoingIdentityKeyError',
message : "The identity of " + this.number + " has changed." message: 'The identity of ' + this.number + ' has changed.',
}); });
} }
inherit(ReplayableError, OutgoingIdentityKeyError); inherit(ReplayableError, OutgoingIdentityKeyError);
@ -85,7 +85,7 @@
functionCode: Type.ENCRYPT_MESSAGE, functionCode: Type.ENCRYPT_MESSAGE,
args: [number, message, timestamp], args: [number, message, timestamp],
name: 'OutgoingMessageError', name: 'OutgoingMessageError',
message : httpError ? httpError.message : 'no http error' message: httpError ? httpError.message : 'no http error',
}); });
if (httpError) { if (httpError) {
@ -103,7 +103,7 @@
functionCode: Type.TRANSMIT_MESSAGE, functionCode: Type.TRANSMIT_MESSAGE,
args: [number, jsonData, timestamp], args: [number, jsonData, timestamp],
name: 'SendMessageNetworkError', name: 'SendMessageNetworkError',
message : httpError.message message: httpError.message,
}); });
appendStack(this, httpError); appendStack(this, httpError);
@ -115,7 +115,7 @@
functionCode: Type.RETRY_SEND_MESSAGE_PROTO, functionCode: Type.RETRY_SEND_MESSAGE_PROTO,
args: [numbers, message, timestamp], args: [numbers, message, timestamp],
name: 'SignedPreKeyRotationError', name: 'SignedPreKeyRotationError',
message : "Too many signed prekey rotation failures" message: 'Too many signed prekey rotation failures',
}); });
} }
inherit(ReplayableError, SignedPreKeyRotationError); inherit(ReplayableError, SignedPreKeyRotationError);
@ -127,7 +127,7 @@
functionCode: Type.REBUILD_MESSAGE, functionCode: Type.REBUILD_MESSAGE,
args: [message], args: [message],
name: 'MessageError', name: 'MessageError',
message : httpError.message message: httpError.message,
}); });
appendStack(this, httpError); appendStack(this, httpError);
@ -161,5 +161,4 @@
window.textsecure.OutgoingMessageError = OutgoingMessageError; window.textsecure.OutgoingMessageError = OutgoingMessageError;
window.textsecure.MessageError = MessageError; window.textsecure.MessageError = MessageError;
window.textsecure.SignedPreKeyRotationError = SignedPreKeyRotationError; window.textsecure.SignedPreKeyRotationError = SignedPreKeyRotationError;
})(); })();

View file

@ -2,12 +2,11 @@
* Implements EventTarget * Implements EventTarget
* https://developer.mozilla.org/en-US/docs/Web/API/EventTarget * https://developer.mozilla.org/en-US/docs/Web/API/EventTarget
*/ */
;(function () { (function() {
'use strict'; 'use strict';
window.textsecure = window.textsecure || {}; window.textsecure = window.textsecure || {};
function EventTarget() { function EventTarget() {}
}
EventTarget.prototype = { EventTarget.prototype = {
constructor: EventTarget, constructor: EventTarget,
@ -73,8 +72,8 @@
this[prop] = obj[prop]; this[prop] = obj[prop];
} }
return this; return this;
} },
}; };
textsecure.EventTarget = EventTarget; textsecure.EventTarget = EventTarget;
}()); })();

View file

@ -16,58 +16,56 @@ function getString(thing) {
if (thing.__proto__ == StaticArrayBufferProto) if (thing.__proto__ == StaticArrayBufferProto)
return getString(new Uint8Array(thing)); return getString(new Uint8Array(thing));
if (thing.__proto__ == StaticByteBufferProto) if (thing.__proto__ == StaticByteBufferProto)
return thing.toString("binary"); return thing.toString('binary');
} }
return thing; return thing;
} }
function getStringable(thing) { function getStringable(thing) {
return (typeof thing == "string" || typeof thing == "number" || typeof thing == "boolean" || return (
typeof thing == 'string' ||
typeof thing == 'number' ||
typeof thing == 'boolean' ||
(thing === Object(thing) && (thing === Object(thing) &&
(thing.__proto__ == StaticArrayBufferProto || (thing.__proto__ == StaticArrayBufferProto ||
thing.__proto__ == StaticUint8ArrayProto || thing.__proto__ == StaticUint8ArrayProto ||
thing.__proto__ == StaticByteBufferProto))); thing.__proto__ == StaticByteBufferProto))
);
} }
// Number formatting utils // Number formatting utils
window.textsecure.utils = function() { window.textsecure.utils = (function() {
var self = {}; var self = {};
self.unencodeNumber = function(number) { self.unencodeNumber = function(number) {
return number.split("."); return number.split('.');
}; };
self.isNumberSane = function(number) { self.isNumberSane = function(number) {
return number[0] == "+" && return number[0] == '+' && /^[0-9]+$/.test(number.substring(1));
/^[0-9]+$/.test(number.substring(1)); };
}
/************************** /**************************
*** JSON'ing Utilities *** *** JSON'ing Utilities ***
**************************/ **************************/
function ensureStringed(thing) { function ensureStringed(thing) {
if (getStringable(thing)) if (getStringable(thing)) return getString(thing);
return getString(thing);
else if (thing instanceof Array) { else if (thing instanceof Array) {
var res = []; var res = [];
for (var i = 0; i < thing.length; i++) for (var i = 0; i < thing.length; i++) res[i] = ensureStringed(thing[i]);
res[i] = ensureStringed(thing[i]);
return res; return res;
} else if (thing === Object(thing)) { } else if (thing === Object(thing)) {
var res = {}; var res = {};
for (var key in thing) for (var key in thing) res[key] = ensureStringed(thing[key]);
res[key] = ensureStringed(thing[key]);
return res; return res;
} else if (thing === null) { } else if (thing === null) {
return null; return null;
} }
throw new Error("unsure of how to jsonify object of type " + typeof thing); throw new Error('unsure of how to jsonify object of type ' + typeof thing);
} }
self.jsonThing = function(thing) { self.jsonThing = function(thing) {
return JSON.stringify(ensureStringed(thing)); return JSON.stringify(ensureStringed(thing));
} };
return self; return self;
}(); })();

View file

@ -31,8 +31,7 @@ window.textsecure.storage.impl = {
*** Override Storage Routines *** *** Override Storage Routines ***
*****************************/ *****************************/
put: function(key, value) { put: function(key, value) {
if (value === undefined) if (value === undefined) throw new Error('Tried to store undefined');
throw new Error("Tried to store undefined");
store[key] = value; store[key] = value;
postMessage({ method: 'set', key: key, value: value }); postMessage({ method: 'set', key: key, value: value });
}, },
@ -56,4 +55,4 @@ onmessage = function(e) {
postMessage({ method: 'done', keys: keys }); postMessage({ method: 'done', keys: keys });
close(); close();
}); });
} };

View file

@ -31,7 +31,7 @@
"lint": "yarn format --list-different && yarn lint-windows", "lint": "yarn format --list-different && yarn lint-windows",
"lint-windows": "yarn eslint && yarn grunt lint && yarn tslint", "lint-windows": "yarn eslint && yarn grunt lint && yarn tslint",
"tslint": "tslint --config tslint.json --format stylish --project .", "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", "transpile": "tsc",
"clean-transpile": "rimraf ts/**/*.js ts/*.js", "clean-transpile": "rimraf ts/**/*.js ts/*.js",
"open-coverage": "open coverage/lcov-report/index.html", "open-coverage": "open coverage/lcov-report/index.html",