Skip to content

Commit

Permalink
Store SQLCipher decryption key in separate file
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
scottnonnenberg-signal committed Aug 17, 2018
1 parent 006700f commit 496ebf2
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 39 deletions.
62 changes: 62 additions & 0 deletions app/key_management.js
@@ -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;
}
9 changes: 3 additions & 6 deletions app/sql_channel.js
@@ -1,5 +1,6 @@
const electron = require('electron');
const sql = require('./sql');
const { remove } = require('./key_management');

const { ipcMain } = electron;

Expand All @@ -12,16 +13,12 @@ let initialized = false;
const SQL_CHANNEL_KEY = 'sql-channel';
const ERASE_SQL_KEY = 'erase-sql-key';

function initialize({ userConfig }) {
function initialize() {
if (initialized) {
throw new Error('sqlChannels: already initialized!');
}
initialized = true;

if (!userConfig) {
throw new Error('initialize: userConfig is required!');
}

ipcMain.on(SQL_CHANNEL_KEY, async (event, jobId, callName, ...args) => {
try {
const fn = sql[callName];
Expand All @@ -44,7 +41,7 @@ function initialize({ userConfig }) {

ipcMain.on(ERASE_SQL_KEY, async event => {
try {
userConfig.set('key', null);
remove();
event.sender.send(`${ERASE_SQL_KEY}-done`);
} catch (error) {
const errorForDisplay = error && error.stack ? error.stack : error;
Expand Down
61 changes: 28 additions & 33 deletions main.js
Expand Up @@ -4,14 +4,17 @@ const path = require('path');
const url = require('url');
const os = require('os');
const fs = require('fs');
const crypto = require('crypto');

const _ = require('lodash');
const pify = require('pify');
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 {
app,
BrowserWindow,
Expand All @@ -22,26 +25,6 @@ const {
shell,
} = 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}`;
console.log('Set Windows Application User Model ID (AUMID)', {
appUserModelId,
Expand All @@ -64,14 +47,32 @@ const usingTrayIcon =

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 =
process.argv.some(arg => arg === '--import') || config.get('import');

const development = config.environment === 'development';

// Very important to put before the single instance check, since it is based on the
// userData directory.
const userConfig = require('./app/user_config');
// We generally want to pull in our own modules after this point, after the user
// data directory has been set.
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() {
if (!mainWindow) {
Expand Down Expand Up @@ -618,15 +619,9 @@ app.on('ready', async () => {
locale = loadLocale({ appLocale, logger });
}

let key = userConfig.get('key');
if (!key) {
// https://www.zetetic.net/sqlcipher/sqlcipher-api/#key
key = crypto.randomBytes(32).toString('hex');
userConfig.set('key', key);
}

const key = keyManagement.initialize({ userConfig });
await sql.initialize({ configDir: userDataPath, key });
await sqlChannels.initialize({ userConfig });
await sqlChannels.initialize();

async function cleanupOrphanedAttachments() {
const allAttachments = await attachments.getAllAttachments(userDataPath);
Expand Down

0 comments on commit 496ebf2

Please sign in to comment.