Skip to content

Commit cf2941f

Browse files
authored
feat(ironfish): Create master key (#5375)
1 parent 57286b0 commit cf2941f

File tree

4 files changed

+208
-0
lines changed

4 files changed

+208
-0
lines changed

ironfish/src/wallet/masterKey.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4+
import { MasterKey } from './masterKey'
5+
6+
describe('MasterKey', () => {
7+
it('can regenerate the master key from parts', async () => {
8+
const passphrase = 'foobar'
9+
const masterKey = MasterKey.generate(passphrase)
10+
const duplicate = new MasterKey({ nonce: masterKey.nonce, salt: masterKey.salt })
11+
12+
const key = await masterKey.unlock(passphrase)
13+
const reconstructed = await duplicate.unlock(passphrase)
14+
expect(key.key().equals(reconstructed.key())).toBe(true)
15+
})
16+
17+
it('can regenerate the child key from parts', async () => {
18+
const passphrase = 'foobar'
19+
const masterKey = MasterKey.generate(passphrase)
20+
await masterKey.unlock(passphrase)
21+
22+
const childKey = masterKey.deriveNewKey()
23+
const duplicate = masterKey.deriveKey(childKey.salt(), childKey.nonce())
24+
expect(childKey.key().equals(duplicate.key())).toBe(true)
25+
})
26+
27+
it('can save and remove the xchacha20poly1305 in memory', async () => {
28+
const passphrase = 'foobar'
29+
const masterKey = MasterKey.generate(passphrase)
30+
31+
await masterKey.unlock(passphrase)
32+
expect(masterKey['masterKey']).not.toBeNull()
33+
34+
await masterKey.lock()
35+
expect(masterKey['masterKey']).toBeNull()
36+
})
37+
})

ironfish/src/wallet/masterKey.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4+
import { xchacha20poly1305 } from '@ironfish/rust-nodejs'
5+
import { Assert } from '../assert'
6+
import { Mutex } from '../mutex'
7+
import { MasterKeyValue } from './walletdb/masterKeyValue'
8+
9+
/**
10+
* A Master Key implementation for XChaCha20Poly1305. This class can be used
11+
* to derive child keys deterministically given the child key's salt and nonces.
12+
*
13+
* This master key does not automatically lock or unlock. You must call those
14+
* explicitly if you would like any default timeout behavior.
15+
*/
16+
export class MasterKey {
17+
private mutex: Mutex
18+
private locked: boolean
19+
20+
readonly salt: Buffer
21+
readonly nonce: Buffer
22+
23+
private masterKey: xchacha20poly1305.XChaCha20Poly1305Key | null
24+
25+
constructor(masterKeyValue: MasterKeyValue) {
26+
this.mutex = new Mutex()
27+
28+
this.salt = masterKeyValue.salt
29+
this.nonce = masterKeyValue.nonce
30+
31+
this.locked = true
32+
this.masterKey = null
33+
}
34+
35+
static generate(passphrase: string): MasterKey {
36+
const key = new xchacha20poly1305.XChaCha20Poly1305Key(passphrase)
37+
return new MasterKey({ salt: key.salt(), nonce: key.nonce() })
38+
}
39+
40+
async lock(): Promise<void> {
41+
const unlock = await this.mutex.lock()
42+
43+
try {
44+
if (this.masterKey) {
45+
this.masterKey.destroy()
46+
this.masterKey = null
47+
}
48+
49+
this.locked = true
50+
} finally {
51+
unlock()
52+
}
53+
}
54+
55+
async unlock(passphrase: string): Promise<xchacha20poly1305.XChaCha20Poly1305Key> {
56+
const unlock = await this.mutex.lock()
57+
58+
try {
59+
this.masterKey = xchacha20poly1305.XChaCha20Poly1305Key.fromParts(
60+
passphrase,
61+
this.salt,
62+
this.nonce,
63+
)
64+
this.locked = false
65+
66+
return this.masterKey
67+
} catch (e) {
68+
if (this.masterKey) {
69+
this.masterKey.destroy()
70+
this.masterKey = null
71+
}
72+
73+
this.locked = true
74+
throw e
75+
} finally {
76+
unlock()
77+
}
78+
}
79+
80+
deriveNewKey(): xchacha20poly1305.XChaCha20Poly1305Key {
81+
Assert.isFalse(this.locked)
82+
Assert.isNotNull(this.masterKey)
83+
84+
return this.masterKey.deriveNewKey()
85+
}
86+
87+
deriveKey(salt: Buffer, nonce: Buffer): xchacha20poly1305.XChaCha20Poly1305Key {
88+
Assert.isFalse(this.locked)
89+
Assert.isNotNull(this.masterKey)
90+
91+
return this.masterKey.deriveKey(salt, nonce)
92+
}
93+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4+
import { xchacha20poly1305 } from '@ironfish/rust-nodejs'
5+
import { MasterKeyValue, NullableMasterKeyValueEncoding } from './masterKeyValue'
6+
7+
describe('MasterKeyValueEncoding', () => {
8+
describe('with a defined value', () => {
9+
it('serializes the value into a buffer and deserializes to the original value', () => {
10+
const encoder = new NullableMasterKeyValueEncoding()
11+
12+
const value: MasterKeyValue = {
13+
nonce: Buffer.alloc(xchacha20poly1305.XNONCE_LENGTH),
14+
salt: Buffer.alloc(xchacha20poly1305.XSALT_LENGTH),
15+
}
16+
const buffer = encoder.serialize(value)
17+
const deserializedValue = encoder.deserialize(buffer)
18+
expect(deserializedValue).toEqual(value)
19+
})
20+
})
21+
22+
describe('with a null value', () => {
23+
it('serializes the value into a buffer and deserializes to the original value', () => {
24+
const encoder = new NullableMasterKeyValueEncoding()
25+
26+
const value = null
27+
const buffer = encoder.serialize(value)
28+
const deserializedValue = encoder.deserialize(buffer)
29+
expect(deserializedValue).toEqual(value)
30+
})
31+
})
32+
})
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4+
import { xchacha20poly1305 } from '@ironfish/rust-nodejs'
5+
import bufio from 'bufio'
6+
import { IDatabaseEncoding } from '../../storage'
7+
8+
export type MasterKeyValue = {
9+
nonce: Buffer
10+
salt: Buffer
11+
}
12+
13+
export class NullableMasterKeyValueEncoding
14+
implements IDatabaseEncoding<MasterKeyValue | null>
15+
{
16+
serialize(value: MasterKeyValue | null): Buffer {
17+
const bw = bufio.write(this.getSize(value))
18+
19+
if (value) {
20+
bw.writeBytes(value.nonce)
21+
bw.writeBytes(value.salt)
22+
}
23+
24+
return bw.render()
25+
}
26+
27+
deserialize(buffer: Buffer): MasterKeyValue | null {
28+
const reader = bufio.read(buffer, true)
29+
30+
if (reader.left()) {
31+
const nonce = reader.readBytes(xchacha20poly1305.XNONCE_LENGTH)
32+
const salt = reader.readBytes(xchacha20poly1305.XSALT_LENGTH)
33+
return { nonce, salt }
34+
}
35+
36+
return null
37+
}
38+
39+
getSize(value: MasterKeyValue | null): number {
40+
if (!value) {
41+
return 0
42+
}
43+
44+
return xchacha20poly1305.XNONCE_LENGTH + xchacha20poly1305.XSALT_LENGTH
45+
}
46+
}

0 commit comments

Comments
 (0)