Skip to content

Commit

Permalink
feat(ironfish): Create master key (#5375)
Browse files Browse the repository at this point in the history
  • Loading branch information
rohanjadvani committed Sep 16, 2024
1 parent 57286b0 commit cf2941f
Show file tree
Hide file tree
Showing 4 changed files with 208 additions and 0 deletions.
37 changes: 37 additions & 0 deletions ironfish/src/wallet/masterKey.test.ts
Original file line number Diff line number Diff line change
@@ -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()
})
})
93 changes: 93 additions & 0 deletions ironfish/src/wallet/masterKey.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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<xchacha20poly1305.XChaCha20Poly1305Key> {
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)
}
}
32 changes: 32 additions & 0 deletions ironfish/src/wallet/walletdb/masterKeyValue.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
})
46 changes: 46 additions & 0 deletions ironfish/src/wallet/walletdb/masterKeyValue.ts
Original file line number Diff line number Diff line change
@@ -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<MasterKeyValue | null>
{
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
}
}

0 comments on commit cf2941f

Please sign in to comment.