Store SQLCipher decryption key in separate file
First, we write the key a whole lot less. We write it on creation, then never again. Second, it's in a file we control very closely. Instead of blindly regenerating the key if the target file generates an error on read, we block startup unless the error is 'ENOENT' - the file isn't there at all. This still allows for the key.txt file to be deleted or corrupted somehow, but it should be a lot less common than the high-traffic config.json used for window location and media permissions.
This commit is contained in:
parent
006700f9a2
commit
496ebf2a47
3 changed files with 93 additions and 39 deletions
62
app/key_management.js
Normal file
62
app/key_management.js
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
const { app } = require('electron');
|
||||||
|
|
||||||
|
const ENCODING = 'utf8';
|
||||||
|
const userDataPath = app.getPath('userData');
|
||||||
|
const targetPath = path.join(userDataPath, 'key.txt');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
get,
|
||||||
|
set,
|
||||||
|
initialize,
|
||||||
|
remove,
|
||||||
|
};
|
||||||
|
|
||||||
|
function get() {
|
||||||
|
try {
|
||||||
|
const key = fs.readFileSync(targetPath, ENCODING);
|
||||||
|
console.log('key/get: Successfully read key file');
|
||||||
|
return key;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'ENOENT') {
|
||||||
|
console.log('key/get: Could not find key file, returning null');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function set(key) {
|
||||||
|
console.log('key/set: Saving key to disk');
|
||||||
|
fs.writeFileSync(targetPath, key, ENCODING);
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove() {
|
||||||
|
console.log('key/remove: Deleting key from disk');
|
||||||
|
fs.unlinkSync(targetPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initialize({ userConfig }) {
|
||||||
|
const keyFromConfig = userConfig.get('key');
|
||||||
|
const keyFromStore = get();
|
||||||
|
|
||||||
|
let key = keyFromStore || keyFromConfig;
|
||||||
|
if (!key) {
|
||||||
|
console.log(
|
||||||
|
'key/initialize: Generating new encryption key, since we did not find it on disk'
|
||||||
|
);
|
||||||
|
// https://www.zetetic.net/sqlcipher/sqlcipher-api/#key
|
||||||
|
key = crypto.randomBytes(32).toString('hex');
|
||||||
|
set(key);
|
||||||
|
} else if (keyFromConfig) {
|
||||||
|
set(key);
|
||||||
|
console.log('key/initialize: Removing key from config.json');
|
||||||
|
userConfig.delete('key');
|
||||||
|
}
|
||||||
|
|
||||||
|
return key;
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
const electron = require('electron');
|
const electron = require('electron');
|
||||||
const sql = require('./sql');
|
const sql = require('./sql');
|
||||||
|
const { remove } = require('./key_management');
|
||||||
|
|
||||||
const { ipcMain } = electron;
|
const { ipcMain } = electron;
|
||||||
|
|
||||||
|
@ -12,16 +13,12 @@ let initialized = false;
|
||||||
const SQL_CHANNEL_KEY = 'sql-channel';
|
const SQL_CHANNEL_KEY = 'sql-channel';
|
||||||
const ERASE_SQL_KEY = 'erase-sql-key';
|
const ERASE_SQL_KEY = 'erase-sql-key';
|
||||||
|
|
||||||
function initialize({ userConfig }) {
|
function initialize() {
|
||||||
if (initialized) {
|
if (initialized) {
|
||||||
throw new Error('sqlChannels: already initialized!');
|
throw new Error('sqlChannels: already initialized!');
|
||||||
}
|
}
|
||||||
initialized = true;
|
initialized = true;
|
||||||
|
|
||||||
if (!userConfig) {
|
|
||||||
throw new Error('initialize: userConfig is required!');
|
|
||||||
}
|
|
||||||
|
|
||||||
ipcMain.on(SQL_CHANNEL_KEY, async (event, jobId, callName, ...args) => {
|
ipcMain.on(SQL_CHANNEL_KEY, async (event, jobId, callName, ...args) => {
|
||||||
try {
|
try {
|
||||||
const fn = sql[callName];
|
const fn = sql[callName];
|
||||||
|
@ -44,7 +41,7 @@ function initialize({ userConfig }) {
|
||||||
|
|
||||||
ipcMain.on(ERASE_SQL_KEY, async event => {
|
ipcMain.on(ERASE_SQL_KEY, async event => {
|
||||||
try {
|
try {
|
||||||
userConfig.set('key', null);
|
remove();
|
||||||
event.sender.send(`${ERASE_SQL_KEY}-done`);
|
event.sender.send(`${ERASE_SQL_KEY}-done`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorForDisplay = error && error.stack ? error.stack : error;
|
const errorForDisplay = error && error.stack ? error.stack : error;
|
||||||
|
|
61
main.js
61
main.js
|
@ -4,14 +4,17 @@ const path = require('path');
|
||||||
const url = require('url');
|
const url = require('url');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const crypto = require('crypto');
|
|
||||||
|
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const pify = require('pify');
|
const pify = require('pify');
|
||||||
const electron = require('electron');
|
const electron = require('electron');
|
||||||
|
|
||||||
const getRealPath = pify(fs.realpath);
|
const packageJson = require('./package.json');
|
||||||
|
const GlobalErrors = require('./app/global_errors');
|
||||||
|
|
||||||
|
GlobalErrors.addHandler();
|
||||||
|
|
||||||
|
const getRealPath = pify(fs.realpath);
|
||||||
const {
|
const {
|
||||||
app,
|
app,
|
||||||
BrowserWindow,
|
BrowserWindow,
|
||||||
|
@ -22,26 +25,6 @@ const {
|
||||||
shell,
|
shell,
|
||||||
} = electron;
|
} = electron;
|
||||||
|
|
||||||
const packageJson = require('./package.json');
|
|
||||||
|
|
||||||
const sql = require('./app/sql');
|
|
||||||
const sqlChannels = require('./app/sql_channel');
|
|
||||||
const attachments = require('./app/attachments');
|
|
||||||
const attachmentChannel = require('./app/attachment_channel');
|
|
||||||
const autoUpdate = require('./app/auto_update');
|
|
||||||
const createTrayIcon = require('./app/tray_icon');
|
|
||||||
const GlobalErrors = require('./app/global_errors');
|
|
||||||
const logging = require('./app/logging');
|
|
||||||
const windowState = require('./app/window_state');
|
|
||||||
const { createTemplate } = require('./app/menu');
|
|
||||||
const {
|
|
||||||
installFileHandler,
|
|
||||||
installWebHandler,
|
|
||||||
} = require('./app/protocol_filter');
|
|
||||||
const { installPermissionsHandler } = require('./app/permissions');
|
|
||||||
|
|
||||||
GlobalErrors.addHandler();
|
|
||||||
|
|
||||||
const appUserModelId = `org.whispersystems.${packageJson.name}`;
|
const appUserModelId = `org.whispersystems.${packageJson.name}`;
|
||||||
console.log('Set Windows Application User Model ID (AUMID)', {
|
console.log('Set Windows Application User Model ID (AUMID)', {
|
||||||
appUserModelId,
|
appUserModelId,
|
||||||
|
@ -64,14 +47,32 @@ const usingTrayIcon =
|
||||||
|
|
||||||
const config = require('./app/config');
|
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 importMode =
|
const importMode =
|
||||||
process.argv.some(arg => arg === '--import') || config.get('import');
|
process.argv.some(arg => arg === '--import') || config.get('import');
|
||||||
|
|
||||||
const development = config.environment === 'development';
|
const development = config.environment === 'development';
|
||||||
|
|
||||||
// Very important to put before the single instance check, since it is based on the
|
// We generally want to pull in our own modules after this point, after the user
|
||||||
// userData directory.
|
// data directory has been set.
|
||||||
const userConfig = require('./app/user_config');
|
const attachments = require('./app/attachments');
|
||||||
|
const attachmentChannel = require('./app/attachment_channel');
|
||||||
|
const autoUpdate = require('./app/auto_update');
|
||||||
|
const createTrayIcon = require('./app/tray_icon');
|
||||||
|
const keyManagement = require('./app/key_management');
|
||||||
|
const logging = require('./app/logging');
|
||||||
|
const sql = require('./app/sql');
|
||||||
|
const sqlChannels = require('./app/sql_channel');
|
||||||
|
const windowState = require('./app/window_state');
|
||||||
|
const { createTemplate } = require('./app/menu');
|
||||||
|
const {
|
||||||
|
installFileHandler,
|
||||||
|
installWebHandler,
|
||||||
|
} = require('./app/protocol_filter');
|
||||||
|
const { installPermissionsHandler } = require('./app/permissions');
|
||||||
|
|
||||||
function showWindow() {
|
function showWindow() {
|
||||||
if (!mainWindow) {
|
if (!mainWindow) {
|
||||||
|
@ -618,15 +619,9 @@ app.on('ready', async () => {
|
||||||
locale = loadLocale({ appLocale, logger });
|
locale = loadLocale({ appLocale, logger });
|
||||||
}
|
}
|
||||||
|
|
||||||
let key = userConfig.get('key');
|
const key = keyManagement.initialize({ userConfig });
|
||||||
if (!key) {
|
|
||||||
// https://www.zetetic.net/sqlcipher/sqlcipher-api/#key
|
|
||||||
key = crypto.randomBytes(32).toString('hex');
|
|
||||||
userConfig.set('key', key);
|
|
||||||
}
|
|
||||||
|
|
||||||
await sql.initialize({ configDir: userDataPath, key });
|
await sql.initialize({ configDir: userDataPath, key });
|
||||||
await sqlChannels.initialize({ userConfig });
|
await sqlChannels.initialize();
|
||||||
|
|
||||||
async function cleanupOrphanedAttachments() {
|
async function cleanupOrphanedAttachments() {
|
||||||
const allAttachments = await attachments.getAllAttachments(userDataPath);
|
const allAttachments = await attachments.getAllAttachments(userDataPath);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue