Skip to content

Commit 3a452da

Browse files
authored
feat(ironfish): Add encrypt method to wallet (#5248)
1 parent ab0847b commit 3a452da

File tree

6 files changed

+217
-2
lines changed

6 files changed

+217
-2
lines changed

ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6898,5 +6898,65 @@
68986898
}
68996899
]
69006900
}
6901+
],
6902+
"Wallet encrypt saves encrypted blobs to disk and updates the wallet account fields": [
6903+
{
6904+
"value": {
6905+
"encrypted": false,
6906+
"version": 4,
6907+
"id": "f49b047c-5140-4b7b-9527-ddc262783e91",
6908+
"name": "A",
6909+
"spendingKey": "0db21b7c8a42e1690a20a0a5fc7522d5e818e219cb1fdce371525d1b4787f2fa",
6910+
"viewKey": "024dbfadd8740380a505fd6038604c72f8796bde51cc6c94631cdd626656379c449f9953ceb45323ba12fe175b4b9897715c0f183dacd6a2eb7e8f04a5e9d4c9",
6911+
"incomingViewKey": "b34d1189c00ee074ecceffee7f5b61272a625df2023dd383af36b1212f576a04",
6912+
"outgoingViewKey": "1af19d206b9ef93946f1f052ac0bb7375ac3f2e8777ca2e3abeb4104c40d44d0",
6913+
"publicAddress": "1fbddcc770972a6c3503b7a6fdd174044cf35d3ef171d68e8cf1e5702c16271b",
6914+
"createdAt": {
6915+
"hash": {
6916+
"type": "Buffer",
6917+
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
6918+
},
6919+
"sequence": 1
6920+
},
6921+
"scanningEnabled": true,
6922+
"proofAuthorizingKey": "c24c3fe541c7f6ea59d3c8a98199dc0b3696928a5abfd4e6314de1b933589a04"
6923+
},
6924+
"head": {
6925+
"hash": {
6926+
"type": "Buffer",
6927+
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
6928+
},
6929+
"sequence": 1
6930+
}
6931+
},
6932+
{
6933+
"value": {
6934+
"encrypted": false,
6935+
"version": 4,
6936+
"id": "666561d9-6ff7-4ae1-a5e3-e0c3775a0a4b",
6937+
"name": "B",
6938+
"spendingKey": "61837c88868454f64f096c139cd7f1dc44e2aba494fa0be78491f65a2dd81b85",
6939+
"viewKey": "caf7b5e225d20021beea59c56ba2844c07ed646434bed5546c731a1eb7914412c92e38c6695ddbaa28f5818057d3757407b71e59479f1a13f4947a6d4930fa2d",
6940+
"incomingViewKey": "1b234f2e510f10540665686dcfb850dc00eaf56363a6cfeffcf614ce5d73b301",
6941+
"outgoingViewKey": "d44acb51dd32912ec1c6d709afbdd8701ee1ea821c019c13da4e0cc5f352b1ed",
6942+
"publicAddress": "a6186075a14d9d6ead67f208d6329cc5582b0ec77e154bca7e1550e25aa19b4e",
6943+
"createdAt": {
6944+
"hash": {
6945+
"type": "Buffer",
6946+
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
6947+
},
6948+
"sequence": 1
6949+
},
6950+
"scanningEnabled": true,
6951+
"proofAuthorizingKey": "0f8be35575f3df948fd6f4eba41e378c87d4588b69825e65c6319bd754835e0a"
6952+
},
6953+
"head": {
6954+
"hash": {
6955+
"type": "Buffer",
6956+
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
6957+
},
6958+
"sequence": 1
6959+
}
6960+
}
69016961
]
69026962
}

ironfish/src/wallet/wallet.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2374,4 +2374,32 @@ describe('Wallet', () => {
23742374
expect(node.wallet.shouldDecryptForAccount(block.header, account)).toBe(true)
23752375
})
23762376
})
2377+
2378+
describe('encrypt', () => {
2379+
it('saves encrypted blobs to disk and updates the wallet account fields', async () => {
2380+
const { node } = nodeTest
2381+
const passphrase = 'foo'
2382+
2383+
const accountA = await useAccountFixture(node.wallet, 'A')
2384+
const accountB = await useAccountFixture(node.wallet, 'B')
2385+
2386+
expect(node.wallet.accounts).toHaveLength(2)
2387+
expect(node.wallet.encryptedAccounts).toHaveLength(0)
2388+
2389+
await node.wallet.encrypt(passphrase)
2390+
2391+
expect(node.wallet.accounts).toHaveLength(0)
2392+
expect(node.wallet.encryptedAccounts).toHaveLength(2)
2393+
2394+
const encryptedAccountA = node.wallet.encryptedAccountById.get(accountA.id)
2395+
Assert.isNotUndefined(encryptedAccountA)
2396+
const decryptedAccountA = encryptedAccountA.decrypt(passphrase)
2397+
expect(accountA.serialize()).toMatchObject(decryptedAccountA.serialize())
2398+
2399+
const encryptedAccountB = node.wallet.encryptedAccountById.get(accountB.id)
2400+
Assert.isNotUndefined(encryptedAccountB)
2401+
const decryptedAccountB = encryptedAccountB.decrypt(passphrase)
2402+
expect(accountB.serialize()).toMatchObject(decryptedAccountB.serialize())
2403+
})
2404+
})
23772405
})

