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