From: Miro Metsänheimo Date: Fri, 22 Apr 2022 20:54:27 +0000 (+0300) Subject: Version 1.4.0 X-Git-Tag: v0.0.1~15^2 X-Git-Url: https://zoso.dev/?a=commitdiff_plain;h=7f5cdc33c8e8a6ecb32867836abffc2e303c3896;p=libnemo.git Version 1.4.0 * Added new functionality to encrypt/decrypt strings with Diffie-Hellman key exchange with Nano addresses and private keys by converting the keys to Curve25519 keys suitable for encryption and using Box functionality from NaCl. The library will generate a random nonce to each encryption and pass the nonce along with the encrypted message encoded in Base64 * Some code refactoring (use static classes and make sure ed25519 and curve classes are always freshly created) --- diff --git a/README.md b/README.md index 2eeeec5..f0c0df3 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ npm install nanocurrency-web ### In web ```html - + @@ -114,7 +114,7 @@ If the account hasn't been opened yet (this is the first block), you will need t ```javascript import { block } from 'nanocurrency-web' -const privateKey = '781186FB9EF17DB6E3D1056550D9FAE5D5BBADA6A6BC370E4CBB938B1DC71DA3'; +const privateKey = '781186FB9EF17DB6E3D1056550D9FAE5D5BBADA6A6BC370E4CBB938B1DC71DA3' const data = { // Current balance from account info walletBalanceRaw: '5618869000000000000000000000000', @@ -147,7 +147,7 @@ const signedBlock = block.send(data, privateKey) ```javascript import { block } from 'nanocurrency-web' -const privateKey = '781186FB9EF17DB6E3D1056550D9FAE5D5BBADA6A6BC370E4CBB938B1DC71DA3'; +const privateKey = '781186FB9EF17DB6E3D1056550D9FAE5D5BBADA6A6BC370E4CBB938B1DC71DA3' const data = { // Your current balance in RAW from account info walletBalanceRaw: '18618869000000000000000000000000', @@ -180,7 +180,7 @@ const signedBlock = block.receive(data, privateKey) ```javascript import { block } from 'nanocurrency-web' -const privateKey = '781186FB9EF17DB6E3D1056550D9FAE5D5BBADA6A6BC370E4CBB938B1DC71DA3'; +const privateKey = '781186FB9EF17DB6E3D1056550D9FAE5D5BBADA6A6BC370E4CBB938B1DC71DA3' const data = { // Your current balance, from account info walletBalanceRaw: '3000000000000000000000000000000', @@ -270,6 +270,19 @@ const valid = tools.validateAddress('nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94 const valid = tools.validateMnemonic('edge defense waste choose enrich upon flee junk siren film clown finish luggage leader kid quick brick print evidence swap drill paddle truly occur') ``` +#### Encrypting and decrypting strings +You are able to encrypt and decrypt strings to implement end-to-end encryption using the Diffie-Hellman key exchange by using the Nano address and private key. The public and private keys are converted to Curve25519 keys which are suitable for encryption within the library. + +```javascript +import { box } from 'nanocurrency-web' + +// Encrypt on device 1 +const encrypted = box.encrypt(message, recipientAddress, senderPrivateKey) + +// Send the encrypted message to the recipient and decrypt on device 2 +const decrypted = box.decrypt(encrypted, senderAddress, recipientPrivateKey) +``` + ## Contributions You are welcome to contribute to the module. To develop, use the following commands. diff --git a/index.ts b/index.ts index 4146b4c..8217f9e 100644 --- a/index.ts +++ b/index.ts @@ -3,16 +3,12 @@ import BigNumber from 'bignumber.js' import AddressGenerator from './lib/address-generator' import AddressImporter, { Account, Wallet } from './lib/address-importer' import BlockSigner, { BlockData, ReceiveBlock, RepresentativeBlock, SendBlock, SignedBlock } from './lib/block-signer' +import Box from './lib/box' import NanoAddress from './lib/nano-address' import NanoConverter from './lib/nano-converter' import Signer from './lib/signer' import Convert from './lib/util/convert' -const nanoAddress = new NanoAddress() -const generator = new AddressGenerator() -const importer = new AddressImporter() -const signer = new Signer() - const wallet = { /** @@ -38,7 +34,7 @@ const wallet = { * @returns {Wallet} The wallet */ generate: (entropy?: string, seedPassword?: string): Wallet => { - return generator.generateWallet(entropy, seedPassword) + return AddressGenerator.generateWallet(entropy, seedPassword) }, /** @@ -57,7 +53,7 @@ const wallet = { * @returns {Wallet} The wallet */ generateLegacy: (seed?: string): Wallet => { - return generator.generateLegacyWallet(seed) + return AddressGenerator.generateLegacyWallet(seed) }, /** @@ -76,7 +72,7 @@ const wallet = { * @returns {Wallet} The wallet */ fromMnemonic: (mnemonic: string, seedPassword?: string): Wallet => { - return importer.fromMnemonic(mnemonic, seedPassword) + return AddressImporter.fromMnemonic(mnemonic, seedPassword) }, /** @@ -93,7 +89,7 @@ const wallet = { * @returns {Wallet} The wallet */ fromLegacyMnemonic: (mnemonic: string): Wallet => { - return importer.fromLegacyMnemonic(mnemonic) + return AddressImporter.fromLegacyMnemonic(mnemonic) }, /** @@ -110,7 +106,7 @@ const wallet = { * @returns {Wallet} The wallet, without the mnemonic phrase because it's not possible to infer backwards */ fromSeed: (seed: string): Wallet => { - return importer.fromSeed(seed) + return AddressImporter.fromSeed(seed) }, /** @@ -127,7 +123,7 @@ const wallet = { * @returns {Wallet} The wallet */ fromLegacySeed: (seed: string): Wallet => { - return importer.fromLegacySeed(seed) + return AddressImporter.fromLegacySeed(seed) }, /** @@ -143,7 +139,7 @@ const wallet = { * @returns {Account[]} a list of accounts */ accounts: (seed: string, from: number, to: number): Account[] => { - return importer.fromSeed(seed, from, to).accounts + return AddressImporter.fromSeed(seed, from, to).accounts }, /** @@ -158,12 +154,11 @@ const wallet = { * @returns {Account[]} a list of accounts */ legacyAccounts: (seed: string, from: number, to: number): Account[] => { - return importer.fromLegacySeed(seed, from, to).accounts + return AddressImporter.fromLegacySeed(seed, from, to).accounts }, } -const blockSigner = new BlockSigner() const block = { /** @@ -185,7 +180,7 @@ const block = { * @returns {SignedBlock} the signed block */ send: (data: SendBlock, privateKey: string): SignedBlock => { - return blockSigner.send(data, privateKey) + return BlockSigner.send(data, privateKey) }, @@ -208,7 +203,7 @@ const block = { * @returns {SignedBlock} the signed block */ receive: (data: ReceiveBlock, privateKey: string): SignedBlock => { - return blockSigner.receive(data, privateKey) + return BlockSigner.receive(data, privateKey) }, @@ -236,7 +231,7 @@ const block = { toAddress: 'nano_1111111111111111111111111111111111111111111111111111hifc8npp', // Burn address } - return blockSigner.send(block, privateKey) + return BlockSigner.send(block, privateKey) }, } @@ -266,7 +261,7 @@ const tools = { */ sign: (privateKey: string, ...input: string[]): string => { const data = input.map(Convert.stringToHex) - return signer.sign(privateKey, ...data) + return Signer.sign(privateKey, ...data) }, /** @@ -279,7 +274,7 @@ const tools = { */ verify: (publicKey: string, signature: string, ...input: string[]): boolean => { const data = input.map(Convert.stringToHex) - return signer.verify(publicKey, signature, ...data) + return Signer.verify(publicKey, signature, ...data) }, /** @@ -291,11 +286,11 @@ const tools = { */ verifyBlock: (publicKey: string, block: BlockData): boolean => { const preamble = 0x6.toString().padStart(64, '0') - return signer.verify(publicKey, block.signature, + return Signer.verify(publicKey, block.signature, preamble, - nanoAddress.nanoAddressToHexString(block.account), + NanoAddress.nanoAddressToHexString(block.account), block.previous, - nanoAddress.nanoAddressToHexString(block.representative), + NanoAddress.nanoAddressToHexString(block.representative), Convert.dec2hex(block.balance, 16).toUpperCase(), block.link) }, @@ -307,7 +302,7 @@ const tools = { * @returns {boolean} valid or not */ validateAddress: (input: string): boolean => { - return nanoAddress.validateNanoAddress(input) + return NanoAddress.validateNanoAddress(input) }, /** @@ -317,7 +312,7 @@ const tools = { * @returns {boolean} valid or not */ validateMnemonic: (input: string): boolean => { - return importer.validateMnemonic(input) + return AddressImporter.validateMnemonic(input) }, /** @@ -327,11 +322,7 @@ const tools = { * @returns {string} the public key */ addressToPublicKey: (input: string): string => { - const cleaned = input - .replace('nano_', '') - .replace('xrb_', '') - const publicKeyBytes = nanoAddress.decodeNanoBase32(cleaned) - return Convert.ab2hex(publicKeyBytes).slice(0, 64) + return NanoAddress.addressToPublicKey(input) }, /** @@ -341,7 +332,7 @@ const tools = { * @returns {string} the nano address */ publicKeyToAddress: (input: string): string => { - return nanoAddress.deriveAddress(input) + return NanoAddress.deriveAddress(input) }, /** @@ -352,16 +343,56 @@ const tools = { */ blake2b: (input: string | string[]): string => { if (Array.isArray(input)) { - return Convert.ab2hex(signer.generateHash(input.map(Convert.stringToHex))) + return Convert.ab2hex(Signer.generateHash(input.map(Convert.stringToHex))) } else { - return Convert.ab2hex(signer.generateHash([Convert.stringToHex(input)])) + return Convert.ab2hex(Signer.generateHash([Convert.stringToHex(input)])) } }, } +const box = { + + /** + * Encrypt a message using a Nano address private key for + * end-to-end encrypted messaging. + * + * Encrypts the message using the recipient's public key, + * the sender's private key and a random nonce generated + * inside the library. The message can be opened with the + * recipient's private key and the sender's public key by + * using the decrypt method. + * + * @param {string} message string to encrypt + * @param {string} address nano address of the recipient + * @param {string} privateKey private key of the sender + * @returns {string} encrypted message encoded in Base64 + */ + encrypt: (message: string, address: string, privateKey: string): string => { + return Box.encrypt(message, address, privateKey) + }, + + /** + * Decrypt a message using a Nano address private key. + * + * Decrypts the message by using the sender's public key, + * the recipient's private key and the nonce which is included + * in the encrypted message. + * + * @param {string} encrypted string to decrypt + * @param {string} address nano address of the sender + * @param {string} privateKey private key of the recipient + * @returns {string} decrypted message encoded in UTF-8 + */ + decrypt: (encrypted: string, address: string, privateKey: string): string => { + return Box.decrypt(encrypted, address, privateKey) + } + +} + export { wallet, block, tools, + box, } diff --git a/lib/address-generator.ts b/lib/address-generator.ts index 46a3246..3977205 100644 --- a/lib/address-generator.ts +++ b/lib/address-generator.ts @@ -9,11 +9,9 @@ export default class AddressGenerator { * @param {string} [entropy] - (Optional) Custom entropy if the caller doesn't want a default generated entropy * @param {string} [seedPassword] - (Optional) Password for the seed */ - generateWallet(entropy = '', seedPassword: string = ''): Wallet { - const bip39 = new Bip39Mnemonic(seedPassword) - const mnemonicSeed = bip39.createWallet(entropy) - const wallet = new AddressImporter().fromSeed(mnemonicSeed.seed, 0, 0) - + static generateWallet = (entropy = '', seedPassword: string = ''): Wallet => { + const mnemonicSeed = Bip39Mnemonic.createWallet(entropy, seedPassword) + const wallet = AddressImporter.fromSeed(mnemonicSeed.seed, 0, 0) return { ...wallet, mnemonic: mnemonicSeed.mnemonic, @@ -22,14 +20,10 @@ export default class AddressGenerator { /** * Generates a legacy Nano wallet - * */ - generateLegacyWallet(seed?: string): Wallet { - const bip39 = new Bip39Mnemonic() - const mnemonicSeed = bip39.createLegacyWallet(seed) - const wallet = new AddressImporter().fromLegacySeed(mnemonicSeed.seed, 0, 0, mnemonicSeed.mnemonic) - - return wallet + static generateLegacyWallet = (seed?: string): Wallet => { + const mnemonicSeed = Bip39Mnemonic.createLegacyWallet(seed) + return AddressImporter.fromLegacySeed(mnemonicSeed.seed, 0, 0, mnemonicSeed.mnemonic) } } diff --git a/lib/address-importer.ts b/lib/address-importer.ts index 1d3ebf0..5e3fd4c 100644 --- a/lib/address-importer.ts +++ b/lib/address-importer.ts @@ -14,13 +14,12 @@ export default class AddressImporter { * @param {string} [seedPassword] - (Optional) The password to use to secure the mnemonic * @returns {Wallet} - The wallet derived from the mnemonic phrase */ - fromMnemonic(mnemonic: string, seedPassword = ''): Wallet { - const bip39 = new Bip39Mnemonic(seedPassword) - if (!bip39.validateMnemonic(mnemonic)) { + static fromMnemonic = (mnemonic: string, seedPassword = ''): Wallet => { + if (!Bip39Mnemonic.validateMnemonic(mnemonic)) { throw new Error('Invalid mnemonic phrase') } - const seed = bip39.mnemonicToSeed(mnemonic) + const seed = Bip39Mnemonic.mnemonicToSeed(mnemonic, seedPassword) const accounts = this.accounts(seed, 0, 0) return { @@ -36,13 +35,12 @@ export default class AddressImporter { * @param {string} mnemonic - The mnemonic words to import the wallet from * @returns {Wallet} - The wallet derived from the mnemonic phrase */ - fromLegacyMnemonic(mnemonic: string): Wallet { - const bip39 = new Bip39Mnemonic() - if (!bip39.validateMnemonic(mnemonic)) { + static fromLegacyMnemonic = (mnemonic: string): Wallet => { + if (!Bip39Mnemonic.validateMnemonic(mnemonic)) { throw new Error('Invalid mnemonic phrase') } - const seed = bip39.mnemonicToLegacySeed(mnemonic) + const seed = Bip39Mnemonic.mnemonicToLegacySeed(mnemonic) return this.fromLegacySeed(seed, 0, 0, mnemonic) } @@ -51,9 +49,8 @@ export default class AddressImporter { * * @param mnemonic {string} mnemonic - The mnemonic words to validate */ - validateMnemonic(mnemonic: string): boolean { - const bip39 = new Bip39Mnemonic() - return bip39.validateMnemonic(mnemonic); + static validateMnemonic = (mnemonic: string): boolean => { + return Bip39Mnemonic.validateMnemonic(mnemonic) } /** @@ -64,7 +61,7 @@ export default class AddressImporter { * @param {number} [to] - (Optional) The end index of the private keys to derive to * @returns {Wallet} The wallet derived from the mnemonic phrase */ - fromSeed(seed: string, from = 0, to = 0): Wallet { + static fromSeed = (seed: string, from = 0, to = 0): Wallet => { if (seed.length !== 128) { throw new Error('Invalid seed length, must be a 128 byte hexadecimal string') } @@ -90,7 +87,7 @@ export default class AddressImporter { * @param {number} [to] - (Optional) The end index of the private keys to derive to * @returns {Wallet} The wallet derived from the seed */ - fromLegacySeed(seed: string, from: number = 0, to: number = 0, mnemonic?: string): Wallet { + static fromLegacySeed = (seed: string, from: number = 0, to: number = 0, mnemonic?: string): Wallet => { if (seed.length !== 64) { throw new Error('Invalid seed length, must be a 64 byte hexadecimal string') } @@ -100,7 +97,7 @@ export default class AddressImporter { const accounts = this.legacyAccounts(seed, from, to) return { - mnemonic: mnemonic || new Bip39Mnemonic().deriveMnemonic(seed), + mnemonic: mnemonic || Bip39Mnemonic.deriveMnemonic(seed), seed, accounts, } @@ -113,19 +110,13 @@ export default class AddressImporter { * @param {number} from - The start index of private keys to derive from * @param {number} to - The end index of private keys to derive to */ - private accounts(seed: string, from: number, to: number): Account[] { + private static accounts = (seed: string, from: number, to: number): Account[] => { const accounts = [] for (let i = from; i <= to; i++) { - const bip44 = new Bip32KeyDerivation(`44'/165'/${i}'`, seed) - const privateKey = bip44.derivePath().key - - const ed25519 = new Ed25519() - const keyPair = ed25519.generateKeys(privateKey) - - const nano = new NanoAddress() - const address = nano.deriveAddress(keyPair.publicKey) - + const privateKey = Bip32KeyDerivation.derivePath(`44'/165'/${i}'`, seed).key + const keyPair = new Ed25519().generateKeys(privateKey) + const address = NanoAddress.deriveAddress(keyPair.publicKey) accounts.push({ accountIndex: i, privateKey: keyPair.privateKey, @@ -144,18 +135,12 @@ export default class AddressImporter { * @param {number} from - The start index of private keys to derive from * @param {number} to - The end index of private keys to derive to */ - private legacyAccounts(seed: string, from: number, to: number): Account[] { - const signer = new Signer() + private static legacyAccounts = (seed: string, from: number, to: number): Account[] => { const accounts: Account[] = [] for (let i = from; i <= to; i++) { - const privateKey = Convert.ab2hex(signer.generateHash([seed, Convert.dec2hex(i, 4)])) - - const ed25519 = new Ed25519() - const keyPair = ed25519.generateKeys(privateKey) - - const nano = new NanoAddress() - const address = nano.deriveAddress(keyPair.publicKey) - + const privateKey = Convert.ab2hex(Signer.generateHash([ seed, Convert.dec2hex(i, 4) ])) + const keyPair = new Ed25519().generateKeys(privateKey) + const address = NanoAddress.deriveAddress(keyPair.publicKey) accounts.push({ accountIndex: i, privateKey: keyPair.privateKey, diff --git a/lib/bip32-key-derivation.ts b/lib/bip32-key-derivation.ts index dd2e554..3320051 100644 --- a/lib/bip32-key-derivation.ts +++ b/lib/bip32-key-derivation.ts @@ -8,17 +8,9 @@ const HARDENED_OFFSET = 0x80000000 export default class Bip32KeyDerivation { - path: string - seed: string - - constructor(path: string, seed: string) { - this.path = path - this.seed = seed - } - - derivePath = (): Chain => { - const { key, chainCode } = this.getKeyFromSeed() - const segments = this.path + static derivePath = (path: string, seed: string): Chain => { + const { key, chainCode } = this.getKeyFromSeed(seed) + const segments = path .split('/') .map(v => v.replace('\'', '')) .map(el => parseInt(el, 10)) @@ -29,13 +21,13 @@ export default class Bip32KeyDerivation { ) } - private getKeyFromSeed = (): Chain => { + private static getKeyFromSeed = (seed: string): Chain => { return this.derive( - enc.Hex.parse(this.seed), + enc.Hex.parse(seed), enc.Utf8.parse(ED25519_CURVE)) } - private CKDPriv = ({ key, chainCode }: Chain, index: number) => { + private static CKDPriv = ({ key, chainCode }: Chain, index: number) => { const ib = [] ib.push((index >> 24) & 0xff) ib.push((index >> 16) & 0xff) @@ -48,7 +40,7 @@ export default class Bip32KeyDerivation { enc.Hex.parse(chainCode)) } - private derive = (data: string, base: string): Chain => { + private static derive = (data: string, base: string): Chain => { const hmac = algo.HMAC.create(algo.SHA512, base) const I = hmac.update(data).finalize().toString() const IL = I.slice(0, I.length / 2) diff --git a/lib/bip39-mnemonic.ts b/lib/bip39-mnemonic.ts index 74f7f42..2b291fb 100644 --- a/lib/bip39-mnemonic.ts +++ b/lib/bip39-mnemonic.ts @@ -7,19 +7,13 @@ import words from './words' export default class Bip39Mnemonic { - password: string - - constructor(password?: string) { - this.password = password - } - /** * Creates a BIP39 wallet * * @param {string} [entropy] - (Optional) the entropy to use instead of generating * @returns {MnemonicSeed} The mnemonic phrase and a seed derived from the (generated) entropy */ - createWallet = (entropy: string): MnemonicSeed => { + static createWallet = (entropy: string, password: string): MnemonicSeed => { if (entropy) { if (entropy.length !== 64) { throw new Error('Invalid entropy length, must be a 64 byte hexadecimal string') @@ -34,7 +28,7 @@ export default class Bip39Mnemonic { } const mnemonic = this.deriveMnemonic(entropy) - const seed = this.mnemonicToSeed(mnemonic) + const seed = this.mnemonicToSeed(mnemonic, password) return { mnemonic, @@ -48,7 +42,7 @@ export default class Bip39Mnemonic { * @param {string} seed - (Optional) the seed to be used for the wallet * @returns {MnemonicSeed} The mnemonic phrase and a generated seed if none provided */ - createLegacyWallet = (seed?: string): MnemonicSeed => { + static createLegacyWallet = (seed?: string): MnemonicSeed => { if (seed) { if (seed.length !== 64) { throw new Error('Invalid entropy length, must be a 64 byte hexadecimal string') @@ -70,7 +64,7 @@ export default class Bip39Mnemonic { } } - deriveMnemonic = (entropy: string): string => { + static deriveMnemonic = (entropy: string): string => { const entropyBinary = Convert.hexStringToBinary(entropy) const entropySha256Binary = Convert.hexStringToBinary(this.calculateChecksum(entropy)) const entropyBinaryWithChecksum = entropyBinary + entropySha256Binary @@ -89,7 +83,7 @@ export default class Bip39Mnemonic { * @param {string} mnemonic - The mnemonic phrase to validate * @returns {boolean} Is the mnemonic phrase valid */ - validateMnemonic = (mnemonic: string): boolean => { + static validateMnemonic = (mnemonic: string): boolean => { const wordArray = Util.normalizeUTF8(mnemonic).split(' ') if (wordArray.length % 3 !== 0) { return false @@ -136,7 +130,7 @@ export default class Bip39Mnemonic { * * @param {string} mnemonic Mnemonic phrase separated by spaces */ - mnemonicToLegacySeed = (mnemonic: string): string => { + static mnemonicToLegacySeed = (mnemonic: string): string => { const wordArray = Util.normalizeUTF8(mnemonic).split(' ') const bits = wordArray.map((w: string) => { const wordIndex = words.indexOf(w) @@ -159,9 +153,9 @@ export default class Bip39Mnemonic { * * @param {string} mnemonic Mnemonic phrase separated by spaces */ - mnemonicToSeed = (mnemonic: string): string => { + static mnemonicToSeed = (mnemonic: string, password: string): string => { const normalizedMnemonic = Util.normalizeUTF8(mnemonic) - const normalizedPassword = 'mnemonic' + Util.normalizeUTF8(this.password) + const normalizedPassword = 'mnemonic' + Util.normalizeUTF8(password) return PBKDF2( normalizedMnemonic, @@ -174,11 +168,11 @@ export default class Bip39Mnemonic { .toString(enc.Hex) } - private randomHex = (length: number): string => { - return lib.WordArray.random(length / 2).toString() + private static randomHex = (length: number): string => { + return lib.WordArray.random(length).toString() } - private calculateChecksum = (entropyHex: string): string => { + private static calculateChecksum = (entropyHex: string): string => { const entropySha256 = SHA256(enc.Hex.parse(entropyHex)).toString() return entropySha256.substr(0, entropySha256.length / 32) } diff --git a/lib/block-signer.ts b/lib/block-signer.ts index 63ff87f..d4e9330 100644 --- a/lib/block-signer.ts +++ b/lib/block-signer.ts @@ -1,8 +1,5 @@ import BigNumber from 'bignumber.js' -//@ts-ignore -import { blake2b } from 'blakejs' -import Ed25519 from './ed25519' import NanoAddress from './nano-address' import NanoConverter from './nano-converter' import Signer from './signer' @@ -10,11 +7,7 @@ import Convert from './util/convert' export default class BlockSigner { - nanoAddress = new NanoAddress() - ed25519 = new Ed25519() - signer = new Signer() - - preamble: string = 0x6.toString().padStart(64, '0') + static readonly preamble: string = 0x6.toString().padStart(64, '0') /** * Sign a receive block @@ -23,7 +16,7 @@ export default class BlockSigner { * @param {string} privateKey Private key to sign the data with * @returns {SignedBlock} the signed block to publish to the blockchain */ - receive(data: ReceiveBlock, privateKey: string): SignedBlock { + static receive = (data: ReceiveBlock, privateKey: string): SignedBlock => { const validateInputRaw = (input: string) => !!input && !isNaN(+input) if (!validateInputRaw(data.walletBalanceRaw)) { throw new Error('Invalid format in wallet balance') @@ -33,11 +26,11 @@ export default class BlockSigner { throw new Error('Invalid format in send amount') } - if (!this.nanoAddress.validateNanoAddress(data.toAddress)) { + if (!NanoAddress.validateNanoAddress(data.toAddress)) { throw new Error('Invalid toAddress') } - if (!this.nanoAddress.validateNanoAddress(data.representativeAddress)) { + if (!NanoAddress.validateNanoAddress(data.representativeAddress)) { throw new Error('Invalid representativeAddress') } @@ -58,11 +51,11 @@ 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.nanoAddress.nanoAddressToHexString(data.toAddress) + const account = NanoAddress.nanoAddressToHexString(data.toAddress) const link = data.transactionHash - const representative = this.nanoAddress.nanoAddressToHexString(data.representativeAddress) + const representative = NanoAddress.nanoAddressToHexString(data.representativeAddress) - const signature = this.signer.sign( + const signature = Signer.sign( privateKey, this.preamble, account, @@ -90,7 +83,7 @@ export default class BlockSigner { * @param {string} privateKey Private key to sign the data with * @returns {SignedBlock} the signed block to publish to the blockchain */ - send(data: SendBlock, privateKey: string): SignedBlock { + static send = (data: SendBlock, privateKey: string): SignedBlock => { const validateInputRaw = (input: string) => !!input && !isNaN(+input) if (!validateInputRaw(data.walletBalanceRaw)) { throw new Error('Invalid format in wallet balance') @@ -100,15 +93,15 @@ export default class BlockSigner { throw new Error('Invalid format in send amount') } - if (!this.nanoAddress.validateNanoAddress(data.toAddress)) { + if (!NanoAddress.validateNanoAddress(data.toAddress)) { throw new Error('Invalid toAddress') } - if (!this.nanoAddress.validateNanoAddress(data.fromAddress)) { + if (!NanoAddress.validateNanoAddress(data.fromAddress)) { throw new Error('Invalid fromAddress') } - if (!this.nanoAddress.validateNanoAddress(data.representativeAddress)) { + if (!NanoAddress.validateNanoAddress(data.representativeAddress)) { throw new Error('Invalid representativeAddress') } @@ -125,11 +118,11 @@ 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.nanoAddress.nanoAddressToHexString(data.fromAddress) - const link = this.nanoAddress.nanoAddressToHexString(data.toAddress) - const representative = this.nanoAddress.nanoAddressToHexString(data.representativeAddress) + const account = NanoAddress.nanoAddressToHexString(data.fromAddress) + const link = NanoAddress.nanoAddressToHexString(data.toAddress) + const representative = NanoAddress.nanoAddressToHexString(data.representativeAddress) - const signature = this.signer.sign( + const signature = Signer.sign( privateKey, this.preamble, account, diff --git a/lib/box.ts b/lib/box.ts new file mode 100644 index 0000000..25c4c16 --- /dev/null +++ b/lib/box.ts @@ -0,0 +1,69 @@ +import * as base64 from 'byte-base64' +//@ts-ignore +import { lib } from 'crypto-js' + +import Ed25519 from './ed25519' +import NanoAddress from './nano-address' +import Convert from './util/convert' +import Curve25519 from './util/curve25519' + +export default class Box { + + static readonly NONCE_LENGTH = 24 + + static encrypt(message: string, address: string, privateKey: string) { + if (!message) { + throw new Error('No message to encrypt') + } + + const publicKey = NanoAddress.addressToPublicKey(address) + const { privateKey: convertedPrivateKey, publicKey: convertedPublicKey } = new Ed25519().convertKeys({ + privateKey, + publicKey, + }) + + const nonce = Convert.hex2ab(lib.WordArray.random(this.NONCE_LENGTH).toString()) + const encrypted = new Curve25519().box( + Convert.str2bin(message), + nonce, + Convert.hex2ab(convertedPublicKey), + Convert.hex2ab(convertedPrivateKey), + ) + + const full = new Uint8Array(nonce.length + encrypted.length) + full.set(nonce) + full.set(encrypted, nonce.length) + + return base64.bytesToBase64(full) + } + + static decrypt(encrypted: string, address: string, privateKey: string) { + if (!encrypted) { + throw new Error('No message to decrypt') + } + + const publicKey = NanoAddress.addressToPublicKey(address) + const { privateKey: convertedPrivateKey, publicKey: convertedPublicKey } = new Ed25519().convertKeys({ + privateKey, + publicKey, + }) + + const decodedEncryptedMessageBytes = base64.base64ToBytes(encrypted) + const nonce = decodedEncryptedMessageBytes.slice(0, this.NONCE_LENGTH) + const encryptedMessage = decodedEncryptedMessageBytes.slice(this.NONCE_LENGTH, encrypted.length) + + const decrypted = new Curve25519().boxOpen( + encryptedMessage, + nonce, + Convert.hex2ab(convertedPublicKey), + Convert.hex2ab(convertedPrivateKey), + ) + + if (!decrypted) { + throw new Error('Could not decrypt message') + } + + return Convert.bin2str(decrypted) + } + +} diff --git a/lib/ed25519.ts b/lib/ed25519.ts index cbf6b76..12f6903 100644 --- a/lib/ed25519.ts +++ b/lib/ed25519.ts @@ -118,6 +118,24 @@ export default class Ed25519 { } } + /** + * Convert ed25519 keypair to curve25519 keypair suitable for Diffie-Hellman key exchange + * + * @param {KeyPair} keyPair ed25519 keypair + * @returns {KeyPair} keyPair Curve25519 keypair + */ + convertKeys(keyPair: KeyPair): KeyPair { + const publicKey = Convert.ab2hex(this.curve.convertEd25519PublicKeyToCurve25519(Convert.hex2ab(keyPair.publicKey))) + if (!publicKey) { + return null + } + const privateKey = Convert.ab2hex(this.curve.convertEd25519SecretKeyToCurve25519(Convert.hex2ab(keyPair.privateKey))) + return { + publicKey, + privateKey, + } + } + /** * Generate a message signature * @param {Uint8Array} msg Message to be signed as byte array @@ -143,7 +161,7 @@ export default class Ed25519 { * @param {Uint8Array} Returns the signature as 64 byte typed array */ verify(msg: Uint8Array, publicKey: Uint8Array, signature: Uint8Array): boolean { - const CURVE = this.curve; + const CURVE = this.curve const p = [CURVE.gf(), CURVE.gf(), CURVE.gf(), CURVE.gf()] const q = [CURVE.gf(), CURVE.gf(), CURVE.gf(), CURVE.gf()] @@ -197,7 +215,7 @@ export default class Ed25519 { const pk = Convert.hex2ab(this.generateKeys(Convert.ab2hex(sk)).publicKey) - this.cryptoHash(d, sk, 32) + this.curve.cryptoHash(d, sk, 32) d[0] &= 248 d[31] &= 127 d[31] |= 64 @@ -211,7 +229,7 @@ export default class Ed25519 { sm[32 + i] = d[32 + i] } - this.cryptoHash(r, sm.subarray(32), n + 32) + this.curve.cryptoHash(r, sm.subarray(32), n + 32) this.reduce(r) this.scalarbase(p, r) this.pack(sm, p) @@ -220,7 +238,7 @@ export default class Ed25519 { sm[i] = pk[i - 32] } - this.cryptoHash(h, sm, n + 64) + this.curve.cryptoHash(h, sm, n + 64) this.reduce(h) for (i = 0; i < 64; i++) { @@ -242,20 +260,6 @@ export default class Ed25519 { return smlen } - private cryptoHash(out: Uint8Array, m: Uint8Array, n: number): number { - const input = new Uint8Array(n) - for (let i = 0; i < n; ++i) { - input[i] = m[i] - } - - const hash = blake2b(input) - for (let i = 0; i < 64; ++i) { - out[i] = hash[i] - } - - return 0 - } - } export interface KeyPair { diff --git a/lib/nano-address.ts b/lib/nano-address.ts index 2ff72ac..030a2ab 100644 --- a/lib/nano-address.ts +++ b/lib/nano-address.ts @@ -5,10 +5,10 @@ import Convert from './util/convert' export default class NanoAddress { - readonly alphabet = '13456789abcdefghijkmnopqrstuwxyz' - readonly prefix = 'nano_' + static readonly alphabet = '13456789abcdefghijkmnopqrstuwxyz' + static readonly prefix = 'nano_' - deriveAddress = (publicKey: string): string => { + static deriveAddress = (publicKey: string): string => { const publicKeyBytes = Convert.hex2ab(publicKey) const checksum = blake2b(publicKeyBytes, undefined, 5).reverse() const encoded = this.encodeNanoBase32(publicKeyBytes) @@ -17,7 +17,7 @@ export default class NanoAddress { return this.prefix + encoded + encodedChecksum } - encodeNanoBase32 = (publicKey: Uint8Array): string => { + static encodeNanoBase32 = (publicKey: Uint8Array): string => { const length = publicKey.length const leftover = (length * 8) % 5 const offset = leftover === 0 ? 0 : 5 - leftover @@ -43,7 +43,15 @@ export default class NanoAddress { return output } - decodeNanoBase32 = (input: string): Uint8Array => { + static addressToPublicKey = (input: string): string => { + const cleaned = input + .replace('nano_', '') + .replace('xrb_', '') + const publicKeyBytes = NanoAddress.decodeNanoBase32(cleaned) + return Convert.ab2hex(publicKeyBytes).slice(0, 64) + } + + static decodeNanoBase32 = (input: string): Uint8Array => { const length = input.length const leftover = (length * 5) % 8 const offset = leftover === 0 ? 0 : 8 - leftover @@ -81,7 +89,7 @@ export default class NanoAddress { * * @param {string} address Nano address */ - validateNanoAddress = (address: string): boolean => { + static validateNanoAddress = (address: string): boolean => { if (address === undefined) { throw Error('Address must be defined.') } @@ -100,7 +108,7 @@ export default class NanoAddress { } const expectedChecksum = address.slice(-8) - const publicKey = address.slice(address.indexOf('_') + 1, -8) + const publicKey = this.stripAddress(address) const publicKeyBuffer = this.decodeNanoBase32(publicKey) const actualChecksumBuffer = blake2b(publicKeyBuffer, null, 5).reverse() const actualChecksum = this.encodeNanoBase32(actualChecksumBuffer) @@ -108,7 +116,7 @@ export default class NanoAddress { return expectedChecksum === actualChecksum } - nanoAddressToHexString = (addr: string): string => { + static nanoAddressToHexString = (addr: string): string => { addr = addr.slice(-60) const isValid = /^[13456789abcdefghijkmnopqrstuwxyz]+$/.test(addr) if (isValid) { @@ -125,7 +133,11 @@ export default class NanoAddress { } } - private readChar(char: string): number { + static stripAddress(address: string): string { + return address.slice(address.indexOf('_') + 1, -8) + } + + private static readChar(char: string): number { const idx = this.alphabet.indexOf(char) if (idx === -1) { diff --git a/lib/nano-converter.ts b/lib/nano-converter.ts index 3c46006..fe60eb2 100644 --- a/lib/nano-converter.ts +++ b/lib/nano-converter.ts @@ -9,7 +9,7 @@ export default class NanoConverter { * @param inputUnit {string} the unit to convert from * @param outputUnit {string} the unit to convert to */ - static convert(input: string | BigNumber, inputUnit: string, outputUnit: string): string { + static convert = (input: string | BigNumber, inputUnit: string, outputUnit: string): string => { let value = new BigNumber(input.toString()) switch (inputUnit) { diff --git a/lib/signer.ts b/lib/signer.ts index 66f8844..2548b3e 100644 --- a/lib/signer.ts +++ b/lib/signer.ts @@ -7,16 +7,14 @@ 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 */ - sign(privateKey: string, ...data: string[]): string { - const signature = this.ed25519.sign( + static sign = (privateKey: string, ...data: string[]): string => { + const signature = new Ed25519().sign( this.generateHash(data), Convert.hex2ab(privateKey)) return Convert.ab2hex(signature) @@ -29,11 +27,11 @@ export default class Signer { * @param signature Signature to verify * @param data Data to verify */ - verify(publicKey: string, signature: string, ...data: string[]): boolean { - return this.ed25519.verify( + static verify = (publicKey: string, signature: string, ...data: string[]): boolean => { + return new Ed25519().verify( this.generateHash(data), Convert.hex2ab(publicKey), - Convert.hex2ab(signature)); + Convert.hex2ab(signature)) } /** @@ -41,7 +39,7 @@ export default class Signer { * * @param data Data to hash */ - generateHash(data: string[]): Uint8Array { + static generateHash = (data: string[]): Uint8Array => { const ctx = blake2bInit(32, undefined) data.forEach(str => blake2bUpdate(ctx, Convert.hex2ab(str))) return blake2bFinal(ctx) diff --git a/lib/util/convert.ts b/lib/util/convert.ts index a7012d1..a6634c2 100644 --- a/lib/util/convert.ts +++ b/lib/util/convert.ts @@ -26,6 +26,21 @@ export default class Convert { return bin.subarray(0, p) } + /** + * Convert a byte array to a UTF-8 encoded string + * + * @param {Uint8Array} arr Byte array + * @return {String} UTF-8 encoded string + */ + static bin2str = (arr: Uint8Array) => { + let i, s = [] + for (i = 0; i < arr.length; i++) { + s.push(String.fromCharCode(arr[i])) + } + + return decodeURIComponent(escape(s.join(''))) + } + /** * Convert Array of 8 bytes (int64) to hex string * diff --git a/lib/util/curve25519.ts b/lib/util/curve25519.ts index 7c71222..026ef99 100644 --- a/lib/util/curve25519.ts +++ b/lib/util/curve25519.ts @@ -1,5 +1,16 @@ +//@ts-ignore +import { blake2b } from 'blakejs' + import Util from './util' +/** + * Derived from: + * - mipher + * - tweetnacl + * - ed2curve-js + * + * With added types etc + */ export default class Curve25519 { gf0: Int32Array @@ -9,6 +20,9 @@ export default class Curve25519 { I: Int32Array _9: Uint8Array _121665: Int32Array + _0: Uint8Array + sigma: Uint8Array + minusp: Uint32Array constructor() { this.gf0 = this.gf() @@ -19,6 +33,9 @@ export default class Curve25519 { this.D = this.gf([0x78a3, 0x1359, 0x4dca, 0x75eb, 0xd8ab, 0x4141, 0x0a4d, 0x0070, 0xe898, 0x7779, 0x4079, 0x8cc7, 0xfe73, 0x2b6f, 0x6cee, 0x5203]) this.D2 = this.gf([0xf159, 0x26b2, 0x9b94, 0xebd6, 0xb156, 0x8283, 0x149a, 0x00e0, 0xd130, 0xeef3, 0x80f2, 0x198e, 0xfce7, 0x56df, 0xd9dc, 0x2406]) this.I = this.gf([0xa0b0, 0x4a0e, 0x1b27, 0xc4ee, 0xe478, 0xad2f, 0x1806, 0x2f43, 0xd7a7, 0x3dfb, 0x0099, 0x2b4d, 0xdf0b, 0x4fc1, 0x2480, 0x2b83]) + this._0 = new Uint8Array(16) + this.sigma = new Uint8Array([101, 120, 112, 97, 110, 100, 32, 51, 50, 45, 98, 121, 116, 101, 32, 107]) + this.minusp = new Uint32Array([5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 252]) } gf(init?: number[]): Int32Array { @@ -413,6 +430,336 @@ export default class Curve25519 { o[15] = t15 } + coreSalsa20(o: Uint8Array, p: Uint8Array, k: Uint8Array, c: Uint8Array) { + const j0 = c[ 0] & 0xff | (c[ 1] & 0xff)<<8 | (c[ 2] & 0xff)<<16 | (c[ 3] & 0xff)<<24, + j1 = k[ 0] & 0xff | (k[ 1] & 0xff)<<8 | (k[ 2] & 0xff)<<16 | (k[ 3] & 0xff)<<24, + j2 = k[ 4] & 0xff | (k[ 5] & 0xff)<<8 | (k[ 6] & 0xff)<<16 | (k[ 7] & 0xff)<<24, + j3 = k[ 8] & 0xff | (k[ 9] & 0xff)<<8 | (k[10] & 0xff)<<16 | (k[11] & 0xff)<<24, + j4 = k[12] & 0xff | (k[13] & 0xff)<<8 | (k[14] & 0xff)<<16 | (k[15] & 0xff)<<24, + j5 = c[ 4] & 0xff | (c[ 5] & 0xff)<<8 | (c[ 6] & 0xff)<<16 | (c[ 7] & 0xff)<<24, + j6 = p[ 0] & 0xff | (p[ 1] & 0xff)<<8 | (p[ 2] & 0xff)<<16 | (p[ 3] & 0xff)<<24, + j7 = p[ 4] & 0xff | (p[ 5] & 0xff)<<8 | (p[ 6] & 0xff)<<16 | (p[ 7] & 0xff)<<24, + j8 = p[ 8] & 0xff | (p[ 9] & 0xff)<<8 | (p[10] & 0xff)<<16 | (p[11] & 0xff)<<24, + j9 = p[12] & 0xff | (p[13] & 0xff)<<8 | (p[14] & 0xff)<<16 | (p[15] & 0xff)<<24, + j10 = c[ 8] & 0xff | (c[ 9] & 0xff)<<8 | (c[10] & 0xff)<<16 | (c[11] & 0xff)<<24, + j11 = k[16] & 0xff | (k[17] & 0xff)<<8 | (k[18] & 0xff)<<16 | (k[19] & 0xff)<<24, + j12 = k[20] & 0xff | (k[21] & 0xff)<<8 | (k[22] & 0xff)<<16 | (k[23] & 0xff)<<24, + j13 = k[24] & 0xff | (k[25] & 0xff)<<8 | (k[26] & 0xff)<<16 | (k[27] & 0xff)<<24, + j14 = k[28] & 0xff | (k[29] & 0xff)<<8 | (k[30] & 0xff)<<16 | (k[31] & 0xff)<<24, + j15 = c[12] & 0xff | (c[13] & 0xff)<<8 | (c[14] & 0xff)<<16 | (c[15] & 0xff)<<24 + + let x0 = j0, x1 = j1, x2 = j2, x3 = j3, x4 = j4, x5 = j5, x6 = j6, x7 = j7, + x8 = j8, x9 = j9, x10 = j10, x11 = j11, x12 = j12, x13 = j13, x14 = j14, + x15 = j15, u + + for (let i = 0; i < 20; i += 2) { + u = x0 + x12 | 0 + x4 ^= u<<7 | u>>>(32-7) + u = x4 + x0 | 0 + x8 ^= u<<9 | u>>>(32-9) + u = x8 + x4 | 0 + x12 ^= u<<13 | u>>>(32-13) + u = x12 + x8 | 0 + x0 ^= u<<18 | u>>>(32-18) + + u = x5 + x1 | 0 + x9 ^= u<<7 | u>>>(32-7) + u = x9 + x5 | 0 + x13 ^= u<<9 | u>>>(32-9) + u = x13 + x9 | 0 + x1 ^= u<<13 | u>>>(32-13) + u = x1 + x13 | 0 + x5 ^= u<<18 | u>>>(32-18) + + u = x10 + x6 | 0 + x14 ^= u<<7 | u>>>(32-7) + u = x14 + x10 | 0 + x2 ^= u<<9 | u>>>(32-9) + u = x2 + x14 | 0 + x6 ^= u<<13 | u>>>(32-13) + u = x6 + x2 | 0 + x10 ^= u<<18 | u>>>(32-18) + + u = x15 + x11 | 0 + x3 ^= u<<7 | u>>>(32-7) + u = x3 + x15 | 0 + x7 ^= u<<9 | u>>>(32-9) + u = x7 + x3 | 0 + x11 ^= u<<13 | u>>>(32-13) + u = x11 + x7 | 0 + x15 ^= u<<18 | u>>>(32-18) + + u = x0 + x3 | 0 + x1 ^= u<<7 | u>>>(32-7) + u = x1 + x0 | 0 + x2 ^= u<<9 | u>>>(32-9) + u = x2 + x1 | 0 + x3 ^= u<<13 | u>>>(32-13) + u = x3 + x2 | 0 + x0 ^= u<<18 | u>>>(32-18) + + u = x5 + x4 | 0 + x6 ^= u<<7 | u>>>(32-7) + u = x6 + x5 | 0 + x7 ^= u<<9 | u>>>(32-9) + u = x7 + x6 | 0 + x4 ^= u<<13 | u>>>(32-13) + u = x4 + x7 | 0 + x5 ^= u<<18 | u>>>(32-18) + + u = x10 + x9 | 0 + x11 ^= u<<7 | u>>>(32-7) + u = x11 + x10 | 0 + x8 ^= u<<9 | u>>>(32-9) + u = x8 + x11 | 0 + x9 ^= u<<13 | u>>>(32-13) + u = x9 + x8 | 0 + x10 ^= u<<18 | u>>>(32-18) + + u = x15 + x14 | 0 + x12 ^= u<<7 | u>>>(32-7) + u = x12 + x15 | 0 + x13 ^= u<<9 | u>>>(32-9) + u = x13 + x12 | 0 + x14 ^= u<<13 | u>>>(32-13) + u = x14 + x13 | 0 + x15 ^= u<<18 | u>>>(32-18) + } + x0 = x0 + j0 | 0 + x1 = x1 + j1 | 0 + x2 = x2 + j2 | 0 + x3 = x3 + j3 | 0 + x4 = x4 + j4 | 0 + x5 = x5 + j5 | 0 + x6 = x6 + j6 | 0 + x7 = x7 + j7 | 0 + x8 = x8 + j8 | 0 + x9 = x9 + j9 | 0 + x10 = x10 + j10 | 0 + x11 = x11 + j11 | 0 + x12 = x12 + j12 | 0 + x13 = x13 + j13 | 0 + x14 = x14 + j14 | 0 + x15 = x15 + j15 | 0 + + o[ 0] = x0 >>> 0 & 0xff + o[ 1] = x0 >>> 8 & 0xff + o[ 2] = x0 >>> 16 & 0xff + o[ 3] = x0 >>> 24 & 0xff + + o[ 4] = x1 >>> 0 & 0xff + o[ 5] = x1 >>> 8 & 0xff + o[ 6] = x1 >>> 16 & 0xff + o[ 7] = x1 >>> 24 & 0xff + + o[ 8] = x2 >>> 0 & 0xff + o[ 9] = x2 >>> 8 & 0xff + o[10] = x2 >>> 16 & 0xff + o[11] = x2 >>> 24 & 0xff + + o[12] = x3 >>> 0 & 0xff + o[13] = x3 >>> 8 & 0xff + o[14] = x3 >>> 16 & 0xff + o[15] = x3 >>> 24 & 0xff + + o[16] = x4 >>> 0 & 0xff + o[17] = x4 >>> 8 & 0xff + o[18] = x4 >>> 16 & 0xff + o[19] = x4 >>> 24 & 0xff + + o[20] = x5 >>> 0 & 0xff + o[21] = x5 >>> 8 & 0xff + o[22] = x5 >>> 16 & 0xff + o[23] = x5 >>> 24 & 0xff + + o[24] = x6 >>> 0 & 0xff + o[25] = x6 >>> 8 & 0xff + o[26] = x6 >>> 16 & 0xff + o[27] = x6 >>> 24 & 0xff + + o[28] = x7 >>> 0 & 0xff + o[29] = x7 >>> 8 & 0xff + o[30] = x7 >>> 16 & 0xff + o[31] = x7 >>> 24 & 0xff + + o[32] = x8 >>> 0 & 0xff + o[33] = x8 >>> 8 & 0xff + o[34] = x8 >>> 16 & 0xff + o[35] = x8 >>> 24 & 0xff + + o[36] = x9 >>> 0 & 0xff + o[37] = x9 >>> 8 & 0xff + o[38] = x9 >>> 16 & 0xff + o[39] = x9 >>> 24 & 0xff + + o[40] = x10 >>> 0 & 0xff + o[41] = x10 >>> 8 & 0xff + o[42] = x10 >>> 16 & 0xff + o[43] = x10 >>> 24 & 0xff + + o[44] = x11 >>> 0 & 0xff + o[45] = x11 >>> 8 & 0xff + o[46] = x11 >>> 16 & 0xff + o[47] = x11 >>> 24 & 0xff + + o[48] = x12 >>> 0 & 0xff + o[49] = x12 >>> 8 & 0xff + o[50] = x12 >>> 16 & 0xff + o[51] = x12 >>> 24 & 0xff + + o[52] = x13 >>> 0 & 0xff + o[53] = x13 >>> 8 & 0xff + o[54] = x13 >>> 16 & 0xff + o[55] = x13 >>> 24 & 0xff + + o[56] = x14 >>> 0 & 0xff + o[57] = x14 >>> 8 & 0xff + o[58] = x14 >>> 16 & 0xff + o[59] = x14 >>> 24 & 0xff + + o[60] = x15 >>> 0 & 0xff + o[61] = x15 >>> 8 & 0xff + o[62] = x15 >>> 16 & 0xff + o[63] = x15 >>> 24 & 0xff + } + + coreHsalsa20(o: Uint8Array, p: Uint8Array, k: Uint8Array, c: Uint8Array) { + const j0 = c[ 0] & 0xff | (c[ 1] & 0xff)<<8 | (c[ 2] & 0xff)<<16 | (c[ 3] & 0xff)<<24, + j1 = k[ 0] & 0xff | (k[ 1] & 0xff)<<8 | (k[ 2] & 0xff)<<16 | (k[ 3] & 0xff)<<24, + j2 = k[ 4] & 0xff | (k[ 5] & 0xff)<<8 | (k[ 6] & 0xff)<<16 | (k[ 7] & 0xff)<<24, + j3 = k[ 8] & 0xff | (k[ 9] & 0xff)<<8 | (k[10] & 0xff)<<16 | (k[11] & 0xff)<<24, + j4 = k[12] & 0xff | (k[13] & 0xff)<<8 | (k[14] & 0xff)<<16 | (k[15] & 0xff)<<24, + j5 = c[ 4] & 0xff | (c[ 5] & 0xff)<<8 | (c[ 6] & 0xff)<<16 | (c[ 7] & 0xff)<<24, + j6 = p[ 0] & 0xff | (p[ 1] & 0xff)<<8 | (p[ 2] & 0xff)<<16 | (p[ 3] & 0xff)<<24, + j7 = p[ 4] & 0xff | (p[ 5] & 0xff)<<8 | (p[ 6] & 0xff)<<16 | (p[ 7] & 0xff)<<24, + j8 = p[ 8] & 0xff | (p[ 9] & 0xff)<<8 | (p[10] & 0xff)<<16 | (p[11] & 0xff)<<24, + j9 = p[12] & 0xff | (p[13] & 0xff)<<8 | (p[14] & 0xff)<<16 | (p[15] & 0xff)<<24, + j10 = c[ 8] & 0xff | (c[ 9] & 0xff)<<8 | (c[10] & 0xff)<<16 | (c[11] & 0xff)<<24, + j11 = k[16] & 0xff | (k[17] & 0xff)<<8 | (k[18] & 0xff)<<16 | (k[19] & 0xff)<<24, + j12 = k[20] & 0xff | (k[21] & 0xff)<<8 | (k[22] & 0xff)<<16 | (k[23] & 0xff)<<24, + j13 = k[24] & 0xff | (k[25] & 0xff)<<8 | (k[26] & 0xff)<<16 | (k[27] & 0xff)<<24, + j14 = k[28] & 0xff | (k[29] & 0xff)<<8 | (k[30] & 0xff)<<16 | (k[31] & 0xff)<<24, + j15 = c[12] & 0xff | (c[13] & 0xff)<<8 | (c[14] & 0xff)<<16 | (c[15] & 0xff)<<24 + + let x0 = j0, x1 = j1, x2 = j2, x3 = j3, x4 = j4, x5 = j5, x6 = j6, x7 = j7, + x8 = j8, x9 = j9, x10 = j10, x11 = j11, x12 = j12, x13 = j13, x14 = j14, + x15 = j15, u + + for (let i = 0; i < 20; i += 2) { + u = x0 + x12 | 0 + x4 ^= u<<7 | u>>>(32-7) + u = x4 + x0 | 0 + x8 ^= u<<9 | u>>>(32-9) + u = x8 + x4 | 0 + x12 ^= u<<13 | u>>>(32-13) + u = x12 + x8 | 0 + x0 ^= u<<18 | u>>>(32-18) + + u = x5 + x1 | 0 + x9 ^= u<<7 | u>>>(32-7) + u = x9 + x5 | 0 + x13 ^= u<<9 | u>>>(32-9) + u = x13 + x9 | 0 + x1 ^= u<<13 | u>>>(32-13) + u = x1 + x13 | 0 + x5 ^= u<<18 | u>>>(32-18) + + u = x10 + x6 | 0 + x14 ^= u<<7 | u>>>(32-7) + u = x14 + x10 | 0 + x2 ^= u<<9 | u>>>(32-9) + u = x2 + x14 | 0 + x6 ^= u<<13 | u>>>(32-13) + u = x6 + x2 | 0 + x10 ^= u<<18 | u>>>(32-18) + + u = x15 + x11 | 0 + x3 ^= u<<7 | u>>>(32-7) + u = x3 + x15 | 0 + x7 ^= u<<9 | u>>>(32-9) + u = x7 + x3 | 0 + x11 ^= u<<13 | u>>>(32-13) + u = x11 + x7 | 0 + x15 ^= u<<18 | u>>>(32-18) + + u = x0 + x3 | 0 + x1 ^= u<<7 | u>>>(32-7) + u = x1 + x0 | 0 + x2 ^= u<<9 | u>>>(32-9) + u = x2 + x1 | 0 + x3 ^= u<<13 | u>>>(32-13) + u = x3 + x2 | 0 + x0 ^= u<<18 | u>>>(32-18) + + u = x5 + x4 | 0 + x6 ^= u<<7 | u>>>(32-7) + u = x6 + x5 | 0 + x7 ^= u<<9 | u>>>(32-9) + u = x7 + x6 | 0 + x4 ^= u<<13 | u>>>(32-13) + u = x4 + x7 | 0 + x5 ^= u<<18 | u>>>(32-18) + + u = x10 + x9 | 0 + x11 ^= u<<7 | u>>>(32-7) + u = x11 + x10 | 0 + x8 ^= u<<9 | u>>>(32-9) + u = x8 + x11 | 0 + x9 ^= u<<13 | u>>>(32-13) + u = x9 + x8 | 0 + x10 ^= u<<18 | u>>>(32-18) + + u = x15 + x14 | 0 + x12 ^= u<<7 | u>>>(32-7) + u = x12 + x15 | 0 + x13 ^= u<<9 | u>>>(32-9) + u = x13 + x12 | 0 + x14 ^= u<<13 | u>>>(32-13) + u = x14 + x13 | 0 + x15 ^= u<<18 | u>>>(32-18) + } + + o[ 0] = x0 >>> 0 & 0xff + o[ 1] = x0 >>> 8 & 0xff + o[ 2] = x0 >>> 16 & 0xff + o[ 3] = x0 >>> 24 & 0xff + + o[ 4] = x5 >>> 0 & 0xff + o[ 5] = x5 >>> 8 & 0xff + o[ 6] = x5 >>> 16 & 0xff + o[ 7] = x5 >>> 24 & 0xff + + o[ 8] = x10 >>> 0 & 0xff + o[ 9] = x10 >>> 8 & 0xff + o[10] = x10 >>> 16 & 0xff + o[11] = x10 >>> 24 & 0xff + + o[12] = x15 >>> 0 & 0xff + o[13] = x15 >>> 8 & 0xff + o[14] = x15 >>> 16 & 0xff + o[15] = x15 >>> 24 & 0xff + + o[16] = x6 >>> 0 & 0xff + o[17] = x6 >>> 8 & 0xff + o[18] = x6 >>> 16 & 0xff + o[19] = x6 >>> 24 & 0xff + + o[20] = x7 >>> 0 & 0xff + o[21] = x7 >>> 8 & 0xff + o[22] = x7 >>> 16 & 0xff + o[23] = x7 >>> 24 & 0xff + + o[24] = x8 >>> 0 & 0xff + o[25] = x8 >>> 8 & 0xff + o[26] = x8 >>> 16 & 0xff + o[27] = x8 >>> 24 & 0xff + + o[28] = x9 >>> 0 & 0xff + o[29] = x9 >>> 8 & 0xff + o[30] = x9 >>> 16 & 0xff + o[31] = x9 >>> 24 & 0xff + } + S(o: Int32Array, a: Int32Array): void { this.M(o, a, a) } @@ -612,6 +959,14 @@ export default class Curve25519 { return 0 } + vn(x: Uint8Array, xi: number, y: Uint8Array, yi: number, n: number) { + let i, d = 0 + for (i = 0; i < n; i++) { + d |= x[xi+i]^y[yi+i] + } + return (1 & ((d - 1) >>> 8)) - 1 + } + /** * Internal scalar mult function * @param {Uint8Array} q Result @@ -671,6 +1026,289 @@ export default class Curve25519 { this.pack25519(q, x16) } + cryptoStreamSalsa20Xor(c: Uint8Array, cpos: number, m: Uint8Array, mpos: number, b: number, n: Uint8Array, k: Uint8Array) { + const z = new Uint8Array(16) + const x = new Uint8Array(64) + let u, i + for (i = 0; i < 16; i++) { + z[i] = 0 + } + for (i = 0; i < 8; i++) { + z[i] = n[i] + } + while (b >= 64) { + this.coreSalsa20(x, z, k, this.sigma) + for (i = 0; i < 64; i++) c[cpos+i] = m[mpos+i] ^ x[i] + u = 1 + for (i = 8; i < 16; i++) { + u = u + (z[i] & 0xff) | 0 + z[i] = u & 0xff + u >>>= 8 + } + b -= 64 + cpos += 64 + mpos += 64 + } + if (b > 0) { + this.coreSalsa20(x, z, k, this.sigma) + for (i = 0; i < b; i++) { + c[cpos+i] = m[mpos+i] ^ x[i] + } + } + return 0 + } + + cryptoStreamSalsa20(c: Uint8Array, cpos: number, b: number, n: Uint8Array, k: Uint8Array) { + const z = new Uint8Array(16), x = new Uint8Array(64) + let u, i + for (i = 0; i < 16; i++) z[i] = 0 + for (i = 0; i < 8; i++) z[i] = n[i] + while (b >= 64) { + this.coreSalsa20(x, z, k, this.sigma) + for (i = 0; i < 64; i++) { + c[cpos+i] = x[i] + } + u = 1 + for (i = 8; i < 16; i++) { + u = u + (z[i] & 0xff) | 0 + z[i] = u & 0xff + u >>>= 8 + } + b -= 64 + cpos += 64 + } + if (b > 0) { + this.coreSalsa20(x, z, k, this.sigma) + for (i = 0; i < b; i++) { + c[cpos+i] = x[i] + } + } + return 0 + } + + add1305(h: Uint32Array, c: Uint32Array) { + let j, u = 0 + for (j = 0; j < 17; j++) { + u = (u + ((h[j] + c[j]) | 0)) | 0 + h[j] = u & 255 + u >>>= 8 + } + } + + cryptoOnetimeauth(out: Uint8Array, outpos: number, m: Uint8Array, mpos: number, n: number, k: Uint8Array) { + let s, i, j, u + const x = new Uint32Array(17), r = new Uint32Array(17), + h = new Uint32Array(17), c = new Uint32Array(17), + g = new Uint32Array(17) + for (j = 0; j < 17; j++) { + r[j]=h[j]=0 + } + for (j = 0; j < 16; j++) { + r[j]=k[j] + } + + r[3]&=15 + r[4]&=252 + r[7]&=15 + r[8]&=252 + r[11]&=15 + r[12]&=252 + r[15]&=15 + + while (n > 0) { + for (j = 0; j < 17; j++) { + c[j] = 0 + } + for (j = 0; (j < 16) && (j < n); ++j) { + c[j] = m[mpos+j] + } + c[j] = 1 + mpos += j; n -= j + this.add1305(h, c) + for (i = 0; i < 17; i++) { + x[i] = 0 + for (j = 0; j < 17; j++) { + x[i] = (x[i] + (h[j] * ((j <= i) ? r[i - j] : ((320 * r[i + 17 - j])|0))) | 0) | 0 + } + } + for (i = 0; i < 17; i++) { + h[i] = x[i] + } + u = 0 + for (j = 0; j < 16; j++) { + u = (u + h[j]) | 0 + h[j] = u & 255 + u >>>= 8 + } + u = (u + h[16]) | 0; h[16] = u & 3 + u = (5 * (u >>> 2)) | 0 + for (j = 0; j < 16; j++) { + u = (u + h[j]) | 0 + h[j] = u & 255 + u >>>= 8 + } + u = (u + h[16]) | 0; h[16] = u + } + + for (j = 0; j < 17; j++) { + g[j] = h[j] + } + this.add1305(h, this.minusp) + s = (-(h[16] >>> 7) | 0) + for (j = 0; j < 17; j++) { + h[j] ^= s & (g[j] ^ h[j]) + } + + for (j = 0; j < 16; j++) { + c[j] = k[j + 16] + } + c[16] = 0 + this.add1305(h, c) + for (j = 0; j < 16; j++) { + out[outpos+j] = h[j] + } + return 0 + } + + cryptoOnetimeauthVerify(h: Uint8Array, hpos: number, m: Uint8Array, mpos: number, n: number, k: Uint8Array) { + const x = new Uint8Array(16) + this.cryptoOnetimeauth(x, 0, m, mpos, n, k) + return this.cryptoVerify16(h, hpos, x, 0) + } + + cryptoVerify16(x: Uint8Array, xi: number, y: Uint8Array, yi: number) { + return this.vn(x, xi, y, yi, 16) + } + + cryptoBoxBeforenm(k: Uint8Array, y: Uint8Array, x: Uint8Array) { + const s = new Uint8Array(32) + this.cryptoScalarmult(s, x, y) + return this.coreHsalsa20(k, this._0, s, this.sigma) + } + + cryptoSecretbox(c: Uint8Array, m: Uint8Array, d: number, n: Uint8Array, k: Uint8Array) { + let i + if (d < 32) { + return -1 + } + this.cryptoStreamXor(c, 0, m, 0, d, n, k) + this.cryptoOnetimeauth(c, 16, c, 32, d - 32, c) + for (i = 0; i < 16; i++) { + c[i] = 0 + } + return 0 + } + + cryptoSecretboxOpen(m: Uint8Array, c: Uint8Array, d: number, n: Uint8Array, k: Uint8Array) { + let i + const x = new Uint8Array(32) + if (d < 32) { + return -1 + } + this.cryptoStream(x, 0, 32, n, k) + if (this.cryptoOnetimeauthVerify(c, 16, c, 32, d - 32, x) !== 0) { + return -1 + } + this.cryptoStreamXor(m, 0, c, 0, d, n, k) + for (i = 0; i < 32; i++) { + m[i] = 0 + } + return 0 + } + + cryptoStream(c: Uint8Array, cpos: number, d: number, n: Uint8Array, k: Uint8Array) { + const s = new Uint8Array(32) + this.coreHsalsa20(s, n, k, this.sigma) + const sn = new Uint8Array(8) + for (var i = 0; i < 8; i++) { + sn[i] = n[i+16] + } + return this.cryptoStreamSalsa20(c, cpos, d, sn, s) + } + + cryptoStreamXor(c: Uint8Array, cpos: number, m: Uint8Array, mpos: number, d: number, n: Uint8Array, k: Uint8Array) { + const s = new Uint8Array(32) + this.coreHsalsa20(s, n, k, this.sigma) + const sn = new Uint8Array(8) + for (var i = 0; i < 8; i++) { + sn[i] = n[i+16] + } + return this.cryptoStreamSalsa20Xor(c, cpos, m, mpos, d, sn, s) + } + + checkLengths(k: Uint8Array, n: Uint8Array) { + if (k.length !== 32) { + throw new Error('bad key size') + } + if (n.length !== 24) { + throw new Error('bad nonce size') + } + } + + checkBoxLengths(pk: Uint8Array, sk: Uint8Array) { + if (pk.length !== 32) { + throw new Error('bad public key size') + } + if (sk.length !== 32) { + throw new Error('bad secret key size') + } + } + + checkArrayTypes(...params: any) { + for (let i = 0; i < params.length; i++) { + if (!(params[i] instanceof Uint8Array)) { + throw new TypeError('unexpected type, use Uint8Array') + } + } + } + + secretbox(msg: Uint8Array, nonce: Uint8Array, key: Uint8Array) { + this.checkArrayTypes(msg, nonce, key) + this.checkLengths(key, nonce) + const m = new Uint8Array(32 + msg.length) + const c = new Uint8Array(m.length) + for (let i = 0; i < msg.length; i++) { + m[i + 32] = msg[i] + } + this.cryptoSecretbox(c, m, m.length, nonce, key) + return c.subarray(16) + } + + secretboxOpen(box: Uint8Array, nonce: Uint8Array, key: Uint8Array) { + this.checkArrayTypes(box, nonce, key) + this.checkLengths(key, nonce) + const c = new Uint8Array(16 + box.length) + const m = new Uint8Array(c.length) + for (let i = 0; i < box.length; i++) { + c[i+16] = box[i] + } + if (c.length < 32) { + return null + } + if (this.cryptoSecretboxOpen(m, c, c.length, nonce, key) !== 0) { + return null + } + return m.subarray(32) + } + + box(msg: Uint8Array, nonce: Uint8Array, publicKey: Uint8Array, secretKey: Uint8Array) { + const k = this.boxBefore(publicKey, secretKey) + return this.secretbox(msg, nonce, k) + } + + boxOpen(msg: Uint8Array, nonce: Uint8Array, publicKey: Uint8Array, secretKey: Uint8Array) { + const k = this.boxBefore(publicKey, secretKey) + return this.secretboxOpen(msg, nonce, k) + } + + boxBefore(publicKey: Uint8Array, secretKey: Uint8Array) { + this.checkArrayTypes(publicKey, secretKey) + this.checkBoxLengths(publicKey, secretKey) + const k = new Uint8Array(32) + this.cryptoBoxBeforenm(k, publicKey, secretKey) + return k + } + /** * Generate the common key as the produkt of sk1 * pk2 * @param {Uint8Array} sk A 32 byte secret key of pair 1 @@ -708,4 +1346,69 @@ export default class Curve25519 { } } + /** + * Converts a ed25519 public key to Curve25519 to be used in + * Diffie-Hellman key exchange + */ + convertEd25519PublicKeyToCurve25519(pk: Uint8Array) { + const z = new Uint8Array(32) + const q = [this.gf(), this.gf(), this.gf(), this.gf()] + const a = this.gf() + const b = this.gf() + + if (this.unpackNeg(q, pk)) { + return null + } + + const y = q[1] + + this.A(a, this.gf1, y) + this.Z(b, this.gf1, y) + this.inv25519(b, b) + this.M(a, a, b) + + this.pack25519(z, a) + + return z + } + + /** + * Converts a ed25519 secret key to Curve25519 to be used in + * Diffie-Hellman key exchange + */ + convertEd25519SecretKeyToCurve25519(sk: Uint8Array) { + const d = new Uint8Array(64) + const o = new Uint8Array(32) + let i + + this.cryptoHash(d, sk, 32) + d[0] &= 248 + d[31] &= 127 + d[31] |= 64 + + for (i = 0; i < 32; i++) { + o[i] = d[i] + } + + for (i = 0; i < 64; i++) { + d[i] = 0 + } + + return o + } + + cryptoHash(out: Uint8Array, m: Uint8Array, n: number): number { + const input = new Uint8Array(n) + for (let i = 0; i < n; ++i) { + input[i] = m[i] + } + + const hash = blake2b(input) + for (let i = 0; i < 64; ++i) { + out[i] = hash[i] + } + + return 0 + } + } diff --git a/lib/util/util.ts b/lib/util/util.ts index 7e3bb8b..439e158 100644 --- a/lib/util/util.ts +++ b/lib/util/util.ts @@ -7,7 +7,7 @@ export default class Util { * @param {Uint8Array} rh Second array of bytes * @return {Boolean} True if the arrays are equal (length and content), false otherwise */ - static compare(lh: Uint8Array, rh: Uint8Array): boolean { + static compare = (lh: Uint8Array, rh: Uint8Array): boolean => { if (lh.length !== rh.length) { return false } diff --git a/package-lock.json b/package-lock.json index a7ddf78..bbe79d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "nanocurrency-web", - "version": "1.3.6", + "version": "1.4.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -31,21 +31,21 @@ } }, "@types/estree": { - "version": "0.0.50", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz", - "integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==", + "version": "0.0.51", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", + "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", "dev": true }, "@types/json-schema": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", - "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "dev": true }, "@types/node": { - "version": "17.0.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.14.tgz", - "integrity": "sha512-SbjLmERksKOGzWzPNuW7fJM7fk3YXVTFiZWB/Hs99gwhk+/dnrQRPBQjPW9aO+fi1tAffi9PrwFvsmOKmDTyng==", + "version": "17.0.25", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.25.tgz", + "integrity": "sha512-wANk6fBrUwdpY4isjWrKTufkrXdu1D2YHCot2fD/DfWxF5sMrVSA+KN7ydckvaTCh0HiqX9IVl0L5/ZoXg5M7w==", "dev": true }, "@ungap/promise-all-settled": { @@ -324,9 +324,9 @@ "dev": true }, "blakejs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.1.1.tgz", - "integrity": "sha512-bLG6PHOCZJKNshTjGRBvET0vTciwQE6zFKOKKXPDJfwFBd4Ac0yBfPZqcGvGJap50l7ktvlpFqc2jGVaUgbJgg==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.2.1.tgz", + "integrity": "sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==" }, "brace-expansion": { "version": "1.1.11", @@ -354,15 +354,15 @@ "dev": true }, "browserslist": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.19.1.tgz", - "integrity": "sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A==", + "version": "4.20.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.2.tgz", + "integrity": "sha512-CQOBCqp/9pDvDbx3xfMi+86pr4KXIf2FDkTTdeuYw8OxS9t898LA1Khq57gtufFILXpfgsSx5woNgsBgvGjpsA==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001286", - "electron-to-chromium": "^1.4.17", + "caniuse-lite": "^1.0.30001317", + "electron-to-chromium": "^1.4.84", "escalade": "^3.1.1", - "node-releases": "^2.0.1", + "node-releases": "^2.0.2", "picocolors": "^1.0.0" } }, @@ -372,6 +372,11 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "byte-base64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/byte-base64/-/byte-base64-1.1.0.tgz", + "integrity": "sha512-56cXelkJrVMdCY9V/3RfDxTh4VfMFCQ5km7B7GkIGfo4bcPL9aACyJLB0Ms3Ezu5rsHmLB2suis96z4fLM03DA==" + }, "camelcase": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", @@ -379,9 +384,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001307", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001307.tgz", - "integrity": "sha512-+MXEMczJ4FuxJAUp0jvAl6Df0NI/OfW1RWEE61eSmzS7hw6lz4IKutbhbXendwq8BljfFuHtu26VWsg4afQ7Ng==", + "version": "1.0.30001332", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001332.tgz", + "integrity": "sha512-10T30NYOEQtN6C11YGg411yebhvpnC6Z102+B95eAsN0oB6KUs01ivE8u+G6FMIRtIrVlYXhL+LUwQ3/hXwDWw==", "dev": true }, "chai": { @@ -558,9 +563,9 @@ "dev": true }, "electron-to-chromium": { - "version": "1.4.64", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.64.tgz", - "integrity": "sha512-8mec/99xgLUZCIZZq3wt61Tpxg55jnOSpxGYapE/1Ma9MpFEYYaz4QNYm0CM1rrnCo7i3FRHhbaWjeCLsveGjQ==", + "version": "1.4.118", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.118.tgz", + "integrity": "sha512-maZIKjnYDvF7Fs35nvVcyr44UcKNwybr93Oba2n3HkKDFAtk0svERkLN/HyczJDS3Fo4wU9th9fUQd09ZLtj1w==", "dev": true }, "emoji-regex": { @@ -570,9 +575,9 @@ "dev": true }, "enhanced-resolve": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.3.tgz", - "integrity": "sha512-EGAbGvH7j7Xt2nc0E7D99La1OiEs8LnyimkRgwExpUMScN6O+3x9tIWs7PLQZVNx4YD+00skHXPXi1yQHpAmZA==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.3.tgz", + "integrity": "sha512-Bq9VSor+kjvW3f9/MiiR4eE3XYgOl7/rS8lnSxbRbF3kS0B2r+Y9w5krBWxZgDxASVZbdYrn5wT4j/Wb0J9qow==", "dev": true, "requires": { "graceful-fs": "^4.2.4", @@ -751,6 +756,17 @@ "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" + }, + "dependencies": { + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } } }, "glob-parent": { @@ -769,9 +785,9 @@ "dev": true }, "graceful-fs": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", - "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", "dev": true }, "growl": { @@ -924,9 +940,9 @@ "dev": true }, "jest-worker": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.4.6.tgz", - "integrity": "sha512-gHWJF/6Xi5CTG5QCvROr6GcmpIqNYpDJyc8A1h/DyXqH1tD6SnRCM0d3U5msV31D2LB/U+E0M+W4oyvKV44oNw==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "dev": true, "requires": { "@types/node": "*", @@ -962,9 +978,9 @@ "dev": true }, "loader-runner": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", - "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", "dev": true }, "locate-path": { @@ -1011,28 +1027,28 @@ "dev": true }, "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", "dev": true, "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" + "braces": "^3.0.2", + "picomatch": "^2.3.1" } }, "mime-db": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", - "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true }, "mime-types": { - "version": "2.1.34", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", - "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, "requires": { - "mime-db": "1.51.0" + "mime-db": "1.52.0" } }, "mimic-fn": { @@ -1042,18 +1058,18 @@ "dev": true }, "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz", + "integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==", "dev": true, "requires": { "brace-expansion": "^1.1.7" } }, "mocha": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.0.tgz", - "integrity": "sha512-kNn7E8g2SzVcq0a77dkphPsDSN7P+iYkqE0ZsGCYWRsoiKjOt+NvXfaagik8vuDa6W5Zw3qxe8Jfpt5qKf+6/Q==", + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz", + "integrity": "sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==", "dev": true, "requires": { "@ungap/promise-all-settled": "1.1.2", @@ -1069,9 +1085,9 @@ "he": "1.2.0", "js-yaml": "4.1.0", "log-symbols": "4.1.0", - "minimatch": "3.0.4", + "minimatch": "4.2.1", "ms": "2.1.3", - "nanoid": "3.2.0", + "nanoid": "3.3.1", "serialize-javascript": "6.0.0", "strip-json-comments": "3.1.1", "supports-color": "8.1.1", @@ -1089,9 +1105,9 @@ "dev": true }, "nanoid": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz", - "integrity": "sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", + "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", "dev": true }, "neo-async": { @@ -1101,9 +1117,9 @@ "dev": true }, "node-releases": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.1.tgz", - "integrity": "sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.3.tgz", + "integrity": "sha512-maHFz6OLqYxz+VQyCAtA3PTX4UP/53pa05fyDNc9CwjvJ0yEh6+xBwKsgCxMNhS8taUKBFYxfuiaD9U/55iFaw==", "dev": true }, "normalize-path": { @@ -1336,9 +1352,9 @@ } }, "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -1453,11 +1469,12 @@ "dev": true }, "terser": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz", - "integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==", + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.12.1.tgz", + "integrity": "sha512-NXbs+7nisos5E+yXwAD+y7zrcTkMqb0dEJxIGtSKPdCBzopf7ni4odPul2aechpV7EXNvOudYOX2bb5tln1jbQ==", "dev": true, "requires": { + "acorn": "^8.5.0", "commander": "^2.20.0", "source-map": "~0.7.2", "source-map-support": "~0.5.20" @@ -1494,9 +1511,9 @@ } }, "ts-loader": { - "version": "9.2.6", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.2.6.tgz", - "integrity": "sha512-QMTC4UFzHmu9wU2VHZEmWWE9cUajjfcdcws+Gh7FhiO+Dy0RnR1bNz0YCHqhI0yRowCE9arVnNxYHqELOy9Hjw==", + "version": "9.2.8", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.2.8.tgz", + "integrity": "sha512-gxSak7IHUuRtwKf3FIPSW1VpZcqF9+MBrHOvBp9cjHh+525SjtCIJKVGjRKIAfxBwDGDGCFF00rTfzB1quxdSw==", "dev": true, "requires": { "chalk": "^4.1.0", @@ -1512,9 +1529,9 @@ "dev": true }, "typescript": { - "version": "4.5.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", - "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz", + "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==", "dev": true }, "uri-js": { @@ -1537,13 +1554,13 @@ } }, "webpack": { - "version": "5.68.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.68.0.tgz", - "integrity": "sha512-zUcqaUO0772UuuW2bzaES2Zjlm/y3kRBQDVFVCge+s2Y8mwuUTdperGaAv65/NtRL/1zanpSJOq/MD8u61vo6g==", + "version": "5.72.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.72.0.tgz", + "integrity": "sha512-qmSmbspI0Qo5ld49htys8GY9XhS9CGqFoHTsOVAnjBdg0Zn79y135R+k4IR4rKK6+eKaabMhJwiVB7xw0SJu5w==", "dev": true, "requires": { - "@types/eslint-scope": "^3.7.0", - "@types/estree": "^0.0.50", + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^0.0.51", "@webassemblyjs/ast": "1.11.1", "@webassemblyjs/wasm-edit": "1.11.1", "@webassemblyjs/wasm-parser": "1.11.1", @@ -1551,7 +1568,7 @@ "acorn-import-assertions": "^1.7.6", "browserslist": "^4.14.5", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.8.3", + "enhanced-resolve": "^5.9.2", "es-module-lexer": "^0.9.0", "eslint-scope": "5.1.1", "events": "^3.2.0", diff --git a/package.json b/package.json index 74b88c5..c2a6615 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nanocurrency-web", - "version": "1.3.6", + "version": "1.4.0", "description": "Toolkit for Nano cryptocurrency client side offline integrations", "author": "Miro Metsänheimo ", "license": "MIT", @@ -19,7 +19,9 @@ "crypto", "wallet", "block", - "sign" + "sign", + "encrypt", + "decrypt" ], "main": "dist/index.js", "types": "dist/index.d.ts", @@ -31,15 +33,16 @@ }, "dependencies": { "bignumber.js": "9.0.2", - "blakejs": "1.1.1", + "blakejs": "1.2.1", + "byte-base64": "1.1.0", "crypto-js": "3.1.9-1" }, "devDependencies": { "chai": "4.3.6", - "mocha": "9.2.0", - "ts-loader": "9.2.6", - "typescript": "4.5.5", - "webpack": "5.68.0", + "mocha": "9.2.2", + "ts-loader": "9.2.8", + "typescript": "4.6.3", + "webpack": "5.72.0", "webpack-cli": "4.9.2" } } diff --git a/test/test.js b/test/test.js index 8187d30..9d94ad9 100644 --- a/test/test.js +++ b/test/test.js @@ -1,7 +1,7 @@ 'use strict' const expect = require('chai').expect -const { wallet, block, tools } = require('../dist/index') +const { wallet, block, tools, box } = require('../dist/index') // WARNING: Do not send any funds to the test vectors below describe('generate wallet test', () => { @@ -261,10 +261,8 @@ describe('unit conversion tests', () => { describe('Signer tests', () => { - let testWallet; - before(() => { - this.testWallet = wallet.generate(); + this.testWallet = wallet.generate() }) // Private key: 3be4fc2ef3f3b7374e6fc4fb6e7bb153f8a2998b3b3dab50853eabe128024143 @@ -330,3 +328,55 @@ describe('Signer tests', () => { }) }) + +describe('Box tests', () => { + + before(() => { + this.message = 'The quick brown fox jumps over the lazy dog' + this.bob = wallet.generate() + this.alice = wallet.generate() + }) + + it('should encrypt and decrypt a message', () => { + const encrypted = box.encrypt(this.message, this.alice.accounts[0].address, this.bob.accounts[0].privateKey) + const encrypted2 = box.encrypt(this.message, this.alice.accounts[0].address, this.bob.accounts[0].privateKey) + const encrypted3 = box.encrypt(this.message + 'asd', this.alice.accounts[0].address, this.bob.accounts[0].privateKey) + + // Just to be safe + expect(this.message).to.not.equal(encrypted) + expect(encrypted).to.not.equal(encrypted2) + expect(encrypted).to.not.equal(encrypted3) + + const decrypted = box.decrypt(encrypted, this.bob.accounts[0].address, this.alice.accounts[0].privateKey) + expect(this.message).to.equal(decrypted) + }) + + it('should fail to decrypt with wrong public key in encryption', () => { + // Encrypt with wrong public key + const aliceAccounts = wallet.accounts(this.alice.seed, 1, 2) + const encrypted = box.encrypt(this.message, aliceAccounts[0].address, this.bob.accounts[0].privateKey) + expect(() => box.decrypt(encrypted, this.bob.accounts[0].address, this.alice.accounts[0].privateKey)).to.throw() + }) + + it('should fail to decrypt with wrong public key in decryption', () => { + // Decrypt with wrong public key + const bobAccounts = wallet.accounts(this.bob.seed, 1, 2) + const encrypted = box.encrypt(this.message, this.alice.accounts[0].address, this.bob.accounts[0].privateKey) + expect(() => box.decrypt(encrypted, bobAccounts[0].address, this.alice.accounts[0].privateKey)).to.throw() + }) + + it('should fail to decrypt with wrong private key in encryption', () => { + // Encrypt with wrong public key + const bobAccounts = wallet.accounts(this.bob.seed, 1, 2) + const encrypted = box.encrypt(this.message, this.alice.accounts[0].address, bobAccounts[0].privateKey) + expect(() => box.decrypt(encrypted, this.bob.accounts[0].address, this.alice.accounts[0].privateKey)).to.throw() + }) + + it('should fail to decrypt with wrong private key in decryption', () => { + // Encrypt with wrong public key + const aliceAccounts = wallet.accounts(this.alice.seed, 1, 2) + const encrypted = box.encrypt(this.message, this.alice.accounts[0].address, this.bob.accounts[0].privateKey) + expect(() => box.decrypt(encrypted, this.bob.accounts[0].address, aliceAccounts[0].privateKey)).to.throw() + }) + +}) \ No newline at end of file