ironfish/src/wallet/wallet.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ export class Wallet {
9595
readonly onAccountRemoved = new Event<[account: Account]>()
9696

9797
protected readonly accountById = new Map<string, Account>()
98-
protected readonly encryptedAccounts = new Map<string, EncryptedAccount>()
98+
readonly encryptedAccountById = new Map<string, EncryptedAccount>()
9999
readonly walletDb: WalletDB
100100
private readonly logger: Logger
101101
readonly workerPool: WorkerPool
@@ -210,13 +210,16 @@ export class Wallet {
210210
}
211211

212212
private async load(): Promise<void> {
213+
this.encryptedAccountById.clear()
214+
this.accountById.clear()
215+
213216
for await (const [id, accountValue] of this.walletDb.loadAccounts()) {
214217
if (accountValue.encrypted) {
215218
const encryptedAccount = new EncryptedAccount({
216219
data: accountValue.data,
217220
walletDb: this.walletDb,
218221
})
219-
this.encryptedAccounts.set(id, encryptedAccount)
222+
this.encryptedAccountById.set(id, encryptedAccount)
220223
} else {
221224
const account = new Account({ accountValue, walletDb: this.walletDb })
222225
this.accountById.set(account.id, account)
@@ -1435,6 +1438,10 @@ export class Wallet {
14351438
return Array.from(this.accountById.values())
14361439
}
14371440

1441+
get encryptedAccounts(): EncryptedAccount[] {
1442+
return Array.from(this.encryptedAccountById.values())
1443+
}
1444+
14381445
accountExists(name: string): boolean {
14391446
return this.getAccountByName(name) !== null
14401447
}
@@ -1767,4 +1774,15 @@ export class Wallet {
17671774
return identity.serialize()
17681775
})
17691776
}
1777+
1778+
async encrypt(passphrase: string, tx?: IDatabaseTransaction): Promise<void> {
1779+
const unlock = await this.createTransactionMutex.lock()
1780+
1781+
try {
1782+
await this.walletDb.encryptAccounts(this.accounts, passphrase, tx)
1783+
await this.load()
1784+
} finally {
1785+
unlock()
1786+
}
1787+
}
17701788
}

ironfish/src/wallet/walletdb/__fixtures__/walletdb.test.ts.fixture

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,5 +790,65 @@
790790
"sequence": 1
791791
}
792792
}
793+
],
794+
"WalletDB encryptAccounts stores encrypted accounts": [
795+
{
796+
"value": {
797+
"encrypted": false,
798+
"version": 4,
799+
"id": "91edb394-fe0a-4ff9-a94b-cbcffc199b76",
800+
"name": "A",
801+
"spendingKey": "aef3a7e8ea8329f61ae3c54b77f02f4fc94f4dd790f3bfd3d6e745e7d68021df",
802+
"viewKey": "8e1605538c7f0d39a292d825e699c04e93d4e883172bea8ce5cf57e6be35062e745fcba8e06b0370e963ebccb2356eed25e886209cdb4b6c6c9bc1a66652ea57",
803+
"incomingViewKey": "3e7270177fa64cd30835284663ceccb12f800e87e3d465aee824f20a93297306",
804+
"outgoingViewKey": "3b07da84fde489af3af658b20573426a6cd5331eb1d827a5edbf52f5aaeb9d22",
805+
"publicAddress": "13aca6bed0937635aebecc987377729edf63d3ed9576c863d9aa7318a714d7ea",
806+
"createdAt": {
807+
"hash": {
808+
"type": "Buffer",
809+
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
810+
},
811+
"sequence": 1
812+
},
813+
"scanningEnabled": true,
814+
"proofAuthorizingKey": "e1b8fa4e84363090d49bee08224f8d516410650dfc6aca1e9f3fef4d1358b708"
815+
},
816+
"head": {
817+
"hash": {
818+
"type": "Buffer",
819+
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
820+
},
821+
"sequence": 1
822+
}
823+
},
824+
{
825+
"value": {
826+
"encrypted": false,
827+
"version": 4,
828+
"id": "57c75b9c-bdb4-4705-8dc7-40847528b5bb",
829+
"name": "B",
830+
"spendingKey": "4377f6743472240a7adba679e5872d58b1e17695662bb384e15984b9ed7eb3ae",
831+
"viewKey": "364d9514508d06da950874cde5cfc1fe2e4d90cd064901e4a73c0f98f82c112fb463fe0a898e3d1976a9c3c7a4e0966d41f75e9a2cbb8767ec05f36b480b80d7",
832+
"incomingViewKey": "4d31ebeefb51dda40f1e9a844321d065794fff7788c675a76699bd6d0a63a402",
833+
"outgoingViewKey": "8b8d94bae8d743938e7940eec2cb3489772a422f3032a3742e8b107dc7bb34ac",
834+
"publicAddress": "61b5033c291a7221a2a35adf7ef2885f5ee3b71ad43e32a6e67be17b3e8d376e",
835+
"createdAt": {
836+
"hash": {
837+
"type": "Buffer",
838+
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
839+
},
840+
"sequence": 1
841+
},
842+
"scanningEnabled": true,
843+
"proofAuthorizingKey": "308703a8a801c918d4fe6168fba0ca7a4a53aadb2c6d7988031f8f9eb33cec08"
844+
},
845+
"head": {
846+
"hash": {
847+
"type": "Buffer",
848+
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
849+
},
850+
"sequence": 1
851+
}
852+
}
793853
]
794854
}

