diff --git a/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture b/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture index 53339cb8d4..5e6c1465ab 100644 --- a/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture +++ b/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture @@ -6898,5 +6898,65 @@ } ] } + ], + "Wallet encrypt saves encrypted blobs to disk and updates the wallet account fields": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "f49b047c-5140-4b7b-9527-ddc262783e91", + "name": "A", + "spendingKey": "0db21b7c8a42e1690a20a0a5fc7522d5e818e219cb1fdce371525d1b4787f2fa", + "viewKey": "024dbfadd8740380a505fd6038604c72f8796bde51cc6c94631cdd626656379c449f9953ceb45323ba12fe175b4b9897715c0f183dacd6a2eb7e8f04a5e9d4c9", + "incomingViewKey": "b34d1189c00ee074ecceffee7f5b61272a625df2023dd383af36b1212f576a04", + "outgoingViewKey": "1af19d206b9ef93946f1f052ac0bb7375ac3f2e8777ca2e3abeb4104c40d44d0", + "publicAddress": "1fbddcc770972a6c3503b7a6fdd174044cf35d3ef171d68e8cf1e5702c16271b", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "c24c3fe541c7f6ea59d3c8a98199dc0b3696928a5abfd4e6314de1b933589a04" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + }, + { + "value": { + "encrypted": false, + "version": 4, + "id": "666561d9-6ff7-4ae1-a5e3-e0c3775a0a4b", + "name": "B", + "spendingKey": "61837c88868454f64f096c139cd7f1dc44e2aba494fa0be78491f65a2dd81b85", + "viewKey": "caf7b5e225d20021beea59c56ba2844c07ed646434bed5546c731a1eb7914412c92e38c6695ddbaa28f5818057d3757407b71e59479f1a13f4947a6d4930fa2d", + "incomingViewKey": "1b234f2e510f10540665686dcfb850dc00eaf56363a6cfeffcf614ce5d73b301", + "outgoingViewKey": "d44acb51dd32912ec1c6d709afbdd8701ee1ea821c019c13da4e0cc5f352b1ed", + "publicAddress": "a6186075a14d9d6ead67f208d6329cc5582b0ec77e154bca7e1550e25aa19b4e", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "0f8be35575f3df948fd6f4eba41e378c87d4588b69825e65c6319bd754835e0a" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } ] } \ No newline at end of file diff --git a/ironfish/src/wallet/wallet.test.ts b/ironfish/src/wallet/wallet.test.ts index 71e3357ad8..ca148b0983 100644 --- a/ironfish/src/wallet/wallet.test.ts +++ b/ironfish/src/wallet/wallet.test.ts @@ -2374,4 +2374,32 @@ describe('Wallet', () => { expect(node.wallet.shouldDecryptForAccount(block.header, account)).toBe(true) }) }) + + describe('encrypt', () => { + it('saves encrypted blobs to disk and updates the wallet account fields', async () => { + const { node } = nodeTest + const passphrase = 'foo' + + const accountA = await useAccountFixture(node.wallet, 'A') + const accountB = await useAccountFixture(node.wallet, 'B') + + expect(node.wallet.accounts).toHaveLength(2) + expect(node.wallet.encryptedAccounts).toHaveLength(0) + + await node.wallet.encrypt(passphrase) + + expect(node.wallet.accounts).toHaveLength(0) + expect(node.wallet.encryptedAccounts).toHaveLength(2) + + const encryptedAccountA = node.wallet.encryptedAccountById.get(accountA.id) + Assert.isNotUndefined(encryptedAccountA) + const decryptedAccountA = encryptedAccountA.decrypt(passphrase) + expect(accountA.serialize()).toMatchObject(decryptedAccountA.serialize()) + + const encryptedAccountB = node.wallet.encryptedAccountById.get(accountB.id) + Assert.isNotUndefined(encryptedAccountB) + const decryptedAccountB = encryptedAccountB.decrypt(passphrase) + expect(accountB.serialize()).toMatchObject(decryptedAccountB.serialize()) + }) + }) }) diff --git a/ironfish/src/wallet/wallet.ts b/ironfish/src/wallet/wallet.ts index aded0a79d4..88a08fd27f 100644 --- a/ironfish/src/wallet/wallet.ts +++ b/ironfish/src/wallet/wallet.ts @@ -95,7 +95,7 @@ export class Wallet { readonly onAccountRemoved = new Event<[account: Account]>() protected readonly accountById = new Map() - protected readonly encryptedAccounts = new Map() + readonly encryptedAccountById = new Map() readonly walletDb: WalletDB private readonly logger: Logger readonly workerPool: WorkerPool @@ -210,13 +210,16 @@ export class Wallet { } private async load(): Promise { + this.encryptedAccountById.clear() + this.accountById.clear() + for await (const [id, accountValue] of this.walletDb.loadAccounts()) { if (accountValue.encrypted) { const encryptedAccount = new EncryptedAccount({ data: accountValue.data, walletDb: this.walletDb, }) - this.encryptedAccounts.set(id, encryptedAccount) + this.encryptedAccountById.set(id, encryptedAccount) } else { const account = new Account({ accountValue, walletDb: this.walletDb }) this.accountById.set(account.id, account) @@ -1435,6 +1438,10 @@ export class Wallet { return Array.from(this.accountById.values()) } + get encryptedAccounts(): EncryptedAccount[] { + return Array.from(this.encryptedAccountById.values()) + } + accountExists(name: string): boolean { return this.getAccountByName(name) !== null } @@ -1767,4 +1774,15 @@ export class Wallet { return identity.serialize() }) } + + async encrypt(passphrase: string, tx?: IDatabaseTransaction): Promise { + const unlock = await this.createTransactionMutex.lock() + + try { + await this.walletDb.encryptAccounts(this.accounts, passphrase, tx) + await this.load() + } finally { + unlock() + } + } } diff --git a/ironfish/src/wallet/walletdb/__fixtures__/walletdb.test.ts.fixture b/ironfish/src/wallet/walletdb/__fixtures__/walletdb.test.ts.fixture index 36fc19d4c0..bd287fc1ba 100644 --- a/ironfish/src/wallet/walletdb/__fixtures__/walletdb.test.ts.fixture +++ b/ironfish/src/wallet/walletdb/__fixtures__/walletdb.test.ts.fixture @@ -790,5 +790,65 @@ "sequence": 1 } } + ], + "WalletDB encryptAccounts stores encrypted accounts": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "91edb394-fe0a-4ff9-a94b-cbcffc199b76", + "name": "A", + "spendingKey": "aef3a7e8ea8329f61ae3c54b77f02f4fc94f4dd790f3bfd3d6e745e7d68021df", + "viewKey": "8e1605538c7f0d39a292d825e699c04e93d4e883172bea8ce5cf57e6be35062e745fcba8e06b0370e963ebccb2356eed25e886209cdb4b6c6c9bc1a66652ea57", + "incomingViewKey": "3e7270177fa64cd30835284663ceccb12f800e87e3d465aee824f20a93297306", + "outgoingViewKey": "3b07da84fde489af3af658b20573426a6cd5331eb1d827a5edbf52f5aaeb9d22", + "publicAddress": "13aca6bed0937635aebecc987377729edf63d3ed9576c863d9aa7318a714d7ea", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "e1b8fa4e84363090d49bee08224f8d516410650dfc6aca1e9f3fef4d1358b708" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + }, + { + "value": { + "encrypted": false, + "version": 4, + "id": "57c75b9c-bdb4-4705-8dc7-40847528b5bb", + "name": "B", + "spendingKey": "4377f6743472240a7adba679e5872d58b1e17695662bb384e15984b9ed7eb3ae", + "viewKey": "364d9514508d06da950874cde5cfc1fe2e4d90cd064901e4a73c0f98f82c112fb463fe0a898e3d1976a9c3c7a4e0966d41f75e9a2cbb8767ec05f36b480b80d7", + "incomingViewKey": "4d31ebeefb51dda40f1e9a844321d065794fff7788c675a76699bd6d0a63a402", + "outgoingViewKey": "8b8d94bae8d743938e7940eec2cb3489772a422f3032a3742e8b107dc7bb34ac", + "publicAddress": "61b5033c291a7221a2a35adf7ef2885f5ee3b71ad43e32a6e67be17b3e8d376e", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "308703a8a801c918d4fe6168fba0ca7a4a53aadb2c6d7988031f8f9eb33cec08" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } ] } \ No newline at end of file diff --git a/ironfish/src/wallet/walletdb/walletdb.test.ts b/ironfish/src/wallet/walletdb/walletdb.test.ts index 90dc863fe7..00632d7b40 100644 --- a/ironfish/src/wallet/walletdb/walletdb.test.ts +++ b/ironfish/src/wallet/walletdb/walletdb.test.ts @@ -11,6 +11,7 @@ import { useTxFixture, } from '../../testUtilities' import { AsyncUtils } from '../../utils' +import { EncryptedAccount } from '../account/encryptedAccount' import { DecryptedNoteValue } from './decryptedNoteValue' describe('WalletDB', () => { @@ -456,4 +457,39 @@ describe('WalletDB', () => { expect(storedSecret.secret).toEqualBuffer(serializedSecret) }) }) + + describe('encryptAccounts', () => { + it('stores encrypted accounts', async () => { + const node = (await nodeTest.createSetup()).node + const walletDb = node.wallet.walletDb + const passphrase = 'test' + + const accountA = await useAccountFixture(node.wallet, 'A') + const accountB = await useAccountFixture(node.wallet, 'B') + + await walletDb.encryptAccounts([accountA, accountB], passphrase) + + const encryptedAccountById = new Map() + for await (const [id, accountValue] of walletDb.loadAccounts()) { + if (!accountValue.encrypted) { + throw new Error('Unexpected behavior') + } + + encryptedAccountById.set( + id, + new EncryptedAccount({ data: accountValue.data, walletDb }), + ) + } + + const encryptedAccountA = encryptedAccountById.get(accountA.id) + Assert.isNotUndefined(encryptedAccountA) + const decryptedAccountA = encryptedAccountA.decrypt(passphrase) + expect(accountA.serialize()).toMatchObject(decryptedAccountA.serialize()) + + const encryptedAccountB = encryptedAccountById.get(accountB.id) + Assert.isNotUndefined(encryptedAccountB) + const decryptedAccountB = encryptedAccountB.decrypt(passphrase) + expect(accountB.serialize()).toMatchObject(decryptedAccountB.serialize()) + }) + }) }) diff --git a/ironfish/src/wallet/walletdb/walletdb.ts b/ironfish/src/wallet/walletdb/walletdb.ts index 7d379922c9..54a95904f3 100644 --- a/ironfish/src/wallet/walletdb/walletdb.ts +++ b/ironfish/src/wallet/walletdb/walletdb.ts @@ -1188,6 +1188,19 @@ export class WalletDB { } } + async encryptAccounts( + accounts: Account[], + passphrase: string, + tx?: IDatabaseTransaction, + ): Promise { + await this.db.withTransaction(tx, async (tx) => { + for (const account of accounts) { + const encryptedAccount = account.encrypt(passphrase) + await this.accounts.put(account.id, encryptedAccount.serialize(), tx) + } + }) + } + async *loadTransactionsByTime( account: Account, tx?: IDatabaseTransaction,