diff --git a/electron/preload.js b/electron/preload.js index ee7fc28d2ce..b057fcb5cb6 100644 --- a/electron/preload.js +++ b/electron/preload.js @@ -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' @@ -149,6 +150,7 @@ window.Zap = { normalizeBackupDir, getPackageDetails, sha256digest, + randomBytes, getPlatform: () => os.platform(), } diff --git a/electron/secureStorage/service.js b/electron/secureStorage/service.js index 8f3bf2d77b3..9fedd2e1c7a 100644 --- a/electron/secureStorage/service.js +++ b/electron/secureStorage/service.js @@ -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) } diff --git a/package.json b/package.json index f3d4873dc5d..d77b106f55a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/renderer/reducers/account/constants.js b/renderer/reducers/account/constants.js index 669a1e1443c..a6296342ac0 100644 --- a/renderer/reducers/account/constants.js +++ b/renderer/reducers/account/constants.js @@ -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' diff --git a/renderer/reducers/account/reducer.js b/renderer/reducers/account/reducer.js index d323f2de861..23fc9f0c6ea 100644 --- a/renderer/reducers/account/reducer.js +++ b/renderer/reducers/account/reducer.js @@ -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' @@ -21,6 +25,7 @@ const { PASSWORD_SET_DIALOG_ID, SET_IS_PASSWORD_ENABLED, LOGIN_NOT_ALLOWED, + SECRET_NONCE, } = constants // ------------------------------------ @@ -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. @@ -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) { @@ -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 }) } @@ -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))) @@ -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. * @@ -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', }) diff --git a/renderer/reducers/account/utils.js b/renderer/reducers/account/utils.js new file mode 100644 index 00000000000..c727d4edfe0 --- /dev/null +++ b/renderer/reducers/account/utils.js @@ -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) +} diff --git a/renderer/reducers/app.js b/renderer/reducers/app.js index 1e8719ad7dc..576ab230de2 100644 --- a/renderer/reducers/app.js +++ b/renderer/reducers/app.js @@ -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 }) diff --git a/renderer/store/db.js b/renderer/store/db.js index 4283b15cb39..9f73890f538 100644 --- a/renderer/store/db.js +++ b/renderer/store/db.js @@ -1,39 +1,23 @@ import Dexie from 'dexie' +import encrypt from 'dexie-encrypted' import config from 'config' +import { byteToHexString } from '@zap/utils/byteutils' import getDbName from '@zap/utils/db' +import { mainLog } from '@zap/utils/log' import Node from './schema/node' import Wallet, { hooks as walletHooks } from './schema/wallet' import Setting from './schema/setting' import dbVersions from './dbVersions' /** - * initDb - Initialise the database and make it globally accessible. - * - * @returns {Promise} Resolves once database connection is open - */ -export const initDb = () => { - const { namespace, domain } = config.db - const { NODE_ENV: environment } = process.env - const dbName = getDbName({ - namespace, - domain, - environment, - }) - window.db = getDb(dbName) - return window.db.open() -} - -/** - * getDb - Define the database. + * setupDb - Define Zap database schema and run migrations. * - * @param {string} name Database name - * @returns {object} Dexie database schema + * @param {Dexie} db Dexie Database instance + * @returns {Promise} Active database connection */ -export const getDb = name => { - const db = new Dexie(name) - +export const setupDb = async db => { // Apply schema versions. - dbVersions(db) + await dbVersions(db) // Apply helper classes. db.nodes.mapToClass(Node, Node.SCHEMA) @@ -45,5 +29,118 @@ export const getDb = name => { db.wallets.hook('updating', walletHooks.updating) db.wallets.hook('reading', walletHooks.reading) - return db + // Open database connection. + return db.open() +} + +/** + * getDefaultDatabaseName - Get the default database name based in config settings. + * + * @returns {string} Default database name + */ +export const getDefaultDatabaseName = () => { + const { namespace, domain } = config.db + const { NODE_ENV: environment } = process.env + return getDbName({ namespace, domain, environment }) +} + +/** + * getDatabase - Get database. + * + * @returns {Promise} Active database connection + */ +export const getDatabase = async () => { + const dbName = getDefaultDatabaseName() + + // Initialise database encryption schema + try { + mainLog.info('Initializing database encryption') + let fakedb = new Dexie(dbName) + encrypt(fakedb, new Uint8Array(32), {}) + fakedb = await setupDb(fakedb) + fakedb.close() + } catch (e) { + mainLog.info('Database encryption already initialised') + } + + return setupDb(new Dexie(dbName)) +} + +/** + * encryptDatabase - Encrypr database `name` using encryption key `key`. + * + * @param {Uint8Array} key Datbase encryption key + * @returns {Promise} Active database connection + */ +export const encryptDatabase = async key => { + mainLog.info('Encrypting database with key: %s', byteToHexString(key)) + const dbName = getDefaultDatabaseName() + const db = new Dexie(dbName) + const settings = { + secrets: { + type: encrypt.BLACKLIST, + fields: ['value'], + }, + } + encrypt(db, key, settings) + return setupDb(db) +} + +/** + * decryptDatabase - Decrypt database `name` using encryption key `key`. + * + * @param {Uint8Array} key Datbase encryption key + * @returns {Promise} Active database connection + */ +export const decryptDatabase = async key => { + mainLog.info('Decrypting database with key: %s', byteToHexString(key)) + const dbName = getDefaultDatabaseName() + const db = new Dexie(dbName) + encrypt(db, key, {}) + return setupDb(db) +} + +/** + * initDb - Initialise the database and make it globally accessible. + * + * @param {object} params Options + * @param {Uint8Array} params.oldKey Existing datbase encryption key + * @param {Uint8Array} params.newKey New database encryption key + * @returns {Promise} Resolves once database connection is open + */ +export const initDb = async ({ oldKey, newKey } = {}) => { + mainLog.info('Initializing database:') + mainLog.info(' - oldKey: %s', byteToHexString(oldKey)) + mainLog.info(' - newKey: %s', byteToHexString(newKey)) + + let db + + const finalize = () => { + window.db = db + return window.db + } + + // If no keys have been supplied, return unencrypted database. + if (!oldKey && !newKey) { + db = getDatabase() + return finalize() + } + + // If the keys match just unencrypt the database. + if (oldKey === newKey) { + db = await encryptDatabase(newKey) + return finalize() + } + + // If an old key has been supplied use this to decrypt the database. + if (oldKey) { + db = await decryptDatabase(oldKey) + } + + // If a new key has been supplied use this to re-encrypt the database. + if (newKey) { + db = await encryptDatabase(newKey) + } + + return finalize() } diff --git a/renderer/store/dbVersions.js b/renderer/store/dbVersions.js index 35b599d5c88..556116eeda2 100644 --- a/renderer/store/dbVersions.js +++ b/renderer/store/dbVersions.js @@ -41,7 +41,7 @@ const dbVersions = db => { }) ) - // ADd autopay table. + // Add autopay table. db.version(3).stores({ autopay: 'id', }) @@ -116,6 +116,11 @@ const dbVersions = db => { // Save the migrated config overrides. await tx.settings.put({ key: 'config', value: newConfig }) }) + + // Add secrets table. + db.version(5).stores({ + secrets: 'key', + }) } export default dbVersions diff --git a/stories/Provider.js b/stories/Provider.js index 6618c52940e..433e7ab61fd 100644 --- a/stories/Provider.js +++ b/stories/Provider.js @@ -7,13 +7,13 @@ import jstz from 'jstimezonedetect' import translations from '@zap/i18n/translation' import { configureStore } from '@zap/renderer/store/configureStore' import { getDefaultLocale } from '@zap/i18n' -import { getDb } from '@zap/renderer/store/db' +import { getDatabase } from '@zap/renderer/store/db' import sha256digest from '@zap/utils/sha256' import getDbName from '@zap/utils/db' import { setTheme } from 'reducers/theme' import { setCryptoUnit } from 'reducers/ticker' -export const db = getDb(getDbName(config)) +export const db = getDatabase(getDbName(config)) db.open() window.db = db diff --git a/test/unit/__mocks__/dexie-encrypted.js b/test/unit/__mocks__/dexie-encrypted.js new file mode 100644 index 00000000000..d0ffd626216 --- /dev/null +++ b/test/unit/__mocks__/dexie-encrypted.js @@ -0,0 +1 @@ +exports.default = jest.fn() diff --git a/test/unit/utils/byteutils.spec.js b/test/unit/utils/byteutils.spec.js new file mode 100644 index 00000000000..19f4f7a9e1a --- /dev/null +++ b/test/unit/utils/byteutils.spec.js @@ -0,0 +1,108 @@ +import { byteToHexString, hexStringToByte } from '@zap/utils/byteutils' + +describe('byteutils', () => { + describe('byteToHexString', () => { + const tests = [ + [new Uint8Array(32), '0000000000000000000000000000000000000000000000000000000000000000'], + [ + new Uint8Array([ + 127, + 85, + 26, + 112, + 162, + 212, + 93, + 237, + 45, + 245, + 111, + 221, + 199, + 22, + 38, + 120, + 190, + 126, + 235, + 197, + 235, + 136, + 230, + 38, + 54, + 2, + 138, + 253, + 209, + 206, + 27, + 173, + ]), + '7F551A70A2D45DED2DF56FDDC7162678BE7EEBC5EB88E62636028AFDD1CE1BAD', + ], + ] + + tests.forEach(test => { + const [from, to] = test + const res = byteToHexString(from) + + it(`should convert uint ${from} to hext string ${to}`, () => { + expect(res).toEqual(to) + }) + }) + }) + + describe('hexStringToByte', () => { + const tests = [ + ['0000000000000000000000000000000000000000000000000000000000000000', new Uint8Array(32)], + [ + '7F551A70A2D45DED2DF56FDDC7162678BE7EEBC5EB88E62636028AFDD1CE1BAD', + + new Uint8Array([ + 127, + 85, + 26, + 112, + 162, + 212, + 93, + 237, + 45, + 245, + 111, + 221, + 199, + 22, + 38, + 120, + 190, + 126, + 235, + 197, + 235, + 136, + 230, + 38, + 54, + 2, + 138, + 253, + 209, + 206, + 27, + 173, + ]), + ], + ] + + tests.forEach(test => { + const [from, to] = test + const res = hexStringToByte(from) + + it(`should convert uint ${from} to hext string ${to}`, () => { + expect(res).toEqual(to) + }) + }) + }) +}) diff --git a/utils/aes.js b/utils/aes.js new file mode 100644 index 00000000000..f712712d167 --- /dev/null +++ b/utils/aes.js @@ -0,0 +1,23 @@ +import crypto from 'crypto' + +const algorithm = 'aes-256-ctr' + +/** + * @param buffer + * @param password + */ +export function encrypt(buffer, password) { + const cipher = crypto.createCipher(algorithm, password) + const crypted = Buffer.concat([cipher.update(buffer), cipher.final()]) + return crypted +} + +/** + * @param buffer + * @param password + */ +export function decrypt(buffer, password) { + const decipher = crypto.createDecipher(algorithm, password) + const dec = Buffer.concat([decipher.update(buffer), decipher.final()]) + return dec +} diff --git a/utils/byteutils/byteToHexString.js b/utils/byteutils/byteToHexString.js new file mode 100644 index 00000000000..f8b7a683b0f --- /dev/null +++ b/utils/byteutils/byteToHexString.js @@ -0,0 +1,17 @@ +/** + * byteToHexString - Convert from bytes to hex string. + * + * @param {Uint8Array} uint8arr Bytes to convert. + * @returns {string} Hex string + */ +function byteToHexString(uint8arr) { + if (!uint8arr) { + return '' + } + return Array.prototype.map + .call(new Uint8Array(uint8arr), x => `00${x.toString(16)}`.slice(-2)) + .join('') + .toUpperCase() +} + +export default byteToHexString diff --git a/utils/byteutils/hexStringToByte.js b/utils/byteutils/hexStringToByte.js new file mode 100644 index 00000000000..a0605e1c29f --- /dev/null +++ b/utils/byteutils/hexStringToByte.js @@ -0,0 +1,20 @@ +/** + * hexStringToByte - Convert from bytes to hex string. + * + * @param {string} str Hex string to convert + * @returns {Uint8Array} Bytes + */ +function hexStringToByte(str) { + if (!str) { + return new Uint8Array() + } + + const a = [] + for (let i = 0, len = str.length; i < len; i += 2) { + a.push(parseInt(str.substr(i, 2), 16)) + } + + return new Uint8Array(a) +} + +export default hexStringToByte diff --git a/utils/byteutils/index.js b/utils/byteutils/index.js new file mode 100644 index 00000000000..8c87a46e71a --- /dev/null +++ b/utils/byteutils/index.js @@ -0,0 +1,2 @@ +export byteToHexString from './byteToHexString' +export hexStringToByte from './hexStringToByte' diff --git a/yarn.lock b/yarn.lock index e0f1f940d91..57187aa8ae8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7921,6 +7921,15 @@ detective@^4.3.1: acorn "^5.2.1" defined "^1.0.0" +dexie-encrypted@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/dexie-encrypted/-/dexie-encrypted-1.1.0.tgz#9cb39d58ba22d67d74c1b271614a6e69227513fd" + integrity sha512-K0VrLigs9mqJLjcbABUzKQtRSOvu73Y30G6Mil7bW7t6FFd7m8+pHmCtnuup4WK2637JeDpf+ulJOgO/BdMXHA== + dependencies: + tweetnacl "^1.0.1" + typeson "^5.13.0" + typeson-registry "^1.0.0-alpha.28" + dexie@3.0.0-rc.1: version "3.0.0-rc.1" resolved "https://registry.yarnpkg.com/dexie/-/dexie-3.0.0-rc.1.tgz#3f7fe711b8d8ad27b69370696d9dbf5817f849d4" @@ -18796,6 +18805,11 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= +tweetnacl@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.1.tgz#2594d42da73cd036bd0d2a54683dd35a6b55ca17" + integrity sha512-kcoMoKTPYnoeS50tzoqjPY3Uv9axeuuFAZY9M/9zFnhoVvRfxz9K29IMPD7jGmt2c8SW7i3gT9WqDl2+nV7p4A== + type-check@~0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" @@ -18868,7 +18882,7 @@ typescript@^3.3.3: resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.2.tgz#27e489b95fa5909445e9fef5ee48d81697ad18fb" integrity sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ== -typeson-registry@^1.0.0-alpha.20: +typeson-registry@^1.0.0-alpha.20, typeson-registry@^1.0.0-alpha.28: version "1.0.0-alpha.29" resolved "https://registry.yarnpkg.com/typeson-registry/-/typeson-registry-1.0.0-alpha.29.tgz#df53c20fc02ec96ffc991161c3dd2c121b037d24" integrity sha512-DqRoIx0CtmBGXuumFk7ex5QE6JCWHNKry6D1psGUUB9uIPRrj/SCtuRAidZjLgieWpwn1EfrTFtG0IN2t//F8A== @@ -18877,7 +18891,7 @@ typeson-registry@^1.0.0-alpha.20: typeson "5.13.0" whatwg-url "7.0.0" -typeson@5.13.0, typeson@^5.8.2: +typeson@5.13.0, typeson@^5.13.0, typeson@^5.8.2: version "5.13.0" resolved "https://registry.yarnpkg.com/typeson/-/typeson-5.13.0.tgz#dc65b23ea1978a315ed4c95e58a5b6936bcc3591" integrity sha512-xcSaSt+hY/VcRYcqZuVkJwMjDXXJb4CZd51qDocpYw8waA314ygyOPlKhsGsw4qKuJ0tfLLUrxccrm+xvyS0AQ==