]> zoso.dev Git - libnemo.git/commitdiff
Version 1.3.3
authorMiro Metsänheimo <miro@metsanheimo.fi>
Thu, 3 Jun 2021 20:19:08 +0000 (23:19 +0300)
committerMiro Metsänheimo <miro@metsanheimo.fi>
Thu, 3 Jun 2021 20:19:08 +0000 (23:19 +0300)
* 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
index.ts
lib/bip32-key-derivation.ts
lib/block-signer.ts
lib/ed25519.ts
lib/nano-address.ts
lib/signer.ts
package-lock.json
package.json
test/test.js

index d7d113337b56daafc0d59f47f929a3d05c13ee9f..cdbfd1cbb0f2286a0fa52f487d47b12db0ac1665 100644 (file)
--- 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
-<script src="https://unpkg.com/nanocurrency-web@1.3.2" type="text/javascript"></script>
+<script src="https://unpkg.com/nanocurrency-web@1.3.3" type="text/javascript"></script>
 <script type="text/javascript">
     NanocurrencyWeb.wallet.generate(...);
 </script>
index b91b08079168b44b8f46458f993c03096428e66c..ea58a274fdacf232855f6dbeeadaa4787efc8c0d 100644 (file)
--- 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)
        },
 
 }
index 5a258016bc0a316d6653d87ba5fa20ff311a5613..dd2e55416015555dabc56c26ade2c15383bf45c0 100644 (file)
@@ -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'
index 15ea3650064f116d996dbb31c41537bd32c3df15..63ff87fbd94690b279714f50c2bc58907c8a8a49 100644 (file)
@@ -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
 }
index e14fd4ef327de89f6eaeadb70e2dcb3077b51dcb..cbf6b7628a75ce4b9dd2eee94bb68999498c6b44 100644 (file)
@@ -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')
index 42d282018607142f76c55811744b5dd0455d8672..2ff72acf02c515adcf3526fd38209f96bea168a5 100644 (file)
@@ -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) {
index a16d2d295fee5ebd369c29c77b8825ce61c26e9f..66f8844cfe48184897ca862356575d123db9e2fa 100644 (file)
@@ -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 {
index 21a485784d72e5edccedaf5e14e79f462f1f99e9..3f8de041d9d88cfdbf01e50e8c9c4937fd9e55c1 100644 (file)
@@ -1,6 +1,6 @@
 {
        "name": "nanocurrency-web",
-       "version": "1.3.2",
+       "version": "1.3.3",
        "lockfileVersion": 1,
        "requires": true,
        "dependencies": {
                        }
                },
                "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": {
                        "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"
index 8418f38173a202cc156de73f1fa2061645005c1f..e19ca6e7d5cf45b402ef2208750123f120fc0b30 100644 (file)
@@ -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 <miro@metsanheimo.fi>",
        "license": "MIT",
index 4f77812c231ba7557e242f2a5c2e1a20737e91f3..71c8a993a606493ac9e7d59e02796516b9506f4a 100644 (file)
@@ -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')
        })
 
 })