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
This commit is contained in:
Miro Metsänheimo 2021-06-03 23:19:08 +03:00
commit 51966303ec
10 changed files with 230 additions and 57 deletions

View file

@ -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>

View file

@ -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)
},
}

View 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'

View 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
}

View 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')

View 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) {

View 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 {

14
package-lock.json generated
View file

@ -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"

View 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",

View 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')
})
})