ironfish/src/wallet/walletdb/walletdb.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
useTxFixture,
1212
} from '../../testUtilities'
1313
import { AsyncUtils } from '../../utils'
14+
import { EncryptedAccount } from '../account/encryptedAccount'
1415
import { DecryptedNoteValue } from './decryptedNoteValue'
1516

1617
describe('WalletDB', () => {
@@ -456,4 +457,39 @@ describe('WalletDB', () => {
456457
expect(storedSecret.secret).toEqualBuffer(serializedSecret)
457458
})
458459
})
460+
461+
describe('encryptAccounts', () => {
462+
it('stores encrypted accounts', async () => {
463+
const node = (await nodeTest.createSetup()).node
464+
const walletDb = node.wallet.walletDb
465+
const passphrase = 'test'
466+
467+
const accountA = await useAccountFixture(node.wallet, 'A')
468+
const accountB = await useAccountFixture(node.wallet, 'B')
469+
470+
await walletDb.encryptAccounts([accountA, accountB], passphrase)
471+
472+
const encryptedAccountById = new Map<string, EncryptedAccount>()
473+
for await (const [id, accountValue] of walletDb.loadAccounts()) {
474+
if (!accountValue.encrypted) {
475+
throw new Error('Unexpected behavior')
476+
}
477+
478+
encryptedAccountById.set(
479+
id,
480+
new EncryptedAccount({ data: accountValue.data, walletDb }),
481+
)
482+
}
483+
484+
const encryptedAccountA = encryptedAccountById.get(accountA.id)
485+
Assert.isNotUndefined(encryptedAccountA)
486+
const decryptedAccountA = encryptedAccountA.decrypt(passphrase)
487+
expect(accountA.serialize()).toMatchObject(decryptedAccountA.serialize())
488+
489+
const encryptedAccountB = encryptedAccountById.get(accountB.id)
490+
Assert.isNotUndefined(encryptedAccountB)
491+
const decryptedAccountB = encryptedAccountB.decrypt(passphrase)
492+
expect(accountB.serialize()).toMatchObject(decryptedAccountB.serialize())
493+
})
494+
})
459495
})

ironfish/src/wallet/walletdb/walletdb.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,6 +1188,19 @@ export class WalletDB {
11881188
}
11891189
}
11901190

1191+
async encryptAccounts(
1192+
accounts: Account[],
1193+
passphrase: string,
1194+
tx?: IDatabaseTransaction,
1195+
): Promise<void> {
1196+
await this.db.withTransaction(tx, async (tx) => {
1197+
for (const account of accounts) {
1198+
const encryptedAccount = account.encrypt(passphrase)
1199+
await this.accounts.put(account.id, encryptedAccount.serialize(), tx)
1200+
}
1201+
})
1202+
}
1203+
11911204
async *loadTransactionsByTime(
11921205
account: Account,
11931206
tx?: IDatabaseTransaction,

0 commit comments

Comments
 (0)