mirror of https://github.com/keeweb/keeweb
config encryption with a key from keychain
parent
3b3c95e57c
commit
b9f37c22d9
|
@ -19,22 +19,20 @@ const StartProfiler = {
|
|||
operations.unshift({ name: 'fetching', elapsed: networkTime });
|
||||
|
||||
const time = Math.round(performance.now());
|
||||
const details = operations.map(op => `${op.name}=${Math.round(op.elapsed)}ms`).join(', ');
|
||||
let message = `Started in ${time}ms: ${details}.`;
|
||||
|
||||
if (this.appProfile) {
|
||||
message += ` Electron app started in ${this.appProfile.totalTime}ms: `;
|
||||
message +=
|
||||
this.appProfile.timings
|
||||
.map(op => `${op.name}=${Math.round(op.elapsed)}ms`)
|
||||
.join(', ') + '.';
|
||||
}
|
||||
|
||||
logger.info(message);
|
||||
this.printReport('App', operations, time);
|
||||
},
|
||||
|
||||
reportAppProfile(data) {
|
||||
this.appProfile = data;
|
||||
this.printReport('Electron app', data.timings, data.totalTime);
|
||||
},
|
||||
|
||||
printReport(name, operations, totalTime) {
|
||||
const message =
|
||||
`${name} started in ${totalTime}ms: ` +
|
||||
operations.map(op => `${op.name}=${Math.round(op.elapsed)}ms`).join(', ');
|
||||
|
||||
logger.info(message);
|
||||
},
|
||||
|
||||
getNetworkTime() {
|
||||
|
|
|
@ -136,6 +136,12 @@ const Launcher = {
|
|||
createFsWatcher(path) {
|
||||
return this.req('fs').watch(path, { persistent: false });
|
||||
},
|
||||
loadConfig(name) {
|
||||
return this.remoteApp().loadConfig(name);
|
||||
},
|
||||
saveConfig(name, data) {
|
||||
return this.remoteApp().saveConfig(name, data);
|
||||
},
|
||||
ensureRunnable(path) {
|
||||
if (process.platform !== 'win32') {
|
||||
const fs = this.req('fs');
|
||||
|
|
|
@ -5,27 +5,15 @@ import { Logger } from 'util/logger';
|
|||
const logger = new Logger('settings');
|
||||
|
||||
const SettingsStore = {
|
||||
fileName(key) {
|
||||
return `${key}.json`;
|
||||
},
|
||||
|
||||
load(key) {
|
||||
if (Launcher) {
|
||||
return Launcher.loadConfig(key).catch(err => {
|
||||
logger.error(`Error loading ${key}`, err);
|
||||
});
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
if (Launcher) {
|
||||
const settingsFile = Launcher.getUserDataPath(this.fileName(key));
|
||||
Launcher.fileExists(settingsFile, exists => {
|
||||
if (exists) {
|
||||
Launcher.readFile(settingsFile, 'utf8', data => {
|
||||
return this.parseData(key, data, resolve);
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const data = localStorage[StringFormat.camelCase(key)];
|
||||
return this.parseData(key, data, resolve);
|
||||
}
|
||||
const data = localStorage[StringFormat.camelCase(key)];
|
||||
return this.parseData(key, data, resolve);
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -37,25 +25,20 @@ const SettingsStore = {
|
|||
resolve();
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('Error loading ' + key, e);
|
||||
logger.error(`Error loading ${key}`, e);
|
||||
resolve();
|
||||
}
|
||||
},
|
||||
|
||||
save(key, data) {
|
||||
return new Promise(resolve => {
|
||||
if (Launcher) {
|
||||
const settingsFile = Launcher.getUserDataPath(this.fileName(key));
|
||||
data = JSON.stringify(data);
|
||||
Launcher.writeFile(settingsFile, data, err => {
|
||||
if (err) {
|
||||
logger.error(`Error saving ${key}`, err);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
} else if (typeof localStorage !== 'undefined') {
|
||||
if (Launcher) {
|
||||
return Launcher.saveConfig(key, JSON.stringify(data)).catch(err => {
|
||||
logger.error(`Error saving ${key}`, err);
|
||||
});
|
||||
}
|
||||
return Promise.resolve().then(() => {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage[StringFormat.camelCase(key)] = JSON.stringify(data);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
256
desktop/app.js
256
desktop/app.js
|
@ -19,9 +19,10 @@ let mainWindowMaximized = false;
|
|||
let usbBinding = null;
|
||||
|
||||
const windowPositionFileName = 'window-position.json';
|
||||
const appSettingsFileName = 'app-settings.json';
|
||||
const portableConfigFileName = 'keeweb-portable.json';
|
||||
|
||||
const isDev = !__dirname.endsWith('.asar');
|
||||
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
if (!gotTheLock) {
|
||||
app.quit();
|
||||
|
@ -37,7 +38,6 @@ initUserDataDir();
|
|||
|
||||
let openFile = process.argv.filter(arg => /\.kdbx$/i.test(arg))[0];
|
||||
|
||||
const isDev = !__dirname.endsWith('.asar');
|
||||
const htmlPath =
|
||||
(isDev && process.env.KEEWEB_HTML_PATH) || 'file://' + path.join(__dirname, 'index.html');
|
||||
const showDevToolsOnStart =
|
||||
|
@ -63,7 +63,18 @@ perfTimestamps?.push({ name: 'defining args', ts: process.hrtime() });
|
|||
setEnv();
|
||||
setDevAppIcon();
|
||||
|
||||
const appSettings = readAppSettings() || {};
|
||||
let configEncryptionKey;
|
||||
let appSettings;
|
||||
|
||||
const settingsPromise = loadSettingsEncryptionKey().then(key => {
|
||||
configEncryptionKey = key;
|
||||
perfTimestamps?.push({ name: 'loading settings key', ts: process.hrtime() });
|
||||
|
||||
return loadConfig('app-settings').then(settings => {
|
||||
appSettings = settings || {};
|
||||
perfTimestamps?.push({ name: 'reading app settings', ts: process.hrtime() });
|
||||
});
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (restartPending) {
|
||||
|
@ -78,12 +89,20 @@ app.on('window-all-closed', () => {
|
|||
app.on('ready', () => {
|
||||
perfTimestamps?.push({ name: 'app on ready', ts: process.hrtime() });
|
||||
appReady = true;
|
||||
setSystemAppearance();
|
||||
createMainWindow();
|
||||
setGlobalShortcuts(appSettings);
|
||||
subscribePowerEvents();
|
||||
deleteOldTempFiles();
|
||||
hookRequestHeaders();
|
||||
|
||||
settingsPromise
|
||||
.then(() => {
|
||||
setSystemAppearance();
|
||||
createMainWindow();
|
||||
setGlobalShortcuts(appSettings);
|
||||
subscribePowerEvents();
|
||||
deleteOldTempFiles();
|
||||
hookRequestHeaders();
|
||||
})
|
||||
.catch(e => {
|
||||
electron.dialog.showErrorBox('KeeWeb', 'Error loading app: ' + e);
|
||||
process.exit(2);
|
||||
});
|
||||
});
|
||||
app.on('open-file', (e, path) => {
|
||||
e.preventDefault();
|
||||
|
@ -156,28 +175,10 @@ app.getMainWindow = function() {
|
|||
return mainWindow;
|
||||
};
|
||||
app.setGlobalShortcuts = setGlobalShortcuts;
|
||||
app.reqNative = function(mod) {
|
||||
const fileName = `${mod}-${process.platform}-${process.arch}.node`;
|
||||
const binding = require(`@keeweb/keeweb-native-modules/${fileName}`);
|
||||
if (mod === 'usb') {
|
||||
usbBinding = initUsb(binding);
|
||||
}
|
||||
return binding;
|
||||
};
|
||||
app.reqNative = reqNative;
|
||||
app.showAndFocusMainWindow = showAndFocusMainWindow;
|
||||
app.isPortable = isPortable;
|
||||
|
||||
function readAppSettings() {
|
||||
const appSettingsFilePath = path.join(app.getPath('userData'), appSettingsFileName);
|
||||
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(appSettingsFilePath, 'utf8'));
|
||||
} catch (e) {
|
||||
return null;
|
||||
} finally {
|
||||
perfTimestamps?.push({ name: 'reading app settings', ts: process.hrtime() });
|
||||
}
|
||||
}
|
||||
app.loadConfig = loadConfig;
|
||||
app.saveConfig = saveConfig;
|
||||
|
||||
function setSystemAppearance() {
|
||||
if (process.platform === 'darwin') {
|
||||
|
@ -539,6 +540,10 @@ function setPortableAndExecPath() {
|
|||
break;
|
||||
}
|
||||
|
||||
if (isDev && process.env.KEEWEB_IS_PORTABLE) {
|
||||
isPortable = !!JSON.parse(process.env.KEEWEB_IS_PORTABLE);
|
||||
}
|
||||
|
||||
perfTimestamps?.push({ name: 'portable check', ts: process.hrtime() });
|
||||
}
|
||||
|
||||
|
@ -623,7 +628,10 @@ function setDevAppIcon() {
|
|||
|
||||
function hookRequestHeaders() {
|
||||
electron.session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => {
|
||||
if (!details.url.startsWith('ws:')) {
|
||||
if (
|
||||
!details.url.startsWith('ws:') &&
|
||||
!details.url.startsWith('https://plugins.keeweb.info/')
|
||||
) {
|
||||
delete details.requestHeaders.Origin;
|
||||
}
|
||||
callback({ requestHeaders: details.requestHeaders });
|
||||
|
@ -672,26 +680,6 @@ function coerceMainWindowPositionToConnectedDisplay() {
|
|||
updateMainWindowPosition();
|
||||
}
|
||||
|
||||
function initUsb(binding) {
|
||||
Object.keys(EventEmitter.prototype).forEach(key => {
|
||||
binding[key] = EventEmitter.prototype[key];
|
||||
});
|
||||
|
||||
binding.on('newListener', () => {
|
||||
if (binding.listenerCount('attach') === 0 && binding.listenerCount('detach') === 0) {
|
||||
binding._enableHotplugEvents();
|
||||
}
|
||||
});
|
||||
|
||||
binding.on('removeListener', () => {
|
||||
if (binding.listenerCount('attach') === 0 && binding.listenerCount('detach') === 0) {
|
||||
binding._disableHotplugEvents();
|
||||
}
|
||||
});
|
||||
|
||||
return binding;
|
||||
}
|
||||
|
||||
function reportStartProfile() {
|
||||
if (!perfTimestamps) {
|
||||
return;
|
||||
|
@ -719,3 +707,169 @@ function reportStartProfile() {
|
|||
const startProfile = { totalTime, timings };
|
||||
emitRemoteEvent('start-profile', startProfile);
|
||||
}
|
||||
|
||||
function reqNative(mod) {
|
||||
const fileName = `${mod}-${process.platform}-${process.arch}.node`;
|
||||
const binding = require(`@keeweb/keeweb-native-modules/${fileName}`);
|
||||
if (mod === 'usb') {
|
||||
usbBinding = initUsb(binding);
|
||||
}
|
||||
return binding;
|
||||
}
|
||||
|
||||
function initUsb(binding) {
|
||||
Object.keys(EventEmitter.prototype).forEach(key => {
|
||||
binding[key] = EventEmitter.prototype[key];
|
||||
});
|
||||
|
||||
binding.on('newListener', () => {
|
||||
if (binding.listenerCount('attach') === 0 && binding.listenerCount('detach') === 0) {
|
||||
binding._enableHotplugEvents();
|
||||
}
|
||||
});
|
||||
|
||||
binding.on('removeListener', () => {
|
||||
if (binding.listenerCount('attach') === 0 && binding.listenerCount('detach') === 0) {
|
||||
binding._disableHotplugEvents();
|
||||
}
|
||||
});
|
||||
|
||||
return binding;
|
||||
}
|
||||
|
||||
function loadSettingsEncryptionKey() {
|
||||
return Promise.resolve().then(() => {
|
||||
if (isPortable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const explicitlyDisabledFile = path.join(app.getPath('userData'), 'disable-keytar');
|
||||
if (fs.existsSync(explicitlyDisabledFile)) {
|
||||
// TODO: remove this fallback if everything goes well on v1.15
|
||||
// This is a protective measure if everything goes terrible with native modules
|
||||
// For example, the app can crash and it won't be possible to use it at all
|
||||
return null;
|
||||
}
|
||||
|
||||
const keytar = reqNative('keytar');
|
||||
return new Promise((resolve, reject) => {
|
||||
keytar.getPassword('KeeWeb', 'settings-key', (err, key) => {
|
||||
if (err) {
|
||||
return reject('Error loading settings key from keytar');
|
||||
}
|
||||
if (key) {
|
||||
return resolve(Buffer.from(key, 'hex'));
|
||||
}
|
||||
key = require('crypto').randomBytes(48);
|
||||
keytar.setPassword('KeeWeb', 'settings-key', key.toString('hex'), err => {
|
||||
if (err) {
|
||||
return reject('Error saving settings key in keytar');
|
||||
}
|
||||
migrateOldConfigs(key).then(() => resolve(key));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function loadConfig(name) {
|
||||
const ext = configEncryptionKey ? 'dat' : 'json';
|
||||
const configFilePath = path.join(app.getPath('userData'), `${name}.${ext}`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(configFilePath, (err, data) => {
|
||||
if (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
resolve(null);
|
||||
} else {
|
||||
reject(`Error reading config ${name}: ${err}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (configEncryptionKey) {
|
||||
const key = configEncryptionKey.slice(0, 32);
|
||||
const iv = configEncryptionKey.slice(32, 48);
|
||||
|
||||
const crypto = require('crypto');
|
||||
const cipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
|
||||
|
||||
data = Buffer.concat([cipher.update(data), cipher.final()]).toString('utf8');
|
||||
} else {
|
||||
data = data.toString('utf8');
|
||||
}
|
||||
|
||||
data = JSON.parse(data);
|
||||
|
||||
resolve(data);
|
||||
} catch (err) {
|
||||
reject(`Error reading config data ${name}: ${err}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function saveConfig(name, data, key) {
|
||||
if (!key) {
|
||||
key = configEncryptionKey;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
if (typeof data !== 'string') {
|
||||
data = JSON.stringify(data);
|
||||
}
|
||||
|
||||
data = Buffer.from(data);
|
||||
|
||||
const crypto = require('crypto');
|
||||
const cipher = crypto.createCipheriv(
|
||||
'aes-256-cbc',
|
||||
key.slice(0, 32),
|
||||
key.slice(32, 48)
|
||||
);
|
||||
|
||||
data = Buffer.concat([cipher.update(data), cipher.final()]);
|
||||
} catch (err) {
|
||||
return reject(`Error writing config data ${name}: ${err}`);
|
||||
}
|
||||
|
||||
const configFilePath = path.join(app.getPath('userData'), `${name}.dat`);
|
||||
fs.writeFile(configFilePath, data, err => {
|
||||
if (err) {
|
||||
reject(`Error writing config ${name}: ${err}`);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: delete in 2021
|
||||
function migrateOldConfigs(key) {
|
||||
const knownConfigs = [
|
||||
'file-info',
|
||||
'app-settings',
|
||||
'runtime-data',
|
||||
'update-info',
|
||||
'plugin-gallery',
|
||||
'plugins'
|
||||
];
|
||||
|
||||
const promises = [];
|
||||
|
||||
for (const configName of knownConfigs) {
|
||||
promises.push(
|
||||
loadConfig(configName).then(data => {
|
||||
if (data) {
|
||||
return saveConfig(configName, data, key).then(() => {
|
||||
fs.unlinkSync(path.join(app.getPath('userData'), `${configName}.json`));
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
|
|
@ -103,7 +103,7 @@
|
|||
"test": "grunt test",
|
||||
"postinstall": "cd desktop && npm install",
|
||||
"build-beta": "grunt --beta && cp dist/index.html ../keeweb-beta/index.html && cd ../keeweb-beta && git add index.html && git commit -a -m 'beta' && git push origin master",
|
||||
"electron": "cross-env ELECTRON_DISABLE_SECURITY_WARNINGS=1 KEEWEB_HTML_PATH=http://localhost:8085 electron desktop",
|
||||
"electron": "cross-env KEEWEB_IS_PORTABLE=0 ELECTRON_DISABLE_SECURITY_WARNINGS=1 KEEWEB_HTML_PATH=http://localhost:8085 electron desktop",
|
||||
"dev": "grunt dev",
|
||||
"dev-desktop-macos": "grunt dev-desktop-darwin --skip-sign",
|
||||
"dev-desktop-windows": "grunt dev-desktop-win32 --skip-sign",
|
||||
|
|
|
@ -2,6 +2,7 @@ Release notes
|
|||
-------------
|
||||
##### v1.15.0 (WIP)
|
||||
`+` YubiKey integration in two modes: OATH and Challenge-Response
|
||||
`+` configs are now encrypted with a key stored in keychain
|
||||
`+` #557: Argon2 speed improvements in desktop apps
|
||||
`+` #1503: ARM64 Windows support
|
||||
`+` #1480: option to create a portable installation
|
||||
|
|
Loading…
Reference in New Issue