// 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
### 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>
+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'
*
*/
fromLegacySeed: (seed: string): Wallet => {
- return importer.fromLegacySeed(seed);
+ return importer.fromLegacySeed(seed)
},
/**
*/
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)
},
/**
* @param {string} input The address to validate
*/
validateAddress: (input: string): boolean => {
- return nanoAddress.validateNanoAddress(input);
+ return nanoAddress.validateNanoAddress(input)
},
/**
* @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)
},
}
//@ts-ignore
-import { enc, algo } from 'crypto-js'
+import { algo, enc } from 'crypto-js'
+
import Convert from './util/convert'
const ED25519_CURVE = 'ed25519 seed'
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,
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,
}
}
- 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 {
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
}
+//@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 {
/**
* 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++) {
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')
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) {
-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
*/
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 {
{
"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"
{
"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",
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')
})
})