From d0b804a3b90317a3d80b8456a3b25374cead2667 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Mets=C3=A4nheimo?= Date: Sat, 2 May 2020 23:06:01 +0300 Subject: [PATCH] Version 1.2.0 * Import wallets with legacy Nano hex seeds --- README.md | 13 ++++++++--- index.ts | 52 +++++++++++++++++++++++++++++++++-------- lib/address-importer.ts | 40 +++++++++++++++++++++++++++++++ lib/signer.ts | 2 +- package-lock.json | 2 +- package.json | 2 +- test/test.js | 25 ++++++++++++++++++++ 7 files changed, 120 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 44a29d0..d5c380d 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ The toolkit supports creating and importing wallets and signing blocks on-device * BIP39/44 private key derivation * Mnemonic is compatible with the Ledger Nano implementation * Import wallets with a mnemonic phrase or a seed +* Import wallets with the legacy Nano hex seed * Sign send, receive and change representative blocks with a private key * Runs in all web browsers and mobile frameworks built with Javascript * Convert Nano units @@ -48,12 +49,18 @@ const wallet = wallet.fromMnemonic(mnemonic, seedPassword?) // Import a wallet with a seed const wallet = wallet.fromSeed(seed) +// Import a wallet with a legacy hex seed +const wallet = wallet.fromLegacySeed(seed) + // Derive private keys for a seed, from and to are number indexes const accounts = wallet.accounts(seed, from, to) + +// Derive private keys for a legacy seed, from and to are number indexes +const accounts = wallet.legacyAccounts(seed, from, to) ``` ```javascript -// The returned wallet JSON format is as follows: +// The returned wallet JSON format is as follows. The mnemonic phrase will be undefined when importing with a seed. { mnemonic: 'edge defense waste choose enrich upon flee junk siren film clown finish luggage leader kid quick brick print evidence swap drill paddle truly occur', seed: '0dc285fde768f7ff29b66ce7252d56ed92fe003b605907f7a4f683c3dc8586d34a914d3c71fc099bb38ee4a59e5b081a3497b7a323e90cc68f67b5837690310c', @@ -177,7 +184,7 @@ const converted = tools.convert('1000000000000000000000000000000', 'RAW', 'NANO' #### Signing any data with the private key -For example implementing client side login with the password being the user's e-mail signed with their private key +For example implementing client side login with the password being the user's e-mail signed with their private key. Make sure that you double check the signature on the back-end side with the public key. ```javascript import { tools } from 'nanocurrency-web' @@ -190,7 +197,7 @@ const signed = tools.sign(privateKey, 'foo@bar.com') ### In web ```html - + diff --git a/index.ts b/index.ts index e6099b9..58d294e 100644 --- a/index.ts +++ b/index.ts @@ -28,8 +28,8 @@ const wallet = { * An optional seed password can be used to encrypt the mnemonic phrase so the seed * cannot be derived correctly without the password. Recovering the password is not possible. * - * @param {string} [entropy] Optional 64 byte hexadecimal string entropy to be used instead of the default - * @param {string} [seedPassword] Optional seed password + * @param {string} [entropy] - (Optional) 64 byte hexadecimal string entropy to be used instead of the default + * @param {string} [seedPassword] - (Optional) seed password * @returns the generated mnemonic, seed and account */ generate: (entropy?: string, seedPassword?: string): Wallet => { @@ -46,10 +46,10 @@ const wallet = { * The Nano address is derived from the public key using standard Nano encoding. * The address is prefixed with 'nano_'. * - * @param {string} mnemonic The mnemonic phrase. Words are separated with a space - * @param {string} [seedPassword] Optional seed password + * @param {string} mnemonic - The mnemonic phrase. Words are separated with a space + * @param {string} [seedPassword] - (Optional) seed password * @throws Throws an error if the mnemonic phrase doesn't pass validations - * @returns the imported mnemonic, seed and account + * @returns the wallet derived from the mnemonic (mnemonic, seed, account) */ fromMnemonic: (mnemonic: string, seedPassword?: string): Wallet => { return importer.fromMnemonic(mnemonic, seedPassword) @@ -65,13 +65,31 @@ const wallet = { * The Nano address is derived from the public key using standard Nano encoding. * The address is prefixed with 'nano_'. * - * @param {string} seed The seed - * @returns the importes seed and account + * @param {string} seed - The seed + * @returns {Wallet} the wallet derived from the seed (seed, account) */ fromSeed: (seed: string): Wallet => { return importer.fromSeed(seed) }, + /** + * Import Nano cryptocurrency accounts from a legacy hex seed + * + * This function imports a wallet from a seed. The private key is derived from the seed using + * simply a blake2b hash function. The public key is derived from the private key using the ed25519 curve + * algorithm. + * + * The Nano address is derived from the public key using standard Nano encoding. + * The address is prefixed with 'nano_'. + * + * @param {string} seed - The seed + * @returns the wallet derived from the seed (seed, account) + * + */ + fromLegacySeed: (seed: string): Wallet => { + return importer.fromLegacySeed(seed); + }, + /** * Derive accounts for the seed * @@ -79,14 +97,28 @@ const wallet = { * from the given seed with input parameters 44'/165' and indexes based on the from and to * parameters. * - * @param {string} seed The seed - * @param {number} from The start index - * @param {number} to The end index + * @param {string} seed - The seed + * @param {number} from - The start index + * @param {number} to - The end index */ accounts: (seed: string, from: number, to: number): Account[] => { return importer.fromSeed(seed, from, to).accounts }, + /** + * Derive accounts for the legacy hex seed + * + * This function derives Nano accounts with the given seed with indexes + * based on the from and to parameters. + * + * @param {string} seed - The seed + * @param {number} from - The start index + * @param {number} to - The end index + */ + legacyAccounts: (seed: string, from: number, to: number): Account[] => { + return importer.fromLegacySeed(seed, from, to).accounts + }, + } const blockSigner = new BlockSigner() diff --git a/lib/address-importer.ts b/lib/address-importer.ts index cbad111..291538d 100644 --- a/lib/address-importer.ts +++ b/lib/address-importer.ts @@ -2,6 +2,8 @@ import Bip32KeyDerivation from './bip32-key-derivation' import Bip39Mnemonic from './bip39-mnemonic' import Ed25519 from './ed25519' import NanoAddress from './nano-address' +import Signer from './signer' +import Convert from './util/convert' export default class AddressImporter { @@ -40,6 +42,43 @@ export default class AddressImporter { return this.nano(seed, from, to, undefined) } + + + /** + * Import a wallet using a legacy seed + * + * @param {string} seed - The seed to import the wallet from + * @param {number} [from] - (Optional) The start index of the private keys to derive from + * @param {number} [to] - (Optional) The end index of the private keys to derive to + * @returns {Wallet} The wallet derived from the seed + */ + fromLegacySeed(seed: string, from: number = 0, to: number = 0): Wallet { + const signer = new Signer() + + const accounts: Account[] = [] + for (let i = from; i <= to; i++) { + const privateKey = Convert.ab2hex(signer.generateHash([seed, Convert.dec2hex(i, 4)])) + + const ed25519 = new Ed25519() + const keyPair = ed25519.generateKeys(privateKey) + + const nano = new NanoAddress() + const address = nano.deriveAddress(keyPair.publicKey) + + accounts.push({ + accountIndex: i, + privateKey: keyPair.privateKey, + publicKey: keyPair.publicKey, + address, + }) + } + + return { + mnemonic: undefined, + seed, + accounts, + } + } /** * Derives the private keys @@ -61,6 +100,7 @@ export default class AddressImporter { const nano = new NanoAddress() const address = nano.deriveAddress(keyPair.publicKey) + accounts.push({ accountIndex: i, privateKey: keyPair.privateKey, diff --git a/lib/signer.ts b/lib/signer.ts index a353d98..a16d2d2 100644 --- a/lib/signer.ts +++ b/lib/signer.ts @@ -26,7 +26,7 @@ export default class Signer { * * @param data Data to hash */ - private generateHash(data: string[]): Uint8Array { + generateHash(data: string[]): Uint8Array { const ctx = blake2bInit(32, undefined) data.forEach(str => blake2bUpdate(ctx, Convert.hex2ab(str))) return blake2bFinal(ctx) diff --git a/package-lock.json b/package-lock.json index 1cbc803..72a9fbb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "nanocurrency-web", - "version": "1.1.1", + "version": "1.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 441ef42..c3b1482 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nanocurrency-web", - "version": "1.1.1", + "version": "1.2.0", "description": "Toolkit for Nano cryptocurrency client side offline integrations", "author": "Miro Metsänheimo ", "license": "MIT", diff --git a/test/test.js b/test/test.js index c67bd7a..8658280 100644 --- a/test/test.js +++ b/test/test.js @@ -85,6 +85,31 @@ describe('import wallet with official test vectors test', () => { expect(result.accounts[0].address).to.equal('nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d') }) + it('should successfully import a legacy hex wallet with the a seed', () => { + const result = wallet.fromLegacySeed('0000000000000000000000000000000000000000000000000000000000000000') + expect(result).to.have.own.property('mnemonic') + expect(result).to.have.own.property('seed') + expect(result).to.have.own.property('accounts') + expect(result.mnemonic).to.be.undefined + expect(result.seed).to.equal('0000000000000000000000000000000000000000000000000000000000000000') + expect(result.accounts[0].privateKey).to.equal('9f0e444c69f77a49bd0be89db92c38fe713e0963165cca12faf5712d7657120f') + expect(result.accounts[0].publicKey).to.equal('c008b814a7d269a1fa3c6528b19201a24d797912db9996ff02a1ff356e45552b') + expect(result.accounts[0].address).to.equal('nano_3i1aq1cchnmbn9x5rsbap8b15akfh7wj7pwskuzi7ahz8oq6cobd99d4r3b7') + }) + + it('should successfully import legacy hex accounts with the a seed', () => { + const accounts = wallet.legacyAccounts('0000000000000000000000000000000000000000000000000000000000000000', 0, 3) + expect(accounts[0]).to.have.own.property('accountIndex') + expect(accounts[0]).to.have.own.property('privateKey') + expect(accounts[0]).to.have.own.property('publicKey') + expect(accounts[0]).to.have.own.property('address') + expect(accounts).to.have.lengthOf(4) + expect(accounts[2].accountIndex).to.equal(2) + expect(accounts[2].privateKey).to.equal('6a1804198020b080996ba45b5891f8227d7a4f41c8479824423780d234939d58') + expect(accounts[2].publicKey).to.equal('2fea520fe54f5d0dca79d553d9c7f5af7db6ac17586dbca6905794caadc639df') + expect(accounts[2].address).to.equal('nano_1dzcca9ycmtx3q79mocmu95zdduxptp3gp5fqkmb1ownscpweggzah8cb4rb') + }) + it('should throw when given a seed with an invalid length', () => { expect(() => wallet.generate('0dc285fde768f7ff29b66ce7252d56ed92fe003b605907f7a4f683c3dc8586d34a914d3c71fc099bb38ee4a59e5b081a3497b7a323e90cc68f67b5837690310')).to.throw(Error) expect(() => wallet.generate('0dc285fde768f7ff29b66ce7252d56ed92fe003b605907f7a4f683c3dc8586d34a914d3c71fc099bb38ee4a59e5b081a3497b7a323e90cc68f67b5837690310cd')).to.throw(Error)