From de709cadc4894645c3496e32024d05a361ecfc77 Mon Sep 17 00:00:00 2001 From: emjshrx Date: Fri, 9 Feb 2024 20:53:24 +0530 Subject: [PATCH 1/4] chore: use node 20 and add bip38 types --- .github/workflows/lint.yml | 2 +- .github/workflows/test.yml | 2 +- .nvmrc | 1 + package-lock.json | 22 ++++++++++++++++------ package.json | 1 + 5 files changed, 20 insertions(+), 8 deletions(-) create mode 100644 .nvmrc diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 0b15bda..43ce6eb 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -13,7 +13,7 @@ jobs: - name: Setup node uses: actions/setup-node@v3 with: - node-version: 18.2.x + node-version: 20.9.x cache: npm - name: Cache node modules id: cache-node-modules diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 394b2e6..5f11c02 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,7 +15,7 @@ jobs: - name: Setup node uses: actions/setup-node@v3 with: - node-version: 18.2.x + node-version: 20.9.x cache: npm - name: Cache node modules id: cache-node-modules diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..805b5a4 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v20.9.0 diff --git a/package-lock.json b/package-lock.json index 2ca6da9..eeba566 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "tiny-secp256k1": "^2.2.3" }, "devDependencies": { + "@types/bip38": "^3.1.2", "@types/create-hash": "^1.2.3", "@types/jest": "^29.5.3", "@types/node": "^20.5.0", @@ -1923,6 +1924,15 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/bip38": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@types/bip38/-/bip38-3.1.2.tgz", + "integrity": "sha512-KF5aiS7DUJs2llJJeg1O1Io129PETszfUfDQotJ4VPBXzytpIUmb7n2MHWEdFYRHs2LYoaRivP/aJbTlF56J+Q==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/create-hash": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@types/create-hash/-/create-hash-1.2.3.tgz", @@ -3923,9 +3933,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", "funding": [ { "type": "individual", @@ -7162,9 +7172,9 @@ } }, "node_modules/vite": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.1.tgz", - "integrity": "sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", + "integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", "dev": true, "dependencies": { "esbuild": "^0.18.10", diff --git a/package.json b/package.json index f71ae5c..0b92f1a 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "tiny-secp256k1": "^2.2.3" }, "devDependencies": { + "@types/bip38": "^3.1.2", "@types/create-hash": "^1.2.3", "@types/jest": "^29.5.3", "@types/node": "^20.5.0", From 85b18efaa16c6d3eee02c40b35702c2841016a82 Mon Sep 17 00:00:00 2001 From: emjshrx Date: Sat, 10 Feb 2024 13:24:46 +0530 Subject: [PATCH 2/4] feat: encrypt and decrypt keys on storage --- src/wallet/db/db.interface.ts | 11 +++++--- src/wallet/db/level/db.ts | 17 +++++-------- src/wallet/wallet.ts | 47 +++++++++++++++++++++++++++++------ 3 files changed, 54 insertions(+), 21 deletions(-) diff --git a/src/wallet/db/db.interface.ts b/src/wallet/db/db.interface.ts index 62fc809..d5800b2 100644 --- a/src/wallet/db/db.interface.ts +++ b/src/wallet/db/db.interface.ts @@ -1,4 +1,3 @@ -import { Buffer } from 'buffer'; import { Coin } from '../coin.ts'; export type DbInterface = { @@ -7,8 +6,14 @@ export type DbInterface = { getStatus(): string; getVersion(): Promise; setVersion(version: number): Promise; - getMasterKey(): Promise<{ privateKey: Buffer; chaincode: Buffer }>; - setMasterKey(privateKey: Buffer, chaincode: Buffer): Promise; + getMasterKey(): Promise<{ + encryptedPrivateKey: string; + encryptedChainCode: string; + }>; + setMasterKey( + encryptedPrivateKey: string, + encryptedChainCode: string, + ): Promise; saveAddress(address: string, path: string): Promise; getAddress(address: string): Promise; hasAddress(address: string): Promise; diff --git a/src/wallet/db/level/db.ts b/src/wallet/db/level/db.ts index 384e82f..6be19ee 100644 --- a/src/wallet/db/level/db.ts +++ b/src/wallet/db/level/db.ts @@ -1,7 +1,6 @@ import { Level } from 'level'; import { wdb } from './layout.ts'; import { DbInterface } from '../db.interface.ts'; -import { Buffer } from 'buffer'; import { Coin } from '../../coin.ts'; export type LevelDBConfigOptions = { @@ -39,24 +38,20 @@ export class WalletDB implements DbInterface { await this.db.put(wdb.V, version.toString()); } - public async getMasterKey(): Promise<{ - privateKey: Buffer; - chaincode: Buffer; - }> { + public async getMasterKey() { const masterKey = await this.db.get(wdb.M); - const privateKey = Buffer.from(masterKey.slice(0, 64), 'hex'); - const chaincode = Buffer.from(masterKey.slice(64), 'hex'); + const [encryptedPrivateKey, encryptedChainCode] = masterKey.split(':'); - return { privateKey, chaincode }; + return { encryptedPrivateKey, encryptedChainCode }; } public async setMasterKey( - privateKey: Buffer, - chaincode: Buffer, + encryptedPrivateKey: string, + encryptedChainCode: string, ): Promise { await this.db.put( wdb.M, - Buffer.concat([privateKey, chaincode]).toString('hex'), + `${encryptedPrivateKey}:${encryptedChainCode}`, ); } diff --git a/src/wallet/wallet.ts b/src/wallet/wallet.ts index 436f625..1506ae3 100644 --- a/src/wallet/wallet.ts +++ b/src/wallet/wallet.ts @@ -11,6 +11,7 @@ import { Coin } from './coin.ts'; import { ECPairFactory } from 'ecpair'; import { createOutputs, encodeSilentPaymentAddress } from '../core'; import { toXOnly } from 'bitcoinjs-lib/src/psbt/bip371'; +import { encrypt, decrypt } from 'bip38'; initEccLib(ecc); const ECPair = ECPairFactory(ecc); @@ -21,6 +22,8 @@ export type WalletConfigOptions = { networkClient: NetworkInterface; }; +const DEFAULT_ENCRYPTION_PASSWORD = '12345678'; + export class Wallet { private readonly db: DbInterface; private readonly network: NetworkInterface; @@ -33,19 +36,29 @@ export class Wallet { this.network = config.networkClient; } - async init(mnemonic?: string) { + async init(params?: { mnemonic?: string; password?: string }) { + const { mnemonic, password } = params; await this.db.open(); if (mnemonic) { const seed = mnemonicToSeedSync(mnemonic).toString('hex'); this.masterKey = bip32.fromSeed(Buffer.from(seed, 'hex')); - await this.db.setMasterKey( - this.masterKey.privateKey, - this.masterKey.chainCode, - ); + this.setPassword(password ?? DEFAULT_ENCRYPTION_PASSWORD); } else { - const { privateKey, chaincode } = await this.db.getMasterKey(); - this.masterKey = bip32.fromPrivateKey(privateKey, chaincode); + const { encryptedPrivateKey, encryptedChainCode } = + await this.db.getMasterKey(); + const { privateKey: decryptedPrivateKey } = decrypt( + encryptedPrivateKey, + password ?? DEFAULT_ENCRYPTION_PASSWORD, + ); + const { privateKey: decryptedChainCode } = decrypt( + encryptedChainCode, + password ?? DEFAULT_ENCRYPTION_PASSWORD, + ); + this.masterKey = bip32.fromPrivateKey( + decryptedPrivateKey, + decryptedChainCode, + ); } } @@ -56,6 +69,26 @@ export class Wallet { await this.db.close(); } + async setPassword(newPassword: string) { + if (!this.masterKey) { + throw new Error( + 'Wallet not initialized. Please call wallet.init()', + ); + } else { + const encryptedPrivateKey = encrypt( + this.masterKey.privateKey, + false, + newPassword, + ); + const encryptedChainCode = encrypt( + this.masterKey.chainCode, + false, + newPassword, + ); + await this.db.setMasterKey(encryptedPrivateKey, encryptedChainCode); + } + } + private async deriveAddress(path: string): Promise { const child = this.masterKey.derivePath(path); const { address } = payments.p2wpkh({ From 54899859fdb992fadad2f0ab2981755a6d4c95c2 Mon Sep 17 00:00:00 2001 From: emjshrx Date: Sat, 10 Feb 2024 14:18:51 +0530 Subject: [PATCH 3/4] test: add tests for new methods --- test/wallet-db.spec.ts | 10 ++++++++++ test/wallet.spec.ts | 14 +++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/test/wallet-db.spec.ts b/test/wallet-db.spec.ts index 64c098d..b2b15d2 100644 --- a/test/wallet-db.spec.ts +++ b/test/wallet-db.spec.ts @@ -24,6 +24,16 @@ describe('Wallet DB', () => { expect(await walletDB.getVersion()).toBe(1); }); + it('should set and retrieve encryptedPrivateKey and encryptedChainCode', async () => { + const samplePrivateKey = 'samplePrivateKey'; + const sampleChainCode = 'sampleChainCode'; + await walletDB.setMasterKey(samplePrivateKey, sampleChainCode); + const { encryptedPrivateKey, encryptedChainCode } = + await walletDB.getMasterKey(); + expect(encryptedPrivateKey).toStrictEqual(samplePrivateKey); + expect(encryptedChainCode).toStrictEqual(sampleChainCode); + }); + afterAll(async () => { await walletDB.close(); fs.rmSync('./test/wallet-db', { recursive: true, force: true }); diff --git a/test/wallet.spec.ts b/test/wallet.spec.ts index 62f333a..8820718 100644 --- a/test/wallet.spec.ts +++ b/test/wallet.spec.ts @@ -32,9 +32,17 @@ describe('Wallet', () => { }); it('should initialise the wallet', async () => { - await wallet.init( - 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about', - ); + await wallet.init({ + mnemonic: + 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about', + }); + }); + + it('should set a new password, close and reopen the wallet with the same password', async () => { + const password = 'notSoSecretPassword'; + await wallet.setPassword(password); + await wallet.close(); + await wallet.init({ password }); }); it('should derive first receive address', async () => { From ad98bdb08341331f067d98057902bf2d4117cde2 Mon Sep 17 00:00:00 2001 From: emjshrx Date: Sat, 10 Feb 2024 14:32:05 +0530 Subject: [PATCH 4/4] fix: fix storage of change and receive depths --- src/wallet/db/level/db.ts | 8 ++++---- src/wallet/db/level/layout.ts | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/wallet/db/level/db.ts b/src/wallet/db/level/db.ts index 6be19ee..a62a8ab 100644 --- a/src/wallet/db/level/db.ts +++ b/src/wallet/db/level/db.ts @@ -68,19 +68,19 @@ export class WalletDB implements DbInterface { } async getReceiveDepth(): Promise { - return parseInt(await this.db.sublevel(wdb.A).get('receiveDepth')); + return parseInt(await this.db.sublevel(wdb.D).get('receiveDepth')); } async setReceiveDepth(depth: number): Promise { - await this.db.sublevel(wdb.A).put('receiveDepth', depth.toString()); + await this.db.sublevel(wdb.D).put('receiveDepth', depth.toString()); } async getChangeDepth(): Promise { - return parseInt(await this.db.sublevel(wdb.A).get('changeDepth')); + return parseInt(await this.db.sublevel(wdb.D).get('changeDepth')); } async setChangeDepth(depth: number): Promise { - await this.db.sublevel(wdb.A).put('changeDepth', depth.toString()); + await this.db.sublevel(wdb.D).put('changeDepth', depth.toString()); } async getAllAddresses(): Promise { diff --git a/src/wallet/db/level/layout.ts b/src/wallet/db/level/layout.ts index 4130835..3e6cd59 100644 --- a/src/wallet/db/level/layout.ts +++ b/src/wallet/db/level/layout.ts @@ -3,6 +3,7 @@ export const wdb = { V: 'V', // Version M: 'M', // Master key A: 'A', // Address + D: 'D', // Address depth C: 'C', // Coins SP: 'SP', // Silent payment address };