Skip to content
This repository has been archived by the owner on Nov 17, 2023. It is now read-only.

Commit

Permalink
feat(security): encrypted database
Browse files Browse the repository at this point in the history
  • Loading branch information
mrfelton committed Nov 29, 2019
1 parent bf947e2 commit 8658848
Show file tree
Hide file tree
Showing 17 changed files with 496 additions and 75 deletions.
2 changes: 2 additions & 0 deletions electron/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import { ipcRenderer, remote, shell } from 'electron'
import url from 'url'
import os from 'os'
import { randomBytes } from 'crypto'
import defaults from 'lodash/defaults'
import fileExists from '@zap/utils/fileExists'
import dirExists from '@zap/utils/dirExists'
Expand Down Expand Up @@ -149,6 +150,7 @@ window.Zap = {
normalizeBackupDir,
getPackageDetails,
sha256digest,
randomBytes,
getPlatform: () => os.platform(),
}

Expand Down
2 changes: 1 addition & 1 deletion electron/secureStorage/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ export default function createStorageService(mainWindow) {
const storage = createStorage(config.secureStorage.namespace)
// helper func to send messages to the renderer process
const send = (msg, params) => mainWindow.webContents.send(msg, params)
createCRUD(storage, 'app-password', 'password', send)
createCRUD(storage, 'encryption-key', 'encryptionKey', send)
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@
"debug": "4.1.1",
"debug-logger": "0.4.1",
"dexie": "3.0.0-rc.1",
"dexie-encrypted": "1.1.0",
"downshift": "3.4.3",
"dropbox": "4.0.30",
"electron-is-dev": "1.1.0",
Expand Down
1 change: 1 addition & 0 deletions renderer/reducers/account/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export const PASSWORD_PROMPT_DIALOG_ID = 'PASSWORD_PROMPT_DIALOG'
export const PASSWORD_SET_DIALOG_ID = 'PASSWORD_SET_DIALOG_ID'
export const SET_IS_PASSWORD_ENABLED = 'SET_IS_PASSWORD_ENABLED'
export const LOGIN_NOT_ALLOWED = 'LOGIN_NOT_ALLOWED'
export const SECRET_NONCE = 'Chancellor on Brink of Second Bailout for Banks'
193 changes: 151 additions & 42 deletions renderer/reducers/account/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import { mainLog } from '@zap/utils/log'
import waitForIpcEvent from '@zap/utils/waitForIpc'
import { closeDialog } from 'reducers/modal'
import { showNotification } from 'reducers/notification'
import accountSelectors from './selectors'
import { loginError } from './selectors'
import { byteToHexString, hexStringToByte } from '@zap/utils/byteutils'
import { encrypt, decrypt } from '@zap/utils/aes'
import { initDb } from '@zap/renderer/store/db'
import { genEncryptionKey, hashPassword } from './utils'
import messages from './messages'
import * as constants from './constants'

Expand All @@ -21,6 +25,7 @@ const {
PASSWORD_SET_DIALOG_ID,
SET_IS_PASSWORD_ENABLED,
LOGIN_NOT_ALLOWED,
SECRET_NONCE,
} = constants

// ------------------------------------
Expand All @@ -41,6 +46,31 @@ export const initialState = {
// Actions
// ------------------------------------

/**
* setIsPasswordEnabled - Whether password is set for the account.
*
* @param {string} value value
* @returns {object} Action
*/
const setIsPasswordEnabled = value => ({
type: SET_IS_PASSWORD_ENABLED,
value,
})

/**
* checkAccountPasswordEnabled - Checks whether app password is enabled.
* Password determined as enabled if there is an encryption key in secure storage or a password hash in database.
*
* @returns {Function} Thunk
*/
const checkAccountPasswordEnabled = () => async dispatch => {
const { value: hasEncryptionKey } = await dispatch(waitForIpcEvent('hasEncryptionKey'))
const hasPassword = await window.db.secrets.get('password')
const isEnabled = hasEncryptionKey || hasPassword
dispatch(setIsPasswordEnabled(isEnabled))
return isEnabled
}

/**
* initAccount - Fetch the current account info from the database and save into the store.
* Should be called once when the app first loads.
Expand All @@ -63,52 +93,140 @@ export const initAccount = () => async dispatch => {
}

/**
* setIsPasswordEnabled - Whether password is set for the account.
* getEncryptionKey - Fetch current encryption key from secure storage and decrypt with user supplied password.
*
* @param {string} value value
* @returns {object} Action
* @param {string} password current password.
* @returns {Function} Thunk
*/
const setIsPasswordEnabled = value => ({
type: SET_IS_PASSWORD_ENABLED,
value,
})
export const getEncryptionKey = password => async dispatch => {
const { encryptionKey } = await dispatch(waitForIpcEvent('getEncryptionKey'))
if (encryptionKey) {
const encoded = hexStringToByte(encryptionKey)
if (!password) {
return encoded
}
const decrypted = decrypt(encoded, password)
return decrypted
}
return null
}

/**
* setPassword - Updates wallet password.
* setEncryptionKey - Updates current encryption key.
*
* @param {string} password new password
* @param {string} key New key
* @param {string} password Password used to encerypt the key
* @returns {Function} Thunk
*/
const setPassword = password => async dispatch => {
const { sha256digest } = window.Zap
dispatch(waitForIpcEvent('setPassword', { value: await sha256digest(password) }))
export const setEncryptionKey = (key, password) => async dispatch => {
// Encrypt the key with the user supplied password.
const encryptedKey = encrypt(key, password)
const encryptionKeyAsSting = byteToHexString(encryptedKey)

// Store the encrypted database encryption key in secure storage.
await dispatch(waitForIpcEvent('setEncryptionKey', { value: encryptionKeyAsSting }))
}

/**
* checkAccountPasswordEnabled - Checks whether app password is set via checking secure storage.
* Dispatches setIsPasswordEnabled on completion.
* changeEncryptionKey - Generate new encryption key encrypted with user password and use to reencrypt database.
*
* @param {string} oldPassword Old password
* @param {string} newPassword New password
* @returns {Function} Thunk
*/
const checkAccountPasswordEnabled = () => async dispatch => {
const { value } = await dispatch(waitForIpcEvent('hasPassword'))
dispatch(setIsPasswordEnabled(value))
return value
export const changeEncryptionKey = (oldPassword, newPassword) => async dispatch => {
try {
// Fetch the existing encryption key.
let oldKey
if (oldPassword) {
oldKey = await dispatch(getEncryptionKey(oldPassword))
}

// Generate a new encryption key.
const newKey = genEncryptionKey()

// Re-encrypt database with new password/key.
await initDb({ oldKey, newKey })

// Save the new encryption key
await dispatch(setEncryptionKey(newKey, newPassword))

// Save updated password hash.
const hashedPassword = await hashPassword(newPassword)
await window.db.secrets.put({ key: 'password', value: hashedPassword })
} catch (e) {
mainLog.error('A problem was encountered when changing encryption key: %s', e.message)
throw e
}
}

/**
* disableEncryption - Decrypt the database and delete encryption keys.
*
* @param {string} oldPassword Existing encryption password
* @returns {Function} Thunk
*/
export const disableEncryption = oldPassword => async dispatch => {
try {
// Fetch the existing encryption key.
const oldKey = await dispatch(getEncryptionKey(oldPassword))

// Decrypt the database..
await initDb({ oldKey, newKey: null })

// Delete old encryption key and password.
await dispatch(waitForIpcEvent('deleteEncryptionKey'))
window.db.secrets.delete('password')
} catch (e) {
mainLog.error('A problem was encountered when disabling encryption: %s', e.message)
throw e
}
}

/**
* requirePassword - Password protect routine. Should be placed before protected code.
*
* @param {string} password Current password.
* @returns {Promise} Promise that fulfills after login attempt (either successful or not)
*/
const requirePassword = password => async dispatch => {
const { sha256digest } = window.Zap
const key = await dispatch(getEncryptionKey(password))

// Use supplied password to decryot the database.
await initDb({ oldKey: key, newKey: key })

// Compare hash received from the main thread to a hash of a password provided
try {
const { value: existingHash } = await window.db.secrets.get('password')
const newHash = await sha256digest(password)

mainLog.info('Comparing password hashes:')
mainLog.info(' - old: %s', existingHash)
mainLog.info(' - new: %s', newHash)

if (existingHash === newHash) {
return true
}
throw new Error('passwords do not match')
} catch (e) {
throw new Error(getIntl().formatMessage(messages.account_invalid_password))
}
}

/**
* changePassword - Changes existing password.
*
* @param {object} params password params
* @param {string} params.newPassword new password
* @param {string} params.oldPassword old password
* @param {string} params.newPassword new password
* @returns {Function} Thunk
*/
export const changePassword = ({ newPassword, oldPassword }) => async dispatch => {
export const changePassword = ({ oldPassword, newPassword }) => async dispatch => {
try {
const intl = getIntl()
await dispatch(requirePassword(oldPassword))
await dispatch(setPassword(newPassword))
await dispatch(changeEncryptionKey(oldPassword, newPassword))
dispatch(closeDialog(CHANGE_PASSWORD_DIALOG_ID))
dispatch(showNotification(intl.formatMessage(messages.account_password_updated)))
} catch (error) {
Expand All @@ -125,10 +243,18 @@ export const changePassword = ({ newPassword, oldPassword }) => async dispatch =
export const enablePassword = ({ password }) => async dispatch => {
try {
const intl = getIntl()
await dispatch(setPassword(password))
await dispatch(changeEncryptionKey(null, password))
dispatch(closeDialog(PASSWORD_SET_DIALOG_ID))
dispatch(setIsPasswordEnabled(true))
dispatch(showNotification(intl.formatMessage(messages.account_password_enabled)))

// Add dummy value to secrets store to provide an easy way to check if encryption/decryption is working.
if (!(await window.db.secrets.get('nonce'))) {
await window.db.secrets.put({
key: 'nonce',
value: SECRET_NONCE,
})
}
} catch (error) {
dispatch({ type: LOGIN_FAILURE, error: error.message })
}
Expand All @@ -144,7 +270,7 @@ export const disablePassword = ({ password }) => async dispatch => {
try {
const intl = getIntl()
await dispatch(requirePassword(password))
await dispatch(waitForIpcEvent('deletePassword'))
await dispatch(disableEncryption(password))
dispatch(setIsPasswordEnabled(false))
dispatch(closeDialog(PASSWORD_PROMPT_DIALOG_ID))
dispatch(showNotification(intl.formatMessage(messages.account_password_disabled)))
Expand All @@ -153,23 +279,6 @@ export const disablePassword = ({ password }) => async dispatch => {
}
}

/**
* requirePassword - Password protect routine. Should be placed before protected code.
*
* @param {string} password current password.
* @returns {Promise} Promise that fulfills after login attempt (either successful or not)
*/
const requirePassword = password => async dispatch => {
const { sha256digest } = window.Zap
const { password: hash } = await dispatch(waitForIpcEvent('getPassword'))
const passwordHash = await sha256digest(password)
// compare hash received from the main thread to a hash of a password provided
if (hash === passwordHash) {
return true
}
throw new Error(getIntl().formatMessage(messages.account_invalid_password))
}

/**
* login - Perform account login.
*
Expand All @@ -192,7 +301,7 @@ export const login = password => async dispatch => {
* @returns {Function} Thunk
*/
export const clearLoginError = () => (dispatch, getState) => {
if (accountSelectors.loginError(getState())) {
if (loginError(getState())) {
dispatch({
type: 'LOGIN_CLEAR_ERROR',
})
Expand Down
20 changes: 20 additions & 0 deletions renderer/reducers/account/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* genEncryptionKey - Generate a new random encryption key.
*
* @returns {Uint8Array} random bytes
*/
export const genEncryptionKey = () => {
const { randomBytes } = window.Zap
return randomBytes(32)
}

/**
* hashPassword - Hashes a password.
*
* @param {string} password Password to hash
* @returns {string} Hashed password
*/
export const hashPassword = async password => {
const { sha256digest } = window.Zap
return sha256digest(password)
}
5 changes: 3 additions & 2 deletions renderer/reducers/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,13 @@ export const logout = () => dispatch => {
/**
* initDatabase - Initialize app database.
*
* @param {string} key Database encryption key
* @returns {Function} Thunk
*/
export const initDatabase = () => async dispatch => {
export const initDatabase = key => async dispatch => {
dispatch({ type: INIT_DATABASE })
try {
await initDb()
window.db = await initDb({ newKey: key })
dispatch({ type: INIT_DATABASE_SUCCESS })
} catch (e) {
dispatch({ type: INIT_DATABASE_FAILURE, error: e.message })
Expand Down
Loading

0 comments on commit 8658848

Please sign in to comment.