From 6b9764e6c69aaa63eb15974d474efa9e19d2d134 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Wed, 5 Dec 2018 13:35:53 +1100 Subject: [PATCH 01/11] Added launcher view. --- app/user_config.js | 2 +- js/launcher_start.js | 7 +++ js/views/launcher_view.js | 26 ++++++++++ launcher.html | 43 ++++++++++++++++ launcher_preload.js | 31 +++++++++++ main.js | 105 +++++++++++++++++++++++++++++++++++++- package.json | 1 + 7 files changed, 212 insertions(+), 3 deletions(-) create mode 100644 js/launcher_start.js create mode 100644 js/views/launcher_view.js create mode 100644 launcher.html create mode 100644 launcher_preload.js diff --git a/app/user_config.js b/app/user_config.js index 4da1f62a0..44a0b1171 100644 --- a/app/user_config.js +++ b/app/user_config.js @@ -1,6 +1,6 @@ const path = require('path'); -const { app } = require('electron'); +const app = require('electron').app || require('electron').remote.app; const { start } = require('./base_config'); const config = require('./config'); diff --git a/js/launcher_start.js b/js/launcher_start.js new file mode 100644 index 000000000..38ec1e9e5 --- /dev/null +++ b/js/launcher_start.js @@ -0,0 +1,7 @@ +/* global $, Whisper, storage */ +const $body = $(document.body); + +// eslint-disable-next-line strict +window.view = new Whisper.LauncherView(); +$body.html(''); +window.view.$el.prependTo($body); diff --git a/js/views/launcher_view.js b/js/views/launcher_view.js new file mode 100644 index 000000000..4c7c8282b --- /dev/null +++ b/js/views/launcher_view.js @@ -0,0 +1,26 @@ +/* global i18n: false */ +/* global Whisper: false */ +/* global $: false */ + +/* eslint-disable no-new */ + +// eslint-disable-next-line func-names +(function() { + 'use strict'; + + window.Whisper = window.Whisper || {}; + + Whisper.LauncherView = Whisper.View.extend({ + className: 'launcher', + templateName: 'launcher', + initialize() { + this.render(); + }, + render_attributes() { + return { + title: 'WOOOWEEE', + }; + }, + }); + +})(); diff --git a/launcher.html b/launcher.html new file mode 100644 index 000000000..448dfe36b --- /dev/null +++ b/launcher.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + +
+
+ +
+ + + +
+
+
+
+ + + diff --git a/launcher_preload.js b/launcher_preload.js new file mode 100644 index 000000000..fdd5041da --- /dev/null +++ b/launcher_preload.js @@ -0,0 +1,31 @@ +/* global window */ + +const { ipcRenderer } = require('electron'); +const url = require('url'); +const i18n = require('./js/modules/i18n'); + +const userConfig = require('./app/user_config'); + +const config = url.parse(window.location.toString(), true).query; +const { locale } = config; +const localeMessages = ipcRenderer.sendSync('locale-data'); + +window.theme = config.theme; +window.i18n = i18n.setup(locale, localeMessages); + +// So far we're only using this for Signal.Types +const Signal = require('./js/modules/signal'); + +window.Signal = Signal.setup({ + Attachments: null, + userDataPath: null, + getRegionCode: () => null, +}); + +window.userConfig = userConfig; +window.getEnvironment = () => config.environment; +window.getVersion = () => config.version; +window.getAppInstance = () => config.appInstance; + +window.onLogin = (passPhrase) => ipcRenderer.send('launcher_login', passPhrase); +require('./js/logging'); diff --git a/main.js b/main.js index 1e794c6da..94a02b58f 100644 --- a/main.js +++ b/main.js @@ -420,6 +420,93 @@ function setupAsStandalone() { } } +let launcherWindow; +function showLauncher() { + if (launcherWindow) { + launcherWindow.show(); + return; + } + + const windowOptions = Object.assign( + { + show: !startInTray, // allow to start minimised in tray + width: DEFAULT_WIDTH, + height: DEFAULT_HEIGHT, + minWidth: MIN_WIDTH, + minHeight: MIN_HEIGHT, + autoHideMenuBar: false, + webPreferences: { + nodeIntegration: false, + nodeIntegrationInWorker: false, + // sandbox: true, + preload: path.join(__dirname, 'launcher_preload.js'), + nativeWindowOpen: true, + }, + icon: path.join(__dirname, 'images', 'icon_256.png'), + }, + _.pick(windowConfig, [ + 'maximized', + 'autoHideMenuBar', + 'width', + 'height', + 'x', + 'y', + ]) + ); + + launcherWindow = new BrowserWindow(windowOptions); + + launcherWindow.loadURL(prepareURL([__dirname, 'launcher.html'])); + + captureClicks(launcherWindow); + + // Ingested in preload.js via a sendSync call + ipc.on('locale-data', event => { + // eslint-disable-next-line no-param-reassign + event.returnValue = locale.messages; + }); + + launcherWindow.on('close', e => { + // If the application is terminating, just do the default + if ( + config.environment === 'test' || + config.environment === 'test-lib' || + (windowState.shouldQuit()) + ) { + return; + } + + // Prevent the shutdown + e.preventDefault(); + launcherWindow.hide(); + + // On Mac, or on other platforms when the tray icon is in use, the window + // should be only hidden, not closed, when the user clicks the close button + if ( + !windowState.shouldQuit() && + (usingTrayIcon || process.platform === 'darwin') + ) { + // toggle the visibility of the show/hide tray icon menu entries + if (tray) { + tray.updateContextMenu(); + } + + return; + } + + launcherWindow.readyForShutdown = true; + app.quit(); + }); + + launcherWindow.on('closed', () => { + launcherWindow = null; + }); + + launcherWindow.once('ready-to-show', () => { + launcherWindow.show(); + }); +} + let aboutWindow; function showAbout() { if (aboutWindow) { @@ -654,7 +741,21 @@ app.on('ready', async () => { key = crypto.randomBytes(32).toString('hex'); userConfig.set('key', key); } - await sql.initialize({ configDir: userDataPath, key }); + + // If we have a password set then show the launcher + // Otherwise show the main window + const passHash = userConfig.get('passHash'); + if (!passHash) { + showLauncher(); + } else { + await showMainWindow(key); + } +}); + +async function showMainWindow(sqlKey) { + const userDataPath = await getRealPath(app.getPath('userData')); + + await sql.initialize({ configDir: userDataPath, key: sqlKey }); await sqlChannels.initialize(); try { @@ -698,7 +799,7 @@ app.on('ready', async () => { } setupMenu(); -}); +} function setupMenu(options) { const { platform } = process; diff --git a/package.json b/package.json index ef51ba443..b37c2b068 100644 --- a/package.json +++ b/package.json @@ -224,6 +224,7 @@ "background.html", "about.html", "settings.html", + "launcher.html", "permissions_popup.html", "debug_log.html", "_locales/**", From 65015063d2d6a64ee69c9e2124588ddd217a4e68 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Wed, 5 Dec 2018 15:52:26 +1100 Subject: [PATCH 02/11] Add css styling. Added input and button. --- js/views/launcher_view.js | 5 +++-- launcher.html | 8 ++++++-- stylesheets/_launcher.scss | 17 +++++++++++++++++ stylesheets/manifest.scss | 1 + 4 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 stylesheets/_launcher.scss diff --git a/js/views/launcher_view.js b/js/views/launcher_view.js index 4c7c8282b..bb0bf28e8 100644 --- a/js/views/launcher_view.js +++ b/js/views/launcher_view.js @@ -11,14 +11,15 @@ window.Whisper = window.Whisper || {}; Whisper.LauncherView = Whisper.View.extend({ - className: 'launcher', + className: 'launcher full-screen-flow', templateName: 'launcher', initialize() { this.render(); }, render_attributes() { return { - title: 'WOOOWEEE', + title: 'Type in your password', + buttonText: 'Unlock', }; }, }); diff --git a/launcher.html b/launcher.html index 448dfe36b..8a872e51b 100644 --- a/launcher.html +++ b/launcher.html @@ -17,8 +17,12 @@ diff --git a/stylesheets/_launcher.scss b/stylesheets/_launcher.scss new file mode 100644 index 000000000..2e59ed30f --- /dev/null +++ b/stylesheets/_launcher.scss @@ -0,0 +1,17 @@ +.launcher { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + background: $color-dark-85; + color: $color-dark-05; + + .inputs { + display: flex; + flex-direction: column; + } + + input { + width: 30em; + } +} diff --git a/stylesheets/manifest.scss b/stylesheets/manifest.scss index 33a4b8ff2..3c7274e5c 100644 --- a/stylesheets/manifest.scss +++ b/stylesheets/manifest.scss @@ -11,6 +11,7 @@ @import 'recorder'; @import 'emoji'; @import 'settings'; +@import 'launcher'; // Build the main view @import 'index'; From 6620244d03dc9a4c774117a8aec225941b05bfe1 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Wed, 5 Dec 2018 16:35:28 +1100 Subject: [PATCH 03/11] Show main window on correct password; --- _locales/en/messages.json | 8 +++++ app/password_util.js | 9 +++++ background.html | 2 +- js/views/launcher_view.js | 22 +++++++++--- launcher.html | 13 +++++--- launcher_preload.js | 13 +++++++- main.js | 68 ++++++++++++++++++++++++++------------ stylesheets/_global.scss | 2 +- stylesheets/_launcher.scss | 24 ++++++++++---- 9 files changed, 122 insertions(+), 39 deletions(-) create mode 100644 app/password_util.js diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 17e41fc90..39ac0e218 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1684,5 +1684,13 @@ "copiedMnemonic": { "message": "Copied mnemonic to clipboard", "description": "A toast message telling the user that the mnemonic was copied" + }, + + "launcherViewTitle": { + "message": "Type in your password", + "description": "The title shown when user needs to type in a password to unlock the messenger" + }, + "unlock": { + "message": "Unlock" } } diff --git a/app/password_util.js b/app/password_util.js new file mode 100644 index 000000000..e2fef063a --- /dev/null +++ b/app/password_util.js @@ -0,0 +1,9 @@ +const { sha512 } = require('js-sha512'); + +const generateHash = (phrase) => sha512(phrase); +const matchesHash = (phrase, hash) => sha512(phrase) === hash; + +module.exports = { + generateHash, + matchesHash, +}; \ No newline at end of file diff --git a/background.html b/background.html index 84f605bda..482796562 100644 --- a/background.html +++ b/background.html @@ -598,7 +598,7 @@ diff --git a/launcher_preload.js b/launcher_preload.js index fdd5041da..3c50a6796 100644 --- a/launcher_preload.js +++ b/launcher_preload.js @@ -4,6 +4,7 @@ const { ipcRenderer } = require('electron'); const url = require('url'); const i18n = require('./js/modules/i18n'); +const passwordUtil = require('./app/password_util'); const userConfig = require('./app/user_config'); const config = url.parse(window.location.toString(), true).query; @@ -22,10 +23,20 @@ window.Signal = Signal.setup({ getRegionCode: () => null, }); +window.passwordUtil = passwordUtil; window.userConfig = userConfig; window.getEnvironment = () => config.environment; window.getVersion = () => config.version; window.getAppInstance = () => config.appInstance; -window.onLogin = (passPhrase) => ipcRenderer.send('launcher_login', passPhrase); +window.onLogin = (passPhrase) => new Promise((resolve, reject) => { + ipcRenderer.once('launcher-login-response', (event, error) => { + if (error) { + return reject(error); + } + return resolve(); + }); + ipcRenderer.send('launcher-login', passPhrase); +}); + require('./js/logging'); diff --git a/main.js b/main.js index 94a02b58f..c921d916c 100644 --- a/main.js +++ b/main.js @@ -51,6 +51,7 @@ const config = require('./app/config'); // Very important to put before the single instance check, since it is based on the // userData directory. const userConfig = require('./app/user_config'); +const passwordUtil = require('./app/password_util'); const importMode = process.argv.some(arg => arg === '--import') || config.get('import'); @@ -306,12 +307,6 @@ function createWindow() { mainWindow.flashFrame(false); }); - // Ingested in preload.js via a sendSync call - ipc.on('locale-data', event => { - // eslint-disable-next-line no-param-reassign - event.returnValue = locale.messages; - }); - if (config.environment === 'test') { mainWindow.loadURL(prepareURL([__dirname, 'test', 'index.html'])); } else if (config.environment === 'test-lib') { @@ -460,12 +455,6 @@ function showLauncher() { captureClicks(launcherWindow); - // Ingested in preload.js via a sendSync call - ipc.on('locale-data', event => { - // eslint-disable-next-line no-param-reassign - event.returnValue = locale.messages; - }); - launcherWindow.on('close', e => { // If the application is terminating, just do the default if ( @@ -732,6 +721,19 @@ app.on('ready', async () => { locale = loadLocale({ appLocale, logger }); } + const key = getDefaultSQLKey(); + + // If we have a password set then show the launcher + // Otherwise show the main window + const passHash = userConfig.get('passHash'); + if (passHash) { + showLauncher(); + } else { + await showMainWindow(key); + } +}); + +function getDefaultSQLKey() { let key = userConfig.get('key'); if (!key) { console.log( @@ -742,15 +744,8 @@ app.on('ready', async () => { userConfig.set('key', key); } - // If we have a password set then show the launcher - // Otherwise show the main window - const passHash = userConfig.get('passHash'); - if (!passHash) { - showLauncher(); - } else { - await showMainWindow(key); - } -}); + return key; +} async function showMainWindow(sqlKey) { const userDataPath = await getRealPath(app.getPath('userData')); @@ -908,6 +903,12 @@ app.on('web-contents-created', (createEvent, contents) => { }); }); +// Ingested in preload.js via a sendSync call +ipc.on('locale-data', event => { + // eslint-disable-next-line no-param-reassign + event.returnValue = locale.messages; +}); + ipc.on('set-badge-count', (event, count) => { app.setBadgeCount(count); }); @@ -961,6 +962,31 @@ ipc.on('update-tray-icon', (event, unreadCount) => { } }); +// Launch screen related IPC calls +ipc.on('launcher-login', async (event, passPhrase) => { + const sendError = (e) => event.sender.send('launcher-login-response', e); + + // Check if the phrase matches with the hash we have stored + const hash = userConfig.get('passHash'); + const hashMatches = passPhrase && passwordUtil.matchesHash(passPhrase, hash); + if (hash && !hashMatches) { + sendError('Invalid password'); + return; + } + + // If we don't have a hash then use the default sql key to unlock the db + const key = hash ? passPhrase : getDefaultSQLKey(); + try { + await showMainWindow(key); + if (launcherWindow) { + launcherWindow.close(); + launcherWindow = null; + } + } catch (e) { + sendError('Failed to decrypt SQL database'); + } +}); + // Debug Log-related IPC calls ipc.on('show-debug-log', showDebugLogWindow); diff --git a/stylesheets/_global.scss b/stylesheets/_global.scss index 12152f21c..972f78f41 100644 --- a/stylesheets/_global.scss +++ b/stylesheets/_global.scss @@ -805,7 +805,7 @@ textarea { overflow-y: auto; } -#standalone { +.standalone { color: $color-dark-05; height: auto; padding: 0; diff --git a/stylesheets/_launcher.scss b/stylesheets/_launcher.scss index 2e59ed30f..7dac09b02 100644 --- a/stylesheets/_launcher.scss +++ b/stylesheets/_launcher.scss @@ -1,10 +1,16 @@ .launcher { - display: flex; - align-items: center; - justify-content: center; - height: 100%; - background: $color-dark-85; - color: $color-dark-05; + .content-wrapper { + display: flex; + align-items: center; + justify-content: center; + color: $color-dark-05; + width: 100%; + height: 100%; + } + + .content { + margin: 3em; + } .inputs { display: flex; @@ -14,4 +20,10 @@ input { width: 30em; } + + .error { + font-weight: bold; + font-size: 16px; + margin-top: 1em; + } } From 7a96b8446a5d18b277cd3dc24a99170f5b60e1a4 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Thu, 6 Dec 2018 10:09:57 +1100 Subject: [PATCH 04/11] Rename launcherView to passwordView. --- _locales/en/messages.json | 2 +- app/user_config.js | 2 +- js/{launcher_start.js => password_start.js} | 4 +- .../{launcher_view.js => password_view.js} | 8 +- main.js | 84 ++++++++----------- package.json | 2 +- launcher.html => password.html | 6 +- launcher_preload.js => password_preload.js | 9 +- stylesheets/_global.scss | 10 ++- .../{_launcher.scss => _password.scss} | 4 +- stylesheets/manifest.scss | 2 +- 11 files changed, 59 insertions(+), 74 deletions(-) rename js/{launcher_start.js => password_start.js} (61%) rename js/views/{launcher_view.js => password_view.js} (79%) rename launcher.html => password.html (89%) rename launcher_preload.js => password_preload.js (75%) rename stylesheets/{_launcher.scss => _password.scss} (96%) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 39ac0e218..a29bb9195 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1686,7 +1686,7 @@ "description": "A toast message telling the user that the mnemonic was copied" }, - "launcherViewTitle": { + "passwordViewTitle": { "message": "Type in your password", "description": "The title shown when user needs to type in a password to unlock the messenger" }, diff --git a/app/user_config.js b/app/user_config.js index 44a0b1171..4da1f62a0 100644 --- a/app/user_config.js +++ b/app/user_config.js @@ -1,6 +1,6 @@ const path = require('path'); -const app = require('electron').app || require('electron').remote.app; +const { app } = require('electron'); const { start } = require('./base_config'); const config = require('./config'); diff --git a/js/launcher_start.js b/js/password_start.js similarity index 61% rename from js/launcher_start.js rename to js/password_start.js index 38ec1e9e5..eba6676d5 100644 --- a/js/launcher_start.js +++ b/js/password_start.js @@ -1,7 +1,7 @@ -/* global $, Whisper, storage */ +/* global $, Whisper */ const $body = $(document.body); // eslint-disable-next-line strict -window.view = new Whisper.LauncherView(); +window.view = new Whisper.PasswordView(); $body.html(''); window.view.$el.prependTo($body); diff --git a/js/views/launcher_view.js b/js/views/password_view.js similarity index 79% rename from js/views/launcher_view.js rename to js/views/password_view.js index ab685f759..e32cfb5e1 100644 --- a/js/views/launcher_view.js +++ b/js/views/password_view.js @@ -9,9 +9,9 @@ window.Whisper = window.Whisper || {}; - Whisper.LauncherView = Whisper.View.extend({ - className: 'launcher full-screen-flow standalone-fullscreen', - templateName: 'launcher', + Whisper.PasswordView = Whisper.View.extend({ + className: 'password full-screen-flow standalone-fullscreen', + templateName: 'password', events: { 'click #unlock-button': 'onLogin', }, @@ -20,7 +20,7 @@ }, render_attributes() { return { - title: i18n('launcherViewTitle'), + title: i18n('passwordViewTitle'), buttonText: i18n('unlock'), }; }, diff --git a/main.js b/main.js index c921d916c..a95b94a24 100644 --- a/main.js +++ b/main.js @@ -415,47 +415,37 @@ function setupAsStandalone() { } } -let launcherWindow; -function showLauncher() { - if (launcherWindow) { - launcherWindow.show(); +let passwordWindow; +function showPasswordWindow() { + if (passwordWindow) { + passwordWindow.show(); return; } - const windowOptions = Object.assign( - { - show: !startInTray, // allow to start minimised in tray - width: DEFAULT_WIDTH, - height: DEFAULT_HEIGHT, - minWidth: MIN_WIDTH, - minHeight: MIN_HEIGHT, - autoHideMenuBar: false, - webPreferences: { - nodeIntegration: false, - nodeIntegrationInWorker: false, - // sandbox: true, - preload: path.join(__dirname, 'launcher_preload.js'), - nativeWindowOpen: true, - }, - icon: path.join(__dirname, 'images', 'icon_256.png'), + const windowOptions = { + show: true, // allow to start minimised in tray + width: DEFAULT_WIDTH, + height: DEFAULT_HEIGHT, + minWidth: MIN_WIDTH, + minHeight: MIN_HEIGHT, + autoHideMenuBar: false, + webPreferences: { + nodeIntegration: false, + nodeIntegrationInWorker: false, + // sandbox: true, + preload: path.join(__dirname, 'password_preload.js'), + nativeWindowOpen: true, }, - _.pick(windowConfig, [ - 'maximized', - 'autoHideMenuBar', - 'width', - 'height', - 'x', - 'y', - ]) - ); + icon: path.join(__dirname, 'images', 'icon_256.png'), + }; - launcherWindow = new BrowserWindow(windowOptions); + passwordWindow = new BrowserWindow(windowOptions); - launcherWindow.loadURL(prepareURL([__dirname, 'launcher.html'])); + passwordWindow.loadURL(prepareURL([__dirname, 'password.html'])); - captureClicks(launcherWindow); + captureClicks(passwordWindow); - launcherWindow.on('close', e => { + passwordWindow.on('close', e => { // If the application is terminating, just do the default if ( config.environment === 'test' || @@ -467,7 +457,7 @@ function showLauncher() { // Prevent the shutdown e.preventDefault(); - launcherWindow.hide(); + passwordWindow.hide(); // On Mac, or on other platforms when the tray icon is in use, the window // should be only hidden, not closed, when the user clicks the close button @@ -483,16 +473,12 @@ function showLauncher() { return; } - launcherWindow.readyForShutdown = true; + passwordWindow.readyForShutdown = true; app.quit(); }); - launcherWindow.on('closed', () => { - launcherWindow = null; - }); - - launcherWindow.once('ready-to-show', () => { - launcherWindow.show(); + passwordWindow.on('closed', () => { + passwordWindow = null; }); } @@ -723,11 +709,11 @@ app.on('ready', async () => { const key = getDefaultSQLKey(); - // If we have a password set then show the launcher + // If we have a password set then show the password window // Otherwise show the main window const passHash = userConfig.get('passHash'); if (passHash) { - showLauncher(); + showPasswordWindow(); } else { await showMainWindow(key); } @@ -962,9 +948,9 @@ ipc.on('update-tray-icon', (event, unreadCount) => { } }); -// Launch screen related IPC calls -ipc.on('launcher-login', async (event, passPhrase) => { - const sendError = (e) => event.sender.send('launcher-login-response', e); +// Password screen related IPC calls +ipc.on('password-window-login', async (event, passPhrase) => { + const sendError = (e) => event.sender.send('password-window-login-response', e); // Check if the phrase matches with the hash we have stored const hash = userConfig.get('passHash'); @@ -978,9 +964,9 @@ ipc.on('launcher-login', async (event, passPhrase) => { const key = hash ? passPhrase : getDefaultSQLKey(); try { await showMainWindow(key); - if (launcherWindow) { - launcherWindow.close(); - launcherWindow = null; + if (passwordWindow) { + passwordWindow.close(); + passwordWindow = null; } } catch (e) { sendError('Failed to decrypt SQL database'); diff --git a/package.json b/package.json index b37c2b068..fe307b1ee 100644 --- a/package.json +++ b/package.json @@ -224,7 +224,7 @@ "background.html", "about.html", "settings.html", - "launcher.html", + "password.html", "permissions_popup.html", "debug_log.html", "_locales/**", diff --git a/launcher.html b/password.html similarity index 89% rename from launcher.html rename to password.html index 6f77193c3..557c74b7e 100644 --- a/launcher.html +++ b/password.html @@ -16,7 +16,7 @@ - - +
@@ -45,6 +45,6 @@
- + diff --git a/launcher_preload.js b/password_preload.js similarity index 75% rename from launcher_preload.js rename to password_preload.js index 3c50a6796..f8dcb3eb3 100644 --- a/launcher_preload.js +++ b/password_preload.js @@ -4,9 +4,6 @@ const { ipcRenderer } = require('electron'); const url = require('url'); const i18n = require('./js/modules/i18n'); -const passwordUtil = require('./app/password_util'); -const userConfig = require('./app/user_config'); - const config = url.parse(window.location.toString(), true).query; const { locale } = config; const localeMessages = ipcRenderer.sendSync('locale-data'); @@ -23,20 +20,18 @@ window.Signal = Signal.setup({ getRegionCode: () => null, }); -window.passwordUtil = passwordUtil; -window.userConfig = userConfig; window.getEnvironment = () => config.environment; window.getVersion = () => config.version; window.getAppInstance = () => config.appInstance; window.onLogin = (passPhrase) => new Promise((resolve, reject) => { - ipcRenderer.once('launcher-login-response', (event, error) => { + ipcRenderer.once('password-window-login-response', (event, error) => { if (error) { return reject(error); } return resolve(); }); - ipcRenderer.send('launcher-login', passPhrase); + ipcRenderer.send('password-window-login', passPhrase); }); require('./js/logging'); diff --git a/stylesheets/_global.scss b/stylesheets/_global.scss index 972f78f41..b61fbf51c 100644 --- a/stylesheets/_global.scss +++ b/stylesheets/_global.scss @@ -637,9 +637,12 @@ textarea { width: 100%; } .step { - height: 100%; - width: 100%; - padding: 70px 0 50px; + display: flex; + align-items: center; + min-width: 100%; + min-height: 100%; + margin: auto; + padding: 10px 0; } .step-body { margin-left: auto; @@ -653,6 +656,7 @@ textarea { justify-content: center; flex-direction: column; height: 100%; + width: 100%; } .banner-image { diff --git a/stylesheets/_launcher.scss b/stylesheets/_password.scss similarity index 96% rename from stylesheets/_launcher.scss rename to stylesheets/_password.scss index 7dac09b02..23c7dd372 100644 --- a/stylesheets/_launcher.scss +++ b/stylesheets/_password.scss @@ -1,4 +1,4 @@ -.launcher { +.password { .content-wrapper { display: flex; align-items: center; @@ -26,4 +26,4 @@ font-size: 16px; margin-top: 1em; } -} +} \ No newline at end of file diff --git a/stylesheets/manifest.scss b/stylesheets/manifest.scss index 3c7274e5c..b5c70eece 100644 --- a/stylesheets/manifest.scss +++ b/stylesheets/manifest.scss @@ -11,7 +11,7 @@ @import 'recorder'; @import 'emoji'; @import 'settings'; -@import 'launcher'; +@import 'password'; // Build the main view @import 'index'; From 08ebc63fb02e2a14fdea9229e73b6cc840ac5408 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Thu, 6 Dec 2018 11:17:48 +1100 Subject: [PATCH 05/11] Added simple tests. --- test/app/password_util_test.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 test/app/password_util_test.js diff --git a/test/app/password_util_test.js b/test/app/password_util_test.js new file mode 100644 index 000000000..c1bf27a44 --- /dev/null +++ b/test/app/password_util_test.js @@ -0,0 +1,30 @@ +const { assert } = require('chai'); + +const passwordUtil = require('../../app/password_util'); + +describe('Password Util', () => { + describe('hash generation', () => { + it('generates the same hash for the same phrase', () => { + const first = passwordUtil.generateHash('phrase'); + const second = passwordUtil.generateHash('phrase'); + assert.equal(first, second); + }); + it('generates different hashes for different phrases', () => { + const first = passwordUtil.generateHash('0'); + const second = passwordUtil.generateHash('1'); + assert.notEqual(first, second); + }); + }); + + describe('hash matching', () => { + it('returns true for the same hash', () => { + const phrase = 'phrase'; + const hash = passwordUtil.generateHash(phrase); + assert.isTrue(passwordUtil.matchesHash(phrase, hash)); + }); + it('returns false for different hashes', () => { + const hash = passwordUtil.generateHash('phrase'); + assert.isFalse(passwordUtil.matchesHash('phrase2', hash)); + }); + }); +}); From f53bec38a5b1fb55ba4c8118f61ec1527caef6b4 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Thu, 6 Dec 2018 11:33:11 +1100 Subject: [PATCH 06/11] Added password inputs on registration screen. Fix case where db is deleted but password hash still remains which causes user to never register. Allow password to have symbols and other characters. Added more tests. Moved passHash from config into the sqlite db. We can do this because we assume if sql failed to initialise then the key provided was wrong and thus we can show the user the password page. --- app/password_util.js | 22 ++++-- app/sql.js | 49 ++++++++++++-- background.html | 12 +++- js/modules/data.js | 8 +++ js/views/password_view.js | 3 +- js/views/standalone_registration_view.js | 86 +++++++++++++++++++++--- main.js | 58 ++++++++++------ password.html | 2 +- preload.js | 12 ++++ stylesheets/_global.scss | 22 +++++- test/app/password_util_test.js | 28 ++++++++ 11 files changed, 256 insertions(+), 46 deletions(-) diff --git a/app/password_util.js b/app/password_util.js index e2fef063a..b6c5e2b39 100644 --- a/app/password_util.js +++ b/app/password_util.js @@ -1,9 +1,23 @@ const { sha512 } = require('js-sha512'); -const generateHash = (phrase) => sha512(phrase); -const matchesHash = (phrase, hash) => sha512(phrase) === hash; +const generateHash = (phrase) => phrase && sha512(phrase.trim()); +const matchesHash = (phrase, hash) => phrase && sha512(phrase.trim()) === hash.trim(); + +const validatePassword = (phrase) => { + if (typeof phrase !== 'string') { + return 'Password must be a string' + } + + if (phrase && phrase.trim().length < 6) { + return 'Password must be atleast 6 characters long'; + } + + // An empty password is still valid :P + return null; +} module.exports = { - generateHash, - matchesHash, + generateHash, + matchesHash, + validatePassword, }; \ No newline at end of file diff --git a/app/sql.js b/app/sql.js index 1160c50e6..ea2509e17 100644 --- a/app/sql.js +++ b/app/sql.js @@ -1,10 +1,11 @@ +const fs = require('fs'); const path = require('path'); const mkdirp = require('mkdirp'); const rimraf = require('rimraf'); const sql = require('@journeyapps/sqlcipher'); const pify = require('pify'); const uuidv4 = require('uuid/v4'); -const { map, isString, fromPairs, forEach, last } = require('lodash'); +const { map, isString, fromPairs, forEach, last, isEmpty } = require('lodash'); // To get long stack traces // https://github.com/mapbox/node-sqlite3/wiki/API#sqlite3verbose @@ -15,6 +16,11 @@ module.exports = { close, removeDB, removeIndexedDBFiles, + setSQLPassword, + + getPasswordHash, + savePasswordHash, + removePasswordHash, createOrUpdateGroup, getGroupById, @@ -181,15 +187,25 @@ async function getSQLCipherVersion(instance) { } } -const INVALID_KEY = /[^0-9A-Fa-f]/; +const HEX_KEY = /[^0-9A-Fa-f]/; async function setupSQLCipher(instance, { key }) { - const match = INVALID_KEY.exec(key); - if (match) { - throw new Error(`setupSQLCipher: key '${key}' is not valid`); - } + // If the key isn't hex then we need to derive a hex key from it + const deriveKey = HEX_KEY.test(key); // https://www.zetetic.net/sqlcipher/sqlcipher-api/#key - await instance.run(`PRAGMA key = "x'${key}'";`); + const value = deriveKey ? `'${key}'` : `"x'${key}'"` + await instance.run(`PRAGMA key = ${value};`); +} + +async function setSQLPassword(password) { + if (!db) { + throw new Error('setSQLPassword: db is not initialized'); + } + + // If the password isn't hex then we need to derive a key from it + const deriveKey = HEX_KEY.test(password); + const value = deriveKey ? `'${password}'` : `"x'${password}'"` + await db.run(`PRAGMA rekey = ${value};`); } async function updateToSchemaVersion1(currentVersion, instance) { @@ -584,6 +600,25 @@ async function removeIndexedDBFiles() { indexedDBPath = null; } +// Password hash +async function getPasswordHash() { + const item = await getItemById('passHash'); + return item && item.value; +} +async function savePasswordHash(hash) { + if (isEmpty(hash)) { + return removePasswordHash(); + } + + const data = { id: 'passHash', value: hash }; + return createOrUpdateItem(data); +} +async function removePasswordHash() { + return removeItemById('passHash'); +} + +// Groups + const GROUPS_TABLE = 'groups'; async function createOrUpdateGroup(data) { return createOrUpdate(GROUPS_TABLE, data); diff --git a/background.html b/background.html index 482796562..83b1806b8 100644 --- a/background.html +++ b/background.html @@ -603,8 +603,16 @@
Create your Loki Messenger Account
-
Enter a name that will be shown to all your contacts
- +
+
Enter a name that will be shown to all your contacts
+ +
+
+
Type an optional password for added security
+ + +
+

Restore using seed

diff --git a/js/modules/data.js b/js/modules/data.js index 1002a743c..e7eb0614f 100644 --- a/js/modules/data.js +++ b/js/modules/data.js @@ -47,6 +47,8 @@ module.exports = { removeDB, removeIndexedDBFiles, + getPasswordHash, + createOrUpdateGroup, getGroupById, getAllGroupIds, @@ -405,6 +407,12 @@ async function removeIndexedDBFiles() { await channels.removeIndexedDBFiles(); } +// Password hash + +async function getPasswordHash() { + return channels.getPasswordHash(); +} + // Groups async function createOrUpdateGroup(data) { diff --git a/js/views/password_view.js b/js/views/password_view.js index e32cfb5e1..71a941d6c 100644 --- a/js/views/password_view.js +++ b/js/views/password_view.js @@ -26,9 +26,10 @@ }, async onLogin() { const passPhrase = this.$('#passPhrase').val(); + const trimmed = passPhrase ? passPhrase.trim() : passPhrase; this.setError(''); try { - await window.onLogin(passPhrase); + await window.onLogin(trimmed); } catch (e) { this.setError(`Error: ${e}`); } diff --git a/js/views/standalone_registration_view.js b/js/views/standalone_registration_view.js index b2eee33c6..6f32d874a 100644 --- a/js/views/standalone_registration_view.js +++ b/js/views/standalone_registration_view.js @@ -1,4 +1,4 @@ -/* global Whisper, $, getAccountManager, textsecure, i18n, storage, ConversationController */ +/* global Whisper, $, getAccountManager, textsecure, i18n, passwordUtil, ConversationController */ /* eslint-disable more/no-then */ @@ -35,6 +35,13 @@ }); this.$('#mnemonic-language').append(options); this.$('#mnemonic-display-language').append(options); + + this.$passwordInput = this.$('#password'); + this.$passwordConfirmationInput = this.$('#password-confirmation'); + this.$passwordConfirmationInput.hide(); + this.$passwordInputError = this.$('.password-inputs .error'); + + this.onValidatePassword(); }, events: { 'validation input.number': 'onValidation', @@ -48,18 +55,32 @@ 'change #mnemonic-display-language': 'onGenerateMnemonic', 'click #copy-mnemonic': 'onCopyMnemonic', 'click .section-toggle': 'toggleSection', + 'keyup #password': 'onPasswordChange', + 'keyup #password-confirmation': 'onValidatePassword', }, - register(mnemonic) { - this.accountManager - .registerSingleDevice( + async register(mnemonic) { + // Make sure the password is valid + if (this.validatePassword()) { + this.showToast('Invalid password'); + return; + } + + const input = this.trim(this.$passwordInput.val()); + + try { + await window.setPassword(input); + await this.accountManager.registerSingleDevice( mnemonic, this.$('#mnemonic-language').val(), this.$('#display-name').val() - ) - .then(() => { - this.$el.trigger('openInbox'); - }) - .catch(this.log.bind(this)); + ); + this.$el.trigger('openInbox'); + } catch (e) { + if (typeof e === 'string') { + this.showToast(e); + } + this.log(e); + } }, registerWithoutMnemonic() { const mnemonic = this.$('#mnemonic-display').text(); @@ -158,5 +179,52 @@ this.$('.section-toggle').not($target).removeClass('section-toggle-visible') this.$('.section-content').not($next).slideUp('fast'); }, + onPasswordChange() { + const input = this.$passwordInput.val(); + if (!input || input.length === 0) { + this.$passwordConfirmationInput.val(''); + this.$passwordConfirmationInput.hide(); + } else { + this.$passwordConfirmationInput.show(); + } + this.onValidatePassword(); + }, + validatePassword() { + const input = this.trim(this.$passwordInput.val()); + const confirmationInput = this.trim(this.$passwordConfirmationInput.val()); + + const error = passwordUtil.validatePassword(input); + if (error) + return error; + + if (input !== confirmationInput) + return 'Password don\'t match'; + + return null; + }, + onValidatePassword() { + const passwordValidation = this.validatePassword(); + if (passwordValidation) { + this.$passwordInput.addClass('error-input'); + this.$passwordConfirmationInput.addClass('error-input'); + this.$passwordInputError.text(passwordValidation); + this.$passwordInputError.show(); + } else { + this.$passwordInput.removeClass('error-input'); + this.$passwordConfirmationInput.removeClass('error-input'); + this.$passwordInputError.text(''); + this.$passwordInputError.hide(); + } + }, + trim(value) { + return value ? value.trim() : value; + }, + showToast(message) { + const toast = new Whisper.MessageToastView({ + message, + }); + toast.$el.appendTo(this.$el); + toast.render(); + }, }); })(); diff --git a/main.js b/main.js index a95b94a24..50e4f2b26 100644 --- a/main.js +++ b/main.js @@ -171,7 +171,7 @@ function captureClicks(window) { } const DEFAULT_WIDTH = 800; -const DEFAULT_HEIGHT = 610; +const DEFAULT_HEIGHT = 710; const MIN_WIDTH = 640; const MIN_HEIGHT = 360; const BOUNDS_BUFFER = 100; @@ -709,13 +709,12 @@ app.on('ready', async () => { const key = getDefaultSQLKey(); - // If we have a password set then show the password window - // Otherwise show the main window - const passHash = userConfig.get('passHash'); - if (passHash) { - showPasswordWindow(); - } else { + // Try to show the main window with the default key + // If that fails then show the password window + try { await showMainWindow(key); + } catch (e) { + showPasswordWindow(); } }); @@ -950,26 +949,45 @@ ipc.on('update-tray-icon', (event, unreadCount) => { // Password screen related IPC calls ipc.on('password-window-login', async (event, passPhrase) => { - const sendError = (e) => event.sender.send('password-window-login-response', e); + const sendResponse = (e) => event.sender.send('password-window-login-response', e); - // Check if the phrase matches with the hash we have stored - const hash = userConfig.get('passHash'); - const hashMatches = passPhrase && passwordUtil.matchesHash(passPhrase, hash); - if (hash && !hashMatches) { - sendError('Invalid password'); - return; - } - - // If we don't have a hash then use the default sql key to unlock the db - const key = hash ? passPhrase : getDefaultSQLKey(); try { - await showMainWindow(key); + await showMainWindow(passPhrase); + sendResponse(); if (passwordWindow) { passwordWindow.close(); passwordWindow = null; } } catch (e) { - sendError('Failed to decrypt SQL database'); + sendResponse('Invalid password'); + } +}); + +ipc.on('set-password', async (event, passPhrase, oldPhrase) => { + const sendResponse = (e) => event.sender.send('set-password-response', e); + + try { + // Check if the hash we have stored matches the hash of the old passphrase. + const hash = await sql.getPasswordHash(); + const hashMatches = oldPhrase && passwordUtil.matchesHash(oldPhrase, hash); + if (hash && !hashMatches) { + sendResponse('Failed to set password: Old password provided is invalid'); + return; + } + + if (_.isEmpty(passPhrase)) { + const defaultKey = getDefaultSQLKey(); + await sql.setSQLPassword(defaultKey); + await sql.removePasswordHash(); + } else { + await sql.setSQLPassword(passPhrase); + const newHash = passwordUtil.generateHash(passPhrase); + await sql.savePasswordHash(newHash); + } + + sendResponse(); + } catch (e) { + sendResponse('Failed to set password'); } }); diff --git a/password.html b/password.html index 557c74b7e..ab818dae3 100644 --- a/password.html +++ b/password.html @@ -21,7 +21,7 @@

{{ title }}

diff --git a/preload.js b/preload.js index 9c5ea103e..e55fedc0a 100644 --- a/preload.js +++ b/preload.js @@ -49,6 +49,17 @@ const localeMessages = ipc.sendSync('locale-data'); window.setBadgeCount = count => ipc.send('set-badge-count', count); +// Set the password for the database +window.setPassword = (passPhrase, oldPhrase) => new Promise((resolve, reject) => { + ipc.once('set-password-response', (event, error) => { + if (error) { + return reject(error); + } + return resolve(); + }); + ipc.send('set-password', passPhrase, oldPhrase); +}); + // We never do these in our code, so we'll prevent it everywhere window.open = () => null; // eslint-disable-next-line no-eval, no-multi-assign @@ -273,6 +284,7 @@ window.libphonenumber.PhoneNumberFormat = require('google-libphonenumber').Phone window.loadImage = require('blueimp-load-image'); window.getGuid = require('uuid/v4'); window.profileImages = require('./app/profile_images'); +window.passwordUtil = require('./app/password_util'); window.React = require('react'); window.ReactDOM = require('react-dom'); diff --git a/stylesheets/_global.scss b/stylesheets/_global.scss index b61fbf51c..3321781a2 100644 --- a/stylesheets/_global.scss +++ b/stylesheets/_global.scss @@ -919,7 +919,7 @@ textarea { } } - .display-name-header { + .input-header { margin-bottom: 8px; font-size: 14px; } @@ -977,8 +977,26 @@ textarea { } } + .password-inputs { + input { + margin-bottom: 0.5em; + } + + .error { + margin-bottom: 1em; + } + + .error-input { + border: 3px solid $color-vermilion; + + &:focus { + outline: none; + } + } + } + @media (min-height: 750px) and (min-width: 700px) { - .display-name-header { + .input-header { font-size: 18px; } diff --git a/test/app/password_util_test.js b/test/app/password_util_test.js index c1bf27a44..8256fe6b5 100644 --- a/test/app/password_util_test.js +++ b/test/app/password_util_test.js @@ -27,4 +27,32 @@ describe('Password Util', () => { assert.isFalse(passwordUtil.matchesHash('phrase2', hash)); }); }); + + describe('password validation', () => { + it('should return nothing if password is valid', () => { + const valid = [ + '123456', + '1a5b3C6g', + 'ABC#DE#F$IJ', + 'AabcDegf', + ]; + valid.forEach(pass => { + assert.isNull(passwordUtil.validatePassword(pass)); + }); + }); + + it('should return an error string if password is invalid', () => { + const invalid = [ + 0, + 123456, + [], + {}, + '123', + '1234$', + ]; + invalid.forEach(pass => { + assert.isNotNull(passwordUtil.validatePassword(pass)); + }); + }); + }); }); From 26ba553e6ae9c593c011ecc36e278445b8af6dd3 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Fri, 7 Dec 2018 11:06:32 +1100 Subject: [PATCH 07/11] Added menu options to set, change and remove password. --- _locales/en/messages.json | 12 ++++++++++++ js/background.js | 6 ++++++ js/views/inbox_view.js | 20 ++++++++++++++++++++ js/views/main_header_view.js | 17 ++++++++++------- preload.js | 1 + 5 files changed, 49 insertions(+), 7 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index a29bb9195..e7420151b 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1692,5 +1692,17 @@ }, "unlock": { "message": "Unlock" + }, + "setPassword": { + "message": "Set Password", + "description": "Button action that the user can click to set a password" + }, + "changePassword": { + "message": "Set Password", + "description": "Button action that the user can click to change a password" + }, + "removePassword": { + "message": "Set Password", + "description": "Button action that the user can click to remove a password" } } diff --git a/js/background.js b/js/background.js index dc94d55ba..faef150b6 100644 --- a/js/background.js +++ b/js/background.js @@ -610,6 +610,12 @@ window.log.error('Error showing PoW cog'); } }); + + Whisper.events.on('password-updated', () => { + if (appView && appView.inboxView) { + appView.inboxView.trigger('password-updated'); + } + }); } window.getSyncRequest = () => diff --git a/js/views/inbox_view.js b/js/views/inbox_view.js index 643cd135e..dc52dbbbe 100644 --- a/js/views/inbox_view.js +++ b/js/views/inbox_view.js @@ -6,6 +6,7 @@ /* global Whisper: false */ /* global textsecure: false */ /* global clipboard: false */ +/* global Signal: false */ // eslint-disable-next-line func-names (function() { @@ -106,6 +107,8 @@ el: this.$('.main-header-placeholder'), items: this.getMainHeaderItems(), }); + this.onPasswordUpdated(); + this.on('password-updated', () => this.onPasswordUpdated()); this.conversation_stack = new Whisper.ConversationStack({ el: this.$('.conversation-stack'), @@ -340,8 +343,25 @@ this._mainHeaderItem('editDisplayName', () => { window.Whisper.events.trigger('onEditProfile'); }), + ...this.passwordHeaderItems || [], ]; }, + async onPasswordUpdated() { + const hasPassword = await Signal.Data.getPasswordHash(); + const items = this.getMainHeaderItems(); + if (hasPassword) { + items.push( + this._mainHeaderItem('changePassword'), + this._mainHeaderItem('removePassword') + ); + } else { + items.push( + this._mainHeaderItem('setPassword') + ); + } + + this.mainHeaderView.updateItems(items); + }, _mainHeaderItem(textKey, onClick) { return { id: textKey, diff --git a/js/views/main_header_view.js b/js/views/main_header_view.js index 6366f914d..77da0632e 100644 --- a/js/views/main_header_view.js +++ b/js/views/main_header_view.js @@ -1,4 +1,4 @@ -/* global Whisper, textsecure, ConversationController, Signal, i18n */ +/* global Whisper, textsecure, ConversationController, Signal */ // eslint-disable-next-line func-names (function() { @@ -14,8 +14,6 @@ 'click .copy-key': 'onCopyKey', }, initialize(options) { - this.items = options.items || []; - this.ourNumber = textsecure.storage.user.getNumber(); const me = ConversationController.getOrCreate(this.ourNumber, 'private'); @@ -34,14 +32,19 @@ this.$content = this.$('.main-header-content-wrapper'); this.$content.hide(); - this.registerCallbacks(); + this.updateItems(options.items); }, - registerCallbacks() { - this.items.forEach(item => { + updateItems(items) { + this.$content.html(''); + (items || []).forEach(item => { + // Add the item + this.$content.append(`
${item.text}
`); + + // Register its callback if (item.onClick) { this.$(`#${item.id}`).click(item.onClick); } - }) + }); }, render_attributes() { return { diff --git a/preload.js b/preload.js index e55fedc0a..830079b3c 100644 --- a/preload.js +++ b/preload.js @@ -55,6 +55,7 @@ window.setPassword = (passPhrase, oldPhrase) => new Promise((resolve, reject) => if (error) { return reject(error); } + Whisper.events.trigger('password-updated'); return resolve(); }); ipc.send('set-password', passPhrase, oldPhrase); From 0b87f136996295821d67da696e61c7df301be083 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Fri, 7 Dec 2018 12:16:04 +1100 Subject: [PATCH 08/11] Added password dialog view. --- _locales/en/messages.json | 49 +++++- app/password_util.js | 6 +- background.html | 30 ++++ js/background.js | 6 + js/views/app_view.js | 4 + js/views/inbox_view.js | 33 +++- js/views/nickname_dialog_view.js | 2 +- js/views/password_dialog_view.js | 214 +++++++++++++++++++++++ js/views/standalone_registration_view.js | 25 ++- password_preload.js | 2 + stylesheets/_conversation.scss | 91 +++++----- stylesheets/_global.scss | 8 + stylesheets/_theme_dark.scss | 38 ++-- test/index.html | 1 + 14 files changed, 430 insertions(+), 79 deletions(-) create mode 100644 js/views/password_dialog_view.js diff --git a/_locales/en/messages.json b/_locales/en/messages.json index e7420151b..e168d78c8 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1698,11 +1698,56 @@ "description": "Button action that the user can click to set a password" }, "changePassword": { - "message": "Set Password", + "message": "Change Password", "description": "Button action that the user can click to change a password" }, "removePassword": { - "message": "Set Password", + "message": "Remove Password", "description": "Button action that the user can click to remove a password" + }, + "typeInOldPassword": { + "message": "Please type in your old password" + }, + "invalidPassword": { + "message": "Invalid password" + }, + "passwordsDoNotMatch": { + "message": "Passwords do not match" + }, + "setPasswordFail": { + "message": "Failed to set password" + }, + "removePasswordFail": { + "message": "Failed to remove password" + }, + "changePasswordFail": { + "message": "Failed to change password" + }, + "setPasswordSuccess": { + "message": "Password set" + }, + "removePasswordSuccess": { + "message": "Password removed" + }, + "changePasswordSuccess": { + "message": "Password changed" + }, + "passwordLengthError": { + "message": "Password must be atleast 6 characters long", + "description": "Error string shown to the user when password doesn't meet length criteria" + }, + "passwordTypeError": { + "message": "Password must be a string", + "description": "Error string shown to the user when password is not a string" + }, + "change": { + "message": "Change" + }, + "set": { + "message": "Set" + }, + "remove": { + "message": "Remove" } + } diff --git a/app/password_util.js b/app/password_util.js index b6c5e2b39..736a1c530 100644 --- a/app/password_util.js +++ b/app/password_util.js @@ -3,13 +3,13 @@ const { sha512 } = require('js-sha512'); const generateHash = (phrase) => phrase && sha512(phrase.trim()); const matchesHash = (phrase, hash) => phrase && sha512(phrase.trim()) === hash.trim(); -const validatePassword = (phrase) => { +const validatePassword = (phrase, i18n) => { if (typeof phrase !== 'string') { - return 'Password must be a string' + return i18n ? i18n('passwordTypeError') : 'Password must be a string' } if (phrase && phrase.trim().length < 6) { - return 'Password must be atleast 6 characters long'; + return i18n ? i18n('passwordLengthError') : 'Password must be atleast 6 characters long'; } // An empty password is still valid :P diff --git a/background.html b/background.html index 83b1806b8..ff0d7a76b 100644 --- a/background.html +++ b/background.html @@ -165,6 +165,35 @@ 0:00 + + + diff --git a/js/background.js b/js/background.js index faef150b6..831ad3044 100644 --- a/js/background.js +++ b/js/background.js @@ -602,6 +602,12 @@ } }); + Whisper.events.on('showPasswordDialog', options => { + if (appView) { + appView.showPasswordDialog(options); + } + }); + Whisper.events.on('calculatingPoW', ({ pubKey, timestamp }) => { try { const conversation = ConversationController.get(pubKey); diff --git a/js/views/app_view.js b/js/views/app_view.js index f21542c24..04c4b1514 100644 --- a/js/views/app_view.js +++ b/js/views/app_view.js @@ -189,5 +189,9 @@ }); this.el.append(dialog.el); }, + showPasswordDialog({ type, resolve, reject }) { + const dialog = Whisper.getPasswordDialogView(type, resolve, reject); + this.el.append(dialog.el); + }, }); })(); diff --git a/js/views/inbox_view.js b/js/views/inbox_view.js index dc52dbbbe..ea5339192 100644 --- a/js/views/inbox_view.js +++ b/js/views/inbox_view.js @@ -334,29 +334,37 @@ const ourNumber = textsecure.storage.user.getNumber(); clipboard.writeText(ourNumber); - const toast = new Whisper.MessageToastView({ - message: i18n('copiedPublicKey'), - }); - toast.$el.appendTo(this.$('.gutter')); - toast.render(); + this.showToastMessageInGutter(i18n('copiedPublicKey')); }), this._mainHeaderItem('editDisplayName', () => { window.Whisper.events.trigger('onEditProfile'); }), - ...this.passwordHeaderItems || [], ]; }, async onPasswordUpdated() { const hasPassword = await Signal.Data.getPasswordHash(); const items = this.getMainHeaderItems(); + + const showPasswordDialog = (type, resolve) => Whisper.events.trigger('showPasswordDialog', { + type, + resolve, + }); + + const passwordItem = (textKey, type) => this._mainHeaderItem( + textKey, + () => showPasswordDialog(type, () => { + this.showToastMessageInGutter(i18n(`${textKey}Success`)); + }) + ); + if (hasPassword) { items.push( - this._mainHeaderItem('changePassword'), - this._mainHeaderItem('removePassword') + passwordItem('changePassword', 'change'), + passwordItem('removePassword', 'remove') ); } else { items.push( - this._mainHeaderItem('setPassword') + passwordItem('setPassword', 'set') ); } @@ -369,6 +377,13 @@ onClick, }; }, + showToastMessageInGutter(message) { + const toast = new Whisper.MessageToastView({ + message, + }); + toast.$el.appendTo(this.$('.gutter')); + toast.render(); + }, }); Whisper.ExpiredAlertBanner = Whisper.View.extend({ diff --git a/js/views/nickname_dialog_view.js b/js/views/nickname_dialog_view.js index 7af20cb46..aa0af8143 100644 --- a/js/views/nickname_dialog_view.js +++ b/js/views/nickname_dialog_view.js @@ -7,7 +7,7 @@ window.Whisper = window.Whisper || {}; Whisper.NicknameDialogView = Whisper.View.extend({ - className: 'nickname-dialog modal', + className: 'loki-dialog nickname-dialog modal', templateName: 'nickname-dialog', initialize(options) { this.message = options.message; diff --git a/js/views/password_dialog_view.js b/js/views/password_dialog_view.js new file mode 100644 index 000000000..d919da1d2 --- /dev/null +++ b/js/views/password_dialog_view.js @@ -0,0 +1,214 @@ +/* global Whisper, i18n, _, Signal, passwordUtil */ + +// eslint-disable-next-line func-names +(function() { + 'use strict'; + + window.Whisper = window.Whisper || {}; + + const PasswordDialogView = Whisper.View.extend({ + className: 'loki-dialog password-dialog modal', + templateName: 'password-dialog', + initialize(options) { + this.type = options.type; + this.resolve = options.resolve; + this.okText = options.okText || i18n('ok'); + + this.reject = options.reject; + this.cancelText = options.cancelText || i18n('cancel'); + + this.title = options.title; + + this.render(); + this.updateUI(); + }, + events: { + keyup: 'onKeyup', + 'click .ok': 'ok', + 'click .cancel': 'cancel', + }, + render_attributes() { + return { + showCancel: !this.hideCancel, + cancel: this.cancelText, + ok: this.okText, + title: this.title, + }; + }, + async updateUI() { + if (this.disableOkButton()) { + this.$('.ok').prop('disabled', true); + } else { + this.$('.ok').prop('disabled', false); + } + }, + disableOkButton() { + const password = this.$('#password').val(); + return _.isEmpty(password); + }, + async validate() { + const password = this.$('#password').val(); + const passwordConfirmation = this.$('#password-confirmation').val(); + + const pairValidation = this.validatePasswordPair(password, passwordConfirmation); + const hashValidation = await this.validatePasswordHash(password); + + return (pairValidation || hashValidation); + }, + async validatePasswordHash(password) { + // Check if the password matches the hash we have stored + const hash = await Signal.Data.getPasswordHash(); + if (hash && !passwordUtil.matchesHash(password, hash)) { + return i18n('invalidPassword'); + } + return null; + }, + validatePasswordPair(password, passwordConfirmation) { + if (!_.isEmpty(password)) { + + // Check if the password is first valid + const passwordValidation = passwordUtil.validatePassword(password, i18n); + if (passwordValidation) { + return passwordValidation; + } + + // Check if the confirmation password is the same + if (!passwordConfirmation || password.trim() !== passwordConfirmation.trim()) { + return i18n('passwordsDoNotMatch'); + } + } + return null; + }, + okPressed() { + const password = this.$('#password').val(); + if (this.type === 'set') { + window.setPassword(password.trim()); + } else if (this.type === 'remove') { + window.setPassword(null, password.trim()); + } + }, + okErrored() { + if (this.type === 'set') { + this.showError(i18n('setPasswordFail')); + } else if (this.type === 'remove') { + this.showError(i18n('removePasswordFail')); + } + }, + async ok() { + const error = await this.validate(); + if (error) { + this.showError(error); + return; + } + + // Clear any errors + this.showError(null); + + try { + this.okPressed(); + + this.remove(); + if (this.resolve) { + this.resolve(); + } + } catch (e) { + this.okErrored(); + } + }, + cancel() { + this.remove(); + if (this.reject) { + this.reject(); + } + }, + onKeyup(event) { + this.updateUI(); + switch (event.key) { + case 'Enter': + this.ok(); + break; + case 'Escape': + case 'Esc': + this.cancel(); + break; + default: + return; + } + event.preventDefault(); + }, + focusCancel() { + this.$('.cancel').focus(); + }, + showError(message) { + if (_.isEmpty(message)) { + this.$('.error').text(''); + this.$('.error').hide(); + } else { + this.$('.error').text(`Error: ${message}`); + this.$('.error').show(); + } + }, + }); + + const ChangePasswordDialogView = PasswordDialogView.extend({ + templateName: 'password-change-dialog', + disableOkButton() { + const oldPassword = this.$('#old-password').val(); + const newPassword = this.$('#new-password').val(); + return _.isEmpty(oldPassword) || _.isEmpty(newPassword); + }, + async validate() { + const oldPassword = this.$('#old-password').val(); + + // Validate the old password + if (!_.isEmpty(oldPassword) ) { + const oldPasswordValidation = passwordUtil.validatePassword(oldPassword, i18n); + if (oldPasswordValidation) { + return oldPasswordValidation; + } + } else { + return i18n('typeInOldPassword'); + } + + const password = this.$('#new-password').val(); + const passwordConfirmation = this.$('#new-password-confirmation').val(); + + const pairValidation = this.validatePasswordPair(password, passwordConfirmation); + const hashValidation = await this.validatePasswordHash(oldPassword); + + return pairValidation || hashValidation; + }, + okPressed() { + const oldPassword = this.$('#old-password').val(); + const newPassword = this.$('#new-password').val(); + window.setPassword(newPassword.trim(), oldPassword.trim()); + }, + okErrored() { + this.showError(i18n('changePasswordFail')); + }, + }); + + Whisper.getPasswordDialogView = (type, resolve, reject) => { + + // This is a differently styled dialog + if (type === 'change') { + return new ChangePasswordDialogView({ + title: i18n('changePassword'), + okTitle: i18n('change'), + resolve, + reject, + }); + } + + // Set and Remove is basically the same UI + const title = type === 'remove' ? i18n('removePassword') : i18n('setPassword'); + const okTitle = type === 'remove' ? i18n('remove') : i18n('set'); + return new PasswordDialogView({ + title, + okTitle, + type, + resolve, + reject, + }); + }; +})(); diff --git a/js/views/standalone_registration_view.js b/js/views/standalone_registration_view.js index 6f32d874a..1c02a1407 100644 --- a/js/views/standalone_registration_view.js +++ b/js/views/standalone_registration_view.js @@ -193,12 +193,14 @@ const input = this.trim(this.$passwordInput.val()); const confirmationInput = this.trim(this.$passwordConfirmationInput.val()); - const error = passwordUtil.validatePassword(input); - if (error) + const error = passwordUtil.validatePassword(input, i18n); + if (error) { return error; + } - if (input !== confirmationInput) + if (input !== confirmationInput) { return 'Password don\'t match'; + } return null; }, @@ -207,13 +209,30 @@ if (passwordValidation) { this.$passwordInput.addClass('error-input'); this.$passwordConfirmationInput.addClass('error-input'); + + this.$passwordInput.removeClass('match-input'); + this.$passwordConfirmationInput.removeClass('match-input'); + this.$passwordInputError.text(passwordValidation); this.$passwordInputError.show(); + } else { this.$passwordInput.removeClass('error-input'); this.$passwordConfirmationInput.removeClass('error-input'); + this.$passwordInputError.text(''); this.$passwordInputError.hide(); + + // Show green box around inputs that match + const input = this.trim(this.$passwordInput.val()); + const confirmationInput = this.trim(this.$passwordConfirmationInput.val()); + if (input && input === confirmationInput) { + this.$passwordInput.addClass('match-input'); + this.$passwordConfirmationInput.addClass('match-input'); + } else { + this.$passwordInput.removeClass('match-input'); + this.$passwordConfirmationInput.removeClass('match-input'); + } } }, trim(value) { diff --git a/password_preload.js b/password_preload.js index f8dcb3eb3..1a857a58e 100644 --- a/password_preload.js +++ b/password_preload.js @@ -24,6 +24,8 @@ window.getEnvironment = () => config.environment; window.getVersion = () => config.version; window.getAppInstance = () => config.appInstance; +window.passwordUtil = require('./app/password_util'); + window.onLogin = (passPhrase) => new Promise((resolve, reject) => { ipcRenderer.once('password-window-login-response', (event, error) => { if (error) { diff --git a/stylesheets/_conversation.scss b/stylesheets/_conversation.scss index bfda09310..4c23e6bf5 100644 --- a/stylesheets/_conversation.scss +++ b/stylesheets/_conversation.scss @@ -353,7 +353,7 @@ } } -.nickname-dialog { +.loki-dialog { display: flex; align-items: center; justify-content: center; @@ -366,55 +366,62 @@ border-radius: $border-radius; overflow: auto; box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, 0.3); + } - .buttons { - - button { - float: right; - margin-left: 10px; - background-color: $color-loki-green; - border-radius: 100px; - padding: 5px 15px; - border: 1px solid $color-loki-green; - color: white; - outline: none; + button { + float: right; + margin-left: 10px; + background-color: $color-loki-green; + border-radius: 100px; + padding: 5px 15px; + border: 1px solid $color-loki-green; + color: white; + outline: none; + + &:hover, &:disabled { + background-color: $color-loki-green-dark; + border-color: $color-loki-green-dark; + } - &:hover { - background-color: $color-loki-green-dark; - border-color: $color-loki-green-dark; - } - } + &:disabled { + cursor: not-allowed; } + } + + input { + width: 100%; + padding: 8px; + margin-bottom: 15px; + border: 0; + outline: none; + border-radius: 4px; + background-color: $color-loki-light-gray; - input { - width: 100%; - padding: 8px; - margin-bottom: 15px; - border: 0; + &:focus { outline: none; - border-radius: 4px; - background-color: $color-loki-light-gray; } + } - h4 { - margin-top: 8px; - margin-bottom: 16px; - white-space: -moz-pre-wrap; /* Mozilla */ - white-space: -hp-pre-wrap; /* HP printers */ - white-space: -o-pre-wrap; /* Opera 7 */ - white-space: -pre-wrap; /* Opera 4-6 */ - white-space: pre-wrap; /* CSS 2.1 */ - white-space: pre-line; /* CSS 3 (and 2.1 as well, actually) */ - word-wrap: break-word; /* IE */ - word-break: break-all; - } + h4 { + margin-top: 8px; + margin-bottom: 16px; + white-space: -moz-pre-wrap; /* Mozilla */ + white-space: -hp-pre-wrap; /* HP printers */ + white-space: -o-pre-wrap; /* Opera 7 */ + white-space: -pre-wrap; /* Opera 4-6 */ + white-space: pre-wrap; /* CSS 2.1 */ + white-space: pre-line; /* CSS 3 (and 2.1 as well, actually) */ + word-wrap: break-word; /* IE */ + word-break: break-all; + } +} - .message { - font-style: italic; - color: $grey; - font-size: 12px; - margin-bottom: 16px; - } +.nickname-dialog { + .message { + font-style: italic; + color: $grey; + font-size: 12px; + margin-bottom: 16px; } } diff --git a/stylesheets/_global.scss b/stylesheets/_global.scss index 3321781a2..d7f670714 100644 --- a/stylesheets/_global.scss +++ b/stylesheets/_global.scss @@ -993,6 +993,14 @@ textarea { outline: none; } } + + .match-input { + border: 3px solid $color-loki-green; + + &:focus { + outline: none; + } + } } @media (min-height: 750px) and (min-width: 700px) { diff --git a/stylesheets/_theme_dark.scss b/stylesheets/_theme_dark.scss index 7d9137355..3c1194d9b 100644 --- a/stylesheets/_theme_dark.scss +++ b/stylesheets/_theme_dark.scss @@ -93,34 +93,34 @@ body.dark-theme { } } - .nickname-dialog { + .loki-dialog { .content { background: $color-black; color: $color-dark-05; + } - .buttons { - button { - background-color: $color-dark-85; - border-radius: $border-radius; - border: 1px solid $color-dark-60; - color: $color-dark-05; - - &:hover { - background-color: $color-dark-70; - border-color: $color-dark-55; - } - } - } + button { + background-color: $color-dark-85; + border-radius: $border-radius; + border: 1px solid $color-dark-60; + color: $color-dark-05; - input { - color: $color-dark-05; + &:hover { background-color: $color-dark-70; border-color: $color-dark-55; } + } - .message { - color: $color-light-35; - } + input { + color: $color-dark-05; + background-color: $color-dark-70; + border-color: $color-dark-55; + } + } + + .nickname-dialog { + .message { + color: $color-light-35; } } diff --git a/test/index.html b/test/index.html index b03374a61..81a82bd38 100644 --- a/test/index.html +++ b/test/index.html @@ -403,6 +403,7 @@ + From 9e995bde6c26cf9ea744c0697cdaca83ace83f4e Mon Sep 17 00:00:00 2001 From: Mikunj Date: Mon, 10 Dec 2018 09:25:36 +1100 Subject: [PATCH 09/11] Review fixes. Updated tests. --- _locales/en/messages.json | 6 ++- app/password_util.js | 24 ++++++++--- app/sql.js | 7 ++-- js/views/standalone_registration_view.js | 5 +++ test/app/password_util_test.js | 52 ++++++++++++++++++++---- 5 files changed, 76 insertions(+), 18 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index e168d78c8..be7b4c1e3 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1733,13 +1733,17 @@ "message": "Password changed" }, "passwordLengthError": { - "message": "Password must be atleast 6 characters long", + "message": "Password must be between 6 and 50 characters long", "description": "Error string shown to the user when password doesn't meet length criteria" }, "passwordTypeError": { "message": "Password must be a string", "description": "Error string shown to the user when password is not a string" }, + "passwordCharacterError": { + "message": "Password must only contain letters, numbers and symbols", + "description": "Error string shown to the user when password contains an invalid character" + }, "change": { "message": "Change" }, diff --git a/app/password_util.js b/app/password_util.js index 736a1c530..d3bbaa44d 100644 --- a/app/password_util.js +++ b/app/password_util.js @@ -1,18 +1,30 @@ const { sha512 } = require('js-sha512'); +const ERRORS = { + TYPE: 'Password must be a string', + LENGTH: 'Password must be between 6 and 50 characters long', + CHARACTER: 'Password must only contain letters, numbers and symbols', +}; + const generateHash = (phrase) => phrase && sha512(phrase.trim()); const matchesHash = (phrase, hash) => phrase && sha512(phrase.trim()) === hash.trim(); const validatePassword = (phrase, i18n) => { - if (typeof phrase !== 'string') { - return i18n ? i18n('passwordTypeError') : 'Password must be a string' + if (!phrase || typeof phrase !== 'string') { + return i18n ? i18n('passwordTypeError') : ERRORS.TYPE; + } + + const trimmed = phrase.trim(); + if (trimmed.length < 6 || trimmed.length > 50) { + return i18n ? i18n('passwordLengthError') : ERRORS.LENGTH; } - if (phrase && phrase.trim().length < 6) { - return i18n ? i18n('passwordLengthError') : 'Password must be atleast 6 characters long'; + // Restrict characters to letters, numbers and symbols + const characterRegex = /^[a-zA-Z0-9-!()._`~@#$%^&*+=[\]{}|<>,;:]+$/ + if (!characterRegex.test(trimmed)) { + return i18n ? i18n('passwordCharacterError') : ERRORS.CHARACTER; } - // An empty password is still valid :P return null; } @@ -20,4 +32,4 @@ module.exports = { generateHash, matchesHash, validatePassword, -}; \ No newline at end of file +}; diff --git a/app/sql.js b/app/sql.js index ea2509e17..be40b0df5 100644 --- a/app/sql.js +++ b/app/sql.js @@ -601,8 +601,9 @@ async function removeIndexedDBFiles() { } // Password hash +const PASS_HASH_ID = 'passHash'; async function getPasswordHash() { - const item = await getItemById('passHash'); + const item = await getItemById(PASS_HASH_ID); return item && item.value; } async function savePasswordHash(hash) { @@ -610,11 +611,11 @@ async function savePasswordHash(hash) { return removePasswordHash(); } - const data = { id: 'passHash', value: hash }; + const data = { id: PASS_HASH_ID, value: hash }; return createOrUpdateItem(data); } async function removePasswordHash() { - return removeItemById('passHash'); + return removeItemById(PASS_HASH_ID); } // Groups diff --git a/js/views/standalone_registration_view.js b/js/views/standalone_registration_view.js index 1c02a1407..be8753f94 100644 --- a/js/views/standalone_registration_view.js +++ b/js/views/standalone_registration_view.js @@ -193,6 +193,11 @@ const input = this.trim(this.$passwordInput.val()); const confirmationInput = this.trim(this.$passwordConfirmationInput.val()); + // If user hasn't set a value then skip + if (!input && !confirmationInput) { + return null; + } + const error = passwordUtil.validatePassword(input, i18n); if (error) { return error; diff --git a/test/app/password_util_test.js b/test/app/password_util_test.js index 8256fe6b5..06293261e 100644 --- a/test/app/password_util_test.js +++ b/test/app/password_util_test.js @@ -7,12 +7,12 @@ describe('Password Util', () => { it('generates the same hash for the same phrase', () => { const first = passwordUtil.generateHash('phrase'); const second = passwordUtil.generateHash('phrase'); - assert.equal(first, second); + assert.strictEqual(first, second); }); it('generates different hashes for different phrases', () => { const first = passwordUtil.generateHash('0'); const second = passwordUtil.generateHash('1'); - assert.notEqual(first, second); + assert.notStrictEqual(first, second); }); }); @@ -33,25 +33,61 @@ describe('Password Util', () => { const valid = [ '123456', '1a5b3C6g', - 'ABC#DE#F$IJ', - 'AabcDegf', + ')CZcy@ccHa', + 'C$D--M;Xv+', + 'X8-;!47IW|', + 'Oi74ZpoSx,p', + '>]K1*g^swHW0]F6}{', + 'TiJf@lk^jsO^z8MUn%)[Sd~UPQ)ci9CGS@jb<^', + '$u&%{r]apg#G@3dQdCkB_p8)gxhNFr=K&yfM_M8O&2Z.vQyvx', + 'bf^OMnYku*iX;{Piw_0zvz', + '#'.repeat(50), ]; valid.forEach(pass => { assert.isNull(passwordUtil.validatePassword(pass)); }); }); - it('should return an error string if password is invalid', () => { + it('should return an error if password is not a string', () => { const invalid = [ 0, 123456, [], {}, - '123', - '1234$', + null, + undefined, ]; invalid.forEach(pass => { - assert.isNotNull(passwordUtil.validatePassword(pass)); + assert.strictEqual(passwordUtil.validatePassword(pass), 'Password must be a string'); + }); + }); + + it('should return an error if password is not between 6 and 50 characters',() => { + const invalid = [ + 'a', + 'abcde', + '#'.repeat(51), + '#'.repeat(100), + ]; + invalid.forEach(pass => { + assert.strictEqual(passwordUtil.validatePassword(pass), 'Password must be between 6 and 50 characters long'); + }); + }); + + it('should return an error if password has invalid characters', () => { + const invalid = [ + 'ʍʪց3Wͪ݌bΉf', + ')É{b)͎ÔȩҜ٣', + 'ߓܑ˿G֖=3¤)P', + 'ݴ`ԚfĬ8ӝrH(', + 'e̹ωͻܺȬۺ#dӄ', + '谀뤼筎笟ꅅ栗塕카ꭴ', + '俈꛷࿩迭䰡钑럭䛩銛뤙', + '봟㉟ⓓ༭꽫㊡䶷쒨⻯颰', + '<@ȦƘΉوۉaҋ<', + ]; + invalid.forEach(pass => { + assert.strictEqual(passwordUtil.validatePassword(pass), 'Password must only contain letters, numbers and symbols'); }); }); }); From 7d0d244e7c2d8fbafdb7c71ebe0be9781bd17596 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Mon, 10 Dec 2018 11:34:34 +1100 Subject: [PATCH 10/11] Added localised error messages. --- _locales/en/messages.json | 3 +++ main.js | 9 ++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index be7b4c1e3..f562ef89b 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1708,6 +1708,9 @@ "typeInOldPassword": { "message": "Please type in your old password" }, + "invalidOldPassword": { + "message": "Old password is invalid" + }, "invalidPassword": { "message": "Invalid password" }, diff --git a/main.js b/main.js index 50e4f2b26..abbb8b418 100644 --- a/main.js +++ b/main.js @@ -959,7 +959,8 @@ ipc.on('password-window-login', async (event, passPhrase) => { passwordWindow = null; } } catch (e) { - sendResponse('Invalid password'); + const localisedError = locale.messages.invalidPassword.message; + sendResponse(localisedError || 'Invalid password'); } }); @@ -971,7 +972,8 @@ ipc.on('set-password', async (event, passPhrase, oldPhrase) => { const hash = await sql.getPasswordHash(); const hashMatches = oldPhrase && passwordUtil.matchesHash(oldPhrase, hash); if (hash && !hashMatches) { - sendResponse('Failed to set password: Old password provided is invalid'); + const incorrectOldPassword = locale.messages.invalidOldPassword.message; + sendResponse(incorrectOldPassword || 'Failed to set password: Old password provided is invalid'); return; } @@ -987,7 +989,8 @@ ipc.on('set-password', async (event, passPhrase, oldPhrase) => { sendResponse(); } catch (e) { - sendResponse('Failed to set password'); + const localisedError = locale.messages.setPasswordFail.message; + sendResponse(localisedError || 'Failed to set password'); } }); From 1303603db90ccf14ee6e71ee0cd3d0289d3f81a6 Mon Sep 17 00:00:00 2001 From: Beaudan Campbell-Brown Date: Mon, 10 Dec 2018 14:44:56 +1100 Subject: [PATCH 11/11] Apply suggestions from code review Co-Authored-By: Mikunj --- app/sql.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/sql.js b/app/sql.js index be40b0df5..050ef7d1b 100644 --- a/app/sql.js +++ b/app/sql.js @@ -193,7 +193,7 @@ async function setupSQLCipher(instance, { key }) { const deriveKey = HEX_KEY.test(key); // https://www.zetetic.net/sqlcipher/sqlcipher-api/#key - const value = deriveKey ? `'${key}'` : `"x'${key}'"` + const value = deriveKey ? `'${key}'` : `"x'${key}'"`; await instance.run(`PRAGMA key = ${value};`); } @@ -204,7 +204,7 @@ async function setSQLPassword(password) { // If the password isn't hex then we need to derive a key from it const deriveKey = HEX_KEY.test(password); - const value = deriveKey ? `'${password}'` : `"x'${password}'"` + const value = deriveKey ? `'${password}'` : `"x'${password}'"`; await db.run(`PRAGMA rekey = ${value};`); }