Move entire application over to using prettier.js (#2324)

Let's make it all pretty, shall we?
This commit is contained in:
Scott Nonnenberg 2018-05-02 13:41:31 -07:00 committed by GitHub
commit 4c7c9e87ad
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 5319 additions and 4415 deletions

View file

@ -14,5 +14,13 @@ 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
test/fixtures.js
libtextsecure/test/blanket_mocha.js
test/blanket_mocha.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', {
stream: process.stdout, level: 'debug',
}, { stream: process.stdout,
type: 'rotating-file', },
path: logFile, {
period: '1d', type: 'rotating-file',
count: 3, path: logFile,
}], period: '1d',
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(
event.sender.send('fetched-log', data); data => {
}, (error) => { event.sender.send('fetched-log', data);
logger.error(`Problem loading log from disk: ${error.stack}`); },
}); 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 { 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(
disableGlob: true, logPath,
}, (error) => { {
if (error) { disableGlob: true,
return reject(error); },
} error => {
if (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,48 +137,53 @@ 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), const start = results[0];
]).then((results) => { const end = results[1].split('\n');
const start = results[0];
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 - 2], date), isLineAfterDate(end[end.length - 1], date) ||
}; isLineAfterDate(end[end.length - 2], date),
};
if (!file.start && !file.end) { if (!file.start && !file.end) {
fs.unlinkSync(file.path); fs.unlinkSync(file.path);
} }
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(
const text = _.map(recent, line => JSON.stringify(line)).join('\n'); 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() { 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(
try { lines.map(line => {
return _.pick(JSON.parse(line), ['level', 'time', 'msg']); try {
} catch (e) { return _.pick(JSON.parse(line), ['level', 'time', 'msg']);
return null; } catch (e) {
} 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,127 +20,129 @@ exports.createTemplate = (options, messages) => {
showSettings, showSettings,
} = options; } = options;
const template = [{ const template = [
label: messages.mainMenuFile.message, {
submenu: [ label: messages.mainMenuFile.message,
{ submenu: [
label: messages.mainMenuSettings.message, {
click: showSettings, label: messages.mainMenuSettings.message,
}, click: showSettings,
{ },
type: 'separator', {
}, type: 'separator',
{ },
role: 'quit', {
}, role: 'quit',
], },
}, ],
{ },
label: messages.mainMenuEdit.message, {
submenu: [ label: messages.mainMenuEdit.message,
{ submenu: [
role: 'undo', {
}, role: 'undo',
{ },
role: 'redo', {
}, role: 'redo',
{ },
type: 'separator', {
}, type: 'separator',
{ },
role: 'cut', {
}, role: 'cut',
{ },
role: 'copy', {
}, role: 'copy',
{ },
role: 'paste', {
}, role: 'paste',
{ },
role: 'pasteandmatchstyle', {
}, role: 'pasteandmatchstyle',
{ },
role: 'delete', {
}, role: 'delete',
{ },
role: 'selectall', {
}, role: 'selectall',
], },
}, ],
{ },
label: messages.mainMenuView.message, {
submenu: [ label: messages.mainMenuView.message,
{ submenu: [
role: 'resetzoom', {
}, role: 'resetzoom',
{ },
role: 'zoomin', {
}, role: 'zoomin',
{ },
role: 'zoomout', {
}, role: 'zoomout',
{ },
type: 'separator', {
}, type: 'separator',
{ },
role: 'togglefullscreen', {
}, role: 'togglefullscreen',
{ },
type: 'separator', {
}, type: 'separator',
{ },
label: messages.debugLog.message, {
click: showDebugLog, label: messages.debugLog.message,
}, click: showDebugLog,
{ },
type: 'separator', {
}, type: 'separator',
{ },
role: 'toggledevtools', {
}, role: 'toggledevtools',
], },
}, ],
{ },
label: messages.mainMenuWindow.message, {
role: 'window', label: messages.mainMenuWindow.message,
submenu: [ role: 'window',
{ submenu: [
role: 'minimize', {
}, role: 'minimize',
], },
}, ],
{ },
label: messages.mainMenuHelp.message, {
role: 'help', label: messages.mainMenuHelp.message,
submenu: [ role: 'help',
{ submenu: [
label: messages.goToReleaseNotes.message, {
click: openReleaseNotes, label: messages.goToReleaseNotes.message,
}, click: openReleaseNotes,
{ },
type: 'separator', {
}, type: 'separator',
{ },
label: messages.goToForums.message, {
click: openForums, label: messages.goToForums.message,
}, click: openForums,
{ },
label: messages.goToSupportPage.message, {
click: openSupportPage, label: messages.goToSupportPage.message,
}, click: openSupportPage,
{ },
label: messages.menuReportIssue.message, {
click: openNewBugForm, label: messages.menuReportIssue.message,
}, click: openNewBugForm,
{ },
type: 'separator', {
}, type: 'separator',
{ },
label: messages.aboutSignalDesktop.message, {
click: showAbout, label: messages.aboutSignalDesktop.message,
}, 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,24 +43,28 @@ 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', {
label: messages[mainWindow.isVisible() ? 'hide' : 'show'].message, id: 'toggleWindowVisibility',
click: tray.toggleWindowVisibility, label: messages[mainWindow.isVisible() ? 'hide' : 'show'].message,
}, click: tray.toggleWindowVisibility,
{ },
id: 'quit', {
label: messages.quit.message, id: 'quit',
click: app.quit.bind(app), label: messages.quit.message,
}]); 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,68 +1,85 @@
(function() { (function() {
'use strict'; 'use strict';
function ProvisioningCipher() {} function ProvisioningCipher() {}
ProvisioningCipher.prototype = { ProvisioningCipher.prototype = {
decrypt: function(provisionEnvelope) { decrypt: function(provisionEnvelope) {
var masterEphemeral = provisionEnvelope.publicKey.toArrayBuffer(); var masterEphemeral = provisionEnvelope.publicKey.toArrayBuffer();
var message = provisionEnvelope.body.toArrayBuffer(); var message = provisionEnvelope.body.toArrayBuffer();
if (new Uint8Array(message)[0] != 1) { if (new Uint8Array(message)[0] != 1) {
throw new Error("Bad version number on ProvisioningMessage"); throw new Error('Bad version number on ProvisioningMessage');
} }
var iv = message.slice(1, 16 + 1); var iv = message.slice(1, 16 + 1);
var mac = message.slice(message.byteLength - 32, message.byteLength); var mac = message.slice(message.byteLength - 32, message.byteLength);
var ivAndCiphertext = message.slice(0, message.byteLength - 32); var ivAndCiphertext = message.slice(0, message.byteLength - 32);
var ciphertext = message.slice(16 + 1, message.byteLength - 32); var ciphertext = message.slice(16 + 1, message.byteLength - 32);
return libsignal.Curve.async.calculateAgreement( return libsignal.Curve.async
masterEphemeral, this.keyPair.privKey .calculateAgreement(masterEphemeral, this.keyPair.privKey)
).then(function(ecRes) { .then(function(ecRes) {
return libsignal.HKDF.deriveSecrets( return libsignal.HKDF.deriveSecrets(
ecRes, new ArrayBuffer(32), "TextSecure Provisioning Message" ecRes,
); new ArrayBuffer(32),
}).then(function(keys) { 'TextSecure Provisioning Message'
return libsignal.crypto.verifyMAC(ivAndCiphertext, keys[1], mac, 32).then(function() { );
return libsignal.crypto.decrypt(keys[0], ciphertext, iv); })
.then(function(keys) {
return libsignal.crypto
.verifyMAC(ivAndCiphertext, keys[1], mac, 32)
.then(function() {
return libsignal.crypto.decrypt(keys[0], ciphertext, iv);
}); });
}).then(function(plaintext) { })
var provisionMessage = textsecure.protobuf.ProvisionMessage.decode(plaintext); .then(function(plaintext) {
var privKey = provisionMessage.identityKeyPrivate.toArrayBuffer(); var provisionMessage = textsecure.protobuf.ProvisionMessage.decode(
plaintext
);
var privKey = provisionMessage.identityKeyPrivate.toArrayBuffer();
return libsignal.Curve.async.createKeyPair(privKey).then(function(keyPair) { return libsignal.Curve.async
var ret = { .createKeyPair(privKey)
identityKeyPair : keyPair, .then(function(keyPair) {
number : provisionMessage.number, var ret = {
provisioningCode : provisionMessage.provisioningCode, identityKeyPair: keyPair,
userAgent : provisionMessage.userAgent, number: provisionMessage.number,
readReceipts : provisionMessage.readReceipts provisioningCode: provisionMessage.provisioningCode,
}; userAgent: provisionMessage.userAgent,
if (provisionMessage.profileKey) { readReceipts: provisionMessage.readReceipts,
ret.profileKey = provisionMessage.profileKey.toArrayBuffer(); };
} if (provisionMessage.profileKey) {
return ret; ret.profileKey = provisionMessage.profileKey.toArrayBuffer();
}
return ret;
}); });
}); });
}, },
getPublicKey: function() { getPublicKey: function() {
return Promise.resolve().then(function() { return Promise.resolve()
if (!this.keyPair) { .then(
return libsignal.Curve.async.generateKeyPair().then(function(keyPair) { function() {
if (!this.keyPair) {
return libsignal.Curve.async.generateKeyPair().then(
function(keyPair) {
this.keyPair = keyPair; this.keyPair = keyPair;
}.bind(this)); }.bind(this)
} );
}.bind(this)).then(function() { }
return this.keyPair.pubKey; }.bind(this)
}.bind(this)); )
} .then(
}; function() {
return this.keyPair.pubKey;
}.bind(this)
);
},
};
libsignal.ProvisioningCipher = function() { libsignal.ProvisioningCipher = function() {
var cipher = new ProvisioningCipher(); var cipher = new ProvisioningCipher();
this.decrypt = cipher.decrypt.bind(cipher); this.decrypt = cipher.decrypt.bind(cipher);
this.getPublicKey = cipher.getPublicKey.bind(cipher); this.getPublicKey = cipher.getPublicKey.bind(cipher);
}; };
})(); })();

View file

@ -1,426 +1,522 @@
;(function () { (function() {
'use strict'; 'use strict';
window.textsecure = window.textsecure || {}; 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) { function AccountManager(url, username, password) {
this.server = new TextSecureServer(url, username, password); this.server = new TextSecureServer(url, username, password);
this.pending = Promise.resolve(); this.pending = Promise.resolve();
}
function getNumber(numberId) {
if (!numberId || !numberId.length) {
return numberId;
} }
function getNumber(numberId) { var parts = numberId.split('.');
if (!numberId || !numberId.length) { if (!parts.length) {
return numberId; return numberId;
}
var parts = numberId.split('.');
if (!parts.length) {
return numberId;
}
return parts[0];
} }
AccountManager.prototype = new textsecure.EventTarget(); return parts[0];
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 this.queueTask(function() { AccountManager.prototype = new textsecure.EventTarget();
return this.server.getMyKeys().then(function(preKeyCount) { AccountManager.prototype.extend({
console.log('prekey count ' + preKeyCount); constructor: AccountManager,
if (preKeyCount < 10) { requestVoiceVerification: function(number) {
return generateKeys().then(registerKeys); return this.server.requestVerificationVoice(number);
} },
}); requestSMSVerification: function(number) {
}.bind(this)); return this.server.requestVerificationSMS(number);
}, },
rotateSignedPreKey: function() { registerSingleDevice: function(number, verificationCode) {
return this.queueTask(function() { var registerKeys = this.server.registerKeys.bind(this.server);
var signedKeyId = textsecure.storage.get('signedKeyId', 1); var createAccount = this.createAccount.bind(this);
if (typeof signedKeyId != 'number') { var clearSessionsAndPreKeys = this.clearSessionsAndPreKeys.bind(this);
throw new Error('Invalid signedKeyId'); var generateKeys = this.generateKeys.bind(this, 100);
} var confirmKeys = this.confirmKeys.bind(this);
var registrationDone = this.registrationDone.bind(this);
var store = textsecure.storage.protocol; return this.queueTask(function() {
var server = this.server; return libsignal.KeyHelper.generateIdentityKeyPair().then(function(
var cleanSignedPreKeys = this.cleanSignedPreKeys; identityKeyPair
) {
// TODO: harden this against missing identity key? Otherwise, we get var profileKey = textsecure.crypto.getRandomBytes(32);
// retries every five seconds. return createAccount(
return store.getIdentityKeyPair().then(function(identityKey) { number,
return libsignal.KeyHelper.generateSignedPreKey(identityKey, signedKeyId); verificationCode,
}, function(error) { identityKeyPair,
console.log('Failed to get identity key. Canceling key rotation.'); profileKey
}).then(function(res) { )
if (!res) { .then(clearSessionsAndPreKeys)
return; .then(generateKeys)
} .then(function(keys) {
console.log('Saving new signed prekey', res.keyId); return registerKeys(keys).then(function() {
return Promise.all([ return confirmKeys(keys);
textsecure.storage.put('signedKeyId', signedKeyId + 1), });
store.storeSignedPreKey(res.keyId, res.keyPair), })
server.setSignedPreKey({ .then(registrationDone);
keyId : res.keyId, });
publicKey : res.keyPair.pubKey, });
signature : res.signature },
}), registerSecondDevice: function(
]).then(function() { setProvisioningUrl,
var confirmed = true; confirmNumber,
console.log('Confirming new signed prekey', res.keyId); progressCallback
return Promise.all([ ) {
textsecure.storage.remove('signedKeyRotationRejected'), var createAccount = this.createAccount.bind(this);
store.storeSignedPreKey(res.keyId, res.keyPair, confirmed), var clearSessionsAndPreKeys = this.clearSessionsAndPreKeys.bind(this);
]); var generateKeys = this.generateKeys.bind(this, 100, progressCallback);
}).then(function() { var confirmKeys = this.confirmKeys.bind(this);
return cleanSignedPreKeys(); var registrationDone = this.registrationDone.bind(this);
}); var registerKeys = this.server.registerKeys.bind(this.server);
}).catch(function(e) { var getSocket = this.server.getProvisioningSocket.bind(this.server);
console.log( var queueTask = this.queueTask.bind(this);
'rotateSignedPrekey error:', var provisioningCipher = new libsignal.ProvisioningCipher();
e && e.stack ? e.stack : e var gotProvisionEnvelope = false;
); return provisioningCipher.getPublicKey().then(function(pubKey) {
return new Promise(function(resolve, reject) {
if (e instanceof Error && e.name == 'HTTPError' && e.code >= 400 && e.code <= 599) { var socket = getSocket();
var rejections = 1 + textsecure.storage.get('signedKeyRotationRejected', 0); socket.onclose = function(e) {
textsecure.storage.put('signedKeyRotationRejected', rejections); console.log('provisioning socket closed', e.code);
console.log('Signed key rotation rejected count:', rejections); if (!gotProvisionEnvelope) {
} else { reject(new Error('websocket closed'));
throw e; }
} };
}); socket.onopen = function(e) {
}.bind(this)); console.log('provisioning socket open');
}, };
queueTask: function(task) { var wsr = new WebSocketResource(socket, {
var taskWithTimeout = textsecure.createTaskWithTimeout(task); keepalive: { path: '/v1/keepalive/provisioning' },
return this.pending = this.pending.then(taskWithTimeout, taskWithTimeout); handleRequest: function(request) {
}, if (request.path === '/v1/address' && request.verb === 'PUT') {
cleanSignedPreKeys: function() { var proto = textsecure.protobuf.ProvisioningUuid.decode(
var MINIMUM_KEYS = 3; request.body
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'
); );
setProvisioningUrl(
var confirmedCount = confirmed.length; [
'tsdevice:/?uuid=',
// Keep MINIMUM_KEYS confirmed keys, then drop if older than a week proto.uuid,
confirmed = confirmed.forEach(function(key, index) { '&pub_key=',
if (index < MINIMUM_KEYS) { encodeURIComponent(btoa(getString(pubKey))),
return; ].join('')
} );
var created_at = key.created_at || 0; request.respond(200, 'OK');
var age = Date.now() - created_at; } else if (
if (age > ARCHIVE_AGE) { request.path === '/v1/message' &&
console.log( request.verb === 'PUT'
'Removing confirmed signed prekey:', ) {
key.keyId, var envelope = textsecure.protobuf.ProvisionEnvelope.decode(
'with timestamp:', request.body,
created_at '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); return this.queueTask(
textsecure.storage.put('signedKeyId', signedKeyId + 1); function() {
return Promise.all(promises).then(function() { return this.server.getMyKeys().then(function(preKeyCount) {
// This is primarily for the signed prekey summary it logs out console.log('prekey count ' + preKeyCount);
return this.cleanSignedPreKeys().then(function() { if (preKeyCount < 10) {
return result; return generateKeys().then(registerKeys);
}); }
}.bind(this)); });
}.bind(this)); }.bind(this)
}, );
registrationDone: function() { },
console.log('registration done'); rotateSignedPreKey: function() {
this.dispatchEvent(new Event('registration')); return this.queueTask(
} function() {
}); var signedKeyId = textsecure.storage.get('signedKeyId', 1);
textsecure.AccountManager = AccountManager; 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;
})();

View file

@ -1,87 +1,93 @@
var TextSecureServer = (function() { var TextSecureServer = (function() {
'use strict'; 'use strict';
function validateResponse(response, schema) { function validateResponse(response, schema) {
try { try {
for (var i in schema) { for (var i in schema) {
switch (schema[i]) { switch (schema[i]) {
case 'object': case 'object':
case 'string': case 'string':
case 'number': case 'number':
if (typeof response[i] !== schema[i]) { if (typeof response[i] !== schema[i]) {
return false; return false;
}
break;
}
} }
} catch(ex) { break;
return false;
} }
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 proxyUrl = window.config.proxyUrl;
var requestOptions; var agent;
if (proxyUrl) { if (proxyUrl) {
requestOptions = { agent = new ProxyAgent(proxyUrl);
ca: window.config.certificateAuthorities,
agent: new ProxyAgent(proxyUrl),
};
} else {
requestOptions = {
ca: window.config.certificateAuthorities,
};
} }
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) { // node-fetch doesn't set content-length like S3 requires
return new Promise(function (resolve, reject) { fetchOptions.headers['Content-Length'] = contentLength;
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; if (options.user && options.password) {
var agent; fetchOptions.headers['Authorization'] =
if (proxyUrl) { 'Basic ' +
agent = new ProxyAgent(proxyUrl); btoa(getString(options.user) + ':' + getString(options.password));
} }
if (options.contentType) {
var fetchOptions = { fetchOptions.headers['Content-Type'] = options.contentType;
method: options.type, }
body: options.data || null, window
headers: { 'X-Signal-Agent': 'OWD' }, .nodeFetch(url, fetchOptions)
agent: agent, .then(function(response) {
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) {
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(
'promise_ajax: invalid response', HTTPError(
response.status, 'promise_ajax: invalid response',
result, response.status,
options.stack result,
)); options.stack
)
);
} }
} }
} }
@ -113,342 +121,379 @@ 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(
'promise_ajax: error response', HTTPError(
response.status, 'promise_ajax: error response',
result, response.status,
options.stack result,
)); 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));
}); });
}); });
} }
function retry_ajax(url, options, limit, count) { function retry_ajax(url, options, limit, count) {
count = count || 0; count = count || 0;
limit = limit || 3; limit = limit || 3;
count++; count++;
return promise_ajax(url, options).catch(function(e) { return promise_ajax(url, options).catch(function(e) {
if (e.name === 'HTTPError' && e.code === -1 && count < limit) { if (e.name === 'HTTPError' && e.code === -1 && count < limit) {
return new Promise(function(resolve) { return new Promise(function(resolve) {
setTimeout(function() { setTimeout(function() {
resolve(retry_ajax(url, options, limit, count)); resolve(retry_ajax(url, options, limit, count));
}, 1000); }, 1000);
});
} else {
throw e;
}
}); });
} 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;
} }
var e = new Error(message + '; code: ' + code);
function ajax(url, options) { e.name = 'HTTPError';
options.stack = new Error().stack; // just in case, save stack here. e.code = code;
return retry_ajax(url, options); e.stack += '\nOriginal stack:\n' + stack;
if (response) {
e.response = response;
} }
return e;
}
function HTTPError(message, code, response, stack) { var URL_CALLS = {
if (code > 999 || code < 100) { accounts: 'v1/accounts',
code = -1; devices: 'v1/devices',
} keys: 'v2/keys',
var e = new Error(message + '; code: ' + code); signed: 'v2/keys/signed',
e.name = 'HTTPError'; messages: 'v1/messages',
e.code = code; attachment: 'v1/attachments',
e.stack += '\nOriginal stack:\n' + stack; profile: 'v1/profile',
if (response) { };
e.response = response;
} function TextSecureServer(url, username, password, cdn_url) {
return e; 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 = { TextSecureServer.prototype = {
accounts : "v1/accounts", constructor: TextSecureServer,
devices : "v1/devices", ajax: function(param) {
keys : "v2/keys", if (!param.urlParameters) {
signed : "v2/keys/signed", param.urlParameters = '';
messages : "v1/messages", }
attachment : "v1/attachments", return ajax(null, {
profile : "v1/profile" host: this.url,
}; path: URL_CALLS[param.call] + param.urlParameters,
type: param.httpType,
function TextSecureServer(url, username, password, cdn_url) { data: param.jsonData && textsecure.utils.jsonThing(param.jsonData),
if (typeof url !== 'string') { contentType: 'application/json; charset=utf-8',
throw new Error('Invalid server url'); 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; var message;
this.cdn_url = cdn_url; switch (code) {
this.username = username; case -1:
this.password = password; message =
} 'Failed to connect to the server, please check your network connection.';
break;
TextSecureServer.prototype = { case 413:
constructor: TextSecureServer, message = 'Rate limit exceeded, please try again later.';
ajax: function(param) { break;
if (!param.urlParameters) { case 403:
param.urlParameters = ''; message = 'Invalid code, please try again.';
} break;
return ajax(null, { case 417:
host : this.url, // TODO: This shouldn't be a thing?, but its in the API doc?
path : URL_CALLS[param.call] + param.urlParameters, message = 'Number already registered.';
type : param.httpType, break;
data : param.jsonData && textsecure.utils.jsonThing(param.jsonData), case 401:
contentType : 'application/json; charset=utf-8', message =
responseType : param.responseType, 'Invalid authentication, most likely someone re-registered and invalidated our registration.';
user : this.username, break;
password : this.password, case 404:
validateResponse: param.validateResponse, message = 'Number is not registered.';
certificateAuthorities: window.config.certificateAuthorities, break;
timeout : param.timeout default:
}).catch(function(e) { message =
var code = e.code; 'The server rejected our query, please file a bug report.';
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');
} }
}; 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;
})(); })();

View file

@ -1,52 +1,52 @@
function ProtoParser(arrayBuffer, protobuf) { function ProtoParser(arrayBuffer, protobuf) {
this.protobuf = protobuf; this.protobuf = protobuf;
this.buffer = new dcodeIO.ByteBuffer(); this.buffer = new dcodeIO.ByteBuffer();
this.buffer.append(arrayBuffer); this.buffer.append(arrayBuffer);
this.buffer.offset = 0; this.buffer.offset = 0;
this.buffer.limit = arrayBuffer.byteLength; this.buffer.limit = arrayBuffer.byteLength;
} }
ProtoParser.prototype = { ProtoParser.prototype = {
constructor: ProtoParser, constructor: ProtoParser,
next: function() { next: function() {
try { try {
if (this.buffer.limit === this.buffer.offset) { if (this.buffer.limit === this.buffer.offset) {
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.
var proto = this.protobuf.decode(nextBuffer); var proto = this.protobuf.decode(nextBuffer);
this.buffer.skip(len); this.buffer.skip(len);
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);
} }
if (proto.profileKey) { if (proto.profileKey) {
proto.profileKey = proto.profileKey.toArrayBuffer(); proto.profileKey = proto.profileKey.toArrayBuffer();
} }
return proto; return proto;
} 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);
}; };
GroupBuffer.prototype = Object.create(ProtoParser.prototype); GroupBuffer.prototype = Object.create(ProtoParser.prototype);
GroupBuffer.prototype.constructor = GroupBuffer; GroupBuffer.prototype.constructor = GroupBuffer;
var ContactBuffer = function(arrayBuffer) { 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 = Object.create(ProtoParser.prototype);
ContactBuffer.prototype.constructor = ContactBuffer; ContactBuffer.prototype.constructor = ContactBuffer;

View file

@ -1,184 +1,233 @@
;(function(){ (function() {
'use strict'; 'use strict';
var encrypt = libsignal.crypto.encrypt; var encrypt = libsignal.crypto.encrypt;
var decrypt = libsignal.crypto.decrypt; var decrypt = libsignal.crypto.decrypt;
var calculateMAC = libsignal.crypto.calculateMAC; var calculateMAC = libsignal.crypto.calculateMAC;
var verifyMAC = libsignal.crypto.verifyMAC; var verifyMAC = libsignal.crypto.verifyMAC;
var PROFILE_IV_LENGTH = 12; // bytes var PROFILE_IV_LENGTH = 12; // bytes
var PROFILE_KEY_LENGTH = 32; // bytes var PROFILE_KEY_LENGTH = 32; // bytes
var PROFILE_TAG_LENGTH = 128; // bits var PROFILE_TAG_LENGTH = 128; // bits
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
var a = new Uint8Array(ourDigest); .digest({ name: 'SHA-256' }, data)
var b = new Uint8Array(theirDigest); .then(function(ourDigest) {
var result = 0; var a = new Uint8Array(ourDigest);
for (var i=0; i < theirDigest.byteLength; ++i) { var b = new Uint8Array(theirDigest);
result = result | (a[i] ^ b[i]); var result = 0;
} for (var i = 0; i < theirDigest.byteLength; ++i) {
if (result !== 0) { result = result | (a[i] ^ b[i]);
throw new Error('Bad digest'); }
} 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 || {}; encryptAttachment: function(plaintext, keys, iv) {
window.textsecure.crypto = { if (
// Decrypts message into a raw string !(plaintext instanceof ArrayBuffer) &&
decryptWebsocketMessage: function(message, signaling_key) { !ArrayBuffer.isView(plaintext)
var decodedMessage = message.toArrayBuffer(); ) {
throw new TypeError(
'`plaintext` must be an `ArrayBuffer` or `ArrayBufferView`; got: ' +
typeof plaintext
);
}
if (signaling_key.byteLength != 52) { if (keys.byteLength != 64) {
throw new Error("Got invalid length signaling_key"); throw new Error('Got invalid length attachment keys');
} }
if (decodedMessage.byteLength < 1 + 16 + 10) { if (iv.byteLength != 16) {
throw new Error("Got invalid length message"); throw new Error('Got invalid length attachment iv');
} }
if (new Uint8Array(decodedMessage)[0] != 1) { var aes_key = keys.slice(0, 32);
throw new Error("Got bad version number: " + decodedMessage[0]); var mac_key = keys.slice(32, 64);
}
var aes_key = signaling_key.slice(0, 32); return encrypt(aes_key, plaintext, iv).then(function(ciphertext) {
var mac_key = signaling_key.slice(32, 32 + 20); 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); return calculateMAC(mac_key, ivAndCiphertext.buffer).then(function(
var ciphertext = decodedMessage.slice(1 + 16, decodedMessage.byteLength - 10); mac
var ivAndCiphertext = decodedMessage.slice(0, decodedMessage.byteLength - 10); ) {
var mac = decodedMessage.slice(decodedMessage.byteLength - 10, decodedMessage.byteLength); var encryptedBin = new Uint8Array(16 + ciphertext.byteLength + 32);
encryptedBin.set(ivAndCiphertext);
return verifyMAC(ivAndCiphertext, mac_key, mac, 10).then(function() { encryptedBin.set(new Uint8Array(mac), 16 + ciphertext.byteLength);
return decrypt(aes_key, ciphertext, iv); return calculateDigest(encryptedBin.buffer).then(function(digest) {
}); return { ciphertext: encryptedBin.buffer, digest: digest };
}, });
});
decryptAttachment: function(encryptedBin, keys, theirDigest) { });
if (keys.byteLength != 64) { },
throw new Error("Got invalid length attachment keys"); encryptProfile: function(data, key) {
} var iv = libsignal.crypto.getRandomBytes(PROFILE_IV_LENGTH);
if (encryptedBin.byteLength < 16 + 32) { if (key.byteLength != PROFILE_KEY_LENGTH) {
throw new Error("Got invalid length attachment"); throw new Error('Got invalid length profile key');
} }
if (iv.byteLength != PROFILE_IV_LENGTH) {
var aes_key = keys.slice(0, 32); throw new Error('Got invalid length profile iv');
var mac_key = keys.slice(32, 64); }
return crypto.subtle
var iv = encryptedBin.slice(0, 16); .importKey('raw', key, { name: 'AES-GCM' }, false, ['encrypt'])
var ciphertext = encryptedBin.slice(16, encryptedBin.byteLength - 32); .then(function(key) {
var ivAndCiphertext = encryptedBin.slice(0, encryptedBin.byteLength - 32); return crypto.subtle
var mac = encryptedBin.slice(encryptedBin.byteLength - 32, encryptedBin.byteLength); .encrypt(
{ name: 'AES-GCM', iv: iv, tagLength: PROFILE_TAG_LENGTH },
return verifyMAC(ivAndCiphertext, mac_key, mac, 32).then(function() { key,
if (theirDigest !== null) { data
return verifyDigest(encryptedBin, theirDigest); )
} .then(function(ciphertext) {
}).then(function() { var ivAndCiphertext = new Uint8Array(
return decrypt(aes_key, ciphertext, iv); PROFILE_IV_LENGTH + ciphertext.byteLength
});
},
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 (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(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;
} }
}); });
}); });
}, },
encryptProfileName: function(name, key) { encryptProfileName: function(name, key) {
var padded = new Uint8Array(PROFILE_NAME_PADDED_LENGTH); var padded = new Uint8Array(PROFILE_NAME_PADDED_LENGTH);
padded.set(new Uint8Array(name)); padded.set(new Uint8Array(name));
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,
// unpad 'base64'
var name = ''; ).toArrayBuffer();
var padded = new Uint8Array(decrypted); return textsecure.crypto
for (var i = padded.length; i > 0; i--) { .decryptProfile(data, key)
if (padded[i-1] !== 0x00) { .then(function(decrypted) {
break; // 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) {
getRandomBytes: function(size) { return libsignal.crypto.getRandomBytes(size);
return libsignal.crypto.getRandomBytes(size); },
} };
};
})(); })();

View file

@ -1,165 +1,164 @@
;(function() { (function() {
'use strict'; 'use strict';
var registeredFunctions = {}; var registeredFunctions = {};
var Type = { var Type = {
ENCRYPT_MESSAGE: 1, ENCRYPT_MESSAGE: 1,
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) {
Child.prototype = Object.create(Parent.prototype, { Child.prototype = Object.create(Parent.prototype, {
constructor: { constructor: {
value: Child, value: Child,
writable: true, writable: true,
configurable: true configurable: true,
} },
}); });
} }
function appendStack(newError, originalError) { function appendStack(newError, originalError) {
newError.stack += '\nOriginal stack:\n' + originalError.stack; 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) { this.functionCode = options.functionCode;
options = options || {}; this.args = options.args;
this.name = options.name || 'ReplayableError'; }
this.message = options.message; 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) function IncomingIdentityKeyError(number, message, key) {
// via https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error this.number = number.split('.')[0];
if (Error.captureStackTrace) { this.identityKey = key;
Error.captureStackTrace(this);
}
this.functionCode = options.functionCode; ReplayableError.call(this, {
this.args = options.args; 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() { function SendMessageNetworkError(number, jsonData, httpError, timestamp) {
var argumentsAsArray = Array.prototype.slice.call(arguments, 0); this.number = number;
var args = this.args.concat(argumentsAsArray); this.code = httpError.code;
return registeredFunctions[this.functionCode].apply(window, args);
};
function IncomingIdentityKeyError(number, message, key) { ReplayableError.call(this, {
this.number = number.split('.')[0]; functionCode: Type.TRANSMIT_MESSAGE,
this.identityKey = key; args: [number, jsonData, timestamp],
name: 'SendMessageNetworkError',
message: httpError.message,
});
ReplayableError.call(this, { appendStack(this, httpError);
functionCode : Type.INIT_SESSION, }
args : [number, message], inherit(ReplayableError, SendMessageNetworkError);
name : 'IncomingIdentityKeyError',
message : "The identity of " + this.number + " has changed." 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;
this.number = number.split('.')[0]; this.code = httpError.code;
this.identityKey = identityKey;
ReplayableError.call(this, { appendStack(this, httpError);
functionCode : Type.ENCRYPT_MESSAGE, }
args : [number, message, timestamp], inherit(Error, UnregisteredUserError);
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;
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;
})(); })();

View file

@ -2,79 +2,78 @@
* 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,
dispatchEvent: function(ev) { dispatchEvent: function(ev) {
if (!(ev instanceof Event)) { if (!(ev instanceof Event)) {
throw new Error('Expects an event'); throw new Error('Expects an event');
} }
if (this.listeners === null || typeof this.listeners !== 'object') { if (this.listeners === null || typeof this.listeners !== 'object') {
this.listeners = {}; this.listeners = {};
} }
var listeners = this.listeners[ev.type]; var listeners = this.listeners[ev.type];
var results = []; var results = [];
if (typeof listeners === 'object') { if (typeof listeners === 'object') {
for (var i = 0, max = listeners.length; i < max; i += 1) { for (var i = 0, max = listeners.length; i < max; i += 1) {
var listener = listeners[i]; var listener = listeners[i];
if (typeof listener === 'function') { if (typeof listener === 'function') {
results.push(listener.call(null, ev)); 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];
} }
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;
}()); })();

View file

@ -10,64 +10,62 @@ var StaticByteBufferProto = new dcodeIO.ByteBuffer().__proto__;
var StaticArrayBufferProto = new ArrayBuffer().__proto__; var StaticArrayBufferProto = new ArrayBuffer().__proto__;
var StaticUint8ArrayProto = new Uint8Array().__proto__; var StaticUint8ArrayProto = new Uint8Array().__proto__;
function getString(thing) { function getString(thing) {
if (thing === Object(thing)) { if (thing === Object(thing)) {
if (thing.__proto__ == StaticUint8ArrayProto) if (thing.__proto__ == StaticUint8ArrayProto)
return String.fromCharCode.apply(null, thing); return String.fromCharCode.apply(null, 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 (
(thing === Object(thing) && typeof thing == 'string' ||
(thing.__proto__ == StaticArrayBufferProto || typeof thing == 'number' ||
thing.__proto__ == StaticUint8ArrayProto || typeof thing == 'boolean' ||
thing.__proto__ == StaticByteBufferProto))); (thing === Object(thing) &&
(thing.__proto__ == StaticArrayBufferProto ||
thing.__proto__ == StaticUint8ArrayProto ||
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 ***
**************************/
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) {
*** JSON'ing Utilities *** return JSON.stringify(ensureStringed(thing));
**************************/ };
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;
}();
return self;
})();

View file

@ -27,33 +27,32 @@
*/ */
var store = {}; var store = {};
window.textsecure.storage.impl = { 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}); },
},
get: function(key, defaultValue) { get: function(key, defaultValue) {
if (key in store) { if (key in store) {
return store[key]; return store[key];
} else { } else {
return defaultValue; return defaultValue;
} }
}, },
remove: function(key) { remove: function(key) {
delete store[key]; delete store[key];
postMessage({method: 'remove', key: key}); postMessage({ method: 'remove', key: key });
}, },
}; };
onmessage = function(e) { onmessage = function(e) {
store = e.data; store = e.data;
textsecure.protocol_wrapper.generateKeys().then(function(keys) { textsecure.protocol_wrapper.generateKeys().then(function(keys) {
postMessage({method: 'done', keys: keys}); postMessage({ method: 'done', keys: keys });
close(); close();
}); });
} };

View file

@ -122,9 +122,10 @@ MessageReceiver.prototype.extend({
this.onEmpty(); this.onEmpty();
} }
// possible 403 or network issue. Make an request to confirm // possible 403 or network issue. Make an request to confirm
return this.server.getDevices(this.number) return this.server
.getDevices(this.number)
.then(this.connect.bind(this)) // No HTTP error? Reconnect .then(this.connect.bind(this)) // No HTTP error? Reconnect
.catch((e) => { .catch(e => {
const event = new Event('error'); const event = new Event('error');
event.error = e; event.error = e;
return this.dispatchAndWait(event); return this.dispatchAndWait(event);
@ -146,35 +147,41 @@ MessageReceiver.prototype.extend({
return; return;
} }
const promise = textsecure.crypto.decryptWebsocketMessage( const promise = textsecure.crypto
request.body, .decryptWebsocketMessage(request.body, this.signalingKey)
this.signalingKey .then(plaintext => {
).then((plaintext) => { const envelope = textsecure.protobuf.Envelope.decode(plaintext);
const envelope = textsecure.protobuf.Envelope.decode(plaintext); // After this point, decoding errors are not the server's
// After this point, decoding errors are not the server's // fault, and we should handle them gracefully and tell the
// fault, and we should handle them gracefully and tell the // user they received an invalid message
// user they received an invalid message
if (this.isBlocked(envelope.source)) { if (this.isBlocked(envelope.source)) {
return request.respond(200, 'OK'); return request.respond(200, 'OK');
} }
return this.addToCache(envelope, plaintext).then(() => { return this.addToCache(envelope, plaintext).then(
request.respond(200, 'OK'); () => {
this.queueEnvelope(envelope); request.respond(200, 'OK');
}, (error) => { this.queueEnvelope(envelope);
console.log( },
'handleRequest error trying to add message to cache:', error => {
error && error.stack ? error.stack : error console.log(
'handleRequest error trying to add message to cache:',
error && error.stack ? error.stack : error
);
}
); );
})
.catch(e => {
request.respond(500, 'Bad encrypted websocket message');
console.log(
'Error handling incoming message:',
e && e.stack ? e.stack : e
);
const ev = new Event('error');
ev.error = e;
return this.dispatchAndWait(ev);
}); });
}).catch((e) => {
request.respond(500, 'Bad encrypted websocket message');
console.log('Error handling incoming message:', e && e.stack ? e.stack : e);
const ev = new Event('error');
ev.error = e;
return this.dispatchAndWait(ev);
});
this.incoming.push(promise); this.incoming.push(promise);
}, },
@ -203,7 +210,7 @@ MessageReceiver.prototype.extend({
this.incoming = []; this.incoming = [];
const dispatchEmpty = () => { const dispatchEmpty = () => {
console.log('MessageReceiver: emitting \'empty\' event'); console.log("MessageReceiver: emitting 'empty' event");
const ev = new Event('empty'); const ev = new Event('empty');
return this.dispatchAndWait(ev); return this.dispatchAndWait(ev);
}; };
@ -224,9 +231,10 @@ MessageReceiver.prototype.extend({
const { incoming } = this; const { incoming } = this;
this.incoming = []; this.incoming = [];
const queueDispatch = () => this.addToQueue(() => { const queueDispatch = () =>
console.log('drained'); this.addToQueue(() => {
}); console.log('drained');
});
// This promise will resolve when there are no more messages to be processed. // This promise will resolve when there are no more messages to be processed.
return Promise.all(incoming).then(queueDispatch, queueDispatch); return Promise.all(incoming).then(queueDispatch, queueDispatch);
@ -241,7 +249,7 @@ MessageReceiver.prototype.extend({
this.dispatchEvent(ev); this.dispatchEvent(ev);
}, },
queueAllCached() { queueAllCached() {
return this.getAllFromCache().then((items) => { return this.getAllFromCache().then(items => {
for (let i = 0, max = items.length; i < max; i += 1) { for (let i = 0, max = items.length; i < max; i += 1) {
this.queueCached(items[i]); this.queueCached(items[i]);
} }
@ -273,7 +281,9 @@ MessageReceiver.prototype.extend({
} }
}, },
getEnvelopeId(envelope) { getEnvelopeId(envelope) {
return `${envelope.source}.${envelope.sourceDevice} ${envelope.timestamp.toNumber()}`; return `${envelope.source}.${
envelope.sourceDevice
} ${envelope.timestamp.toNumber()}`;
}, },
stringToArrayBuffer(string) { stringToArrayBuffer(string) {
// eslint-disable-next-line new-cap // eslint-disable-next-line new-cap
@ -281,23 +291,28 @@ MessageReceiver.prototype.extend({
}, },
getAllFromCache() { getAllFromCache() {
console.log('getAllFromCache'); console.log('getAllFromCache');
return textsecure.storage.unprocessed.getAll().then((items) => { return textsecure.storage.unprocessed.getAll().then(items => {
console.log('getAllFromCache loaded', items.length, 'saved envelopes'); console.log('getAllFromCache loaded', items.length, 'saved envelopes');
return Promise.all(_.map(items, (item) => { return Promise.all(
const attempts = 1 + (item.attempts || 0); _.map(items, item => {
if (attempts >= 5) { const attempts = 1 + (item.attempts || 0);
console.log('getAllFromCache final attempt for envelope', item.id); if (attempts >= 5) {
return textsecure.storage.unprocessed.remove(item.id); console.log('getAllFromCache final attempt for envelope', item.id);
return textsecure.storage.unprocessed.remove(item.id);
}
return textsecure.storage.unprocessed.update(item.id, { attempts });
})
).then(
() => items,
error => {
console.log(
'getAllFromCache error updating items after load:',
error && error.stack ? error.stack : error
);
return items;
} }
return textsecure.storage.unprocessed.update(item.id, { attempts }); );
})).then(() => items, (error) => {
console.log(
'getAllFromCache error updating items after load:',
error && error.stack ? error.stack : error
);
return items;
});
}); });
}, },
addToCache(envelope, plaintext) { addToCache(envelope, plaintext) {
@ -332,7 +347,7 @@ MessageReceiver.prototype.extend({
); );
const promise = this.addToQueue(taskWithTimeout); const promise = this.addToQueue(taskWithTimeout);
return promise.catch((error) => { return promise.catch(error => {
console.log( console.log(
'queueDecryptedEnvelope error handling envelope', 'queueDecryptedEnvelope error handling envelope',
id, id,
@ -346,10 +361,13 @@ MessageReceiver.prototype.extend({
console.log('queueing envelope', id); console.log('queueing envelope', id);
const task = this.handleEnvelope.bind(this, envelope); const task = this.handleEnvelope.bind(this, envelope);
const taskWithTimeout = textsecure.createTaskWithTimeout(task, `queueEnvelope ${id}`); const taskWithTimeout = textsecure.createTaskWithTimeout(
task,
`queueEnvelope ${id}`
);
const promise = this.addToQueue(taskWithTimeout); const promise = this.addToQueue(taskWithTimeout);
return promise.catch((error) => { return promise.catch(error => {
console.log( console.log(
'queueEnvelope error handling envelope', 'queueEnvelope error handling envelope',
id, id,
@ -448,46 +466,56 @@ MessageReceiver.prototype.extend({
switch (envelope.type) { switch (envelope.type) {
case textsecure.protobuf.Envelope.Type.CIPHERTEXT: case textsecure.protobuf.Envelope.Type.CIPHERTEXT:
console.log('message from', this.getEnvelopeId(envelope)); console.log('message from', this.getEnvelopeId(envelope));
promise = sessionCipher.decryptWhisperMessage(ciphertext).then(this.unpad); promise = sessionCipher
.decryptWhisperMessage(ciphertext)
.then(this.unpad);
break; break;
case textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE: case textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE:
console.log('prekey message from', this.getEnvelopeId(envelope)); console.log('prekey message from', this.getEnvelopeId(envelope));
promise = this.decryptPreKeyWhisperMessage(ciphertext, sessionCipher, address); promise = this.decryptPreKeyWhisperMessage(
ciphertext,
sessionCipher,
address
);
break; break;
default: default:
promise = Promise.reject(new Error('Unknown message type')); promise = Promise.reject(new Error('Unknown message type'));
} }
return promise.then(plaintext => this.updateCache( return promise
envelope, .then(plaintext =>
plaintext this.updateCache(envelope, plaintext).then(
).then(() => plaintext, (error) => { () => plaintext,
console.log( error => {
'decrypt failed to save decrypted message contents to cache:', console.log(
error && error.stack ? error.stack : error 'decrypt failed to save decrypted message contents to cache:',
); error && error.stack ? error.stack : error
return plaintext; );
})).catch((error) => { return plaintext;
let errorToThrow = error; }
)
)
.catch(error => {
let errorToThrow = error;
if (error.message === 'Unknown identity key') { if (error.message === 'Unknown identity key') {
// create an error that the UI will pick up and ask the // create an error that the UI will pick up and ask the
// user if they want to re-negotiate // user if they want to re-negotiate
const buffer = dcodeIO.ByteBuffer.wrap(ciphertext); const buffer = dcodeIO.ByteBuffer.wrap(ciphertext);
errorToThrow = new textsecure.IncomingIdentityKeyError( errorToThrow = new textsecure.IncomingIdentityKeyError(
address.toString(), address.toString(),
buffer.toArrayBuffer(), buffer.toArrayBuffer(),
error.identityKey error.identityKey
); );
} }
const ev = new Event('error'); const ev = new Event('error');
ev.error = errorToThrow; ev.error = errorToThrow;
ev.proto = envelope; ev.proto = envelope;
ev.confirm = this.removeFromCache.bind(this, envelope); ev.confirm = this.removeFromCache.bind(this, envelope);
const returnError = () => Promise.reject(errorToThrow); const returnError = () => Promise.reject(errorToThrow);
return this.dispatchAndWait(ev).then(returnError, returnError); return this.dispatchAndWait(ev).then(returnError, returnError);
}); });
}, },
async decryptPreKeyWhisperMessage(ciphertext, sessionCipher, address) { async decryptPreKeyWhisperMessage(ciphertext, sessionCipher, address) {
const padded = await sessionCipher.decryptPreKeyWhisperMessage(ciphertext); const padded = await sessionCipher.decryptPreKeyWhisperMessage(ciphertext);
@ -508,30 +536,34 @@ MessageReceiver.prototype.extend({
throw e; throw e;
} }
}, },
handleSentMessage(envelope, destination, timestamp, msg, expirationStartTimestamp) { handleSentMessage(
envelope,
destination,
timestamp,
msg,
expirationStartTimestamp
) {
let p = Promise.resolve(); let p = Promise.resolve();
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
if (msg.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) { if (msg.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) {
p = this.handleEndSession(destination); p = this.handleEndSession(destination);
} }
return p.then(() => this.processDecrypted( return p.then(() =>
envelope, this.processDecrypted(envelope, msg, this.number).then(message => {
msg, const ev = new Event('sent');
this.number ev.confirm = this.removeFromCache.bind(this, envelope);
).then((message) => { ev.data = {
const ev = new Event('sent'); destination,
ev.confirm = this.removeFromCache.bind(this, envelope); timestamp: timestamp.toNumber(),
ev.data = { device: envelope.sourceDevice,
destination, message,
timestamp: timestamp.toNumber(), };
device: envelope.sourceDevice, if (expirationStartTimestamp) {
message, ev.data.expirationStartTimestamp = expirationStartTimestamp.toNumber();
}; }
if (expirationStartTimestamp) { return this.dispatchAndWait(ev);
ev.data.expirationStartTimestamp = expirationStartTimestamp.toNumber(); })
} );
return this.dispatchAndWait(ev);
}));
}, },
handleDataMessage(envelope, msg) { handleDataMessage(envelope, msg) {
console.log('data message from', this.getEnvelopeId(envelope)); console.log('data message from', this.getEnvelopeId(envelope));
@ -540,38 +572,34 @@ MessageReceiver.prototype.extend({
if (msg.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) { if (msg.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) {
p = this.handleEndSession(envelope.source); p = this.handleEndSession(envelope.source);
} }
return p.then(() => this.processDecrypted( return p.then(() =>
envelope, this.processDecrypted(envelope, msg, envelope.source).then(message => {
msg, const ev = new Event('message');
envelope.source ev.confirm = this.removeFromCache.bind(this, envelope);
).then((message) => { ev.data = {
const ev = new Event('message'); source: envelope.source,
ev.confirm = this.removeFromCache.bind(this, envelope); sourceDevice: envelope.sourceDevice,
ev.data = { timestamp: envelope.timestamp.toNumber(),
source: envelope.source, receivedAt: envelope.receivedAt,
sourceDevice: envelope.sourceDevice, message,
timestamp: envelope.timestamp.toNumber(), };
receivedAt: envelope.receivedAt, return this.dispatchAndWait(ev);
message, })
}; );
return this.dispatchAndWait(ev);
}));
}, },
handleLegacyMessage(envelope) { handleLegacyMessage(envelope) {
return this.decrypt( return this.decrypt(envelope, envelope.legacyMessage).then(plaintext =>
envelope, this.innerHandleLegacyMessage(envelope, plaintext)
envelope.legacyMessage );
).then(plaintext => this.innerHandleLegacyMessage(envelope, plaintext));
}, },
innerHandleLegacyMessage(envelope, plaintext) { innerHandleLegacyMessage(envelope, plaintext) {
const message = textsecure.protobuf.DataMessage.decode(plaintext); const message = textsecure.protobuf.DataMessage.decode(plaintext);
return this.handleDataMessage(envelope, message); return this.handleDataMessage(envelope, message);
}, },
handleContentMessage(envelope) { handleContentMessage(envelope) {
return this.decrypt( return this.decrypt(envelope, envelope.content).then(plaintext =>
envelope, this.innerHandleContentMessage(envelope, plaintext)
envelope.content );
).then(plaintext => this.innerHandleContentMessage(envelope, plaintext));
}, },
innerHandleContentMessage(envelope, plaintext) { innerHandleContentMessage(envelope, plaintext) {
const content = textsecure.protobuf.Content.decode(plaintext); const content = textsecure.protobuf.Content.decode(plaintext);
@ -595,7 +623,9 @@ MessageReceiver.prototype.extend({
}, },
handleReceiptMessage(envelope, receiptMessage) { handleReceiptMessage(envelope, receiptMessage) {
const results = []; const results = [];
if (receiptMessage.type === textsecure.protobuf.ReceiptMessage.Type.DELIVERY) { if (
receiptMessage.type === textsecure.protobuf.ReceiptMessage.Type.DELIVERY
) {
for (let i = 0; i < receiptMessage.timestamp.length; i += 1) { for (let i = 0; i < receiptMessage.timestamp.length; i += 1) {
const ev = new Event('delivery'); const ev = new Event('delivery');
ev.confirm = this.removeFromCache.bind(this, envelope); ev.confirm = this.removeFromCache.bind(this, envelope);
@ -606,7 +636,9 @@ MessageReceiver.prototype.extend({
}; };
results.push(this.dispatchAndWait(ev)); results.push(this.dispatchAndWait(ev));
} }
} else if (receiptMessage.type === textsecure.protobuf.ReceiptMessage.Type.READ) { } else if (
receiptMessage.type === textsecure.protobuf.ReceiptMessage.Type.READ
) {
for (let i = 0; i < receiptMessage.timestamp.length; i += 1) { for (let i = 0; i < receiptMessage.timestamp.length; i += 1) {
const ev = new Event('read'); const ev = new Event('read');
ev.confirm = this.removeFromCache.bind(this, envelope); ev.confirm = this.removeFromCache.bind(this, envelope);
@ -734,12 +766,13 @@ MessageReceiver.prototype.extend({
let groupDetails = groupBuffer.next(); let groupDetails = groupBuffer.next();
const promises = []; const promises = [];
while (groupDetails !== undefined) { while (groupDetails !== undefined) {
const getGroupDetails = (details) => { const getGroupDetails = details => {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
details.id = details.id.toBinary(); details.id = details.id.toBinary();
if (details.active) { if (details.active) {
return textsecure.storage.groups.getGroup(details.id) return textsecure.storage.groups
.then((existingGroup) => { .getGroup(details.id)
.then(existingGroup => {
if (existingGroup === undefined) { if (existingGroup === undefined) {
return textsecure.storage.groups.createNewGroup( return textsecure.storage.groups.createNewGroup(
details.members, details.members,
@ -750,19 +783,22 @@ MessageReceiver.prototype.extend({
details.id, details.id,
details.members details.members
); );
}).then(() => details); })
.then(() => details);
} }
return Promise.resolve(details); return Promise.resolve(details);
}; };
const promise = getGroupDetails(groupDetails).then((details) => { const promise = getGroupDetails(groupDetails)
const ev = new Event('group'); .then(details => {
ev.confirm = this.removeFromCache.bind(this, envelope); const ev = new Event('group');
ev.groupDetails = details; ev.confirm = this.removeFromCache.bind(this, envelope);
return this.dispatchAndWait(ev); ev.groupDetails = details;
}).catch((e) => { return this.dispatchAndWait(ev);
console.log('error processing group', e); })
}); .catch(e => {
console.log('error processing group', e);
});
groupDetails = groupBuffer.next(); groupDetails = groupBuffer.next();
promises.push(promise); promises.push(promise);
} }
@ -803,7 +839,8 @@ MessageReceiver.prototype.extend({
attachment.data = data; attachment.data = data;
} }
return this.server.getAttachment(attachment.id) return this.server
.getAttachment(attachment.id)
.then(decryptAttachment) .then(decryptAttachment)
.then(updateAttachment); .then(updateAttachment);
}, },
@ -825,8 +862,14 @@ MessageReceiver.prototype.extend({
// It's most likely that dataMessage will be populated, so we look at it in detail // It's most likely that dataMessage will be populated, so we look at it in detail
const data = content.dataMessage; const data = content.dataMessage;
if (data && !data.attachments.length && !data.body && !data.expireTimer && if (
!data.flags && !data.group) { data &&
!data.attachments.length &&
!data.body &&
!data.expireTimer &&
!data.flags &&
!data.group
) {
return false; return false;
} }
@ -857,7 +900,7 @@ MessageReceiver.prototype.extend({
ciphertext, ciphertext,
sessionCipher, sessionCipher,
address address
).then((plaintext) => { ).then(plaintext => {
const envelope = { const envelope = {
source: number, source: number,
sourceDevice: device, sourceDevice: device,
@ -901,16 +944,18 @@ MessageReceiver.prototype.extend({
console.log('got end session'); console.log('got end session');
const deviceIds = await textsecure.storage.protocol.getDeviceIds(number); const deviceIds = await textsecure.storage.protocol.getDeviceIds(number);
return Promise.all(deviceIds.map((deviceId) => { return Promise.all(
const address = new libsignal.SignalProtocolAddress(number, deviceId); deviceIds.map(deviceId => {
const sessionCipher = new libsignal.SessionCipher( const address = new libsignal.SignalProtocolAddress(number, deviceId);
textsecure.storage.protocol, const sessionCipher = new libsignal.SessionCipher(
address textsecure.storage.protocol,
); address
);
console.log('deleting sessions for', address.toString()); console.log('deleting sessions for', address.toString());
return sessionCipher.deleteAllSessionsForDevice(); return sessionCipher.deleteAllSessionsForDevice();
})); })
);
}, },
processDecrypted(envelope, decrypted, source) { processDecrypted(envelope, decrypted, source) {
/* eslint-disable no-bitwise, no-param-reassign */ /* eslint-disable no-bitwise, no-param-reassign */
@ -928,7 +973,6 @@ MessageReceiver.prototype.extend({
decrypted.expireTimer = 0; decrypted.expireTimer = 0;
} }
if (decrypted.flags & FLAGS.END_SESSION) { if (decrypted.flags & FLAGS.END_SESSION) {
decrypted.body = null; decrypted.body = null;
decrypted.attachments = []; decrypted.attachments = [];
@ -949,7 +993,9 @@ MessageReceiver.prototype.extend({
if (decrypted.group !== null) { if (decrypted.group !== null) {
decrypted.group.id = decrypted.group.id.toBinary(); decrypted.group.id = decrypted.group.id.toBinary();
if (decrypted.group.type === textsecure.protobuf.GroupContext.Type.UPDATE) { if (
decrypted.group.type === textsecure.protobuf.GroupContext.Type.UPDATE
) {
if (decrypted.group.avatar !== null) { if (decrypted.group.avatar !== null) {
promises.push(this.handleAttachment(decrypted.group.avatar)); promises.push(this.handleAttachment(decrypted.group.avatar));
} }
@ -957,49 +1003,61 @@ MessageReceiver.prototype.extend({
const storageGroups = textsecure.storage.groups; const storageGroups = textsecure.storage.groups;
promises.push(storageGroups.getNumbers(decrypted.group.id).then((existingGroup) => { promises.push(
if (existingGroup === undefined) { storageGroups.getNumbers(decrypted.group.id).then(existingGroup => {
if (decrypted.group.type !== textsecure.protobuf.GroupContext.Type.UPDATE) { if (existingGroup === undefined) {
decrypted.group.members = [source]; if (
console.log('Got message for unknown group'); decrypted.group.type !==
} textsecure.protobuf.GroupContext.Type.UPDATE
return textsecure.storage.groups.createNewGroup( ) {
decrypted.group.members, decrypted.group.members = [source];
decrypted.group.id console.log('Got message for unknown group');
);
}
const fromIndex = existingGroup.indexOf(source);
if (fromIndex < 0) {
// TODO: This could be indication of a race...
console.log('Sender was not a member of the group they were sending from');
}
switch (decrypted.group.type) {
case textsecure.protobuf.GroupContext.Type.UPDATE:
decrypted.body = null;
decrypted.attachments = [];
return textsecure.storage.groups.updateNumbers(
decrypted.group.id,
decrypted.group.members
);
case textsecure.protobuf.GroupContext.Type.QUIT:
decrypted.body = null;
decrypted.attachments = [];
if (source === this.number) {
return textsecure.storage.groups.deleteGroup(decrypted.group.id);
} }
return textsecure.storage.groups.removeNumber(decrypted.group.id, source); return textsecure.storage.groups.createNewGroup(
case textsecure.protobuf.GroupContext.Type.DELIVER: decrypted.group.members,
decrypted.group.name = null; decrypted.group.id
decrypted.group.members = []; );
decrypted.group.avatar = null; }
return Promise.resolve(); const fromIndex = existingGroup.indexOf(source);
default:
this.removeFromCache(envelope); if (fromIndex < 0) {
throw new Error('Unknown group message type'); // TODO: This could be indication of a race...
} console.log(
})); 'Sender was not a member of the group they were sending from'
);
}
switch (decrypted.group.type) {
case textsecure.protobuf.GroupContext.Type.UPDATE:
decrypted.body = null;
decrypted.attachments = [];
return textsecure.storage.groups.updateNumbers(
decrypted.group.id,
decrypted.group.members
);
case textsecure.protobuf.GroupContext.Type.QUIT:
decrypted.body = null;
decrypted.attachments = [];
if (source === this.number) {
return textsecure.storage.groups.deleteGroup(
decrypted.group.id
);
}
return textsecure.storage.groups.removeNumber(
decrypted.group.id,
source
);
case textsecure.protobuf.GroupContext.Type.DELIVER:
decrypted.group.name = null;
decrypted.group.members = [];
decrypted.group.avatar = null;
return Promise.resolve();
default:
this.removeFromCache(envelope);
throw new Error('Unknown group message type');
}
})
);
} }
for (let i = 0, max = decrypted.attachments.length; i < max; i += 1) { for (let i = 0, max = decrypted.attachments.length; i < max; i += 1) {
@ -1021,12 +1079,14 @@ MessageReceiver.prototype.extend({
if (thumbnail) { if (thumbnail) {
// We don't want the failure of a thumbnail download to fail the handling of // We don't want the failure of a thumbnail download to fail the handling of
// this message entirely, like we do for full attachments. // this message entirely, like we do for full attachments.
promises.push(this.handleAttachment(thumbnail).catch((error) => { promises.push(
console.log( this.handleAttachment(thumbnail).catch(error => {
'Problem loading thumbnail for quote', console.log(
error && error.stack ? error.stack : error 'Problem loading thumbnail for quote',
); error && error.stack ? error.stack : error
})); );
})
);
} }
} }
} }
@ -1052,8 +1112,12 @@ textsecure.MessageReceiver = function MessageReceiverWrapper(
signalingKey, signalingKey,
options options
); );
this.addEventListener = messageReceiver.addEventListener.bind(messageReceiver); this.addEventListener = messageReceiver.addEventListener.bind(
this.removeEventListener = messageReceiver.removeEventListener.bind(messageReceiver); messageReceiver
);
this.removeEventListener = messageReceiver.removeEventListener.bind(
messageReceiver
);
this.getStatus = messageReceiver.getStatus.bind(messageReceiver); this.getStatus = messageReceiver.getStatus.bind(messageReceiver);
this.close = messageReceiver.close.bind(messageReceiver); this.close = messageReceiver.close.bind(messageReceiver);
messageReceiver.connect(); messageReceiver.connect();
@ -1067,4 +1131,3 @@ textsecure.MessageReceiver = function MessageReceiverWrapper(
textsecure.MessageReceiver.prototype = { textsecure.MessageReceiver.prototype = {
constructor: textsecure.MessageReceiver, constructor: textsecure.MessageReceiver,
}; };

View file

@ -1,241 +1,352 @@
function OutgoingMessage(server, timestamp, numbers, message, silent, callback) { function OutgoingMessage(
if (message instanceof textsecure.protobuf.DataMessage) { server,
var content = new textsecure.protobuf.Content(); timestamp,
content.dataMessage = message; numbers,
message = content; message,
} silent,
this.server = server; callback
this.timestamp = timestamp; ) {
this.numbers = numbers; if (message instanceof textsecure.protobuf.DataMessage) {
this.message = message; // ContentMessage proto var content = new textsecure.protobuf.Content();
this.callback = callback; content.dataMessage = message;
this.silent = silent; message = content;
}
this.server = server;
this.timestamp = timestamp;
this.numbers = numbers;
this.message = message; // ContentMessage proto
this.callback = callback;
this.silent = silent;
this.numbersCompleted = 0; this.numbersCompleted = 0;
this.errors = []; this.errors = [];
this.successfulNumbers = []; this.successfulNumbers = [];
} }
OutgoingMessage.prototype = { OutgoingMessage.prototype = {
constructor: OutgoingMessage, constructor: OutgoingMessage,
numberCompleted: function() { numberCompleted: function() {
this.numbersCompleted++; this.numbersCompleted++;
if (this.numbersCompleted >= this.numbers.length) { if (this.numbersCompleted >= this.numbers.length) {
this.callback({successfulNumbers: this.successfulNumbers, errors: this.errors}); this.callback({
} successfulNumbers: this.successfulNumbers,
}, errors: this.errors,
registerError: function(number, reason, error) { });
if (!error || error.name === 'HTTPError' && error.code !== 404) {
error = new textsecure.OutgoingMessageError(number, this.message.toArrayBuffer(), this.timestamp, error);
}
error.number = number;
error.reason = reason;
this.errors[this.errors.length] = error;
this.numberCompleted();
},
reloadDevicesAndSend: function(number, recurse) {
return function() {
return textsecure.storage.protocol.getDeviceIds(number).then(function(deviceIds) {
if (deviceIds.length == 0) {
return this.registerError(number, "Got empty device list when loading device keys", null);
}
return this.doSendMessage(number, deviceIds, recurse);
}.bind(this));
}.bind(this);
},
getKeysForNumber: function(number, updateDevices) {
var handleResult = function(response) {
return Promise.all(response.devices.map(function(device) {
device.identityKey = response.identityKey;
if (updateDevices === undefined || updateDevices.indexOf(device.deviceId) > -1) {
var address = new libsignal.SignalProtocolAddress(number, device.deviceId);
var builder = new libsignal.SessionBuilder(textsecure.storage.protocol, address);
if (device.registrationId === 0) {
console.log("device registrationId 0!");
}
return builder.processPreKey(device).catch(function(error) {
if (error.message === "Identity key changed") {
error.timestamp = this.timestamp;
error.originalMessage = this.message.toArrayBuffer();
error.identityKey = device.identityKey;
}
throw error;
}.bind(this));
}
}.bind(this)));
}.bind(this);
if (updateDevices === undefined) {
return this.server.getKeysForNumber(number).then(handleResult);
} else {
var promise = Promise.resolve();
updateDevices.forEach(function(device) {
promise = promise.then(function() {
return this.server.getKeysForNumber(number, device).then(handleResult).catch(function(e) {
if (e.name === 'HTTPError' && e.code === 404) {
if (device !== 1) {
return this.removeDeviceIdsForNumber(number, [device]);
} else {
throw new textsecure.UnregisteredUserError(number, e);
}
} else {
throw e;
}
}.bind(this));
}.bind(this));
}.bind(this));
return promise;
}
},
transmitMessage: function(number, jsonData, timestamp) {
return this.server.sendMessages(number, jsonData, timestamp, this.silent).catch(function(e) {
if (e.name === 'HTTPError' && (e.code !== 409 && e.code !== 410)) {
// 409 and 410 should bubble and be handled by doSendMessage
// 404 should throw UnregisteredUserError
// all other network errors can be retried later.
if (e.code === 404) {
throw new textsecure.UnregisteredUserError(number, e);
}
throw new textsecure.SendMessageNetworkError(number, jsonData, e, timestamp);
}
throw e;
});
},
getPaddedMessageLength: function(messageLength) {
var messageLengthWithTerminator = messageLength + 1;
var messagePartCount = Math.floor(messageLengthWithTerminator / 160);
if (messageLengthWithTerminator % 160 !== 0) {
messagePartCount++;
}
return messagePartCount * 160;
},
getPlaintext: function() {
if (!this.plaintext) {
var messageBuffer = this.message.toArrayBuffer();
this.plaintext = new Uint8Array(
this.getPaddedMessageLength(messageBuffer.byteLength + 1) - 1
);
this.plaintext.set(new Uint8Array(messageBuffer));
this.plaintext[messageBuffer.byteLength] = 0x80;
}
return this.plaintext;
},
doSendMessage: function(number, deviceIds, recurse) {
var ciphers = {};
var plaintext = this.getPlaintext();
return Promise.all(deviceIds.map(function(deviceId) {
var address = new libsignal.SignalProtocolAddress(number, deviceId);
var ourNumber = textsecure.storage.user.getNumber();
var options = {};
// No limit on message keys if we're communicating with our other devices
if (ourNumber === number) {
options.messageKeysLimit = false;
}
var sessionCipher = new libsignal.SessionCipher(textsecure.storage.protocol, address, options);
ciphers[address.getDeviceId()] = sessionCipher;
return sessionCipher.encrypt(plaintext).then(function(ciphertext) {
return {
type : ciphertext.type,
destinationDeviceId : address.getDeviceId(),
destinationRegistrationId : ciphertext.registrationId,
content : btoa(ciphertext.body)
};
});
}.bind(this))).then(function(jsonData) {
return this.transmitMessage(number, jsonData, this.timestamp).then(function() {
this.successfulNumbers[this.successfulNumbers.length] = number;
this.numberCompleted();
}.bind(this));
}.bind(this)).catch(function(error) {
if (error instanceof Error && error.name == "HTTPError" && (error.code == 410 || error.code == 409)) {
if (!recurse)
return this.registerError(number, "Hit retry limit attempting to reload device list", error);
var p;
if (error.code == 409) {
p = this.removeDeviceIdsForNumber(number, error.response.extraDevices);
} else {
p = Promise.all(error.response.staleDevices.map(function(deviceId) {
return ciphers[deviceId].closeOpenSessionForDevice();
}));
}
return p.then(function() {
var resetDevices = ((error.code == 410) ? error.response.staleDevices : error.response.missingDevices);
return this.getKeysForNumber(number, resetDevices)
.then(this.reloadDevicesAndSend(number, error.code == 409));
}.bind(this));
} else if (error.message === "Identity key changed") {
error.timestamp = this.timestamp;
error.originalMessage = this.message.toArrayBuffer();
console.log('Got "key changed" error from encrypt - no identityKey for application layer', number, deviceIds)
throw error;
} else {
this.registerError(number, "Failed to create or send message", error);
}
}.bind(this));
},
getStaleDeviceIdsForNumber: function(number) {
return textsecure.storage.protocol.getDeviceIds(number).then(function(deviceIds) {
if (deviceIds.length === 0) {
return [1];
}
var updateDevices = [];
return Promise.all(deviceIds.map(function(deviceId) {
var address = new libsignal.SignalProtocolAddress(number, deviceId);
var sessionCipher = new libsignal.SessionCipher(textsecure.storage.protocol, address);
return sessionCipher.hasOpenSession().then(function(hasSession) {
if (!hasSession) {
updateDevices.push(deviceId);
}
});
})).then(function() {
return updateDevices;
});
});
},
removeDeviceIdsForNumber: function(number, deviceIdsToRemove) {
var promise = Promise.resolve();
for (var j in deviceIdsToRemove) {
promise = promise.then(function() {
var encodedNumber = number + "." + deviceIdsToRemove[j];
return textsecure.storage.protocol.removeSession(encodedNumber);
});
}
return promise;
},
sendToNumber: function(number) {
return this.getStaleDeviceIdsForNumber(number).then(function(updateDevices) {
return this.getKeysForNumber(number, updateDevices)
.then(this.reloadDevicesAndSend(number, true))
.catch(function(error) {
if (error.message === "Identity key changed") {
error = new textsecure.OutgoingIdentityKeyError(
number, error.originalMessage, error.timestamp, error.identityKey
);
this.registerError(number, "Identity key changed", error);
} else {
this.registerError(
number, "Failed to retrieve new device keys for number " + number, error
);
}
}.bind(this));
}.bind(this));
} }
},
registerError: function(number, reason, error) {
if (!error || (error.name === 'HTTPError' && error.code !== 404)) {
error = new textsecure.OutgoingMessageError(
number,
this.message.toArrayBuffer(),
this.timestamp,
error
);
}
error.number = number;
error.reason = reason;
this.errors[this.errors.length] = error;
this.numberCompleted();
},
reloadDevicesAndSend: function(number, recurse) {
return function() {
return textsecure.storage.protocol.getDeviceIds(number).then(
function(deviceIds) {
if (deviceIds.length == 0) {
return this.registerError(
number,
'Got empty device list when loading device keys',
null
);
}
return this.doSendMessage(number, deviceIds, recurse);
}.bind(this)
);
}.bind(this);
},
getKeysForNumber: function(number, updateDevices) {
var handleResult = function(response) {
return Promise.all(
response.devices.map(
function(device) {
device.identityKey = response.identityKey;
if (
updateDevices === undefined ||
updateDevices.indexOf(device.deviceId) > -1
) {
var address = new libsignal.SignalProtocolAddress(
number,
device.deviceId
);
var builder = new libsignal.SessionBuilder(
textsecure.storage.protocol,
address
);
if (device.registrationId === 0) {
console.log('device registrationId 0!');
}
return builder.processPreKey(device).catch(
function(error) {
if (error.message === 'Identity key changed') {
error.timestamp = this.timestamp;
error.originalMessage = this.message.toArrayBuffer();
error.identityKey = device.identityKey;
}
throw error;
}.bind(this)
);
}
}.bind(this)
)
);
}.bind(this);
if (updateDevices === undefined) {
return this.server.getKeysForNumber(number).then(handleResult);
} else {
var promise = Promise.resolve();
updateDevices.forEach(
function(device) {
promise = promise.then(
function() {
return this.server
.getKeysForNumber(number, device)
.then(handleResult)
.catch(
function(e) {
if (e.name === 'HTTPError' && e.code === 404) {
if (device !== 1) {
return this.removeDeviceIdsForNumber(number, [device]);
} else {
throw new textsecure.UnregisteredUserError(number, e);
}
} else {
throw e;
}
}.bind(this)
);
}.bind(this)
);
}.bind(this)
);
return promise;
}
},
transmitMessage: function(number, jsonData, timestamp) {
return this.server
.sendMessages(number, jsonData, timestamp, this.silent)
.catch(function(e) {
if (e.name === 'HTTPError' && (e.code !== 409 && e.code !== 410)) {
// 409 and 410 should bubble and be handled by doSendMessage
// 404 should throw UnregisteredUserError
// all other network errors can be retried later.
if (e.code === 404) {
throw new textsecure.UnregisteredUserError(number, e);
}
throw new textsecure.SendMessageNetworkError(
number,
jsonData,
e,
timestamp
);
}
throw e;
});
},
getPaddedMessageLength: function(messageLength) {
var messageLengthWithTerminator = messageLength + 1;
var messagePartCount = Math.floor(messageLengthWithTerminator / 160);
if (messageLengthWithTerminator % 160 !== 0) {
messagePartCount++;
}
return messagePartCount * 160;
},
getPlaintext: function() {
if (!this.plaintext) {
var messageBuffer = this.message.toArrayBuffer();
this.plaintext = new Uint8Array(
this.getPaddedMessageLength(messageBuffer.byteLength + 1) - 1
);
this.plaintext.set(new Uint8Array(messageBuffer));
this.plaintext[messageBuffer.byteLength] = 0x80;
}
return this.plaintext;
},
doSendMessage: function(number, deviceIds, recurse) {
var ciphers = {};
var plaintext = this.getPlaintext();
return Promise.all(
deviceIds.map(
function(deviceId) {
var address = new libsignal.SignalProtocolAddress(number, deviceId);
var ourNumber = textsecure.storage.user.getNumber();
var options = {};
// No limit on message keys if we're communicating with our other devices
if (ourNumber === number) {
options.messageKeysLimit = false;
}
var sessionCipher = new libsignal.SessionCipher(
textsecure.storage.protocol,
address,
options
);
ciphers[address.getDeviceId()] = sessionCipher;
return sessionCipher.encrypt(plaintext).then(function(ciphertext) {
return {
type: ciphertext.type,
destinationDeviceId: address.getDeviceId(),
destinationRegistrationId: ciphertext.registrationId,
content: btoa(ciphertext.body),
};
});
}.bind(this)
)
)
.then(
function(jsonData) {
return this.transmitMessage(number, jsonData, this.timestamp).then(
function() {
this.successfulNumbers[this.successfulNumbers.length] = number;
this.numberCompleted();
}.bind(this)
);
}.bind(this)
)
.catch(
function(error) {
if (
error instanceof Error &&
error.name == 'HTTPError' &&
(error.code == 410 || error.code == 409)
) {
if (!recurse)
return this.registerError(
number,
'Hit retry limit attempting to reload device list',
error
);
var p;
if (error.code == 409) {
p = this.removeDeviceIdsForNumber(
number,
error.response.extraDevices
);
} else {
p = Promise.all(
error.response.staleDevices.map(function(deviceId) {
return ciphers[deviceId].closeOpenSessionForDevice();
})
);
}
return p.then(
function() {
var resetDevices =
error.code == 410
? error.response.staleDevices
: error.response.missingDevices;
return this.getKeysForNumber(number, resetDevices).then(
this.reloadDevicesAndSend(number, error.code == 409)
);
}.bind(this)
);
} else if (error.message === 'Identity key changed') {
error.timestamp = this.timestamp;
error.originalMessage = this.message.toArrayBuffer();
console.log(
'Got "key changed" error from encrypt - no identityKey for application layer',
number,
deviceIds
);
throw error;
} else {
this.registerError(
number,
'Failed to create or send message',
error
);
}
}.bind(this)
);
},
getStaleDeviceIdsForNumber: function(number) {
return textsecure.storage.protocol
.getDeviceIds(number)
.then(function(deviceIds) {
if (deviceIds.length === 0) {
return [1];
}
var updateDevices = [];
return Promise.all(
deviceIds.map(function(deviceId) {
var address = new libsignal.SignalProtocolAddress(number, deviceId);
var sessionCipher = new libsignal.SessionCipher(
textsecure.storage.protocol,
address
);
return sessionCipher.hasOpenSession().then(function(hasSession) {
if (!hasSession) {
updateDevices.push(deviceId);
}
});
})
).then(function() {
return updateDevices;
});
});
},
removeDeviceIdsForNumber: function(number, deviceIdsToRemove) {
var promise = Promise.resolve();
for (var j in deviceIdsToRemove) {
promise = promise.then(function() {
var encodedNumber = number + '.' + deviceIdsToRemove[j];
return textsecure.storage.protocol.removeSession(encodedNumber);
});
}
return promise;
},
sendToNumber: function(number) {
return this.getStaleDeviceIdsForNumber(number).then(
function(updateDevices) {
return this.getKeysForNumber(number, updateDevices)
.then(this.reloadDevicesAndSend(number, true))
.catch(
function(error) {
if (error.message === 'Identity key changed') {
error = new textsecure.OutgoingIdentityKeyError(
number,
error.originalMessage,
error.timestamp,
error.identityKey
);
this.registerError(number, 'Identity key changed', error);
} else {
this.registerError(
number,
'Failed to retrieve new device keys for number ' + number,
error
);
}
}.bind(this)
);
}.bind(this)
);
},
}; };

View file

@ -1,29 +1,42 @@
;(function() { (function() {
'use strict'; 'use strict';
window.textsecure = window.textsecure || {}; window.textsecure = window.textsecure || {};
window.textsecure.protobuf = {}; window.textsecure.protobuf = {};
function loadProtoBufs(filename) { function loadProtoBufs(filename) {
return dcodeIO.ProtoBuf.loadProtoFile({root: window.PROTO_ROOT, file: filename}, function(error, result) { return dcodeIO.ProtoBuf.loadProtoFile(
if (error) { { root: window.PROTO_ROOT, file: filename },
var text = 'Error loading protos from ' + filename + ' (root: ' + window.PROTO_ROOT + ') ' function(error, result) {
+ (error && error.stack ? error.stack : error); if (error) {
console.log(text); var text =
throw error; 'Error loading protos from ' +
} filename +
var protos = result.build('signalservice'); ' (root: ' +
if (!protos) { window.PROTO_ROOT +
var text = 'Error loading protos from ' + filename + ' (root: ' + window.PROTO_ROOT + ')'; ') ' +
console.log(text); (error && error.stack ? error.stack : error);
throw new Error(text); console.log(text);
} throw error;
for (var protoName in protos) { }
textsecure.protobuf[protoName] = protos[protoName]; var protos = result.build('signalservice');
} if (!protos) {
}); var text =
}; 'Error loading protos from ' +
filename +
' (root: ' +
window.PROTO_ROOT +
')';
console.log(text);
throw new Error(text);
}
for (var protoName in protos) {
textsecure.protobuf[protoName] = protos[protoName];
}
}
);
}
loadProtoBufs('SignalService.proto'); loadProtoBufs('SignalService.proto');
loadProtoBufs('SubProtocol.proto'); loadProtoBufs('SubProtocol.proto');
loadProtoBufs('DeviceMessages.proto'); loadProtoBufs('DeviceMessages.proto');
})(); })();

View file

@ -1,11 +1,11 @@
;(function() { (function() {
'use strict'; 'use strict';
window.textsecure = window.textsecure || {}; window.textsecure = window.textsecure || {};
window.textsecure.storage = window.textsecure.storage || {}; window.textsecure.storage = window.textsecure.storage || {};
textsecure.storage.protocol = new SignalProtocolStore(); textsecure.storage.protocol = new SignalProtocolStore();
textsecure.ProvisioningCipher = libsignal.ProvisioningCipher; textsecure.ProvisioningCipher = libsignal.ProvisioningCipher;
textsecure.startWorker = libsignal.worker.startWorker; textsecure.startWorker = libsignal.worker.startWorker;
textsecure.stopWorker = libsignal.worker.stopWorker; textsecure.stopWorker = libsignal.worker.stopWorker;
})(); })();

File diff suppressed because it is too large Load diff

View file

@ -1,46 +1,42 @@
'use strict'; 'use strict';
;(function() { (function() {
/************************************************
*** Utilities to store data in local storage ***
************************************************/
window.textsecure = window.textsecure || {};
window.textsecure.storage = window.textsecure.storage || {};
/************************************************ // Overrideable storage implementation
*** Utilities to store data in local storage *** window.textsecure.storage.impl = window.textsecure.storage.impl || {
************************************************/ /*****************************
window.textsecure = window.textsecure || {}; *** Base Storage Routines ***
window.textsecure.storage = window.textsecure.storage || {}; *****************************/
put: function(key, value) {
if (value === undefined) throw new Error('Tried to store undefined');
localStorage.setItem('' + key, textsecure.utils.jsonThing(value));
},
// Overrideable storage implementation get: function(key, defaultValue) {
window.textsecure.storage.impl = window.textsecure.storage.impl || { var value = localStorage.getItem('' + key);
/***************************** if (value === null) return defaultValue;
*** Base Storage Routines *** return JSON.parse(value);
*****************************/ },
put: function(key, value) {
if (value === undefined)
throw new Error("Tried to store undefined");
localStorage.setItem("" + key, textsecure.utils.jsonThing(value));
},
get: function(key, defaultValue) { remove: function(key) {
var value = localStorage.getItem("" + key); localStorage.removeItem('' + key);
if (value === null) },
return defaultValue; };
return JSON.parse(value);
},
remove: function(key) { window.textsecure.storage.put = function(key, value) {
localStorage.removeItem("" + key); return textsecure.storage.impl.put(key, value);
}, };
};
window.textsecure.storage.put = function(key, value) { window.textsecure.storage.get = function(key, defaultValue) {
return textsecure.storage.impl.put(key, value); return textsecure.storage.impl.get(key, defaultValue);
}; };
window.textsecure.storage.get = function(key, defaultValue) { window.textsecure.storage.remove = function(key) {
return textsecure.storage.impl.get(key, defaultValue); return textsecure.storage.impl.remove(key);
}; };
window.textsecure.storage.remove = function(key) {
return textsecure.storage.impl.remove(key);
};
})(); })();

View file

@ -1,144 +1,164 @@
;(function() { (function() {
'use strict'; 'use strict';
/********************* /*********************
*** Group Storage *** *** Group Storage ***
*********************/ *********************/
window.textsecure = window.textsecure || {}; window.textsecure = window.textsecure || {};
window.textsecure.storage = window.textsecure.storage || {}; window.textsecure.storage = window.textsecure.storage || {};
// create a random group id that we haven't seen before. // create a random group id that we haven't seen before.
function generateNewGroupId() { function generateNewGroupId() {
var groupId = getString(libsignal.crypto.getRandomBytes(16)); var groupId = getString(libsignal.crypto.getRandomBytes(16));
return textsecure.storage.protocol.getGroup(groupId).then(function(group) { return textsecure.storage.protocol.getGroup(groupId).then(function(group) {
if (group === undefined) { if (group === undefined) {
return groupId; return groupId;
} else { } else {
console.warn('group id collision'); // probably a bad sign. console.warn('group id collision'); // probably a bad sign.
return generateNewGroupId(); return generateNewGroupId();
} }
}); });
} }
window.textsecure.storage.groups = { window.textsecure.storage.groups = {
createNewGroup: function(numbers, groupId) { createNewGroup: function(numbers, groupId) {
var groupId = groupId; var groupId = groupId;
return new Promise(function(resolve) { return new Promise(function(resolve) {
if (groupId !== undefined) { if (groupId !== undefined) {
resolve(textsecure.storage.protocol.getGroup(groupId).then(function(group) { resolve(
if (group !== undefined) { textsecure.storage.protocol.getGroup(groupId).then(function(group) {
throw new Error("Tried to recreate group"); if (group !== undefined) {
} throw new Error('Tried to recreate group');
})); }
} else { })
resolve(generateNewGroupId().then(function(newGroupId) { );
groupId = newGroupId; } else {
})); resolve(
} generateNewGroupId().then(function(newGroupId) {
}).then(function() { groupId = newGroupId;
var me = textsecure.storage.user.getNumber(); })
var haveMe = false; );
var finalNumbers = [];
for (var i in numbers) {
var number = numbers[i];
if (!textsecure.utils.isNumberSane(number))
throw new Error("Invalid number in group");
if (number == me)
haveMe = true;
if (finalNumbers.indexOf(number) < 0)
finalNumbers.push(number);
}
if (!haveMe)
finalNumbers.push(me);
var groupObject = {numbers: finalNumbers, numberRegistrationIds: {}};
for (var i in finalNumbers)
groupObject.numberRegistrationIds[finalNumbers[i]] = {};
return textsecure.storage.protocol.putGroup(groupId, groupObject).then(function() {
return {id: groupId, numbers: finalNumbers};
});
});
},
getNumbers: function(groupId) {
return textsecure.storage.protocol.getGroup(groupId).then(function(group) {
if (group === undefined)
return undefined;
return group.numbers;
});
},
removeNumber: function(groupId, number) {
return textsecure.storage.protocol.getGroup(groupId).then(function(group) {
if (group === undefined)
return undefined;
var me = textsecure.storage.user.getNumber();
if (number == me)
throw new Error("Cannot remove ourselves from a group, leave the group instead");
var i = group.numbers.indexOf(number);
if (i > -1) {
group.numbers.splice(i, 1);
delete group.numberRegistrationIds[number];
return textsecure.storage.protocol.putGroup(groupId, group).then(function() {
return group.numbers;
});
}
return group.numbers;
});
},
addNumbers: function(groupId, numbers) {
return textsecure.storage.protocol.getGroup(groupId).then(function(group) {
if (group === undefined)
return undefined;
for (var i in numbers) {
var number = numbers[i];
if (!textsecure.utils.isNumberSane(number))
throw new Error("Invalid number in set to add to group");
if (group.numbers.indexOf(number) < 0) {
group.numbers.push(number);
group.numberRegistrationIds[number] = {};
}
}
return textsecure.storage.protocol.putGroup(groupId, group).then(function() {
return group.numbers;
});
});
},
deleteGroup: function(groupId) {
return textsecure.storage.protocol.removeGroup(groupId);
},
getGroup: function(groupId) {
return textsecure.storage.protocol.getGroup(groupId).then(function(group) {
if (group === undefined)
return undefined;
return { id: groupId, numbers: group.numbers };
});
},
updateNumbers: function(groupId, numbers) {
return textsecure.storage.protocol.getGroup(groupId).then(function(group) {
if (group === undefined)
throw new Error("Tried to update numbers for unknown group");
if (numbers.filter(textsecure.utils.isNumberSane).length < numbers.length)
throw new Error("Invalid number in new group members");
var added = numbers.filter(function(number) { return group.numbers.indexOf(number) < 0; });
return textsecure.storage.groups.addNumbers(groupId, added);
});
} }
}; }).then(function() {
var me = textsecure.storage.user.getNumber();
var haveMe = false;
var finalNumbers = [];
for (var i in numbers) {
var number = numbers[i];
if (!textsecure.utils.isNumberSane(number))
throw new Error('Invalid number in group');
if (number == me) haveMe = true;
if (finalNumbers.indexOf(number) < 0) finalNumbers.push(number);
}
if (!haveMe) finalNumbers.push(me);
var groupObject = { numbers: finalNumbers, numberRegistrationIds: {} };
for (var i in finalNumbers)
groupObject.numberRegistrationIds[finalNumbers[i]] = {};
return textsecure.storage.protocol
.putGroup(groupId, groupObject)
.then(function() {
return { id: groupId, numbers: finalNumbers };
});
});
},
getNumbers: function(groupId) {
return textsecure.storage.protocol
.getGroup(groupId)
.then(function(group) {
if (group === undefined) return undefined;
return group.numbers;
});
},
removeNumber: function(groupId, number) {
return textsecure.storage.protocol
.getGroup(groupId)
.then(function(group) {
if (group === undefined) return undefined;
var me = textsecure.storage.user.getNumber();
if (number == me)
throw new Error(
'Cannot remove ourselves from a group, leave the group instead'
);
var i = group.numbers.indexOf(number);
if (i > -1) {
group.numbers.splice(i, 1);
delete group.numberRegistrationIds[number];
return textsecure.storage.protocol
.putGroup(groupId, group)
.then(function() {
return group.numbers;
});
}
return group.numbers;
});
},
addNumbers: function(groupId, numbers) {
return textsecure.storage.protocol
.getGroup(groupId)
.then(function(group) {
if (group === undefined) return undefined;
for (var i in numbers) {
var number = numbers[i];
if (!textsecure.utils.isNumberSane(number))
throw new Error('Invalid number in set to add to group');
if (group.numbers.indexOf(number) < 0) {
group.numbers.push(number);
group.numberRegistrationIds[number] = {};
}
}
return textsecure.storage.protocol
.putGroup(groupId, group)
.then(function() {
return group.numbers;
});
});
},
deleteGroup: function(groupId) {
return textsecure.storage.protocol.removeGroup(groupId);
},
getGroup: function(groupId) {
return textsecure.storage.protocol
.getGroup(groupId)
.then(function(group) {
if (group === undefined) return undefined;
return { id: groupId, numbers: group.numbers };
});
},
updateNumbers: function(groupId, numbers) {
return textsecure.storage.protocol
.getGroup(groupId)
.then(function(group) {
if (group === undefined)
throw new Error('Tried to update numbers for unknown group');
if (
numbers.filter(textsecure.utils.isNumberSane).length <
numbers.length
)
throw new Error('Invalid number in new group members');
var added = numbers.filter(function(number) {
return group.numbers.indexOf(number) < 0;
});
return textsecure.storage.groups.addNumbers(groupId, added);
});
},
};
})(); })();

View file

@ -1,24 +1,24 @@
;(function() { (function() {
'use strict'; 'use strict';
/***************************************** /*****************************************
*** Not-yet-processed message storage *** *** Not-yet-processed message storage ***
*****************************************/ *****************************************/
window.textsecure = window.textsecure || {}; window.textsecure = window.textsecure || {};
window.textsecure.storage = window.textsecure.storage || {}; window.textsecure.storage = window.textsecure.storage || {};
window.textsecure.storage.unprocessed = { window.textsecure.storage.unprocessed = {
getAll: function() { getAll: function() {
return textsecure.storage.protocol.getAllUnprocessed(); return textsecure.storage.protocol.getAllUnprocessed();
}, },
add: function(data) { add: function(data) {
return textsecure.storage.protocol.addUnprocessed(data); return textsecure.storage.protocol.addUnprocessed(data);
}, },
update: function(id, updates) { update: function(id, updates) {
return textsecure.storage.protocol.updateUnprocessed(id, updates); return textsecure.storage.protocol.updateUnprocessed(id, updates);
}, },
remove: function(id) { remove: function(id) {
return textsecure.storage.protocol.removeUnprocessed(id); return textsecure.storage.protocol.removeUnprocessed(id);
}, },
}; };
})(); })();

View file

@ -1,36 +1,34 @@
'use strict'; 'use strict';
;(function() { (function() {
/********************************************* /*********************************************
*** Utilities to store data about the user *** *** Utilities to store data about the user ***
**********************************************/ **********************************************/
window.textsecure = window.textsecure || {}; window.textsecure = window.textsecure || {};
window.textsecure.storage = window.textsecure.storage || {}; window.textsecure.storage = window.textsecure.storage || {};
window.textsecure.storage.user = { window.textsecure.storage.user = {
setNumberAndDeviceId: function(number, deviceId, deviceName) { setNumberAndDeviceId: function(number, deviceId, deviceName) {
textsecure.storage.put("number_id", number + "." + deviceId); textsecure.storage.put('number_id', number + '.' + deviceId);
if (deviceName) { if (deviceName) {
textsecure.storage.put("device_name", deviceName); textsecure.storage.put('device_name', deviceName);
} }
}, },
getNumber: function(key, defaultValue) { getNumber: function(key, defaultValue) {
var number_id = textsecure.storage.get("number_id"); var number_id = textsecure.storage.get('number_id');
if (number_id === undefined) if (number_id === undefined) return undefined;
return undefined; return textsecure.utils.unencodeNumber(number_id)[0];
return textsecure.utils.unencodeNumber(number_id)[0]; },
},
getDeviceId: function(key) { getDeviceId: function(key) {
var number_id = textsecure.storage.get("number_id"); var number_id = textsecure.storage.get('number_id');
if (number_id === undefined) if (number_id === undefined) return undefined;
return undefined; return textsecure.utils.unencodeNumber(number_id)[1];
return textsecure.utils.unencodeNumber(number_id)[1]; },
},
getDeviceName: function(key) { getDeviceName: function(key) {
return textsecure.storage.get("device_name"); return textsecure.storage.get('device_name');
} },
}; };
})(); })();

View file

@ -1,82 +1,93 @@
;(function() { (function() {
"use strict"; 'use strict';
window.StringView = { window.StringView = {
/*
/*
* These functions from the Mozilla Developer Network * These functions from the Mozilla Developer Network
* and have been placed in the public domain. * and have been placed in the public domain.
* https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding * https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding
* https://developer.mozilla.org/en-US/docs/MDN/About#Copyrights_and_licenses * https://developer.mozilla.org/en-US/docs/MDN/About#Copyrights_and_licenses
*/ */
b64ToUint6: function(nChr) { b64ToUint6: function(nChr) {
return nChr > 64 && nChr < 91 ? return nChr > 64 && nChr < 91
nChr - 65 ? nChr - 65
: nChr > 96 && nChr < 123 ? : nChr > 96 && nChr < 123
nChr - 71 ? nChr - 71
: nChr > 47 && nChr < 58 ? : nChr > 47 && nChr < 58
nChr + 4 ? nChr + 4
: nChr === 43 ? : nChr === 43
62 ? 62
: nChr === 47 ? : nChr === 47
63 ? 63
: : 0;
0; },
},
base64ToBytes: function(sBase64, nBlocksSize) { base64ToBytes: function(sBase64, nBlocksSize) {
var var sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ''),
sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ""), nInLen = sB64Enc.length, nInLen = sB64Enc.length,
nOutLen = nBlocksSize ? Math.ceil((nInLen * 3 + 1 >> 2) / nBlocksSize) * nBlocksSize : nInLen * 3 + 1 >> 2; nOutLen = nBlocksSize
var aBBytes = new ArrayBuffer(nOutLen); ? Math.ceil(((nInLen * 3 + 1) >> 2) / nBlocksSize) * nBlocksSize
var taBytes = new Uint8Array(aBBytes); : (nInLen * 3 + 1) >> 2;
var aBBytes = new ArrayBuffer(nOutLen);
var taBytes = new Uint8Array(aBBytes);
for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) { for (
nMod4 = nInIdx & 3; var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0;
nUint24 |= StringView.b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4; nInIdx < nInLen;
if (nMod4 === 3 || nInLen - nInIdx === 1) { nInIdx++
for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) { ) {
taBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255; nMod4 = nInIdx & 3;
} nUint24 |=
nUint24 = 0; StringView.b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << (18 - 6 * nMod4);
if (nMod4 === 3 || nInLen - nInIdx === 1) {
for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) {
taBytes[nOutIdx] = (nUint24 >>> ((16 >>> nMod3) & 24)) & 255;
} }
nUint24 = 0;
} }
return aBBytes;
},
uint6ToB64: function(nUint6) {
return nUint6 < 26 ?
nUint6 + 65
: nUint6 < 52 ?
nUint6 + 71
: nUint6 < 62 ?
nUint6 - 4
: nUint6 === 62 ?
43
: nUint6 === 63 ?
47
:
65;
},
bytesToBase64: function(aBytes) {
var nMod3, sB64Enc = "";
for (var nLen = aBytes.length, nUint24 = 0, nIdx = 0; nIdx < nLen; nIdx++) {
nMod3 = nIdx % 3;
if (nIdx > 0 && (nIdx * 4 / 3) % 76 === 0) { sB64Enc += "\r\n"; }
nUint24 |= aBytes[nIdx] << (16 >>> nMod3 & 24);
if (nMod3 === 2 || aBytes.length - nIdx === 1) {
sB64Enc += String.fromCharCode(
StringView.uint6ToB64(nUint24 >>> 18 & 63),
StringView.uint6ToB64(nUint24 >>> 12 & 63),
StringView.uint6ToB64(nUint24 >>> 6 & 63),
StringView.uint6ToB64(nUint24 & 63)
);
nUint24 = 0;
}
}
return sB64Enc.replace(/A(?=A$|$)/g, "=");
} }
}; return aBBytes;
}()); },
uint6ToB64: function(nUint6) {
return nUint6 < 26
? nUint6 + 65
: nUint6 < 52
? nUint6 + 71
: nUint6 < 62
? nUint6 - 4
: nUint6 === 62
? 43
: nUint6 === 63
? 47
: 65;
},
bytesToBase64: function(aBytes) {
var nMod3,
sB64Enc = '';
for (
var nLen = aBytes.length, nUint24 = 0, nIdx = 0;
nIdx < nLen;
nIdx++
) {
nMod3 = nIdx % 3;
if (nIdx > 0 && (nIdx * 4 / 3) % 76 === 0) {
sB64Enc += '\r\n';
}
nUint24 |= aBytes[nIdx] << ((16 >>> nMod3) & 24);
if (nMod3 === 2 || aBytes.length - nIdx === 1) {
sB64Enc += String.fromCharCode(
StringView.uint6ToB64((nUint24 >>> 18) & 63),
StringView.uint6ToB64((nUint24 >>> 12) & 63),
StringView.uint6ToB64((nUint24 >>> 6) & 63),
StringView.uint6ToB64(nUint24 & 63)
);
nUint24 = 0;
}
}
return sB64Enc.replace(/A(?=A$|$)/g, '=');
},
};
})();

View file

@ -1,74 +1,82 @@
;(function () { (function() {
'use strict'; 'use strict';
window.textsecure = window.textsecure || {}; window.textsecure = window.textsecure || {};
function SyncRequest(sender, receiver) { function SyncRequest(sender, receiver) {
if (!(sender instanceof textsecure.MessageSender) || !(receiver instanceof textsecure.MessageReceiver)) { if (
throw new Error('Tried to construct a SyncRequest without MessageSender and MessageReceiver'); !(sender instanceof textsecure.MessageSender) ||
} !(receiver instanceof textsecure.MessageReceiver)
this.receiver = receiver; ) {
throw new Error(
this.oncontact = this.onContactSyncComplete.bind(this); 'Tried to construct a SyncRequest without MessageSender and MessageReceiver'
receiver.addEventListener('contactsync', this.oncontact); );
this.ongroup = this.onGroupSyncComplete.bind(this);
receiver.addEventListener('groupsync', this.ongroup);
console.log('SyncRequest created. Sending contact sync message...');
sender.sendRequestContactSyncMessage().then(function() {
console.log('SyncRequest now sending group sync messsage...');
return sender.sendRequestGroupSyncMessage();
}).catch(function(error) {
console.log(
'SyncRequest error:',
error && error.stack ? error.stack : error
);
});
this.timeout = setTimeout(this.onTimeout.bind(this), 60000);
} }
this.receiver = receiver;
SyncRequest.prototype = new textsecure.EventTarget(); this.oncontact = this.onContactSyncComplete.bind(this);
SyncRequest.prototype.extend({ receiver.addEventListener('contactsync', this.oncontact);
constructor: SyncRequest,
onContactSyncComplete: function() {
this.contactSync = true;
this.update();
},
onGroupSyncComplete: function() {
this.groupSync = true;
this.update();
},
update: function() {
if (this.contactSync && this.groupSync) {
this.dispatchEvent(new Event('success'));
this.cleanup();
}
},
onTimeout: function() {
if (this.contactSync || this.groupSync) {
this.dispatchEvent(new Event('success'));
} else {
this.dispatchEvent(new Event('timeout'));
}
this.cleanup();
},
cleanup: function() {
clearTimeout(this.timeout);
this.receiver.removeEventListener('contactsync', this.oncontact);
this.receiver.removeEventListener('groupSync', this.ongroup);
delete this.listeners;
}
});
textsecure.SyncRequest = function(sender, receiver) { this.ongroup = this.onGroupSyncComplete.bind(this);
var syncRequest = new SyncRequest(sender, receiver); receiver.addEventListener('groupsync', this.ongroup);
this.addEventListener = syncRequest.addEventListener.bind(syncRequest);
this.removeEventListener = syncRequest.removeEventListener.bind(syncRequest);
};
textsecure.SyncRequest.prototype = { console.log('SyncRequest created. Sending contact sync message...');
constructor: textsecure.SyncRequest sender
}; .sendRequestContactSyncMessage()
.then(function() {
console.log('SyncRequest now sending group sync messsage...');
return sender.sendRequestGroupSyncMessage();
})
.catch(function(error) {
console.log(
'SyncRequest error:',
error && error.stack ? error.stack : error
);
});
this.timeout = setTimeout(this.onTimeout.bind(this), 60000);
}
SyncRequest.prototype = new textsecure.EventTarget();
SyncRequest.prototype.extend({
constructor: SyncRequest,
onContactSyncComplete: function() {
this.contactSync = true;
this.update();
},
onGroupSyncComplete: function() {
this.groupSync = true;
this.update();
},
update: function() {
if (this.contactSync && this.groupSync) {
this.dispatchEvent(new Event('success'));
this.cleanup();
}
},
onTimeout: function() {
if (this.contactSync || this.groupSync) {
this.dispatchEvent(new Event('success'));
} else {
this.dispatchEvent(new Event('timeout'));
}
this.cleanup();
},
cleanup: function() {
clearTimeout(this.timeout);
this.receiver.removeEventListener('contactsync', this.oncontact);
this.receiver.removeEventListener('groupSync', this.ongroup);
delete this.listeners;
},
});
}()); textsecure.SyncRequest = function(sender, receiver) {
var syncRequest = new SyncRequest(sender, receiver);
this.addEventListener = syncRequest.addEventListener.bind(syncRequest);
this.removeEventListener = syncRequest.removeEventListener.bind(
syncRequest
);
};
textsecure.SyncRequest.prototype = {
constructor: textsecure.SyncRequest,
};
})();

View file

@ -1,68 +1,70 @@
(function () { (function() {
window.textsecure = window.textsecure || {}; window.textsecure = window.textsecure || {};
window.textsecure.createTaskWithTimeout = function(task, id, options) { window.textsecure.createTaskWithTimeout = function(task, id, options) {
options = options || {}; options = options || {};
options.timeout = options.timeout || (1000 * 60 * 2); // two minutes options.timeout = options.timeout || 1000 * 60 * 2; // two minutes
var errorForStack = new Error('for stack'); var errorForStack = new Error('for stack');
return function() { return function() {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
var complete = false; var complete = false;
var timer = setTimeout(function() { var timer = setTimeout(
if (!complete) { function() {
var message = if (!complete) {
(id || '') var message =
+ ' task did not complete in time. Calling stack: ' (id || '') +
+ errorForStack.stack; ' task did not complete in time. Calling stack: ' +
errorForStack.stack;
console.log(message); console.log(message);
return reject(new Error(message)); return reject(new Error(message));
} }
}.bind(this), options.timeout); }.bind(this),
var clearTimer = function() { options.timeout
try { );
var localTimer = timer; var clearTimer = function() {
if (localTimer) { try {
timer = null; var localTimer = timer;
clearTimeout(localTimer); if (localTimer) {
} timer = null;
} clearTimeout(localTimer);
catch (error) { }
console.log( } catch (error) {
id || '', console.log(
'task ran into problem canceling timer. Calling stack:', id || '',
errorForStack.stack 'task ran into problem canceling timer. Calling stack:',
); errorForStack.stack
} );
}; }
var success = function(result) {
clearTimer();
complete = true;
return resolve(result);
};
var failure = function(error) {
clearTimer();
complete = true;
return reject(error);
};
var promise;
try {
promise = task();
} catch(error) {
clearTimer();
throw error;
}
if (!promise || !promise.then) {
clearTimer();
complete = true;
return resolve(promise);
}
return promise.then(success, failure);
});
}; };
var success = function(result) {
clearTimer();
complete = true;
return resolve(result);
};
var failure = function(error) {
clearTimer();
complete = true;
return reject(error);
};
var promise;
try {
promise = task();
} catch (error) {
clearTimer();
throw error;
}
if (!promise || !promise.then) {
clearTimer();
complete = true;
return resolve(promise);
}
return promise.then(success, failure);
});
}; };
};
})(); })();

View file

@ -1,4 +1,4 @@
mocha.setup("bdd"); mocha.setup('bdd');
window.assert = chai.assert; window.assert = chai.assert;
window.PROTO_ROOT = '../../protos'; window.PROTO_ROOT = '../../protos';
@ -27,7 +27,7 @@ window.PROTO_ROOT = '../../protos';
result: false, result: false,
message: err.message, message: err.message,
stack: err.stack, stack: err.stack,
titles: flattenTitles(test) titles: flattenTitles(test),
}); });
}); });
@ -37,21 +37,21 @@ window.PROTO_ROOT = '../../protos';
SauceReporter.prototype = OriginalReporter.prototype; SauceReporter.prototype = OriginalReporter.prototype;
mocha.reporter(SauceReporter); mocha.reporter(SauceReporter);
}()); })();
/* /*
* global helpers for tests * global helpers for tests
*/ */
function assertEqualArrayBuffers(ab1, ab2) { function assertEqualArrayBuffers(ab1, ab2) {
assert.deepEqual(new Uint8Array(ab1), new Uint8Array(ab2)); assert.deepEqual(new Uint8Array(ab1), new Uint8Array(ab2));
}; }
function hexToArrayBuffer(str) { function hexToArrayBuffer(str) {
var ret = new ArrayBuffer(str.length / 2); var ret = new ArrayBuffer(str.length / 2);
var array = new Uint8Array(ret); var array = new Uint8Array(ret);
for (var i = 0; i < str.length/2; i++) for (var i = 0; i < str.length / 2; i++)
array[i] = parseInt(str.substr(i*2, 2), 16); array[i] = parseInt(str.substr(i * 2, 2), 16);
return ret; return ret;
}; }
window.MockSocket.prototype.addEventListener = function() {}; window.MockSocket.prototype.addEventListener = function() {};

View file

@ -1,6 +1,6 @@
'use strict'; 'use strict';
describe("AccountManager", function() { describe('AccountManager', function() {
let accountManager; let accountManager;
let originalServer; let originalServer;
@ -35,19 +35,23 @@ describe("AccountManager", function() {
it('keeps three confirmed keys even if over a week old', function() { it('keeps three confirmed keys even if over a week old', function() {
const now = Date.now(); const now = Date.now();
signedPreKeys = [{ signedPreKeys = [
keyId: 1, {
created_at: now - DAY * 21, keyId: 1,
confirmed: true, created_at: now - DAY * 21,
}, { confirmed: true,
keyId: 2, },
created_at: now - DAY * 14, {
confirmed: true, keyId: 2,
}, { created_at: now - DAY * 14,
keyId: 3, confirmed: true,
created_at: now - DAY * 18, },
confirmed: true, {
}]; keyId: 3,
created_at: now - DAY * 18,
confirmed: true,
},
];
// should be no calls to store.removeSignedPreKey, would cause crash // should be no calls to store.removeSignedPreKey, would cause crash
return accountManager.cleanSignedPreKeys(); return accountManager.cleanSignedPreKeys();
@ -55,27 +59,33 @@ describe("AccountManager", function() {
it('eliminates confirmed keys over a week old, if more than three', function() { it('eliminates confirmed keys over a week old, if more than three', function() {
const now = Date.now(); const now = Date.now();
signedPreKeys = [{ signedPreKeys = [
keyId: 1, {
created_at: now - DAY * 21, keyId: 1,
confirmed: true, created_at: now - DAY * 21,
}, { confirmed: true,
keyId: 2, },
created_at: now - DAY * 14, {
confirmed: true, keyId: 2,
}, { created_at: now - DAY * 14,
keyId: 3, confirmed: true,
created_at: now - DAY * 4, },
confirmed: true, {
}, { keyId: 3,
keyId: 4, created_at: now - DAY * 4,
created_at: now - DAY * 18, confirmed: true,
confirmed: true, },
}, { {
keyId: 5, keyId: 4,
created_at: now - DAY, created_at: now - DAY * 18,
confirmed: true, confirmed: true,
}]; },
{
keyId: 5,
created_at: now - DAY,
confirmed: true,
},
];
let count = 0; let count = 0;
window.textsecure.storage.protocol.removeSignedPreKey = function(keyId) { window.textsecure.storage.protocol.removeSignedPreKey = function(keyId) {
@ -93,19 +103,24 @@ describe("AccountManager", function() {
it('keeps at least three unconfirmed keys if no confirmed', function() { it('keeps at least three unconfirmed keys if no confirmed', function() {
const now = Date.now(); const now = Date.now();
signedPreKeys = [{ signedPreKeys = [
keyId: 1, {
created_at: now - DAY * 14, keyId: 1,
}, { created_at: now - DAY * 14,
keyId: 2, },
created_at: now - DAY * 21, {
}, { keyId: 2,
keyId: 3, created_at: now - DAY * 21,
created_at: now - DAY * 18, },
}, { {
keyId: 4, keyId: 3,
created_at: now - DAY created_at: now - DAY * 18,
}]; },
{
keyId: 4,
created_at: now - DAY,
},
];
let count = 0; let count = 0;
window.textsecure.storage.protocol.removeSignedPreKey = function(keyId) { window.textsecure.storage.protocol.removeSignedPreKey = function(keyId) {
@ -123,21 +138,26 @@ describe("AccountManager", function() {
it('if some confirmed keys, keeps unconfirmed to addd up to three total', function() { it('if some confirmed keys, keeps unconfirmed to addd up to three total', function() {
const now = Date.now(); const now = Date.now();
signedPreKeys = [{ signedPreKeys = [
keyId: 1, {
created_at: now - DAY * 21, keyId: 1,
confirmed: true, created_at: now - DAY * 21,
}, { confirmed: true,
keyId: 2, },
created_at: now - DAY * 14, {
confirmed: true, keyId: 2,
}, { created_at: now - DAY * 14,
keyId: 3, confirmed: true,
created_at: now - DAY * 12, },
}, { {
keyId: 4, keyId: 3,
created_at: now - DAY * 8, created_at: now - DAY * 12,
}]; },
{
keyId: 4,
created_at: now - DAY * 8,
},
];
let count = 0; let count = 0;
window.textsecure.storage.protocol.removeSignedPreKey = function(keyId) { window.textsecure.storage.protocol.removeSignedPreKey = function(keyId) {

View file

@ -1,19 +1,19 @@
'use strict'; 'use strict';
describe("ContactBuffer", function() { describe('ContactBuffer', function() {
function getTestBuffer() { function getTestBuffer() {
var buffer = new dcodeIO.ByteBuffer(); var buffer = new dcodeIO.ByteBuffer();
var avatarBuffer = new dcodeIO.ByteBuffer(); var avatarBuffer = new dcodeIO.ByteBuffer();
var avatarLen = 255; var avatarLen = 255;
for (var i=0; i < avatarLen; ++i) { for (var i = 0; i < avatarLen; ++i) {
avatarBuffer.writeUint8(i); avatarBuffer.writeUint8(i);
} }
avatarBuffer.limit = avatarBuffer.offset; avatarBuffer.limit = avatarBuffer.offset;
avatarBuffer.offset = 0; avatarBuffer.offset = 0;
var contactInfo = new textsecure.protobuf.ContactDetails({ var contactInfo = new textsecure.protobuf.ContactDetails({
name: "Zero Cool", name: 'Zero Cool',
number: "+10000000000", number: '+10000000000',
avatar: { contentType: "image/jpeg", length: avatarLen } avatar: { contentType: 'image/jpeg', length: avatarLen },
}); });
var contactInfoBuffer = contactInfo.encode().toArrayBuffer(); var contactInfoBuffer = contactInfo.encode().toArrayBuffer();
@ -28,21 +28,21 @@ describe("ContactBuffer", function() {
return buffer.toArrayBuffer(); return buffer.toArrayBuffer();
} }
it("parses an array buffer of contacts", function() { it('parses an array buffer of contacts', function() {
var arrayBuffer = getTestBuffer(); var arrayBuffer = getTestBuffer();
var contactBuffer = new ContactBuffer(arrayBuffer); var contactBuffer = new ContactBuffer(arrayBuffer);
var contact = contactBuffer.next(); var contact = contactBuffer.next();
var count = 0; var count = 0;
while (contact !== undefined) { while (contact !== undefined) {
count++; count++;
assert.strictEqual(contact.name, "Zero Cool"); assert.strictEqual(contact.name, 'Zero Cool');
assert.strictEqual(contact.number, "+10000000000"); assert.strictEqual(contact.number, '+10000000000');
assert.strictEqual(contact.avatar.contentType, "image/jpeg"); assert.strictEqual(contact.avatar.contentType, 'image/jpeg');
assert.strictEqual(contact.avatar.length, 255); assert.strictEqual(contact.avatar.length, 255);
assert.strictEqual(contact.avatar.data.byteLength, 255); assert.strictEqual(contact.avatar.data.byteLength, 255);
var avatarBytes = new Uint8Array(contact.avatar.data); var avatarBytes = new Uint8Array(contact.avatar.data);
for (var j=0; j < 255; ++j) { for (var j = 0; j < 255; ++j) {
assert.strictEqual(avatarBytes[j],j); assert.strictEqual(avatarBytes[j], j);
} }
contact = contactBuffer.next(); contact = contactBuffer.next();
} }
@ -50,21 +50,21 @@ describe("ContactBuffer", function() {
}); });
}); });
describe("GroupBuffer", function() { describe('GroupBuffer', function() {
function getTestBuffer() { function getTestBuffer() {
var buffer = new dcodeIO.ByteBuffer(); var buffer = new dcodeIO.ByteBuffer();
var avatarBuffer = new dcodeIO.ByteBuffer(); var avatarBuffer = new dcodeIO.ByteBuffer();
var avatarLen = 255; var avatarLen = 255;
for (var i=0; i < avatarLen; ++i) { for (var i = 0; i < avatarLen; ++i) {
avatarBuffer.writeUint8(i); avatarBuffer.writeUint8(i);
} }
avatarBuffer.limit = avatarBuffer.offset; avatarBuffer.limit = avatarBuffer.offset;
avatarBuffer.offset = 0; avatarBuffer.offset = 0;
var groupInfo = new textsecure.protobuf.GroupDetails({ var groupInfo = new textsecure.protobuf.GroupDetails({
id: new Uint8Array([1, 3, 3, 7]).buffer, id: new Uint8Array([1, 3, 3, 7]).buffer,
name: "Hackers", name: 'Hackers',
members: ['cereal', 'burn', 'phreak', 'joey'], members: ['cereal', 'burn', 'phreak', 'joey'],
avatar: { contentType: "image/jpeg", length: avatarLen } avatar: { contentType: 'image/jpeg', length: avatarLen },
}); });
var groupInfoBuffer = groupInfo.encode().toArrayBuffer(); var groupInfoBuffer = groupInfo.encode().toArrayBuffer();
@ -79,22 +79,25 @@ describe("GroupBuffer", function() {
return buffer.toArrayBuffer(); return buffer.toArrayBuffer();
} }
it("parses an array buffer of groups", function() { it('parses an array buffer of groups', function() {
var arrayBuffer = getTestBuffer(); var arrayBuffer = getTestBuffer();
var groupBuffer = new GroupBuffer(arrayBuffer); var groupBuffer = new GroupBuffer(arrayBuffer);
var group = groupBuffer.next(); var group = groupBuffer.next();
var count = 0; var count = 0;
while (group !== undefined) { while (group !== undefined) {
count++; count++;
assert.strictEqual(group.name, "Hackers"); assert.strictEqual(group.name, 'Hackers');
assertEqualArrayBuffers(group.id.toArrayBuffer(), new Uint8Array([1,3,3,7]).buffer); assertEqualArrayBuffers(
group.id.toArrayBuffer(),
new Uint8Array([1, 3, 3, 7]).buffer
);
assert.sameMembers(group.members, ['cereal', 'burn', 'phreak', 'joey']); assert.sameMembers(group.members, ['cereal', 'burn', 'phreak', 'joey']);
assert.strictEqual(group.avatar.contentType, "image/jpeg"); assert.strictEqual(group.avatar.contentType, 'image/jpeg');
assert.strictEqual(group.avatar.length, 255); assert.strictEqual(group.avatar.length, 255);
assert.strictEqual(group.avatar.data.byteLength, 255); assert.strictEqual(group.avatar.data.byteLength, 255);
var avatarBytes = new Uint8Array(group.avatar.data); var avatarBytes = new Uint8Array(group.avatar.data);
for (var j=0; j < 255; ++j) { for (var j = 0; j < 255; ++j) {
assert.strictEqual(avatarBytes[j],j); assert.strictEqual(avatarBytes[j], j);
} }
group = groupBuffer.next(); group = groupBuffer.next();
} }

View file

@ -6,24 +6,38 @@ describe('encrypting and decrypting profile data', function() {
var buffer = dcodeIO.ByteBuffer.wrap(name).toArrayBuffer(); var buffer = dcodeIO.ByteBuffer.wrap(name).toArrayBuffer();
var key = libsignal.crypto.getRandomBytes(32); var key = libsignal.crypto.getRandomBytes(32);
return textsecure.crypto.encryptProfileName(buffer, key).then(function(encrypted) { return textsecure.crypto
assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12); .encryptProfileName(buffer, key)
return textsecure.crypto.decryptProfileName(encrypted, key).then(function(decrypted) { .then(function(encrypted) {
assert.strictEqual(dcodeIO.ByteBuffer.wrap(decrypted).toString('utf8'), 'Alice'); assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12);
return textsecure.crypto
.decryptProfileName(encrypted, key)
.then(function(decrypted) {
assert.strictEqual(
dcodeIO.ByteBuffer.wrap(decrypted).toString('utf8'),
'Alice'
);
});
}); });
});
}); });
it('works for empty string', function() { it('works for empty string', function() {
var name = dcodeIO.ByteBuffer.wrap('').toArrayBuffer(); var name = dcodeIO.ByteBuffer.wrap('').toArrayBuffer();
var key = libsignal.crypto.getRandomBytes(32); var key = libsignal.crypto.getRandomBytes(32);
return textsecure.crypto.encryptProfileName(name.buffer, key).then(function(encrypted) { return textsecure.crypto
assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12); .encryptProfileName(name.buffer, key)
return textsecure.crypto.decryptProfileName(encrypted, key).then(function(decrypted) { .then(function(encrypted) {
assert.strictEqual(decrypted.byteLength, 0); assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12);
assert.strictEqual(dcodeIO.ByteBuffer.wrap(decrypted).toString('utf8'), ''); return textsecure.crypto
.decryptProfileName(encrypted, key)
.then(function(decrypted) {
assert.strictEqual(decrypted.byteLength, 0);
assert.strictEqual(
dcodeIO.ByteBuffer.wrap(decrypted).toString('utf8'),
''
);
});
}); });
});
}); });
}); });
describe('encrypting and decrypting profile avatars', function() { describe('encrypting and decrypting profile avatars', function() {
@ -31,24 +45,32 @@ describe('encrypting and decrypting profile data', function() {
var buffer = dcodeIO.ByteBuffer.wrap('This is an avatar').toArrayBuffer(); var buffer = dcodeIO.ByteBuffer.wrap('This is an avatar').toArrayBuffer();
var key = libsignal.crypto.getRandomBytes(32); var key = libsignal.crypto.getRandomBytes(32);
return textsecure.crypto.encryptProfile(buffer, key).then(function(encrypted) { return textsecure.crypto
assert(encrypted.byteLength === buffer.byteLength + 16 + 12); .encryptProfile(buffer, key)
return textsecure.crypto.decryptProfile(encrypted, key).then(function(decrypted) { .then(function(encrypted) {
assertEqualArrayBuffers(buffer, decrypted) assert(encrypted.byteLength === buffer.byteLength + 16 + 12);
return textsecure.crypto
.decryptProfile(encrypted, key)
.then(function(decrypted) {
assertEqualArrayBuffers(buffer, decrypted);
});
}); });
});
}); });
it('throws when decrypting with the wrong key', function() { it('throws when decrypting with the wrong key', function() {
var buffer = dcodeIO.ByteBuffer.wrap('This is an avatar').toArrayBuffer(); var buffer = dcodeIO.ByteBuffer.wrap('This is an avatar').toArrayBuffer();
var key = libsignal.crypto.getRandomBytes(32); var key = libsignal.crypto.getRandomBytes(32);
var bad_key = libsignal.crypto.getRandomBytes(32); var bad_key = libsignal.crypto.getRandomBytes(32);
return textsecure.crypto.encryptProfile(buffer, key).then(function(encrypted) { return textsecure.crypto
assert(encrypted.byteLength === buffer.byteLength + 16 + 12); .encryptProfile(buffer, key)
return textsecure.crypto.decryptProfile(encrypted, bad_key).catch(function(error) { .then(function(encrypted) {
assert.strictEqual(error.name, 'ProfileDecryptError'); assert(encrypted.byteLength === buffer.byteLength + 16 + 12);
return textsecure.crypto
.decryptProfile(encrypted, bad_key)
.catch(function(error) {
assert.strictEqual(error.name, 'ProfileDecryptError');
});
}); });
});
}); });
}); });
}); });

View file

@ -1,26 +1,29 @@
var getKeysForNumberMap = {}; var getKeysForNumberMap = {};
TextSecureServer.getKeysForNumber = function(number, deviceId) { TextSecureServer.getKeysForNumber = function(number, deviceId) {
var res = getKeysForNumberMap[number]; var res = getKeysForNumberMap[number];
if (res !== undefined) { if (res !== undefined) {
delete getKeysForNumberMap[number]; delete getKeysForNumberMap[number];
return Promise.resolve(res); return Promise.resolve(res);
} else } else throw new Error('getKeysForNumber of unknown/used number');
throw new Error("getKeysForNumber of unknown/used number");
}; };
var messagesSentMap = {}; var messagesSentMap = {};
TextSecureServer.sendMessages = function(destination, messageArray) { TextSecureServer.sendMessages = function(destination, messageArray) {
for (i in messageArray) { for (i in messageArray) {
var msg = messageArray[i]; var msg = messageArray[i];
if ((msg.type != 1 && msg.type != 3) || if (
msg.destinationDeviceId === undefined || (msg.type != 1 && msg.type != 3) ||
msg.destinationRegistrationId === undefined || msg.destinationDeviceId === undefined ||
msg.body === undefined || msg.destinationRegistrationId === undefined ||
msg.timestamp == undefined || msg.body === undefined ||
msg.relay !== undefined || msg.timestamp == undefined ||
msg.destination !== undefined) msg.relay !== undefined ||
throw new Error("Invalid message"); msg.destination !== undefined
)
throw new Error('Invalid message');
messagesSentMap[destination + "." + messageArray[i].destinationDeviceId] = msg; messagesSentMap[
} destination + '.' + messageArray[i].destinationDeviceId
] = msg;
}
}; };

View file

@ -1,163 +1,190 @@
'use strict'; 'use strict';
describe("Key generation", function() { describe('Key generation', function() {
var count = 10; var count = 10;
this.timeout(count*2000); this.timeout(count * 2000);
function validateStoredKeyPair(keyPair) { function validateStoredKeyPair(keyPair) {
/* Ensure the keypair matches the format used internally by libsignal-protocol */ /* Ensure the keypair matches the format used internally by libsignal-protocol */
assert.isObject(keyPair, 'Stored keyPair is not an object'); assert.isObject(keyPair, 'Stored keyPair is not an object');
assert.instanceOf(keyPair.pubKey, ArrayBuffer); assert.instanceOf(keyPair.pubKey, ArrayBuffer);
assert.instanceOf(keyPair.privKey, ArrayBuffer); assert.instanceOf(keyPair.privKey, ArrayBuffer);
assert.strictEqual(keyPair.pubKey.byteLength, 33); assert.strictEqual(keyPair.pubKey.byteLength, 33);
assert.strictEqual(new Uint8Array(keyPair.pubKey)[0], 5); assert.strictEqual(new Uint8Array(keyPair.pubKey)[0], 5);
assert.strictEqual(keyPair.privKey.byteLength, 32); assert.strictEqual(keyPair.privKey.byteLength, 32);
} }
function itStoresPreKey(keyId) { function itStoresPreKey(keyId) {
it('prekey ' + keyId + ' is valid', function(done) { it('prekey ' + keyId + ' is valid', function(done) {
return textsecure.storage.protocol.loadPreKey(keyId).then(function(keyPair) { return textsecure.storage.protocol
validateStoredKeyPair(keyPair); .loadPreKey(keyId)
}).then(done,done); .then(function(keyPair) {
}); validateStoredKeyPair(keyPair);
} })
function itStoresSignedPreKey(keyId) { .then(done, done);
it('signed prekey ' + keyId + ' is valid', function(done) {
return textsecure.storage.protocol.loadSignedPreKey(keyId).then(function(keyPair) {
validateStoredKeyPair(keyPair);
}).then(done,done);
});
}
function validateResultKey(resultKey) {
return textsecure.storage.protocol.loadPreKey(resultKey.keyId).then(function(keyPair) {
assertEqualArrayBuffers(resultKey.publicKey, keyPair.pubKey);
});
}
function validateResultSignedKey(resultSignedKey) {
return textsecure.storage.protocol.loadSignedPreKey(resultSignedKey.keyId).then(function(keyPair) {
assertEqualArrayBuffers(resultSignedKey.publicKey, keyPair.pubKey);
});
}
before(function(done) {
localStorage.clear();
libsignal.KeyHelper.generateIdentityKeyPair().then(function(keyPair) {
return textsecure.storage.protocol.put('identityKey', keyPair);
}).then(done, done);
}); });
}
function itStoresSignedPreKey(keyId) {
it('signed prekey ' + keyId + ' is valid', function(done) {
return textsecure.storage.protocol
.loadSignedPreKey(keyId)
.then(function(keyPair) {
validateStoredKeyPair(keyPair);
})
.then(done, done);
});
}
function validateResultKey(resultKey) {
return textsecure.storage.protocol
.loadPreKey(resultKey.keyId)
.then(function(keyPair) {
assertEqualArrayBuffers(resultKey.publicKey, keyPair.pubKey);
});
}
function validateResultSignedKey(resultSignedKey) {
return textsecure.storage.protocol
.loadSignedPreKey(resultSignedKey.keyId)
.then(function(keyPair) {
assertEqualArrayBuffers(resultSignedKey.publicKey, keyPair.pubKey);
});
}
describe('the first time', function() { before(function(done) {
var result; localStorage.clear();
/* result should have this format libsignal.KeyHelper.generateIdentityKeyPair()
.then(function(keyPair) {
return textsecure.storage.protocol.put('identityKey', keyPair);
})
.then(done, done);
});
describe('the first time', function() {
var result;
/* result should have this format
* { * {
* preKeys: [ { keyId, publicKey }, ... ], * preKeys: [ { keyId, publicKey }, ... ],
* signedPreKey: { keyId, publicKey, signature }, * signedPreKey: { keyId, publicKey, signature },
* identityKey: <ArrayBuffer> * identityKey: <ArrayBuffer>
* } * }
*/ */
before(function(done) { before(function(done) {
var accountManager = new textsecure.AccountManager(''); var accountManager = new textsecure.AccountManager('');
accountManager.generateKeys(count).then(function(res) { accountManager
result = res; .generateKeys(count)
}).then(done,done); .then(function(res) {
}); result = res;
for (var i = 1; i <= count; i++) { })
itStoresPreKey(i); .then(done, done);
} });
itStoresSignedPreKey(1); for (var i = 1; i <= count; i++) {
itStoresPreKey(i);
}
itStoresSignedPreKey(1);
it('result contains ' + count + ' preKeys', function() { it('result contains ' + count + ' preKeys', function() {
assert.isArray(result.preKeys); assert.isArray(result.preKeys);
assert.lengthOf(result.preKeys, count); assert.lengthOf(result.preKeys, count);
for (var i = 0; i < count; i++) { for (var i = 0; i < count; i++) {
assert.isObject(result.preKeys[i]); assert.isObject(result.preKeys[i]);
} }
});
it('result contains the correct keyIds', function() {
for (var i = 0; i < count; i++) {
assert.strictEqual(result.preKeys[i].keyId, i+1);
}
});
it('result contains the correct public keys', function(done) {
Promise.all(result.preKeys.map(validateResultKey)).then(function() {
done();
}).catch(done);
});
it('returns a signed prekey', function(done) {
assert.strictEqual(result.signedPreKey.keyId, 1);
assert.instanceOf(result.signedPreKey.signature, ArrayBuffer);
validateResultSignedKey(result.signedPreKey).then(done,done);
});
}); });
describe('the second time', function() { it('result contains the correct keyIds', function() {
var result; for (var i = 0; i < count; i++) {
before(function(done) { assert.strictEqual(result.preKeys[i].keyId, i + 1);
var accountManager = new textsecure.AccountManager(''); }
accountManager.generateKeys(count).then(function(res) {
result = res;
}).then(done,done);
});
for (var i = 1; i <= 2*count; i++) {
itStoresPreKey(i);
}
itStoresSignedPreKey(1);
itStoresSignedPreKey(2);
it('result contains ' + count + ' preKeys', function() {
assert.isArray(result.preKeys);
assert.lengthOf(result.preKeys, count);
for (var i = 0; i < count; i++) {
assert.isObject(result.preKeys[i]);
}
});
it('result contains the correct keyIds', function() {
for (var i = 1; i <= count; i++) {
assert.strictEqual(result.preKeys[i-1].keyId, i+count);
}
});
it('result contains the correct public keys', function(done) {
Promise.all(result.preKeys.map(validateResultKey)).then(function() {
done();
}).catch(done);
});
it('returns a signed prekey', function(done) {
assert.strictEqual(result.signedPreKey.keyId, 2);
assert.instanceOf(result.signedPreKey.signature, ArrayBuffer);
validateResultSignedKey(result.signedPreKey).then(done,done);
});
}); });
describe('the third time', function() { it('result contains the correct public keys', function(done) {
var result; Promise.all(result.preKeys.map(validateResultKey))
before(function(done) { .then(function() {
var accountManager = new textsecure.AccountManager(''); done();
accountManager.generateKeys(count).then(function(res) { })
result = res; .catch(done);
}).then(done,done);
});
for (var i = 1; i <= 3*count; i++) {
itStoresPreKey(i);
}
itStoresSignedPreKey(2);
itStoresSignedPreKey(3);
it('result contains ' + count + ' preKeys', function() {
assert.isArray(result.preKeys);
assert.lengthOf(result.preKeys, count);
for (var i = 0; i < count; i++) {
assert.isObject(result.preKeys[i]);
}
});
it('result contains the correct keyIds', function() {
for (var i = 1; i <= count; i++) {
assert.strictEqual(result.preKeys[i-1].keyId, i+2*count);
}
});
it('result contains the correct public keys', function(done) {
Promise.all(result.preKeys.map(validateResultKey)).then(function() {
done();
}).catch(done);
});
it('result contains a signed prekey', function(done) {
assert.strictEqual(result.signedPreKey.keyId, 3);
assert.instanceOf(result.signedPreKey.signature, ArrayBuffer);
validateResultSignedKey(result.signedPreKey).then(done,done);
});
}); });
it('returns a signed prekey', function(done) {
assert.strictEqual(result.signedPreKey.keyId, 1);
assert.instanceOf(result.signedPreKey.signature, ArrayBuffer);
validateResultSignedKey(result.signedPreKey).then(done, done);
});
});
describe('the second time', function() {
var result;
before(function(done) {
var accountManager = new textsecure.AccountManager('');
accountManager
.generateKeys(count)
.then(function(res) {
result = res;
})
.then(done, done);
});
for (var i = 1; i <= 2 * count; i++) {
itStoresPreKey(i);
}
itStoresSignedPreKey(1);
itStoresSignedPreKey(2);
it('result contains ' + count + ' preKeys', function() {
assert.isArray(result.preKeys);
assert.lengthOf(result.preKeys, count);
for (var i = 0; i < count; i++) {
assert.isObject(result.preKeys[i]);
}
});
it('result contains the correct keyIds', function() {
for (var i = 1; i <= count; i++) {
assert.strictEqual(result.preKeys[i - 1].keyId, i + count);
}
});
it('result contains the correct public keys', function(done) {
Promise.all(result.preKeys.map(validateResultKey))
.then(function() {
done();
})
.catch(done);
});
it('returns a signed prekey', function(done) {
assert.strictEqual(result.signedPreKey.keyId, 2);
assert.instanceOf(result.signedPreKey.signature, ArrayBuffer);
validateResultSignedKey(result.signedPreKey).then(done, done);
});
});
describe('the third time', function() {
var result;
before(function(done) {
var accountManager = new textsecure.AccountManager('');
accountManager
.generateKeys(count)
.then(function(res) {
result = res;
})
.then(done, done);
});
for (var i = 1; i <= 3 * count; i++) {
itStoresPreKey(i);
}
itStoresSignedPreKey(2);
itStoresSignedPreKey(3);
it('result contains ' + count + ' preKeys', function() {
assert.isArray(result.preKeys);
assert.lengthOf(result.preKeys, count);
for (var i = 0; i < count; i++) {
assert.isObject(result.preKeys[i]);
}
});
it('result contains the correct keyIds', function() {
for (var i = 1; i <= count; i++) {
assert.strictEqual(result.preKeys[i - 1].keyId, i + 2 * count);
}
});
it('result contains the correct public keys', function(done) {
Promise.all(result.preKeys.map(validateResultKey))
.then(function() {
done();
})
.catch(done);
});
it('result contains a signed prekey', function(done) {
assert.strictEqual(result.signedPreKey.keyId, 3);
assert.instanceOf(result.signedPreKey.signature, ArrayBuffer);
validateResultSignedKey(result.signedPreKey).then(done, done);
});
});
}); });

View file

@ -1,30 +1,32 @@
'use strict'; 'use strict';
describe("Helpers", function() { describe('Helpers', function() {
describe("ArrayBuffer->String conversion", function() { describe('ArrayBuffer->String conversion', function() {
it('works', function() { it('works', function() {
var b = new ArrayBuffer(3); var b = new ArrayBuffer(3);
var a = new Uint8Array(b); var a = new Uint8Array(b);
a[0] = 0; a[0] = 0;
a[1] = 255; a[1] = 255;
a[2] = 128; a[2] = 128;
assert.equal(getString(b), "\x00\xff\x80"); assert.equal(getString(b), '\x00\xff\x80');
}); });
}); });
describe("stringToArrayBuffer", function() { describe('stringToArrayBuffer', function() {
it('returns ArrayBuffer when passed string', function() { it('returns ArrayBuffer when passed string', function() {
var StaticArrayBufferProto = new ArrayBuffer().__proto__; var StaticArrayBufferProto = new ArrayBuffer().__proto__;
var anArrayBuffer = new ArrayBuffer(1); var anArrayBuffer = new ArrayBuffer(1);
var typedArray = new Uint8Array(anArrayBuffer); var typedArray = new Uint8Array(anArrayBuffer);
typedArray[0] = 'a'.charCodeAt(0); typedArray[0] = 'a'.charCodeAt(0);
assertEqualArrayBuffers(stringToArrayBuffer('a'), anArrayBuffer); assertEqualArrayBuffers(stringToArrayBuffer('a'), anArrayBuffer);
}); });
it('throws an error when passed a non string', function() { it('throws an error when passed a non string', function() {
var notStringable = [{}, undefined, null, new ArrayBuffer()]; var notStringable = [{}, undefined, null, new ArrayBuffer()];
notStringable.forEach(function(notString) { notStringable.forEach(function(notString) {
assert.throw(function() { stringToArrayBuffer(notString) }, Error); assert.throw(function() {
}); stringToArrayBuffer(notString);
}, Error);
}); });
});
}); });
}); });

View file

@ -1,145 +1,184 @@
function SignalProtocolStore() { function SignalProtocolStore() {
this.store = {}; this.store = {};
} }
SignalProtocolStore.prototype = { SignalProtocolStore.prototype = {
Direction: { SENDING: 1, RECEIVING: 2}, Direction: { SENDING: 1, RECEIVING: 2 },
getIdentityKeyPair: function() { getIdentityKeyPair: function() {
return Promise.resolve(this.get('identityKey')); return Promise.resolve(this.get('identityKey'));
}, },
getLocalRegistrationId: function() { getLocalRegistrationId: function() {
return Promise.resolve(this.get('registrationId')); return Promise.resolve(this.get('registrationId'));
}, },
put: function(key, value) { put: function(key, value) {
if (key === undefined || value === undefined || key === null || value === null) if (
throw new Error("Tried to store undefined/null"); key === undefined ||
this.store[key] = value; value === undefined ||
}, key === null ||
get: function(key, defaultValue) { value === null
if (key === null || key === undefined) )
throw new Error("Tried to get value for undefined/null key"); throw new Error('Tried to store undefined/null');
if (key in this.store) { this.store[key] = value;
return this.store[key]; },
} else { get: function(key, defaultValue) {
return defaultValue; if (key === null || key === undefined)
} throw new Error('Tried to get value for undefined/null key');
}, if (key in this.store) {
remove: function(key) { return this.store[key];
if (key === null || key === undefined) } else {
throw new Error("Tried to remove value for undefined/null key"); return defaultValue;
delete this.store[key]; }
}, },
remove: function(key) {
if (key === null || key === undefined)
throw new Error('Tried to remove value for undefined/null key');
delete this.store[key];
},
isTrustedIdentity: function(identifier, identityKey) { isTrustedIdentity: function(identifier, identityKey) {
if (identifier === null || identifier === undefined) { if (identifier === null || identifier === undefined) {
throw new error("tried to check identity key for undefined/null key"); throw new error('tried to check identity key for undefined/null key');
} }
if (!(identityKey instanceof ArrayBuffer)) { if (!(identityKey instanceof ArrayBuffer)) {
throw new error("Expected identityKey to be an ArrayBuffer"); throw new error('Expected identityKey to be an ArrayBuffer');
} }
var trusted = this.get('identityKey' + identifier); var trusted = this.get('identityKey' + identifier);
if (trusted === undefined) { if (trusted === undefined) {
return Promise.resolve(true); return Promise.resolve(true);
} }
return Promise.resolve(identityKey === trusted); return Promise.resolve(identityKey === trusted);
}, },
loadIdentityKey: function(identifier) { loadIdentityKey: function(identifier) {
if (identifier === null || identifier === undefined) if (identifier === null || identifier === undefined)
throw new Error("Tried to get identity key for undefined/null key"); throw new Error('Tried to get identity key for undefined/null key');
return new Promise(function(resolve) { return new Promise(
resolve(this.get('identityKey' + identifier)); function(resolve) {
}.bind(this)); resolve(this.get('identityKey' + identifier));
}, }.bind(this)
saveIdentity: function(identifier, identityKey) { );
if (identifier === null || identifier === undefined) },
throw new Error("Tried to put identity key for undefined/null key"); saveIdentity: function(identifier, identityKey) {
return new Promise(function(resolve) { if (identifier === null || identifier === undefined)
var existing = this.get('identityKey' + identifier); throw new Error('Tried to put identity key for undefined/null key');
this.put('identityKey' + identifier, identityKey); return new Promise(
if (existing && existing !== identityKey) { function(resolve) {
resolve(true); var existing = this.get('identityKey' + identifier);
} else { this.put('identityKey' + identifier, identityKey);
resolve(false); if (existing && existing !== identityKey) {
} resolve(true);
}.bind(this)); } else {
}, resolve(false);
/* Returns a prekeypair object or undefined */
loadPreKey: function(keyId) {
return new Promise(function(resolve) {
var res = this.get('25519KeypreKey' + keyId);
resolve(res);
}.bind(this));
},
storePreKey: function(keyId, keyPair) {
return new Promise(function(resolve) {
resolve(this.put('25519KeypreKey' + keyId, keyPair));
}.bind(this));
},
removePreKey: function(keyId) {
return new Promise(function(resolve) {
resolve(this.remove('25519KeypreKey' + keyId));
}.bind(this));
},
/* Returns a signed keypair object or undefined */
loadSignedPreKey: function(keyId) {
return new Promise(function(resolve) {
var res = this.get('25519KeysignedKey' + keyId);
resolve(res);
}.bind(this));
},
loadSignedPreKeys: function() {
return new Promise(function(resolve) {
var res = [];
for (var i in this.store) {
if (i.startsWith('25519KeysignedKey')) {
res.push(this.store[i]);
}
}
resolve(res);
}.bind(this));
},
storeSignedPreKey: function(keyId, keyPair) {
return new Promise(function(resolve) {
resolve(this.put('25519KeysignedKey' + keyId, keyPair));
}.bind(this));
},
removeSignedPreKey: function(keyId) {
return new Promise(function(resolve) {
resolve(this.remove('25519KeysignedKey' + keyId));
}.bind(this));
},
loadSession: function(identifier) {
return new Promise(function(resolve) {
resolve(this.get('session' + identifier));
}.bind(this));
},
storeSession: function(identifier, record) {
return new Promise(function(resolve) {
resolve(this.put('session' + identifier, record));
}.bind(this));
},
removeAllSessions: function(identifier) {
return new Promise(function(resolve) {
for (key in this.store) {
if (key.match(RegExp('^session' + identifier.replace('\+','\\\+') + '.+'))) {
delete this.store[key];
} }
} }.bind(this)
resolve(); );
}.bind(this)); },
/* Returns a prekeypair object or undefined */
loadPreKey: function(keyId) {
return new Promise(
function(resolve) {
var res = this.get('25519KeypreKey' + keyId);
resolve(res);
}.bind(this)
);
},
storePreKey: function(keyId, keyPair) {
return new Promise(
function(resolve) {
resolve(this.put('25519KeypreKey' + keyId, keyPair));
}.bind(this)
);
},
removePreKey: function(keyId) {
return new Promise(
function(resolve) {
resolve(this.remove('25519KeypreKey' + keyId));
}.bind(this)
);
},
/* Returns a signed keypair object or undefined */
loadSignedPreKey: function(keyId) {
return new Promise(
function(resolve) {
var res = this.get('25519KeysignedKey' + keyId);
resolve(res);
}.bind(this)
);
},
loadSignedPreKeys: function() {
return new Promise(
function(resolve) {
var res = [];
for (var i in this.store) {
if (i.startsWith('25519KeysignedKey')) {
res.push(this.store[i]);
}
}
resolve(res);
}.bind(this)
);
},
storeSignedPreKey: function(keyId, keyPair) {
return new Promise(
function(resolve) {
resolve(this.put('25519KeysignedKey' + keyId, keyPair));
}.bind(this)
);
},
removeSignedPreKey: function(keyId) {
return new Promise(
function(resolve) {
resolve(this.remove('25519KeysignedKey' + keyId));
}.bind(this)
);
},
loadSession: function(identifier) {
return new Promise(
function(resolve) {
resolve(this.get('session' + identifier));
}.bind(this)
);
},
storeSession: function(identifier, record) {
return new Promise(
function(resolve) {
resolve(this.put('session' + identifier, record));
}.bind(this)
);
},
removeAllSessions: function(identifier) {
return new Promise(
function(resolve) {
for (key in this.store) {
if (
key.match(
RegExp('^session' + identifier.replace('+', '\\+') + '.+')
)
) {
delete this.store[key];
}
}
resolve();
}.bind(this)
);
}, },
getDeviceIds: function(identifier) { getDeviceIds: function(identifier) {
return new Promise(function(resolve) { return new Promise(
var deviceIds = []; function(resolve) {
for (key in this.store) { var deviceIds = [];
if (key.match(RegExp('^session' + identifier.replace('\+','\\\+') + '.+'))) { for (key in this.store) {
deviceIds.push(parseInt(key.split('.')[1])); if (
key.match(
RegExp('^session' + identifier.replace('+', '\\+') + '.+')
)
) {
deviceIds.push(parseInt(key.split('.')[1]));
}
} }
} resolve(deviceIds);
resolve(deviceIds); }.bind(this)
}.bind(this)); );
} },
}; };

View file

@ -1,71 +1,99 @@
describe('MessageReceiver', function() { describe('MessageReceiver', function() {
textsecure.storage.impl = new SignalProtocolStore(); textsecure.storage.impl = new SignalProtocolStore();
var WebSocket = window.WebSocket; var WebSocket = window.WebSocket;
var number = '+19999999999'; var number = '+19999999999';
var deviceId = 1; var deviceId = 1;
var signalingKey = libsignal.crypto.getRandomBytes(32 + 20); var signalingKey = libsignal.crypto.getRandomBytes(32 + 20);
before(function() { before(function() {
window.WebSocket = MockSocket; window.WebSocket = MockSocket;
textsecure.storage.user.setNumberAndDeviceId(number, deviceId, 'name'); textsecure.storage.user.setNumberAndDeviceId(number, deviceId, 'name');
textsecure.storage.put("password", "password"); textsecure.storage.put('password', 'password');
textsecure.storage.put("signaling_key", signalingKey); textsecure.storage.put('signaling_key', signalingKey);
});
after(function() {
window.WebSocket = WebSocket;
});
describe('connecting', function() {
var blob = null;
var attrs = {
type: textsecure.protobuf.Envelope.Type.CIPHERTEXT,
source: number,
sourceDevice: deviceId,
timestamp: Date.now(),
};
var websocketmessage = new textsecure.protobuf.WebSocketMessage({
type: textsecure.protobuf.WebSocketMessage.Type.REQUEST,
request: { verb: 'PUT', path: '/messages' },
}); });
after (function() { window.WebSocket = WebSocket; });
describe('connecting', function() { before(function(done) {
var blob = null; var signal = new textsecure.protobuf.Envelope(attrs).toArrayBuffer();
var attrs = { var data = new textsecure.protobuf.DataMessage({ body: 'hello' });
type: textsecure.protobuf.Envelope.Type.CIPHERTEXT,
source: number,
sourceDevice: deviceId,
timestamp: Date.now(),
};
var websocketmessage = new textsecure.protobuf.WebSocketMessage({
type: textsecure.protobuf.WebSocketMessage.Type.REQUEST,
request: { verb: 'PUT', path: '/messages' }
});
before(function(done) { var signaling_key = signalingKey;
var signal = new textsecure.protobuf.Envelope(attrs).toArrayBuffer(); var aes_key = signaling_key.slice(0, 32);
var data = new textsecure.protobuf.DataMessage({ body: 'hello' }); var mac_key = signaling_key.slice(32, 32 + 20);
var signaling_key = signalingKey; window.crypto.subtle
var aes_key = signaling_key.slice(0, 32); .importKey('raw', aes_key, { name: 'AES-CBC' }, false, ['encrypt'])
var mac_key = signaling_key.slice(32, 32 + 20); .then(function(key) {
var iv = libsignal.crypto.getRandomBytes(16);
window.crypto.subtle.importKey('raw', aes_key, {name: 'AES-CBC'}, false, ['encrypt']).then(function(key) { window.crypto.subtle
var iv = libsignal.crypto.getRandomBytes(16); .encrypt({ name: 'AES-CBC', iv: new Uint8Array(iv) }, key, signal)
window.crypto.subtle.encrypt({name: 'AES-CBC', iv: new Uint8Array(iv)}, key, signal).then(function(ciphertext) { .then(function(ciphertext) {
window.crypto.subtle.importKey('raw', mac_key, {name: 'HMAC', hash: {name: 'SHA-256'}}, false, ['sign']).then(function(key) { window.crypto.subtle
window.crypto.subtle.sign( {name: 'HMAC', hash: 'SHA-256'}, key, signal).then(function(mac) { .importKey(
var version = new Uint8Array([1]); 'raw',
var message = dcodeIO.ByteBuffer.concat([version, iv, ciphertext, mac ]); mac_key,
websocketmessage.request.body = message.toArrayBuffer(); { name: 'HMAC', hash: { name: 'SHA-256' } },
console.log(new Uint8Array(message.toArrayBuffer())); false,
done(); ['sign']
}); )
.then(function(key) {
window.crypto.subtle
.sign({ name: 'HMAC', hash: 'SHA-256' }, key, signal)
.then(function(mac) {
var version = new Uint8Array([1]);
var message = dcodeIO.ByteBuffer.concat([
version,
iv,
ciphertext,
mac,
]);
websocketmessage.request.body = message.toArrayBuffer();
console.log(new Uint8Array(message.toArrayBuffer()));
done();
}); });
}); });
}); });
}); });
it('connects', function(done) {
var mockServer = new MockServer('ws://localhost:8080/v1/websocket/?login='+ encodeURIComponent(number) +'.1&password=password');
mockServer.on('connection', function(server) {
server.send(new Blob([ websocketmessage.toArrayBuffer() ]));
});
window.addEventListener('textsecure:message', function(ev) {
var signal = ev.proto;
for (var key in attrs) {
assert.strictEqual(attrs[key], signal[key]);
}
assert.strictEqual(signal.message.body, 'hello');
server.close();
done();
});
var messageReceiver = new textsecure.MessageReceiver('ws://localhost:8080', window);
});
}); });
it('connects', function(done) {
var mockServer = new MockServer(
'ws://localhost:8080/v1/websocket/?login=' +
encodeURIComponent(number) +
'.1&password=password'
);
mockServer.on('connection', function(server) {
server.send(new Blob([websocketmessage.toArrayBuffer()]));
});
window.addEventListener('textsecure:message', function(ev) {
var signal = ev.proto;
for (var key in attrs) {
assert.strictEqual(attrs[key], signal[key]);
}
assert.strictEqual(signal.message.body, 'hello');
server.close();
done();
});
var messageReceiver = new textsecure.MessageReceiver(
'ws://localhost:8080',
window
);
});
});
}); });

View file

@ -1,32 +1,38 @@
'use strict'; 'use strict';
describe('Protocol', function() { describe('Protocol', function() {
describe('Unencrypted PushMessageProto "decrypt"', function() {
//exclusive
it('works', function(done) {
localStorage.clear();
describe('Unencrypted PushMessageProto "decrypt"', function() { var text_message = new textsecure.protobuf.DataMessage();
//exclusive text_message.body = 'Hi Mom';
it('works', function(done) { var server_message = {
localStorage.clear(); type: 4, // unencrypted
source: '+19999999999',
timestamp: 42,
message: text_message.encode(),
};
var text_message = new textsecure.protobuf.DataMessage(); return textsecure.protocol_wrapper
text_message.body = "Hi Mom"; .handleEncryptedMessage(
var server_message = { server_message.source,
type: 4, // unencrypted server_message.source_device,
source: "+19999999999", server_message.type,
timestamp: 42, server_message.message
message: text_message.encode() )
}; .then(function(message) {
assert.equal(message.body, text_message.body);
return textsecure.protocol_wrapper.handleEncryptedMessage( assert.equal(
server_message.source, message.attachments.length,
server_message.source_device, text_message.attachments.length
server_message.type, );
server_message.message assert.equal(text_message.attachments.length, 0);
).then(function(message) { })
assert.equal(message.body, text_message.body); .then(done)
assert.equal(message.attachments.length, text_message.attachments.length); .catch(done);
assert.equal(text_message.attachments.length, 0);
}).then(done).catch(done);
});
}); });
});
// TODO: Use fake_api's hiding of api.sendMessage to test sendmessage.js' maze // TODO: Use fake_api's hiding of api.sendMessage to test sendmessage.js' maze
}); });

View file

@ -1,32 +1,40 @@
'use strict'; 'use strict';
describe('Protocol Wrapper', function() { describe('Protocol Wrapper', function() {
var store = textsecure.storage.protocol; var store = textsecure.storage.protocol;
var identifier = '+5558675309'; var identifier = '+5558675309';
var another_identifier = '+5555590210'; var another_identifier = '+5555590210';
var prekeys, identityKey, testKey; var prekeys, identityKey, testKey;
this.timeout(5000); this.timeout(5000);
before(function(done) { before(function(done) {
localStorage.clear(); localStorage.clear();
libsignal.KeyHelper.generateIdentityKeyPair().then(function(identityKey) { libsignal.KeyHelper.generateIdentityKeyPair()
return textsecure.storage.protocol.saveIdentity(identifier, identityKey); .then(function(identityKey) {
}).then(function() { return textsecure.storage.protocol.saveIdentity(
done(); identifier,
}); identityKey
}); );
describe('processPreKey', function() { })
it('rejects if the identity key changes', function(done) { .then(function() {
var address = new libsignal.SignalProtocolAddress(identifier, 1); done();
var builder = new libsignal.SessionBuilder(store, address); });
return builder.processPreKey({ });
identityKey: textsecure.crypto.getRandomBytes(33), describe('processPreKey', function() {
encodedNumber: address.toString() it('rejects if the identity key changes', function(done) {
}).then(function() { var address = new libsignal.SignalProtocolAddress(identifier, 1);
done(new Error('Allowed to overwrite identity key')); var builder = new libsignal.SessionBuilder(store, address);
}).catch(function(e) { return builder
assert.strictEqual(e.message, 'Identity key changed'); .processPreKey({
done(); identityKey: textsecure.crypto.getRandomBytes(33),
}); encodedNumber: address.toString(),
})
.then(function() {
done(new Error('Allowed to overwrite identity key'));
})
.catch(function(e) {
assert.strictEqual(e.message, 'Identity key changed');
done();
}); });
}); });
});
}); });

View file

@ -1,158 +1,200 @@
'use strict'; 'use strict';
describe("SignalProtocolStore", function() { describe('SignalProtocolStore', function() {
before(function() { localStorage.clear(); }); before(function() {
var store = textsecure.storage.protocol; localStorage.clear();
var identifier = '+5558675309'; });
var another_identifier = '+5555590210'; var store = textsecure.storage.protocol;
var identityKey = { var identifier = '+5558675309';
pubKey: libsignal.crypto.getRandomBytes(33), var another_identifier = '+5555590210';
privKey: libsignal.crypto.getRandomBytes(32), var identityKey = {
}; pubKey: libsignal.crypto.getRandomBytes(33),
var testKey = { privKey: libsignal.crypto.getRandomBytes(32),
pubKey: libsignal.crypto.getRandomBytes(33), };
privKey: libsignal.crypto.getRandomBytes(32), var testKey = {
}; pubKey: libsignal.crypto.getRandomBytes(33),
it('retrieves my registration id', function(done) { privKey: libsignal.crypto.getRandomBytes(32),
store.put('registrationId', 1337); };
store.getLocalRegistrationId().then(function(reg) { it('retrieves my registration id', function(done) {
assert.strictEqual(reg, 1337); store.put('registrationId', 1337);
}).then(done, done); store
}); .getLocalRegistrationId()
it('retrieves my identity key', function(done) { .then(function(reg) {
store.put('identityKey', identityKey); assert.strictEqual(reg, 1337);
store.getIdentityKeyPair().then(function(key) { })
assertEqualArrayBuffers(key.pubKey, identityKey.pubKey); .then(done, done);
assertEqualArrayBuffers(key.privKey, identityKey.privKey); });
}).then(done,done); it('retrieves my identity key', function(done) {
}); store.put('identityKey', identityKey);
it('stores identity keys', function(done) { store
store.saveIdentity(identifier, testKey.pubKey).then(function() { .getIdentityKeyPair()
return store.loadIdentityKey(identifier).then(function(key) { .then(function(key) {
assertEqualArrayBuffers(key, testKey.pubKey); assertEqualArrayBuffers(key.pubKey, identityKey.pubKey);
}); assertEqualArrayBuffers(key.privKey, identityKey.privKey);
}).then(done,done); })
}); .then(done, done);
it('returns whether a key is trusted', function(done) { });
var newIdentity = libsignal.crypto.getRandomBytes(33); it('stores identity keys', function(done) {
store.saveIdentity(identifier, testKey.pubKey).then(function() { store
store.isTrustedIdentity(identifier, newIdentity).then(function(trusted) { .saveIdentity(identifier, testKey.pubKey)
if (trusted) { .then(function() {
done(new Error('Allowed to overwrite identity key')); return store.loadIdentityKey(identifier).then(function(key) {
} else { assertEqualArrayBuffers(key, testKey.pubKey);
done();
}
}).catch(done);
}); });
})
.then(done, done);
});
it('returns whether a key is trusted', function(done) {
var newIdentity = libsignal.crypto.getRandomBytes(33);
store.saveIdentity(identifier, testKey.pubKey).then(function() {
store
.isTrustedIdentity(identifier, newIdentity)
.then(function(trusted) {
if (trusted) {
done(new Error('Allowed to overwrite identity key'));
} else {
done();
}
})
.catch(done);
}); });
it('returns whether a key is untrusted', function(done) { });
var newIdentity = libsignal.crypto.getRandomBytes(33); it('returns whether a key is untrusted', function(done) {
store.saveIdentity(identifier, testKey.pubKey).then(function() { var newIdentity = libsignal.crypto.getRandomBytes(33);
store.isTrustedIdentity(identifier, testKey.pubKey).then(function(trusted) { store.saveIdentity(identifier, testKey.pubKey).then(function() {
if (trusted) { store
done(); .isTrustedIdentity(identifier, testKey.pubKey)
} else { .then(function(trusted) {
done(new Error('Allowed to overwrite identity key')); if (trusted) {
} done();
}).catch(done); } else {
done(new Error('Allowed to overwrite identity key'));
}
})
.catch(done);
});
});
it('stores prekeys', function(done) {
store
.storePreKey(1, testKey)
.then(function() {
return store.loadPreKey(1).then(function(key) {
assertEqualArrayBuffers(key.pubKey, testKey.pubKey);
assertEqualArrayBuffers(key.privKey, testKey.privKey);
}); });
})
.then(done, done);
});
it('deletes prekeys', function(done) {
before(function(done) {
store.storePreKey(2, testKey).then(done);
}); });
it('stores prekeys', function(done) { store
store.storePreKey(1, testKey).then(function() { .removePreKey(2, testKey)
return store.loadPreKey(1).then(function(key) { .then(function() {
assertEqualArrayBuffers(key.pubKey, testKey.pubKey); return store.loadPreKey(2).then(function(key) {
assertEqualArrayBuffers(key.privKey, testKey.privKey); assert.isUndefined(key);
});
}).then(done,done);
});
it('deletes prekeys', function(done) {
before(function(done) {
store.storePreKey(2, testKey).then(done);
}); });
store.removePreKey(2, testKey).then(function() { })
return store.loadPreKey(2).then(function(key) { .then(done, done);
assert.isUndefined(key); });
}); it('stores signed prekeys', function(done) {
}).then(done,done); store
}); .storeSignedPreKey(3, testKey)
it('stores signed prekeys', function(done) { .then(function() {
store.storeSignedPreKey(3, testKey).then(function() { return store.loadSignedPreKey(3).then(function(key) {
return store.loadSignedPreKey(3).then(function(key) { assertEqualArrayBuffers(key.pubKey, testKey.pubKey);
assertEqualArrayBuffers(key.pubKey, testKey.pubKey); assertEqualArrayBuffers(key.privKey, testKey.privKey);
assertEqualArrayBuffers(key.privKey, testKey.privKey);
});
}).then(done,done);
});
it('deletes signed prekeys', function(done) {
before(function(done) {
store.storeSignedPreKey(4, testKey).then(done);
}); });
store.removeSignedPreKey(4, testKey).then(function() { })
return store.loadSignedPreKey(4).then(function(key) { .then(done, done);
assert.isUndefined(key); });
}); it('deletes signed prekeys', function(done) {
}).then(done,done); before(function(done) {
store.storeSignedPreKey(4, testKey).then(done);
}); });
it('stores sessions', function(done) { store
var testRecord = "an opaque string"; .removeSignedPreKey(4, testKey)
var devices = [1, 2, 3].map(function(deviceId) { .then(function() {
return [identifier, deviceId].join('.'); return store.loadSignedPreKey(4).then(function(key) {
assert.isUndefined(key);
}); });
var promise = Promise.resolve(); })
devices.forEach(function(encodedNumber) { .then(done, done);
promise = promise.then(function() { });
return store.storeSession(encodedNumber, testRecord + encodedNumber) it('stores sessions', function(done) {
}); var testRecord = 'an opaque string';
}); var devices = [1, 2, 3].map(function(deviceId) {
promise.then(function() { return [identifier, deviceId].join('.');
return Promise.all(devices.map(store.loadSession.bind(store))).then(function(records) {
for (var i in records) {
assert.strictEqual(records[i], testRecord + devices[i]);
};
});
}).then(done,done);
}); });
it('removes all sessions for a number', function(done) { var promise = Promise.resolve();
var testRecord = "an opaque string"; devices.forEach(function(encodedNumber) {
var devices = [1, 2, 3].map(function(deviceId) { promise = promise.then(function() {
return [identifier, deviceId].join('.'); return store.storeSession(encodedNumber, testRecord + encodedNumber);
}); });
var promise = Promise.resolve();
devices.forEach(function(encodedNumber) {
promise = promise.then(function() {
return store.storeSession(encodedNumber, testRecord + encodedNumber)
});
});
promise.then(function() {
return store.removeAllSessions(identifier).then(function(record) {
return Promise.all(devices.map(store.loadSession.bind(store))).then(function(records) {
for (var i in records) {
assert.isUndefined(records[i]);
};
});
});
}).then(done,done);
}); });
it('returns deviceIds for a number', function(done) { promise
var testRecord = "an opaque string"; .then(function() {
var devices = [1, 2, 3].map(function(deviceId) { return Promise.all(devices.map(store.loadSession.bind(store))).then(
return [identifier, deviceId].join('.'); function(records) {
}); for (var i in records) {
var promise = Promise.resolve(); assert.strictEqual(records[i], testRecord + devices[i]);
devices.forEach(function(encodedNumber) { }
promise = promise.then(function() { }
return store.storeSession(encodedNumber, testRecord + encodedNumber) );
}); })
}); .then(done, done);
promise.then(function() { });
return store.getDeviceIds(identifier).then(function(deviceIds) { it('removes all sessions for a number', function(done) {
assert.sameMembers(deviceIds, [1, 2, 3]); var testRecord = 'an opaque string';
}); var devices = [1, 2, 3].map(function(deviceId) {
}).then(done,done); return [identifier, deviceId].join('.');
}); });
it('returns empty array for a number with no device ids', function(done) { var promise = Promise.resolve();
return store.getDeviceIds('foo').then(function(deviceIds) { devices.forEach(function(encodedNumber) {
assert.sameMembers(deviceIds,[]); promise = promise.then(function() {
}).then(done,done); return store.storeSession(encodedNumber, testRecord + encodedNumber);
});
}); });
promise
.then(function() {
return store.removeAllSessions(identifier).then(function(record) {
return Promise.all(devices.map(store.loadSession.bind(store))).then(
function(records) {
for (var i in records) {
assert.isUndefined(records[i]);
}
}
);
});
})
.then(done, done);
});
it('returns deviceIds for a number', function(done) {
var testRecord = 'an opaque string';
var devices = [1, 2, 3].map(function(deviceId) {
return [identifier, deviceId].join('.');
});
var promise = Promise.resolve();
devices.forEach(function(encodedNumber) {
promise = promise.then(function() {
return store.storeSession(encodedNumber, testRecord + encodedNumber);
});
});
promise
.then(function() {
return store.getDeviceIds(identifier).then(function(deviceIds) {
assert.sameMembers(deviceIds, [1, 2, 3]);
});
})
.then(done, done);
});
it('returns empty array for a number with no device ids', function(done) {
return store
.getDeviceIds('foo')
.then(function(deviceIds) {
assert.sameMembers(deviceIds, []);
})
.then(done, done);
});
}); });

View file

@ -1,74 +1,80 @@
'use strict'; 'use strict';
describe('createTaskWithTimeout', function() { describe('createTaskWithTimeout', function() {
it('resolves when promise resolves', function() { it('resolves when promise resolves', function() {
var task = function() { var task = function() {
return Promise.resolve('hi!'); return Promise.resolve('hi!');
}; };
var taskWithTimeout = textsecure.createTaskWithTimeout(task); var taskWithTimeout = textsecure.createTaskWithTimeout(task);
return taskWithTimeout().then(function(result) { return taskWithTimeout().then(function(result) {
assert.strictEqual(result, 'hi!') assert.strictEqual(result, 'hi!');
});
}); });
it('flows error from promise back', function() { });
var error = new Error('original'); it('flows error from promise back', function() {
var task = function() { var error = new Error('original');
return Promise.reject(error); var task = function() {
}; return Promise.reject(error);
var taskWithTimeout = textsecure.createTaskWithTimeout(task); };
var taskWithTimeout = textsecure.createTaskWithTimeout(task);
return taskWithTimeout().catch(function(flowedError) { return taskWithTimeout().catch(function(flowedError) {
assert.strictEqual(error, flowedError); assert.strictEqual(error, flowedError);
}); });
});
it('rejects if promise takes too long (this one logs error to console)', function() {
var error = new Error('original');
var complete = false;
var task = function() {
return new Promise(function(resolve) {
setTimeout(function() {
complete = true;
resolve();
}, 3000);
});
};
var taskWithTimeout = textsecure.createTaskWithTimeout(task, this.name, {
timeout: 10,
}); });
it('rejects if promise takes too long (this one logs error to console)', function() {
var error = new Error('original');
var complete = false;
var task = function() {
return new Promise(function(resolve) {
setTimeout(function() {
complete = true;
resolve();
}, 3000);
});
};
var taskWithTimeout = textsecure.createTaskWithTimeout(task, this.name, {
timeout: 10
});
return taskWithTimeout().then(function() { return taskWithTimeout().then(
throw new Error('it was not supposed to resolve!'); function() {
}, function() { throw new Error('it was not supposed to resolve!');
assert.strictEqual(complete, false); },
}); function() {
assert.strictEqual(complete, false);
}
);
});
it('resolves if task returns something falsey', function() {
var task = function() {};
var taskWithTimeout = textsecure.createTaskWithTimeout(task);
return taskWithTimeout();
});
it('resolves if task returns a non-promise', function() {
var task = function() {
return 'hi!';
};
var taskWithTimeout = textsecure.createTaskWithTimeout(task);
return taskWithTimeout().then(function(result) {
assert.strictEqual(result, 'hi!');
}); });
it('resolves if task returns something falsey', function() { });
var task = function() {}; it('rejects if task throws (and does not log about taking too long)', function() {
var taskWithTimeout = textsecure.createTaskWithTimeout(task); var error = new Error('Task is throwing!');
return taskWithTimeout(); var task = function() {
}); throw error;
it('resolves if task returns a non-promise', function() { };
var task = function() { var taskWithTimeout = textsecure.createTaskWithTimeout(task, this.name, {
return 'hi!'; timeout: 10,
};
var taskWithTimeout = textsecure.createTaskWithTimeout(task);
return taskWithTimeout().then(function(result) {
assert.strictEqual(result, 'hi!')
});
});
it('rejects if task throws (and does not log about taking too long)', function() {
var error = new Error('Task is throwing!');
var task = function() {
throw error;
};
var taskWithTimeout = textsecure.createTaskWithTimeout(task, this.name, {
timeout: 10
});
return taskWithTimeout().then(function(result) {
throw new Error('Overall task should reject!')
}, function(flowedError) {
assert.strictEqual(flowedError, error);
});
}); });
return taskWithTimeout().then(
function(result) {
throw new Error('Overall task should reject!');
},
function(flowedError) {
assert.strictEqual(flowedError, error);
}
);
});
}); });

View file

@ -1,173 +1,210 @@
;(function() { (function() {
'use strict'; 'use strict';
describe('WebSocket-Resource', function() { describe('WebSocket-Resource', function() {
describe('requests and responses', function () { describe('requests and responses', function() {
it('receives requests and sends responses', function(done) { it('receives requests and sends responses', function(done) {
// mock socket // mock socket
var request_id = '1'; var request_id = '1';
var socket = { var socket = {
send: function(data) { send: function(data) {
var message = textsecure.protobuf.WebSocketMessage.decode(data); var message = textsecure.protobuf.WebSocketMessage.decode(data);
assert.strictEqual(message.type, textsecure.protobuf.WebSocketMessage.Type.RESPONSE); assert.strictEqual(
assert.strictEqual(message.response.message, 'OK'); message.type,
assert.strictEqual(message.response.status, 200); textsecure.protobuf.WebSocketMessage.Type.RESPONSE
assert.strictEqual(message.response.id.toString(), request_id); );
done(); assert.strictEqual(message.response.message, 'OK');
}, assert.strictEqual(message.response.status, 200);
addEventListener: function() {}, assert.strictEqual(message.response.id.toString(), request_id);
}; done();
},
addEventListener: function() {},
};
// actual test // actual test
var resource = new WebSocketResource(socket, { var resource = new WebSocketResource(socket, {
handleRequest: function (request) { handleRequest: function(request) {
assert.strictEqual(request.verb, 'PUT'); assert.strictEqual(request.verb, 'PUT');
assert.strictEqual(request.path, '/some/path'); assert.strictEqual(request.path, '/some/path');
assertEqualArrayBuffers(request.body.toArrayBuffer(), new Uint8Array([1,2,3]).buffer); assertEqualArrayBuffers(
request.respond(200, 'OK'); request.body.toArrayBuffer(),
} new Uint8Array([1, 2, 3]).buffer
}); );
request.respond(200, 'OK');
// mock socket request },
socket.onmessage({
data: new Blob([
new textsecure.protobuf.WebSocketMessage({
type: textsecure.protobuf.WebSocketMessage.Type.REQUEST,
request: {
id: request_id,
verb: 'PUT',
path: '/some/path',
body: new Uint8Array([1,2,3]).buffer
}
}).encode().toArrayBuffer()
])
});
});
it('sends requests and receives responses', function(done) {
// mock socket and request handler
var request_id;
var socket = {
send: function(data) {
var message = textsecure.protobuf.WebSocketMessage.decode(data);
assert.strictEqual(message.type, textsecure.protobuf.WebSocketMessage.Type.REQUEST);
assert.strictEqual(message.request.verb, 'PUT');
assert.strictEqual(message.request.path, '/some/path');
assertEqualArrayBuffers(message.request.body.toArrayBuffer(), new Uint8Array([1,2,3]).buffer);
request_id = message.request.id;
},
addEventListener: function() {},
};
// actual test
var resource = new WebSocketResource(socket);
resource.sendRequest({
verb: 'PUT',
path: '/some/path',
body: new Uint8Array([1,2,3]).buffer,
error: done,
success: function(message, status, request) {
assert.strictEqual(message, 'OK');
assert.strictEqual(status, 200);
done();
}
});
// mock socket response
socket.onmessage({
data: new Blob([
new textsecure.protobuf.WebSocketMessage({
type: textsecure.protobuf.WebSocketMessage.Type.RESPONSE,
response: { id: request_id, message: 'OK', status: 200 }
}).encode().toArrayBuffer()
])
});
});
}); });
describe('close', function() { // mock socket request
before(function() { window.WebSocket = MockSocket; }); socket.onmessage({
after (function() { window.WebSocket = WebSocket; }); data: new Blob([
it('closes the connection', function(done) { new textsecure.protobuf.WebSocketMessage({
var mockServer = new MockServer('ws://localhost:8081'); type: textsecure.protobuf.WebSocketMessage.Type.REQUEST,
mockServer.on('connection', function(server) { request: {
server.on('close', done); id: request_id,
}); verb: 'PUT',
var resource = new WebSocketResource(new WebSocket('ws://localhost:8081')); path: '/some/path',
resource.close(); body: new Uint8Array([1, 2, 3]).buffer,
}); },
})
.encode()
.toArrayBuffer(),
]),
});
});
it('sends requests and receives responses', function(done) {
// mock socket and request handler
var request_id;
var socket = {
send: function(data) {
var message = textsecure.protobuf.WebSocketMessage.decode(data);
assert.strictEqual(
message.type,
textsecure.protobuf.WebSocketMessage.Type.REQUEST
);
assert.strictEqual(message.request.verb, 'PUT');
assert.strictEqual(message.request.path, '/some/path');
assertEqualArrayBuffers(
message.request.body.toArrayBuffer(),
new Uint8Array([1, 2, 3]).buffer
);
request_id = message.request.id;
},
addEventListener: function() {},
};
// actual test
var resource = new WebSocketResource(socket);
resource.sendRequest({
verb: 'PUT',
path: '/some/path',
body: new Uint8Array([1, 2, 3]).buffer,
error: done,
success: function(message, status, request) {
assert.strictEqual(message, 'OK');
assert.strictEqual(status, 200);
done();
},
}); });
describe.skip('with a keepalive config', function() { // mock socket response
before(function() { window.WebSocket = MockSocket; }); socket.onmessage({
after (function() { window.WebSocket = WebSocket; }); data: new Blob([
this.timeout(60000); new textsecure.protobuf.WebSocketMessage({
it('sends keepalives once a minute', function(done) { type: textsecure.protobuf.WebSocketMessage.Type.RESPONSE,
var mockServer = new MockServer('ws://localhost:8081'); response: { id: request_id, message: 'OK', status: 200 },
mockServer.on('connection', function(server) { })
server.on('message', function(data) { .encode()
var message = textsecure.protobuf.WebSocketMessage.decode(data); .toArrayBuffer(),
assert.strictEqual(message.type, textsecure.protobuf.WebSocketMessage.Type.REQUEST); ]),
assert.strictEqual(message.request.verb, 'GET');
assert.strictEqual(message.request.path, '/v1/keepalive');
server.close();
done();
});
});
new WebSocketResource(new WebSocket('ws://localhost:8081'), {
keepalive: { path: '/v1/keepalive' }
});
});
it('uses / as a default path', function(done) {
var mockServer = new MockServer('ws://localhost:8081');
mockServer.on('connection', function(server) {
server.on('message', function(data) {
var message = textsecure.protobuf.WebSocketMessage.decode(data);
assert.strictEqual(message.type, textsecure.protobuf.WebSocketMessage.Type.REQUEST);
assert.strictEqual(message.request.verb, 'GET');
assert.strictEqual(message.request.path, '/');
server.close();
done();
});
});
new WebSocketResource(new WebSocket('ws://localhost:8081'), {
keepalive: true
});
});
it('optionally disconnects if no response', function(done) {
this.timeout(65000);
var mockServer = new MockServer('ws://localhost:8081');
var socket = new WebSocket('ws://localhost:8081');
mockServer.on('connection', function(server) {
server.on('close', done);
});
new WebSocketResource(socket, { keepalive: true });
});
it('allows resetting the keepalive timer', function(done) {
this.timeout(65000);
var mockServer = new MockServer('ws://localhost:8081');
var socket = new WebSocket('ws://localhost:8081');
var startTime = Date.now();
mockServer.on('connection', function(server) {
server.on('message', function(data) {
var message = textsecure.protobuf.WebSocketMessage.decode(data);
assert.strictEqual(message.type, textsecure.protobuf.WebSocketMessage.Type.REQUEST);
assert.strictEqual(message.request.verb, 'GET');
assert.strictEqual(message.request.path, '/');
assert(Date.now() > startTime + 60000, 'keepalive time should be longer than a minute');
server.close();
done();
});
});
var resource = new WebSocketResource(socket, { keepalive: true });
setTimeout(function() {
resource.resetKeepAliveTimer()
}, 5000);
});
}); });
});
}); });
}());
describe('close', function() {
before(function() {
window.WebSocket = MockSocket;
});
after(function() {
window.WebSocket = WebSocket;
});
it('closes the connection', function(done) {
var mockServer = new MockServer('ws://localhost:8081');
mockServer.on('connection', function(server) {
server.on('close', done);
});
var resource = new WebSocketResource(
new WebSocket('ws://localhost:8081')
);
resource.close();
});
});
describe.skip('with a keepalive config', function() {
before(function() {
window.WebSocket = MockSocket;
});
after(function() {
window.WebSocket = WebSocket;
});
this.timeout(60000);
it('sends keepalives once a minute', function(done) {
var mockServer = new MockServer('ws://localhost:8081');
mockServer.on('connection', function(server) {
server.on('message', function(data) {
var message = textsecure.protobuf.WebSocketMessage.decode(data);
assert.strictEqual(
message.type,
textsecure.protobuf.WebSocketMessage.Type.REQUEST
);
assert.strictEqual(message.request.verb, 'GET');
assert.strictEqual(message.request.path, '/v1/keepalive');
server.close();
done();
});
});
new WebSocketResource(new WebSocket('ws://localhost:8081'), {
keepalive: { path: '/v1/keepalive' },
});
});
it('uses / as a default path', function(done) {
var mockServer = new MockServer('ws://localhost:8081');
mockServer.on('connection', function(server) {
server.on('message', function(data) {
var message = textsecure.protobuf.WebSocketMessage.decode(data);
assert.strictEqual(
message.type,
textsecure.protobuf.WebSocketMessage.Type.REQUEST
);
assert.strictEqual(message.request.verb, 'GET');
assert.strictEqual(message.request.path, '/');
server.close();
done();
});
});
new WebSocketResource(new WebSocket('ws://localhost:8081'), {
keepalive: true,
});
});
it('optionally disconnects if no response', function(done) {
this.timeout(65000);
var mockServer = new MockServer('ws://localhost:8081');
var socket = new WebSocket('ws://localhost:8081');
mockServer.on('connection', function(server) {
server.on('close', done);
});
new WebSocketResource(socket, { keepalive: true });
});
it('allows resetting the keepalive timer', function(done) {
this.timeout(65000);
var mockServer = new MockServer('ws://localhost:8081');
var socket = new WebSocket('ws://localhost:8081');
var startTime = Date.now();
mockServer.on('connection', function(server) {
server.on('message', function(data) {
var message = textsecure.protobuf.WebSocketMessage.decode(data);
assert.strictEqual(
message.type,
textsecure.protobuf.WebSocketMessage.Type.REQUEST
);
assert.strictEqual(message.request.verb, 'GET');
assert.strictEqual(message.request.path, '/');
assert(
Date.now() > startTime + 60000,
'keepalive time should be longer than a minute'
);
server.close();
done();
});
});
var resource = new WebSocketResource(socket, { keepalive: true });
setTimeout(function() {
resource.resetKeepAliveTimer();
}, 5000);
});
});
});
})();

View file

@ -1,62 +1,64 @@
describe('TextSecureWebSocket', function() { describe('TextSecureWebSocket', function() {
var RealWebSocket = window.WebSocket; var RealWebSocket = window.WebSocket;
before(function() { window.WebSocket = MockSocket; }); before(function() {
after (function() { window.WebSocket = RealWebSocket; }); window.WebSocket = MockSocket;
it('connects and disconnects', function(done) { });
var mockServer = new MockServer('ws://localhost:8080'); after(function() {
mockServer.on('connection', function(server) { window.WebSocket = RealWebSocket;
socket.close(); });
server.close(); it('connects and disconnects', function(done) {
done(); var mockServer = new MockServer('ws://localhost:8080');
}); mockServer.on('connection', function(server) {
var socket = new TextSecureWebSocket('ws://localhost:8080'); socket.close();
server.close();
done();
}); });
var socket = new TextSecureWebSocket('ws://localhost:8080');
});
it('sends and receives', function(done) { it('sends and receives', function(done) {
var mockServer = new MockServer('ws://localhost:8080'); var mockServer = new MockServer('ws://localhost:8080');
mockServer.on('connection', function(server) { mockServer.on('connection', function(server) {
server.on('message', function(data) { server.on('message', function(data) {
server.send('ack'); server.send('ack');
server.close(); server.close();
}); });
});
var socket = new TextSecureWebSocket('ws://localhost:8080');
socket.onmessage = function(response) {
assert.strictEqual(response.data, 'ack');
socket.close();
done();
};
socket.send('syn');
}); });
var socket = new TextSecureWebSocket('ws://localhost:8080');
socket.onmessage = function(response) {
assert.strictEqual(response.data, 'ack');
socket.close();
done();
};
socket.send('syn');
});
it('exposes the socket status', function(done) { it('exposes the socket status', function(done) {
var mockServer = new MockServer('ws://localhost:8082'); var mockServer = new MockServer('ws://localhost:8082');
mockServer.on('connection', function(server) { mockServer.on('connection', function(server) {
assert.strictEqual(socket.getStatus(), WebSocket.OPEN); assert.strictEqual(socket.getStatus(), WebSocket.OPEN);
server.close(); server.close();
socket.close(); socket.close();
});
var socket = new TextSecureWebSocket('ws://localhost:8082');
socket.onclose = function() {
assert.strictEqual(socket.getStatus(), WebSocket.CLOSING);
done();
};
});
it('reconnects', function(done) {
this.timeout(60000);
var mockServer = new MockServer('ws://localhost:8082');
var socket = new TextSecureWebSocket('ws://localhost:8082');
socket.onclose = function() {
var mockServer = new MockServer('ws://localhost:8082');
mockServer.on('connection', function(server) {
socket.close();
server.close();
done();
});
};
mockServer.close();
}); });
var socket = new TextSecureWebSocket('ws://localhost:8082');
socket.onclose = function() {
assert.strictEqual(socket.getStatus(), WebSocket.CLOSING);
done();
};
});
it('reconnects', function(done) {
this.timeout(60000);
var mockServer = new MockServer('ws://localhost:8082');
var socket = new TextSecureWebSocket('ws://localhost:8082');
socket.onclose = function() {
var mockServer = new MockServer('ws://localhost:8082');
mockServer.on('connection', function(server) {
socket.close();
server.close();
done();
});
};
mockServer.close();
});
}); });

View file

@ -1,7 +1,7 @@
;(function(){ (function() {
'use strict'; 'use strict';
/* /*
* WebSocket-Resources * WebSocket-Resources
* *
* Create a request-response interface over websockets using the * Create a request-response interface over websockets using the
@ -23,212 +23,233 @@
* *
*/ */
var Request = function(options) { var Request = function(options) {
this.verb = options.verb || options.type; this.verb = options.verb || options.type;
this.path = options.path || options.url; this.path = options.path || options.url;
this.body = options.body || options.data; this.body = options.body || options.data;
this.success = options.success; this.success = options.success;
this.error = options.error; this.error = options.error;
this.id = options.id; this.id = options.id;
if (this.id === undefined) { if (this.id === undefined) {
var bits = new Uint32Array(2); var bits = new Uint32Array(2);
window.crypto.getRandomValues(bits); window.crypto.getRandomValues(bits);
this.id = dcodeIO.Long.fromBits(bits[0], bits[1], true); this.id = dcodeIO.Long.fromBits(bits[0], bits[1], true);
}
if (this.body === undefined) {
this.body = null;
}
};
var IncomingWebSocketRequest = function(options) {
var request = new Request(options);
var socket = options.socket;
this.verb = request.verb;
this.path = request.path;
this.body = request.body;
this.respond = function(status, message) {
socket.send(
new textsecure.protobuf.WebSocketMessage({
type: textsecure.protobuf.WebSocketMessage.Type.RESPONSE,
response: { id: request.id, message: message, status: status }
}).encode().toArrayBuffer()
);
};
};
var outgoing = {};
var OutgoingWebSocketRequest = function(options, socket) {
var request = new Request(options);
outgoing[request.id] = request;
socket.send(
new textsecure.protobuf.WebSocketMessage({
type: textsecure.protobuf.WebSocketMessage.Type.REQUEST,
request: {
verb : request.verb,
path : request.path,
body : request.body,
id : request.id
}
}).encode().toArrayBuffer()
);
};
window.WebSocketResource = function(socket, opts) {
opts = opts || {};
var handleRequest = opts.handleRequest;
if (typeof handleRequest !== 'function') {
handleRequest = function(request) {
request.respond(404, 'Not found');
};
}
this.sendRequest = function(options) {
return new OutgoingWebSocketRequest(options, socket);
};
socket.onmessage = function(socketMessage) {
var blob = socketMessage.data;
var handleArrayBuffer = function(buffer) {
var message = textsecure.protobuf.WebSocketMessage.decode(buffer);
if (message.type === textsecure.protobuf.WebSocketMessage.Type.REQUEST ) {
handleRequest(
new IncomingWebSocketRequest({
verb : message.request.verb,
path : message.request.path,
body : message.request.body,
id : message.request.id,
socket : socket
})
);
}
else if (message.type === textsecure.protobuf.WebSocketMessage.Type.RESPONSE ) {
var response = message.response;
var request = outgoing[response.id];
if (request) {
request.response = response;
var callback = request.error;
if (response.status >= 200 && response.status < 300) {
callback = request.success;
}
if (typeof callback === 'function') {
callback(response.message, response.status, request);
}
} else {
throw 'Received response for unknown request ' + message.response.id;
}
}
};
if (blob instanceof ArrayBuffer) {
handleArrayBuffer(blob);
} else {
var reader = new FileReader();
reader.onload = function() {
handleArrayBuffer(reader.result);
};
reader.readAsArrayBuffer(blob);
}
};
if (opts.keepalive) {
this.keepalive = new KeepAlive(this, {
path : opts.keepalive.path,
disconnect : opts.keepalive.disconnect
});
var resetKeepAliveTimer = this.keepalive.reset.bind(this.keepalive);
socket.addEventListener('open', resetKeepAliveTimer);
socket.addEventListener('message', resetKeepAliveTimer);
socket.addEventListener('close', this.keepalive.stop.bind(this.keepalive));
}
socket.addEventListener('close', function() {
this.closed = true;
}.bind(this))
this.close = function(code, reason) {
if (this.closed) {
return;
}
console.log('WebSocketResource.close()');
if (!code) {
code = 3000;
}
if (this.keepalive) {
this.keepalive.stop();
}
socket.close(code, reason);
socket.onmessage = null;
// On linux the socket can wait a long time to emit its close event if we've
// lost the internet connection. On the order of minutes. This speeds that
// process up.
setTimeout(function() {
if (this.closed) {
return;
}
this.closed = true;
console.log('Dispatching our own socket close event');
var ev = new Event('close');
ev.code = code;
ev.reason = reason;
this.dispatchEvent(ev);
}.bind(this), 1000);
};
};
window.WebSocketResource.prototype = new textsecure.EventTarget();
function KeepAlive(websocketResource, opts) {
if (websocketResource instanceof WebSocketResource) {
opts = opts || {};
this.path = opts.path;
if (this.path === undefined) {
this.path = '/';
}
this.disconnect = opts.disconnect;
if (this.disconnect === undefined) {
this.disconnect = true;
}
this.wsr = websocketResource;
} else {
throw new TypeError('KeepAlive expected a WebSocketResource');
}
} }
KeepAlive.prototype = { if (this.body === undefined) {
constructor: KeepAlive, this.body = null;
stop: function() { }
clearTimeout(this.keepAliveTimer); };
clearTimeout(this.disconnectTimer);
}, var IncomingWebSocketRequest = function(options) {
reset: function() { var request = new Request(options);
clearTimeout(this.keepAliveTimer); var socket = options.socket;
clearTimeout(this.disconnectTimer);
this.keepAliveTimer = setTimeout(function() { this.verb = request.verb;
if (this.disconnect) { this.path = request.path;
// automatically disconnect if server doesn't ack this.body = request.body;
this.disconnectTimer = setTimeout(function() {
clearTimeout(this.keepAliveTimer); this.respond = function(status, message) {
this.wsr.close(3001, 'No response to keepalive request'); socket.send(
}.bind(this), 1000); new textsecure.protobuf.WebSocketMessage({
} else { type: textsecure.protobuf.WebSocketMessage.Type.RESPONSE,
this.reset(); response: { id: request.id, message: message, status: status },
} })
console.log('Sending a keepalive message'); .encode()
this.wsr.sendRequest({ .toArrayBuffer()
verb: 'GET', );
path: this.path, };
success: this.reset.bind(this) };
});
}.bind(this), 55000); var outgoing = {};
var OutgoingWebSocketRequest = function(options, socket) {
var request = new Request(options);
outgoing[request.id] = request;
socket.send(
new textsecure.protobuf.WebSocketMessage({
type: textsecure.protobuf.WebSocketMessage.Type.REQUEST,
request: {
verb: request.verb,
path: request.path,
body: request.body,
id: request.id,
}, },
})
.encode()
.toArrayBuffer()
);
};
window.WebSocketResource = function(socket, opts) {
opts = opts || {};
var handleRequest = opts.handleRequest;
if (typeof handleRequest !== 'function') {
handleRequest = function(request) {
request.respond(404, 'Not found');
};
}
this.sendRequest = function(options) {
return new OutgoingWebSocketRequest(options, socket);
}; };
}()); socket.onmessage = function(socketMessage) {
var blob = socketMessage.data;
var handleArrayBuffer = function(buffer) {
var message = textsecure.protobuf.WebSocketMessage.decode(buffer);
if (
message.type === textsecure.protobuf.WebSocketMessage.Type.REQUEST
) {
handleRequest(
new IncomingWebSocketRequest({
verb: message.request.verb,
path: message.request.path,
body: message.request.body,
id: message.request.id,
socket: socket,
})
);
} else if (
message.type === textsecure.protobuf.WebSocketMessage.Type.RESPONSE
) {
var response = message.response;
var request = outgoing[response.id];
if (request) {
request.response = response;
var callback = request.error;
if (response.status >= 200 && response.status < 300) {
callback = request.success;
}
if (typeof callback === 'function') {
callback(response.message, response.status, request);
}
} else {
throw 'Received response for unknown request ' +
message.response.id;
}
}
};
if (blob instanceof ArrayBuffer) {
handleArrayBuffer(blob);
} else {
var reader = new FileReader();
reader.onload = function() {
handleArrayBuffer(reader.result);
};
reader.readAsArrayBuffer(blob);
}
};
if (opts.keepalive) {
this.keepalive = new KeepAlive(this, {
path: opts.keepalive.path,
disconnect: opts.keepalive.disconnect,
});
var resetKeepAliveTimer = this.keepalive.reset.bind(this.keepalive);
socket.addEventListener('open', resetKeepAliveTimer);
socket.addEventListener('message', resetKeepAliveTimer);
socket.addEventListener(
'close',
this.keepalive.stop.bind(this.keepalive)
);
}
socket.addEventListener(
'close',
function() {
this.closed = true;
}.bind(this)
);
this.close = function(code, reason) {
if (this.closed) {
return;
}
console.log('WebSocketResource.close()');
if (!code) {
code = 3000;
}
if (this.keepalive) {
this.keepalive.stop();
}
socket.close(code, reason);
socket.onmessage = null;
// On linux the socket can wait a long time to emit its close event if we've
// lost the internet connection. On the order of minutes. This speeds that
// process up.
setTimeout(
function() {
if (this.closed) {
return;
}
this.closed = true;
console.log('Dispatching our own socket close event');
var ev = new Event('close');
ev.code = code;
ev.reason = reason;
this.dispatchEvent(ev);
}.bind(this),
1000
);
};
};
window.WebSocketResource.prototype = new textsecure.EventTarget();
function KeepAlive(websocketResource, opts) {
if (websocketResource instanceof WebSocketResource) {
opts = opts || {};
this.path = opts.path;
if (this.path === undefined) {
this.path = '/';
}
this.disconnect = opts.disconnect;
if (this.disconnect === undefined) {
this.disconnect = true;
}
this.wsr = websocketResource;
} else {
throw new TypeError('KeepAlive expected a WebSocketResource');
}
}
KeepAlive.prototype = {
constructor: KeepAlive,
stop: function() {
clearTimeout(this.keepAliveTimer);
clearTimeout(this.disconnectTimer);
},
reset: function() {
clearTimeout(this.keepAliveTimer);
clearTimeout(this.disconnectTimer);
this.keepAliveTimer = setTimeout(
function() {
if (this.disconnect) {
// automatically disconnect if server doesn't ack
this.disconnectTimer = setTimeout(
function() {
clearTimeout(this.keepAliveTimer);
this.wsr.close(3001, 'No response to keepalive request');
}.bind(this),
1000
);
} else {
this.reset();
}
console.log('Sending a keepalive message');
this.wsr.sendRequest({
verb: 'GET',
path: this.path,
success: this.reset.bind(this),
});
}.bind(this),
55000
);
},
};
})();

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