From 51966303ec73be754a863961a9a5655d537cd103 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Miro=20Mets=C3=A4nheimo?= Date: Thu, 3 Jun 2021 23:19:08 +0300 Subject: [PATCH] Version 1.3.3 * New feature: verify signatures with the public key * New feature: convert nano address to a public key * Add documentation about how to verify ownership of user's Nano address by doing a signature challenge * npm audit fix --- README.md | 28 ++++++++++++++++++- index.ts | 54 +++++++++++++++++++++++++++++++++---- lib/bip32-key-derivation.ts | 3 ++- lib/block-signer.ts | 34 +++++++---------------- lib/ed25519.ts | 51 ++++++++++++++++++++++++++++++----- lib/nano-address.ts | 19 ++++++++++++- lib/signer.ts | 29 +++++++++++++++----- package-lock.json | 14 +++++----- package.json | 2 +- test/test.js | 53 +++++++++++++++++++++++++++++++++--- 10 files changed, 230 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index d7d1133..cdbfd1c 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,32 @@ const data = { // Returns a correctly formatted and signed block ready to be sent to the blockchain const signedBlock = block.representative(data, privateKey) +``` +#### Verifying signatures +Cryptocurrencies rely on public key cryptographgy. This means that you can use the public key to validate the signature of the block that is signed with the private key. +```javascript +import { tools } from 'nanocurrency-web' + +const valid = tools.verifyBlock(publicKey, block) +``` +##### Using signature verification to prove ownership of the address +You are able to challenge an user to prove ownership of a Nano address simply by making the user sign any string with the private key and validating the signature. +```javascript +import { tools } from 'nanocurrency-web' + +const nanoAddress = 'nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d' +const privateKey = '3be4fc2ef3f3b7374e6fc4fb6e7bb153f8a2998b3b3dab50853eabe128024143' +const data = 'sign this' + +// Make the user sign the data +const signature = tools.sign(privateKey, data) + +// Infer the user's public key from the address (if not already known) +const publicKey = tools.addressToPublicKey(nanoAddress) + +// Verify the signature using the public key, the signature and the original data +const validSignature = tools.verify(publicKey, signature, data) + ``` #### Converting units @@ -217,7 +243,7 @@ const valid = tools.validateMnemonic('edge defense waste choose enrich upon flee ### In web ```html - + diff --git a/index.ts b/index.ts index b91b080..ea58a27 100644 --- a/index.ts +++ b/index.ts @@ -1,8 +1,10 @@ +import { TextDecoder } from 'util' + import BigNumber from 'bignumber.js' import AddressGenerator from './lib/address-generator' import AddressImporter, { Account, Wallet } from './lib/address-importer' -import BlockSigner, { ReceiveBlock, RepresentativeBlock, SendBlock, SignedBlock } from './lib/block-signer' +import BlockSigner, { BlockData, ReceiveBlock, RepresentativeBlock, SendBlock, SignedBlock } from './lib/block-signer' import NanoAddress from './lib/nano-address' import NanoConverter from './lib/nano-converter' import Signer from './lib/signer' @@ -127,7 +129,7 @@ const wallet = { * */ fromLegacySeed: (seed: string): Wallet => { - return importer.fromLegacySeed(seed); + return importer.fromLegacySeed(seed) }, /** @@ -260,7 +262,36 @@ const tools = { */ sign: (privateKey: string, ...input: string[]): string => { const data = input.map(Convert.stringToHex) - return signer.sign(privateKey, ...data); + return signer.sign(privateKey, ...data) + }, + + /** + * Verifies the signature of any input string + * + * @param {string} publicKey The public key to verify with + * @param {string} signature The signature to verify + * @param {...string} input Data to verify + */ + verify: (publicKey: string, signature: string, ...input: string[]): boolean => { + const data = input.map(Convert.stringToHex) + return signer.verify(publicKey, signature, ...data) + }, + + /** + * Verifies the signature of any input string + * + * @param {string} publicKey The public key to verify with + * @param {BlockData} block The block to verify + */ + verifyBlock: (publicKey: string, block: BlockData) => { + const preamble = 0x6.toString().padStart(64, '0') + return signer.verify(publicKey, block.signature, + preamble, + nanoAddress.nanoAddressToHexString(block.account), + block.previous, + nanoAddress.nanoAddressToHexString(block.representative), + Convert.dec2hex(block.balance, 16).toUpperCase(), + block.link) }, /** @@ -269,7 +300,7 @@ const tools = { * @param {string} input The address to validate */ validateAddress: (input: string): boolean => { - return nanoAddress.validateNanoAddress(input); + return nanoAddress.validateNanoAddress(input) }, /** @@ -278,7 +309,20 @@ const tools = { * @param {string} input The address to validate */ validateMnemonic: (input: string): boolean => { - return importer.validateMnemonic(input); + return importer.validateMnemonic(input) + }, + + /** + * Convert a Nano address to a public key + * + * @param {string} input Nano address to convert + */ + addressToPublicKey: (input: string): string => { + const cleaned = input + .replace('nano_', '') + .replace('xrb_', '') + const publicKeyBytes = nanoAddress.decodeNanoBase32(cleaned) + return Convert.ab2hex(publicKeyBytes).slice(0, 64) }, } diff --git a/lib/bip32-key-derivation.ts b/lib/bip32-key-derivation.ts index 5a25801..dd2e554 100644 --- a/lib/bip32-key-derivation.ts +++ b/lib/bip32-key-derivation.ts @@ -1,5 +1,6 @@ //@ts-ignore -import { enc, algo } from 'crypto-js' +import { algo, enc } from 'crypto-js' + import Convert from './util/convert' const ED25519_CURVE = 'ed25519 seed' diff --git a/lib/block-signer.ts b/lib/block-signer.ts index 15ea365..63ff87f 100644 --- a/lib/block-signer.ts +++ b/lib/block-signer.ts @@ -58,9 +58,9 @@ export default class BlockSigner { const newBalanceNano = new BigNumber(balanceNano).plus(new BigNumber(amountNano)) const newBalanceRaw = NanoConverter.convert(newBalanceNano, 'NANO', 'RAW') const newBalanceHex = Convert.dec2hex(newBalanceRaw, 16).toUpperCase() - const account = this.nanoAddressToHexString(data.toAddress) + const account = this.nanoAddress.nanoAddressToHexString(data.toAddress) const link = data.transactionHash - const representative = this.nanoAddressToHexString(data.representativeAddress) + const representative = this.nanoAddress.nanoAddressToHexString(data.representativeAddress) const signature = this.signer.sign( privateKey, @@ -125,9 +125,9 @@ export default class BlockSigner { const newBalanceNano = new BigNumber(balanceNano).minus(new BigNumber(amountNano)) const newBalanceRaw = NanoConverter.convert(newBalanceNano, 'NANO', 'RAW') const newBalanceHex = Convert.dec2hex(newBalanceRaw, 16).toUpperCase() - const account = this.nanoAddressToHexString(data.fromAddress) - const link = this.nanoAddressToHexString(data.toAddress) - const representative = this.nanoAddressToHexString(data.representativeAddress) + const account = this.nanoAddress.nanoAddressToHexString(data.fromAddress) + const link = this.nanoAddress.nanoAddressToHexString(data.toAddress) + const representative = this.nanoAddress.nanoAddressToHexString(data.representativeAddress) const signature = this.signer.sign( privateKey, @@ -150,23 +150,6 @@ export default class BlockSigner { } } - private nanoAddressToHexString(addr: string): string { - addr = addr.slice(-60) - const isValid = /^[13456789abcdefghijkmnopqrstuwxyz]+$/.test(addr) - if (isValid) { - const keyBytes = this.nanoAddress.decodeNanoBase32(addr.substring(0, 52)) - const hashBytes = this.nanoAddress.decodeNanoBase32(addr.substring(52, 60)) - const blakeHash = blake2b(keyBytes, undefined, 5).reverse() - if (Convert.ab2hex(hashBytes) == Convert.ab2hex(blakeHash)) { - const key = Convert.ab2hex(keyBytes).toUpperCase() - return key - } - throw new Error('Checksum mismatch in address') - } else { - throw new Error('Illegal characters in address') - } - } - } export interface ReceiveBlock { @@ -197,13 +180,16 @@ export interface RepresentativeBlock { work?: string } -export interface SignedBlock { +export interface SignedBlock extends BlockData { type: 'state' + work?: string +} + +export interface BlockData { account: string previous: string representative: string balance: string link: string signature: string - work: string } diff --git a/lib/ed25519.ts b/lib/ed25519.ts index e14fd4e..cbf6b76 100644 --- a/lib/ed25519.ts +++ b/lib/ed25519.ts @@ -1,8 +1,9 @@ +//@ts-ignore +import { blake2b, blake2bFinal, blake2bInit, blake2bUpdate } from 'blakejs' + import Convert from './util/convert' import Curve25519 from './util/curve25519' - -//@ts-ignore -import { blake2b } from 'blakejs' +import Util from './util/util' export default class Ed25519 { @@ -120,11 +121,11 @@ export default class Ed25519 { /** * Generate a message signature * @param {Uint8Array} msg Message to be signed as byte array - * @param {Uint8Array} secretKey Secret key as byte array + * @param {Uint8Array} privateKey Secret key as byte array * @param {Uint8Array} Returns the signature as 64 byte typed array */ - sign(msg: Uint8Array, secretKey: Uint8Array): Uint8Array { - const signedMsg = this.naclSign(msg, secretKey) + sign(msg: Uint8Array, privateKey: Uint8Array): Uint8Array { + const signedMsg = this.naclSign(msg, privateKey) const sig = new Uint8Array(64) for (let i = 0; i < sig.length; i++) { @@ -134,6 +135,44 @@ export default class Ed25519 { return sig } + /** + * Verify a message signature + * @param {Uint8Array} msg Message to be signed as byte array + * @param {Uint8Array} publicKey Public key as byte array + * @param {Uint8Array} signature Signature as byte array + * @param {Uint8Array} Returns the signature as 64 byte typed array + */ + verify(msg: Uint8Array, publicKey: Uint8Array, signature: Uint8Array): boolean { + const CURVE = this.curve; + const p = [CURVE.gf(), CURVE.gf(), CURVE.gf(), CURVE.gf()] + const q = [CURVE.gf(), CURVE.gf(), CURVE.gf(), CURVE.gf()] + + if (signature.length !== 64) { + return false + } + if (publicKey.length !== 32) { + return false + } + if (CURVE.unpackNeg(q, publicKey)) { + return false + } + + const ctx = blake2bInit(64, undefined) + blake2bUpdate(ctx, signature.subarray(0, 32)) + blake2bUpdate(ctx, publicKey) + blake2bUpdate(ctx, msg) + let k = blake2bFinal(ctx) + this.reduce(k) + this.scalarmult(p, q, k) + + let t = new Uint8Array(32) + this.scalarbase(q, signature.subarray(32)) + CURVE.add(p, q) + this.pack(t, p) + + return Util.compare(signature.subarray(0, 32), t) + } + private naclSign(msg: Uint8Array, secretKey: Uint8Array): Uint8Array { if (secretKey.length !== 32) { throw new Error('bad secret key size') diff --git a/lib/nano-address.ts b/lib/nano-address.ts index 42d2820..2ff72ac 100644 --- a/lib/nano-address.ts +++ b/lib/nano-address.ts @@ -108,7 +108,24 @@ export default class NanoAddress { return expectedChecksum === actualChecksum } - readChar(char: string): number { + nanoAddressToHexString = (addr: string): string => { + addr = addr.slice(-60) + const isValid = /^[13456789abcdefghijkmnopqrstuwxyz]+$/.test(addr) + if (isValid) { + const keyBytes = this.decodeNanoBase32(addr.substring(0, 52)) + const hashBytes = this.decodeNanoBase32(addr.substring(52, 60)) + const blakeHash = blake2b(keyBytes, undefined, 5).reverse() + if (Convert.ab2hex(hashBytes) == Convert.ab2hex(blakeHash)) { + const key = Convert.ab2hex(keyBytes).toUpperCase() + return key + } + throw new Error('Checksum mismatch in address') + } else { + throw new Error('Illegal characters in address') + } + } + + private readChar(char: string): number { const idx = this.alphabet.indexOf(char) if (idx === -1) { diff --git a/lib/signer.ts b/lib/signer.ts index a16d2d2..66f8844 100644 --- a/lib/signer.ts +++ b/lib/signer.ts @@ -1,16 +1,17 @@ -import Convert from './util/convert' -import Ed25519 from './ed25519' - //@ts-ignore -import { blake2bInit, blake2bUpdate, blake2bFinal } from 'blakejs' + +import { blake2bFinal, blake2bInit, blake2bUpdate } from 'blakejs' + +import Ed25519 from './ed25519' +import Convert from './util/convert' export default class Signer { - + ed25519 = new Ed25519() /** * Signs any data using the ed25519 signature system - * + * * @param privateKey Private key to sign the data with * @param data Data to sign */ @@ -21,9 +22,23 @@ export default class Signer { return Convert.ab2hex(signature) } + /** + * Verify the signature with a public key + * + * @param publicKey Public key to verify the data with + * @param signature Signature to verify + * @param data Data to verify + */ + verify(publicKey: string, signature: string, ...data: string[]): boolean { + return this.ed25519.verify( + this.generateHash(data), + Convert.hex2ab(publicKey), + Convert.hex2ab(signature)); + } + /** * Creates a blake2b hash of the input data - * + * * @param data Data to hash */ generateHash(data: string[]): Uint8Array { diff --git a/package-lock.json b/package-lock.json index 21a4857..3f8de04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "nanocurrency-web", - "version": "1.3.2", + "version": "1.3.3", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2247,9 +2247,9 @@ } }, "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, "log-symbols": { @@ -3463,9 +3463,9 @@ "dev": true }, "ssri": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", - "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz", + "integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==", "dev": true, "requires": { "figgy-pudding": "^3.5.1" diff --git a/package.json b/package.json index 8418f38..e19ca6e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nanocurrency-web", - "version": "1.3.2", + "version": "1.3.3", "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 4f77812..71c8a99 100644 --- a/test/test.js +++ b/test/test.js @@ -256,14 +256,59 @@ describe('unit conversion tests', () => { describe('Signer tests', () => { + let testWallet; + + before(() => { + this.testWallet = wallet.generate(); + }) + + // Private key: 3be4fc2ef3f3b7374e6fc4fb6e7bb153f8a2998b3b3dab50853eabe128024143 + // Public key: 5b65b0e8173ee0802c2c3e6c9080d1a16b06de1176c938a924f58670904e82c4 + it('should sign data with a single parameter', () => { - const result = tools.sign('781186FB9EF17DB6E3D1056550D9FAE5D5BBADA6A6BC370E4CBB938B1DC71DA3', 'miro@metsanheimo.fi') - expect(result).to.equal('0ede9f287b7d58a053aa9ad84419c856ac39ec4c2453098ef19abf9638b07b1993e0cd3747723aada71602e92e781060dc3b91c410d32def1b4780a62fd0eb02') + const result = tools.sign('3be4fc2ef3f3b7374e6fc4fb6e7bb153f8a2998b3b3dab50853eabe128024143', 'miro@metsanheimo.fi') + expect(result).to.equal('fecb9b084065adc969904b55a0099c63746b68df41fecb713244d387eed83a80b9d4907278c5ebc0998a5fc8ba597fbaaabbfce0abd2ca2212acfe788637040c') }) it('should sign data with multiple parameters', () => { - const result = tools.sign('781186FB9EF17DB6E3D1056550D9FAE5D5BBADA6A6BC370E4CBB938B1DC71DA3', 'miro@metsanheimo.fi', 'somePassword') - expect(result).to.equal('a7b88357a160f54cf4db2826c86483eb60e66e8ccb36f9a37f3fb636c9d80f7b59d1fba88d0be27f85ac3fcbe5c6e13f911d7e5b713e86fb8e9a635932a2af05') + const result = tools.sign('3be4fc2ef3f3b7374e6fc4fb6e7bb153f8a2998b3b3dab50853eabe128024143', 'miro@metsanheimo.fi', 'somePassword') + expect(result).to.equal('bb534f9b469af451b1941ffef8ee461fc5d284b5d393140900c6e13a65ef08d0ae2bc77131ee182922f66c250c7237a83878160457d5c39a70e55f7fce925804') + }) + + it('should verify a signature using the public key', () => { + const result = tools.verify('5b65b0e8173ee0802c2c3e6c9080d1a16b06de1176c938a924f58670904e82c4', 'fecb9b084065adc969904b55a0099c63746b68df41fecb713244d387eed83a80b9d4907278c5ebc0998a5fc8ba597fbaaabbfce0abd2ca2212acfe788637040c', 'miro@metsanheimo.fi') + expect(result).to.be.true + + const result2 = tools.verify('5b65b0e8173ee0802c2c3e6c9080d1a16b06de1176c938a924f58670904e82c4', 'fecb9b084065adc969904b55a0099c63746b68df41fecb713244d387eed83a80b9d4907278c5ebc0998a5fc8ba597fbaaabbfce0abd2ca2212acfe788637040c', 'mir@metsanheimo.fi') + expect(result2).to.be.false + + const result3 = tools.verify('5b65b0e8173ee0802c2c3e6c9080d1a16b06de1176c938a924f58670904e82c4', 'aecb9b084065adc969904b55a0099c63746b68df41fecb713244d387eed83a80b9d4907278c5ebc0998a5fc8ba597fbaaabbfce0abd2ca2212acfe788637040c', 'miro@metsanheimo.fi') + expect(result3).to.be.false + }) + + it('should verify a block using the public key', () => { + const sendBlock = block.send({ + walletBalanceRaw: '5618869000000000000000000000000', + fromAddress: this.testWallet.accounts[0].address, + toAddress: 'nano_1q3hqecaw15cjt7thbtxu3pbzr1eihtzzpzxguoc37bj1wc5ffoh7w74gi6p', + representativeAddress: 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou', + frontier: '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', + amountRaw: '2000000000000000000000000000000', + }, this.testWallet.accounts[0].privateKey) + + const publicKey = tools.addressToPublicKey(this.testWallet.accounts[0].address) + + const valid = tools.verifyBlock(publicKey, sendBlock) + expect(valid).to.be.true + + sendBlock.account = 'nano_1q3hqecaw15cjt7thbtxu3pbzr1eihtzzpzxguoc37bj1wc5ffoh7w74gi6p' + const valid2 = tools.verifyBlock(this.testWallet.accounts[0].publicKey, sendBlock) + expect(valid2).to.be.false + }) + + it('should convert a Nano address to public key', () => { + const publicKey = tools.addressToPublicKey('nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d') + expect(publicKey).to.equal('5b65b0e8173ee0802c2c3e6c9080d1a16b06de1176c938a924f58670904e82c4') }) }) -- 2.34.1