Skip to content

Commit

Permalink
Version 1.3.3
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
numsu committed Jun 3, 2021
1 parent 1134d96 commit 5196630
Show file tree
Hide file tree
Showing 10 changed files with 230 additions and 57 deletions.
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -217,7 +243,7 @@ const valid = tools.validateMnemonic('edge defense waste choose enrich upon flee
### In web
```html
<script src="https://unpkg.com/[email protected].2" type="text/javascript"></script>
<script src="https://unpkg.com/[email protected].3" type="text/javascript"></script>
<script type="text/javascript">
NanocurrencyWeb.wallet.generate(...);
</script>
Expand Down
54 changes: 49 additions & 5 deletions index.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -127,7 +129,7 @@ const wallet = {
*
*/
fromLegacySeed: (seed: string): Wallet => {
return importer.fromLegacySeed(seed);
return importer.fromLegacySeed(seed)
},

/**
Expand Down Expand Up @@ -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)
},

/**
Expand All @@ -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)
},

/**
Expand All @@ -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)
},

}
Expand Down
3 changes: 2 additions & 1 deletion lib/bip32-key-derivation.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
34 changes: 10 additions & 24 deletions lib/block-signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
51 changes: 45 additions & 6 deletions lib/ed25519.ts
Original file line number Diff line number Diff line change
@@ -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 {

Expand Down Expand Up @@ -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++) {
Expand All @@ -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')
Expand Down
19 changes: 18 additions & 1 deletion lib/nano-address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
29 changes: 22 additions & 7 deletions lib/signer.ts
Original file line number Diff line number Diff line change
@@ -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
*/
Expand All @@ -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 {
Expand Down
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>",
"license": "MIT",
Expand Down
Loading

0 comments on commit 5196630

Please sign in to comment.