diff --git a/ironfish/src/wallet/masterKey.test.ts b/ironfish/src/wallet/masterKey.test.ts new file mode 100644 index 0000000000..7af4ec390b --- /dev/null +++ b/ironfish/src/wallet/masterKey.test.ts @@ -0,0 +1,37 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { MasterKey } from './masterKey' + +describe('MasterKey', () => { + it('can regenerate the master key from parts', async () => { + const passphrase = 'foobar' + const masterKey = MasterKey.generate(passphrase) + const duplicate = new MasterKey({ nonce: masterKey.nonce, salt: masterKey.salt }) + + const key = await masterKey.unlock(passphrase) + const reconstructed = await duplicate.unlock(passphrase) + expect(key.key().equals(reconstructed.key())).toBe(true) + }) + + it('can regenerate the child key from parts', async () => { + const passphrase = 'foobar' + const masterKey = MasterKey.generate(passphrase) + await masterKey.unlock(passphrase) + + const childKey = masterKey.deriveNewKey() + const duplicate = masterKey.deriveKey(childKey.salt(), childKey.nonce()) + expect(childKey.key().equals(duplicate.key())).toBe(true) + }) + + it('can save and remove the xchacha20poly1305 in memory', async () => { + const passphrase = 'foobar' + const masterKey = MasterKey.generate(passphrase) + + await masterKey.unlock(passphrase) + expect(masterKey['masterKey']).not.toBeNull() + + await masterKey.lock() + expect(masterKey['masterKey']).toBeNull() + }) +}) diff --git a/ironfish/src/wallet/masterKey.ts b/ironfish/src/wallet/masterKey.ts new file mode 100644 index 0000000000..b2dab80726 --- /dev/null +++ b/ironfish/src/wallet/masterKey.ts @@ -0,0 +1,93 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { xchacha20poly1305 } from '@ironfish/rust-nodejs' +import { Assert } from '../assert' +import { Mutex } from '../mutex' +import { MasterKeyValue } from './walletdb/masterKeyValue' + +/** + * A Master Key implementation for XChaCha20Poly1305. This class can be used + * to derive child keys deterministically given the child key's salt and nonces. + * + * This master key does not automatically lock or unlock. You must call those + * explicitly if you would like any default timeout behavior. + */ +export class MasterKey { + private mutex: Mutex + private locked: boolean + + readonly salt: Buffer + readonly nonce: Buffer + + private masterKey: xchacha20poly1305.XChaCha20Poly1305Key | null + + constructor(masterKeyValue: MasterKeyValue) { + this.mutex = new Mutex() + + this.salt = masterKeyValue.salt + this.nonce = masterKeyValue.nonce + + this.locked = true + this.masterKey = null + } + + static generate(passphrase: string): MasterKey { + const key = new xchacha20poly1305.XChaCha20Poly1305Key(passphrase) + return new MasterKey({ salt: key.salt(), nonce: key.nonce() }) + } + + async lock(): Promise { + const unlock = await this.mutex.lock() + + try { + if (this.masterKey) { + this.masterKey.destroy() + this.masterKey = null + } + + this.locked = true + } finally { + unlock() + } + } + + async unlock(passphrase: string): Promise { + const unlock = await this.mutex.lock() + + try { + this.masterKey = xchacha20poly1305.XChaCha20Poly1305Key.fromParts( + passphrase, + this.salt, + this.nonce, + ) + this.locked = false + + return this.masterKey + } catch (e) { + if (this.masterKey) { + this.masterKey.destroy() + this.masterKey = null + } + + this.locked = true + throw e + } finally { + unlock() + } + } + + deriveNewKey(): xchacha20poly1305.XChaCha20Poly1305Key { + Assert.isFalse(this.locked) + Assert.isNotNull(this.masterKey) + + return this.masterKey.deriveNewKey() + } + + deriveKey(salt: Buffer, nonce: Buffer): xchacha20poly1305.XChaCha20Poly1305Key { + Assert.isFalse(this.locked) + Assert.isNotNull(this.masterKey) + + return this.masterKey.deriveKey(salt, nonce) + } +} diff --git a/ironfish/src/wallet/walletdb/masterKeyValue.test.ts b/ironfish/src/wallet/walletdb/masterKeyValue.test.ts new file mode 100644 index 0000000000..351f643200 --- /dev/null +++ b/ironfish/src/wallet/walletdb/masterKeyValue.test.ts @@ -0,0 +1,32 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { xchacha20poly1305 } from '@ironfish/rust-nodejs' +import { MasterKeyValue, NullableMasterKeyValueEncoding } from './masterKeyValue' + +describe('MasterKeyValueEncoding', () => { + describe('with a defined value', () => { + it('serializes the value into a buffer and deserializes to the original value', () => { + const encoder = new NullableMasterKeyValueEncoding() + + const value: MasterKeyValue = { + nonce: Buffer.alloc(xchacha20poly1305.XNONCE_LENGTH), + salt: Buffer.alloc(xchacha20poly1305.XSALT_LENGTH), + } + const buffer = encoder.serialize(value) + const deserializedValue = encoder.deserialize(buffer) + expect(deserializedValue).toEqual(value) + }) + }) + + describe('with a null value', () => { + it('serializes the value into a buffer and deserializes to the original value', () => { + const encoder = new NullableMasterKeyValueEncoding() + + const value = null + const buffer = encoder.serialize(value) + const deserializedValue = encoder.deserialize(buffer) + expect(deserializedValue).toEqual(value) + }) + }) +}) diff --git a/ironfish/src/wallet/walletdb/masterKeyValue.ts b/ironfish/src/wallet/walletdb/masterKeyValue.ts new file mode 100644 index 0000000000..a706dcfafa --- /dev/null +++ b/ironfish/src/wallet/walletdb/masterKeyValue.ts @@ -0,0 +1,46 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { xchacha20poly1305 } from '@ironfish/rust-nodejs' +import bufio from 'bufio' +import { IDatabaseEncoding } from '../../storage' + +export type MasterKeyValue = { + nonce: Buffer + salt: Buffer +} + +export class NullableMasterKeyValueEncoding + implements IDatabaseEncoding +{ + serialize(value: MasterKeyValue | null): Buffer { + const bw = bufio.write(this.getSize(value)) + + if (value) { + bw.writeBytes(value.nonce) + bw.writeBytes(value.salt) + } + + return bw.render() + } + + deserialize(buffer: Buffer): MasterKeyValue | null { + const reader = bufio.read(buffer, true) + + if (reader.left()) { + const nonce = reader.readBytes(xchacha20poly1305.XNONCE_LENGTH) + const salt = reader.readBytes(xchacha20poly1305.XSALT_LENGTH) + return { nonce, salt } + } + + return null + } + + getSize(value: MasterKeyValue | null): number { + if (!value) { + return 0 + } + + return xchacha20poly1305.XNONCE_LENGTH + xchacha20poly1305.XSALT_LENGTH + } +}