diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6804f455dc..c1fbad4b55 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -32,7 +32,9 @@ /packages/jellyfish-transaction-signature/ @fuxingloh @ivan-zynesis /packages/jellyfish-wallet/ @fuxingloh @ivan-zynesis @monstrobishi /packages/jellyfish-wallet-classic/ @fuxingloh @ivan-zynesis @monstrobishi +/packages/jellyfish-wallet-encrypted/ @fuxingloh @ivan-zynesis /packages/jellyfish-wallet-mnemonic/ @fuxingloh @ivan-zynesis + /packages/testcontainers/ @fuxingloh /packages/testing/ @fuxingloh @canonbrother diff --git a/.github/governance.yml b/.github/governance.yml index 1cd11245dd..3a97edc9b2 100644 --- a/.github/governance.yml +++ b/.github/governance.yml @@ -49,6 +49,7 @@ issue: - jellyfish-transaction-builder - jellyfish-transaction-signature - jellyfish-wallet + - jellyfish-wallet-encrypted - jellyfish-wallet-classic - jellyfish-wallet-mnemonic - testcontainers diff --git a/.github/labeler.yml b/.github/labeler.yml index de0a49cf5a..dd65e3e94c 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -61,6 +61,11 @@ labels: matcher: files: "packages/jellyfish-transaction-signature/**" + - label: area/jellyfish-wallet-encrypted + sync: true + matcher: + files: "packages/jellyfish-wallet-encrypted/**" + - label: area/jellyfish-wallet-classic sync: true matcher: diff --git a/.github/labels.yml b/.github/labels.yml index e6513e7184..06e5365f46 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -80,6 +80,8 @@ name: area/jellyfish-wallet - color: fbca04 name: area/jellyfish-wallet-classic +- color: fbca04 + name: area/jellyfish-wallet-encrypted - color: fbca04 name: area/jellyfish-wallet-mnemonic - color: fbca04 diff --git a/README.md b/README.md index 429197b546..71e8769ef2 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ Package | Description `@defichain/jellyfish-transaction-builder` | Provides a high-high level abstraction for constructing transaction ready to be broadcast for DeFi Blockchain. `@defichain/jellyfish-wallet-classic` | WalletClassic implements a simple, single elliptic pair wallet. `@defichain/jellyfish-wallet` | Jellyfish wallet is a managed wallet, where account can get discovered from an HD seed. +`@defichain/jellyfish-wallet-encrypted` | Library to encrypt MnemonicHdNode as EncryptedMnemonicHdNode. Able to perform as MnemonicHdNode with passphrase known. `@defichain/jellyfish-wallet-mnemonic` | MnemonicHdNode implements the WalletHdNode from jellyfish-wallet; a CoinType-agnostic HD Wallet for noncustodial DeFi. `@defichain/testcontainers` | Provides a lightweight, throw away instances for DeFiD node provisioned automatically in a Docker container. `@defichain/testing` | Provides rich test fixture setup functions for effective and effortless testing. diff --git a/jest.config.js b/jest.config.js index 29d4e380b1..7b8b314451 100644 --- a/jest.config.js +++ b/jest.config.js @@ -12,6 +12,7 @@ module.exports = { '@defichain/jellyfish-transaction-builder': '/packages/jellyfish-transaction-builder/src', '@defichain/jellyfish-transaction': '/packages/jellyfish-transaction/src', '@defichain/jellyfish-wallet-classic': '/packages/jellyfish-wallet-classic/src', + '@defichain/jellyfish-wallet-encrypted': '/packages/jellyfish-wallet-encrypted/src', '@defichain/jellyfish-wallet-mnemonic': '/packages/jellyfish-wallet-mnemonic/src', '@defichain/jellyfish-wallet': '/packages/jellyfish-wallet/src', '@defichain/testcontainers': '/packages/testcontainers/src', diff --git a/package-lock.json b/package-lock.json index d13bceb6fa..7ad4d8c8d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1529,6 +1529,10 @@ "resolved": "packages/jellyfish-wallet-classic", "link": true }, + "node_modules/@defichain/jellyfish-wallet-encrypted": { + "resolved": "packages/jellyfish-wallet-encrypted", + "link": true + }, "node_modules/@defichain/jellyfish-wallet-mnemonic": { "resolved": "packages/jellyfish-wallet-mnemonic", "link": true @@ -13283,6 +13287,15 @@ "@types/node": "*" } }, + "node_modules/@types/scryptsy": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/scryptsy/-/scryptsy-2.0.0.tgz", + "integrity": "sha512-iDmneBKWSsmsR3SlGisOVnpCz7sB5Mqhv4I/pLg1DYq2zttWT65r+1gOVZtoxXiTsSf/JO8NhgyCKhx7cvGbaA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.0", "dev": true, @@ -26445,6 +26458,11 @@ "node": ">=10" } }, + "node_modules/scryptsy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/scryptsy/-/scryptsy-2.1.0.tgz", + "integrity": "sha512-1CdSqHQowJBnMAFyPEBRfqag/YP9OF394FV+4YREIJX4ljD7OxvQRDayyoyyCk+senRjSkP6VnUNQmVQqB6g7w==" + }, "node_modules/semver": { "version": "7.3.4", "dev": true, @@ -29541,6 +29559,7 @@ } }, "packages/jellyfish-wallet-classic": { + "name": "@defichain/jellyfish-wallet-classic", "version": "0.0.0", "license": "MIT", "dependencies": { @@ -29548,6 +29567,19 @@ "@defichain/jellyfish-wallet": "0.0.0" } }, + "packages/jellyfish-wallet-encrypted": { + "name": "@defichain/jellyfish-wallet-encrypted", + "version": "0.0.0", + "license": "MIT", + "dependencies": { + "@defichain/jellyfish-crypto": "0.0.0", + "@defichain/jellyfish-wallet": "0.0.0", + "scryptsy": "^2.1.0" + }, + "devDependencies": { + "@types/scryptsy": "^2.0.0" + } + }, "packages/jellyfish-wallet-mnemonic": { "name": "@defichain/jellyfish-wallet-mnemonic", "version": "0.0.0", @@ -30831,6 +30863,15 @@ "@defichain/jellyfish-wallet": "0.0.0" } }, + "@defichain/jellyfish-wallet-encrypted": { + "version": "file:packages/jellyfish-wallet-encrypted", + "requires": { + "@defichain/jellyfish-crypto": "0.0.0", + "@defichain/jellyfish-wallet": "0.0.0", + "@types/scryptsy": "^2.0.0", + "scryptsy": "^2.1.0" + } + }, "@defichain/jellyfish-wallet-mnemonic": { "version": "file:packages/jellyfish-wallet-mnemonic", "requires": { @@ -40225,6 +40266,15 @@ "@types/node": "*" } }, + "@types/scryptsy": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/scryptsy/-/scryptsy-2.0.0.tgz", + "integrity": "sha512-iDmneBKWSsmsR3SlGisOVnpCz7sB5Mqhv4I/pLg1DYq2zttWT65r+1gOVZtoxXiTsSf/JO8NhgyCKhx7cvGbaA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/stack-utils": { "version": "2.0.0", "dev": true @@ -49710,6 +49760,11 @@ "xmlchars": "^2.2.0" } }, + "scryptsy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/scryptsy/-/scryptsy-2.1.0.tgz", + "integrity": "sha512-1CdSqHQowJBnMAFyPEBRfqag/YP9OF394FV+4YREIJX4ljD7OxvQRDayyoyyCk+senRjSkP6VnUNQmVQqB6g7w==" + }, "semver": { "version": "7.3.4", "dev": true, diff --git a/packages/jellyfish-wallet-encrypted/README.md b/packages/jellyfish-wallet-encrypted/README.md new file mode 100644 index 0000000000..695414c765 --- /dev/null +++ b/packages/jellyfish-wallet-encrypted/README.md @@ -0,0 +1,7 @@ +[![npm](https://img.shields.io/npm/v/@defichain/jellyfish-wallet-encrypted)](https://www.npmjs.com/package/@defichain/jellyfish-wallet-encrypted/v/latest) +[![npm@next](https://img.shields.io/npm/v/@defichain/jellyfish-wallet-encrypted/next)](https://www.npmjs.com/package/@defichain/jellyfish-wallet-encrypted/v/next) + +# @defichain/jellyfish-wallet-encrypted + +This is an extended wallet hd node which has similar interface as @defichain/jellyfish-wallet-mnemonic. The library assists caller to encrypt mnemonic seed into provided storage. See example/demo.ts for more detail. + diff --git a/packages/jellyfish-wallet-encrypted/__tests__/scrypt-storage.test.ts b/packages/jellyfish-wallet-encrypted/__tests__/scrypt-storage.test.ts new file mode 100644 index 0000000000..6547ddf363 --- /dev/null +++ b/packages/jellyfish-wallet-encrypted/__tests__/scrypt-storage.test.ts @@ -0,0 +1,100 @@ +import { ScryptStorage, Storage, SimpleScryptsy } from '../src' + +class MockStorage implements Storage { + inMemory: string | undefined + async getter (): Promise { + return this.inMemory + } + + async setter (encrypted: string | undefined): Promise { + this.inMemory = encrypted + } +} +// Mock storage +let scryptStorage: ScryptStorage +const encryptedSeedStorage = new MockStorage() +const seedHashStorage = new MockStorage() + +const scryptProvider = new SimpleScryptsy() + +// 32 bytes +const samplePrivateKey = 'e9873d79c6d87dc0fb6a5778633389f4e93213303da61f20bd67fc233aa33262' + +beforeEach(async () => { + scryptStorage = new ScryptStorage(scryptProvider, encryptedSeedStorage, seedHashStorage) + await encryptedSeedStorage.setter(undefined) + await seedHashStorage.setter(undefined) +}) + +it('Should be able to encrypt / decrypt', async () => { + const pass = 'password' + + const privateKey = Buffer.from(samplePrivateKey, 'hex') + await scryptStorage.encrypt(privateKey, pass) + + // encrypted data stored + expect(await encryptedSeedStorage.getter()).not.toStrictEqual(null) + + const decrypted = await scryptStorage.decrypt(pass) + expect(decrypted?.toString('hex')).toStrictEqual(samplePrivateKey) +}) + +it('Should be able to encrypt / decrypt - simple passphrase', async () => { + // let say a 6 digit pin + const pass = '135790' + + const privateKey = Buffer.from(samplePrivateKey, 'hex') + await scryptStorage.encrypt(privateKey, pass) + + const decrypted = await scryptStorage.decrypt(pass) + expect(decrypted?.toString('hex')).toStrictEqual(samplePrivateKey) +}) + +it('decrypt() - should return null when no data in storage', async () => { + // let say a 6 digit pin + const pass = 'password' + + const decrypted = await scryptStorage.decrypt(pass) + expect(decrypted).toStrictEqual(null) +}) + +it('Should work with variable data length - long', async () => { + const longData = samplePrivateKey + samplePrivateKey + samplePrivateKey + samplePrivateKey + const pass = 'password' + + const data = Buffer.from(longData, 'hex') + await scryptStorage.encrypt(data, pass) + + const decrypted = await scryptStorage.decrypt(pass) + expect(decrypted?.toString('hex')).toStrictEqual(longData) +}) + +it('Should work with variable data length - short', async () => { + const shortData = 'ffaa' + const pass = 'password' + + const data = Buffer.from(shortData, 'hex') + await scryptStorage.encrypt(data, pass) + + const decrypted = await scryptStorage.decrypt(pass) + expect(decrypted?.toString('hex')).toStrictEqual(shortData) +}) + +it('Should reject "odd" number long data, only accept "even" number long data', async () => { + const pass = 'password' + + const fourBytes = 'eeffaabb' + await scryptStorage.encrypt(Buffer.from(fourBytes, 'hex'), pass) + + const threeBytes = 'ffaabb' + const data = Buffer.from(threeBytes, 'hex') + await expect(async () => { + await scryptStorage.encrypt(data, pass) + }).rejects.toThrow('Data length must be even number') + + const padded = Buffer.from('00' + threeBytes, 'hex') + await scryptStorage.encrypt(padded, pass) + + const decrypted = await scryptStorage.decrypt(pass) + expect(decrypted?.toString('hex')).toStrictEqual(padded.toString('hex')) +}) diff --git a/packages/jellyfish-wallet-encrypted/__tests__/scryptsy.test.ts b/packages/jellyfish-wallet-encrypted/__tests__/scryptsy.test.ts new file mode 100644 index 0000000000..ea29f0bd8b --- /dev/null +++ b/packages/jellyfish-wallet-encrypted/__tests__/scryptsy.test.ts @@ -0,0 +1,38 @@ +import { SimpleScryptsy } from '../src' + +describe('passphraseToKey()', () => { + it('Should be able to generate from random (same) passphrase into multiple desired length secret', () => { + const scryptsy = new SimpleScryptsy() + const secret1 = scryptsy.passphraseToKey('random password', Buffer.from('abcd', 'hex'), 64) + expect(secret1.length).toStrictEqual(64) + + const secret2 = scryptsy.passphraseToKey('random password', Buffer.from('abcd', 'hex'), 40) + expect(secret2.length).toStrictEqual(40) + }) + + it('Configurable params (difficulty)', () => { + const easy = new SimpleScryptsy({ + N: 16384, // 2 ^ 14 + r: 8, + p: 1 + }) + + const hard = new SimpleScryptsy({ + N: 16384, // 2 ^ 14 + r: 8, + p: 8 + }) + + const easyStart = Date.now() + easy.passphraseToKey('random password', Buffer.from('abcd', 'hex'), 32) + const easyTime = Date.now() - easyStart + + const hardStart = Date.now() + hard.passphraseToKey('random password', Buffer.from('abcd', 'hex'), 32) + const hardTime = Date.now() - hardStart + + // significantly slower + // technically it is 8x harder, but they can be processed in parallel + expect(hardTime).toBeGreaterThan(easyTime * 2) + }) +}) diff --git a/packages/jellyfish-wallet-encrypted/jest.config.js b/packages/jellyfish-wallet-encrypted/jest.config.js new file mode 100644 index 0000000000..11d9802ff4 --- /dev/null +++ b/packages/jellyfish-wallet-encrypted/jest.config.js @@ -0,0 +1,12 @@ +module.exports = { + testEnvironment: 'node', + testMatch: [ + '**/__tests__/**/*.test.ts' + ], + transform: { + '^.+\\.ts$': 'ts-jest' + }, + verbose: true, + clearMocks: true, + testTimeout: 120000 +} diff --git a/packages/jellyfish-wallet-encrypted/package.json b/packages/jellyfish-wallet-encrypted/package.json new file mode 100644 index 0000000000..7d1b042adf --- /dev/null +++ b/packages/jellyfish-wallet-encrypted/package.json @@ -0,0 +1,45 @@ +{ + "private": false, + "name": "@defichain/jellyfish-wallet-encrypted", + "version": "0.0.0", + "description": "A collection of TypeScript + JavaScript tools and libraries for DeFi Blockchain developers to build decentralized finance on Bitcoin", + "keywords": [ + "DeFiChain", + "DeFi", + "Blockchain", + "API", + "Bitcoin" + ], + "repository": "DeFiCh/jellyfish", + "bugs": "https://github.com/DeFiCh/jellyfish/issues", + "license": "MIT", + "contributors": [ + { + "name": "DeFiChain Foundation", + "email": "engineering@defichain.com", + "url": "https://defichain.com/" + }, + { + "name": "DeFi Blockchain Contributors" + }, + { + "name": "DeFi Jellyfish Contributors" + } + ], + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc -b ./tsconfig.build.json" + }, + "dependencies": { + "@defichain/jellyfish-crypto": "0.0.0", + "@defichain/jellyfish-wallet": "0.0.0", + "scryptsy": "^2.1.0" + }, + "devDependencies": { + "@types/scryptsy": "^2.0.0" + } +} diff --git a/packages/jellyfish-wallet-encrypted/src/index.ts b/packages/jellyfish-wallet-encrypted/src/index.ts new file mode 100644 index 0000000000..d4ab38bfce --- /dev/null +++ b/packages/jellyfish-wallet-encrypted/src/index.ts @@ -0,0 +1,2 @@ +export * from './scryptsy' +export * from './scrypt_storage' diff --git a/packages/jellyfish-wallet-encrypted/src/scrypt_storage.ts b/packages/jellyfish-wallet-encrypted/src/scrypt_storage.ts new file mode 100644 index 0000000000..9d4cb168b2 --- /dev/null +++ b/packages/jellyfish-wallet-encrypted/src/scrypt_storage.ts @@ -0,0 +1,165 @@ +import { dSHA256, AES256 } from '@defichain/jellyfish-crypto' + +export interface ScryptProvider { + passphraseToKey: (nfcUtf8: string, salt: Buffer, desiredKeyLen: number) => Buffer +} + +export type InitVectorProvider = () => Buffer + +export interface Storage { + getter: () => Promise + setter: (encrypted: string | undefined) => Promise +} + +export class EncryptedData { + constructor ( + // total = 7 + 2n bytes + readonly prefix: number, // 0x01 + readonly type: number, // 0x42 or 0x43 (only 0x42 for now) + readonly flags: number, // 1 byte (only true for 2 most significant bit for now) + readonly hash: Buffer, // 4 bytes, checksum and salt + readonly encryptedFirstHalf: Buffer, // n bytes + readonly encryptedSecondHalf: Buffer // n bytes + ) { + this.prefix = prefix + this.type = type + this.flags = flags + this.hash = hash + this.encryptedFirstHalf = encryptedFirstHalf + this.encryptedSecondHalf = encryptedSecondHalf + + if (encryptedFirstHalf.length !== encryptedSecondHalf.length) { + throw new Error('Unxpected data size, first and second half should have same length') + } + } + + encode (): string { + const first3Bytes = Buffer.from([this.prefix, this.type, this.flags]).toString('hex') + const full = first3Bytes + this.hash.toString('hex') + this.encryptedFirstHalf.toString('hex') + this.encryptedSecondHalf.toString('hex') + return full + } + + static decode (encoded: string): EncryptedData { + if (encoded.length < 18) { // min length is 9 bytes + throw new Error('Invalid encrypted data') + } + + const dataLen = encoded.length - 14 + if (dataLen % 2 !== 0) { + throw new Error('Invalid encrypted data') + } + + const firstHalfEndIndex = 14 + (dataLen / 2) + return new EncryptedData( + Number(encoded.slice(0, 2)), + Number(encoded.slice(2, 4)), + Number(encoded.slice(4, 6)), + Buffer.from(encoded.slice(6, 14), 'hex'), + Buffer.from(encoded.slice(14, firstHalfEndIndex), 'hex'), + Buffer.from(encoded.slice(firstHalfEndIndex), 'hex') + ) + } +} + +export class ScryptStorage { + /** + * @param {ScryptProvider} scryptProvider to convert a utf8 string into a secret, cryptographically secured + * @param {Storage} encryptedStorage to store encrypted data + * @param {Storage} hashStorage to store hash of the data, for passphrase verification use + * @param {InitVectorProvider} ivProvider `() => Buffer` as AES encryption iv randomizer, default = `crypto-js.randomBytes` + */ + constructor ( + readonly scryptProvider: ScryptProvider, + readonly encryptedStorage: Storage, + readonly hashStorage: Storage, + readonly ivProvider?: InitVectorProvider + ) { + this.scryptProvider = scryptProvider + this.encryptedStorage = encryptedStorage + this.hashStorage = hashStorage + this.ivProvider = ivProvider + } + + /** + * To encrypt `data` with a `passphrase` derived secret (derivation based on provided `ScryptProvider`) + * @see https://github.com/bitcoin/bips/blob/master/bip-0038.mediawiki#Encryption_when_EC_multiply_flag_is_not_used for encryption methodology + * + * @param {Buffer} data data with even number length + * @param {string} passphrase to derived encryption secret, utf8 string in normalization format C + */ + async encrypt (data: Buffer, passphrase: string): Promise { + if (data.length % 2 !== 0) { + throw new Error('Data length must be even number') + } + + const hash = dSHA256(data).slice(0, 4) + const key = this.scryptProvider.passphraseToKey(passphrase, hash, 64) + + const k1a = Buffer.from(key.toString('hex').slice(0, 32), 'hex') // 16 bytes + const k1b = Buffer.from(key.toString('hex').slice(32, 64), 'hex') // 16 bytes + const k2 = Buffer.from(key.toString('hex').slice(64), 'hex') // 32 bytes + + const d1 = Buffer.from(data.toString('hex').slice(0, data.length), 'hex') + const d2 = Buffer.from(data.toString('hex').slice(data.length), 'hex') + + const xor1 = this._xor(k1a, d1) + const xor2 = this._xor(k1b, d2) + + const b1 = AES256.encrypt(k2, xor1, this.ivProvider) + const b2 = AES256.encrypt(k2, xor2, this.ivProvider) + + const encrypted = new EncryptedData(0x01, 0x42, 0xc0, hash, b1, b2) + await this.encryptedStorage.setter(encrypted.encode()) + + const dataHash = dSHA256(data).slice(0, 4) + await this.hashStorage.setter(dataHash.toString('hex')) + } + + /** + * To decrypt raw data + * @param {string} passphrase to decrypted data, utf8 string in normalization format C + * @returns {Promise} null if no data found in storage + */ + async decrypt (passphrase: string): Promise { + const encrypted = await this.encryptedStorage.getter() + + if (encrypted === undefined) { + return null + } + + const data = EncryptedData.decode(encrypted) + const key = this.scryptProvider.passphraseToKey(passphrase, data.hash, 64) + + const k1a = Buffer.from(key.toString('hex').slice(0, 32), 'hex') // 16 bytes + const k1b = Buffer.from(key.toString('hex').slice(32, 64), 'hex') // 16 bytes + const k2 = Buffer.from(key.toString('hex').slice(64), 'hex') // 32 bytes + + const dec1 = AES256.decrypt(k2, data.encryptedFirstHalf) // 16 bytes = decipher(32 bytes) - salt + const dec2 = AES256.decrypt(k2, data.encryptedSecondHalf) + + const d1 = this._xor(k1a, dec1) + const d2 = this._xor(k1b, dec2) + const decrypted = Buffer.from([...d1, ...d2]) + + const dataHash = dSHA256(decrypted).slice(0, 4) + const expected = await this.hashStorage.getter() + if (dataHash.toString('hex') !== expected) { + throw new Error('InvalidPassphrase') + } + + return decrypted + } + + private _xor (key: Buffer, data: Buffer): Buffer { + const output = Buffer.alloc(data.length) + for (let i = 0, j = 0; i < data.length && i < data.length; i++) { + output[i] = data[i] ^ key[j] + if (j + 1 === data.length) { + j = 0 + } else { + j++ + } + } + return output + } +} diff --git a/packages/jellyfish-wallet-encrypted/src/scryptsy.ts b/packages/jellyfish-wallet-encrypted/src/scryptsy.ts new file mode 100644 index 0000000000..825b3fa36e --- /dev/null +++ b/packages/jellyfish-wallet-encrypted/src/scryptsy.ts @@ -0,0 +1,46 @@ +import scrypt from 'scryptsy' +import { ScryptProvider } from './scrypt_storage' + +export interface ScryptParams { + N: number + r: number + p: number +} + +const DEFAULT_SCRYPT_PARAMS: ScryptParams = { + N: 16384, + r: 8, + p: 8 +} + +/** + * A simple ScryptProvider implementation using + * {@link https://www.npmjs.com/package/scryptsy} (Javascript implementation of the scrypt key derivation) + * + * Mainly for testing and prototyping purpose, required for {@link EncryptedMnemonicProvider} instantiation + * Scrypty library may not compatible with other platforms, eg: react-native + */ +export class SimpleScryptsy implements ScryptProvider { + constructor (private readonly scryptParams: ScryptParams = DEFAULT_SCRYPT_PARAMS) {} + + /** + * Derive a specific length buffer via Scrypt implementation + * Recommended (by bip38) to serve as an private key encryption key + * + * @param {string} passphrase utf8 string + * @param {Buffer} salt + * @param {number} keyLength desired output buffer length + * @returns {Buffer} + */ + passphraseToKey (passphrase: string, salt: Buffer, keyLength: number): Buffer { + const secret = Buffer.from(passphrase.normalize('NFC'), 'utf8') + return scrypt( + secret, + salt, + this.scryptParams.N, + this.scryptParams.r, + this.scryptParams.p, + keyLength + ) + } +} diff --git a/packages/jellyfish-wallet-encrypted/tsconfig.build.json b/packages/jellyfish-wallet-encrypted/tsconfig.build.json new file mode 100644 index 0000000000..d4e0fc2696 --- /dev/null +++ b/packages/jellyfish-wallet-encrypted/tsconfig.build.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.build.json", + "include": [ + "./src/**/*" + ], + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + }, + "references": [ + {"path": "../jellyfish-crypto/tsconfig.build.json"}, + {"path": "../jellyfish-wallet/tsconfig.build.json"}, + {"path": "../jellyfish-wallet-mnemonic/tsconfig.build.json"} + ] +} diff --git a/tsconfig.build.json b/tsconfig.build.json index a044af1438..45bbdb3a38 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -19,6 +19,7 @@ {"path": "./packages/jellyfish-transaction-signature/tsconfig.build.json"}, {"path": "./packages/jellyfish-wallet/tsconfig.build.json"}, {"path": "./packages/jellyfish-wallet-classic/tsconfig.build.json"}, + {"path": "./packages/jellyfish-wallet-encrypted/tsconfig.build.json"}, {"path": "./packages/jellyfish-wallet-mnemonic/tsconfig.build.json"}, {"path": "./packages/testcontainers/tsconfig.build.json"}, {"path": "./packages/testing/tsconfig.build.json"} diff --git a/tsconfig.json b/tsconfig.json index 8a55a200bc..1ac46d6c1a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,6 +14,7 @@ "@defichain/jellyfish-transaction-signature": ["jellyfish-transaction-signature/src"], "@defichain/jellyfish-wallet": ["jellyfish-wallet/src"], "@defichain/jellyfish-wallet-classic": ["jellyfish-wallet-classic/src"], + "@defichain/jellyfish-wallet-encrypted": ["jellyfish-wallet-encrypted/src"], "@defichain/jellyfish-wallet-mnemonic": ["jellyfish-wallet-mnemonic/src"], "@defichain/testcontainers": ["testcontainers/src"], "@defichain/testing": ["testing/src"], @@ -29,6 +30,7 @@ "@defichain/jellyfish-transaction-signature/*": ["jellyfish-transaction-signature/src/*"], "@defichain/jellyfish-wallet/*": ["jellyfish-wallet/src/*"], "@defichain/jellyfish-wallet-classic/*": ["jellyfish-wallet-classic/src/*"], + "@defichain/jellyfish-wallet-encrypted/*": ["jellyfish-wallet-encrypted/src/*"], "@defichain/jellyfish-wallet-mnemonic/*": ["jellyfish-wallet-mnemonic/src/*"], "@defichain/testcontainers/*": ["testcontainers/src/*"], "@defichain/testing/*": ["testing/src/*"], diff --git a/website/docs/introduction.md b/website/docs/introduction.md index 9be68bf7db..5da403bbc6 100644 --- a/website/docs/introduction.md +++ b/website/docs/introduction.md @@ -39,6 +39,7 @@ Package | Description `@defichain/jellyfish-transaction-signature` | Stateless utility library to perform transaction signing. `@defichain/jellyfish-wallet` | Jellyfish wallet is a managed wallet, where account can get discovered from an HD seed. `@defichain/jellyfish-wallet-classic` | WalletClassic implements a simple, single elliptic pair wallet. +`@defichain/jellyfish-wallet-encrypted` | Library to encrypt MnemonicHdNode as EncryptedMnemonicHdNode. Able to perform as MnemonicHdNode with passphrase known. `@defichain/jellyfish-wallet-mnemonic` | MnemonicHdNode implements the WalletHdNode from jellyfish-wallet; a CoinType-agnostic HD Wallet for noncustodial DeFi. `@defichain/testcontainers` | Provides a lightweight, throw away instances for DeFiD node provisioned automatically in a Docker container. `@defichain/testing` | Provides rich test fixture setup functions for effective and effortless testing.