diff --git a/src-network-wallet/index.ts b/src-network-wallet/index.ts index 07fa2a2e..acf9c06b 100644 --- a/src-network-wallet/index.ts +++ b/src-network-wallet/index.ts @@ -9,4 +9,5 @@ export * from "./userWallet"; export * from "./userKeys"; export * from "./validatorKeys"; export * from "./userSigner"; +export * from "./userVerifier"; export * from "./validatorSigner"; diff --git a/src-network-wallet/userKeys.ts b/src-network-wallet/userKeys.ts index 7c9cf513..1bb44f5e 100644 --- a/src-network-wallet/userKeys.ts +++ b/src-network-wallet/userKeys.ts @@ -2,6 +2,8 @@ import * as tweetnacl from "tweetnacl"; import { Address } from "../address"; import { guardLength } from "../utils"; import { parseUserKey } from "./pem"; +import {SignableMessage} from "../signableMessage"; +import {Logger} from "../logger"; export const USER_SEED_LENGTH = 32; export const USER_PUBKEY_LENGTH = 32; @@ -60,6 +62,17 @@ export class UserPublicKey { this.buffer = buffer; } + verify(message: Buffer, signature: Buffer): boolean { + try { + const unopenedMessage = Buffer.concat([signature, message]); + const unsignedMessage = tweetnacl.sign.open(unopenedMessage, this.buffer); + return unsignedMessage != null; + } catch (err) { + Logger.error(err); + return false; + } + } + hex(): string { return this.buffer.toString("hex"); } diff --git a/src-network-wallet/userVerifier.ts b/src-network-wallet/userVerifier.ts new file mode 100644 index 00000000..d39bfa88 --- /dev/null +++ b/src-network-wallet/userVerifier.ts @@ -0,0 +1,29 @@ +import {IVerifiable, IVerifier} from "../interface"; +import {Address} from "../address"; +import {UserPublicKey} from "./userKeys"; + +/** + * ed25519 signature verification + */ +export class UserVerifier implements IVerifier { + + publicKey: UserPublicKey; + constructor(publicKey: UserPublicKey) { + this.publicKey = publicKey; + } + + static fromAddress(address: Address): IVerifier { + let publicKey = new UserPublicKey(address.pubkey()); + return new UserVerifier(publicKey); + } + + /** + * Verify a message's signature. + * @param message the message to be verified. + */ + verify(message: IVerifiable): boolean { + return this.publicKey.verify( + message.serializeForSigning(this.publicKey.toAddress()), + Buffer.from(message.getSignature().hex(), 'hex')); + } +} diff --git a/src-network-wallet/userWallet.ts b/src-network-wallet/userWallet.ts index d42c468a..c0f7064d 100644 --- a/src-network-wallet/userWallet.ts +++ b/src-network-wallet/userWallet.ts @@ -1,22 +1,10 @@ -import * as errors from "../errors"; -import nacl from "tweetnacl"; import { UserPublicKey, UserSecretKey } from "./userKeys"; -const crypto = require("crypto"); -import { v4 as uuidv4 } from "uuid"; -import scryptsy from "scryptsy"; - -// In a future PR, improve versioning infrastructure for key-file objects in erdjs. -const Version = 4; -const CipherAlgorithm = "aes-128-ctr"; -const DigestAlgorithm = "sha256"; -const KeyDerivationFunction = "scrypt"; +import { EncryptedData, Encryptor, Decryptor, CipherAlgorithm, Version, KeyDerivationFunction, Randomness } from "../crypto"; +import {ScryptKeyDerivationParams} from "../crypto/derivationParams"; export class UserWallet { private readonly publicKey: UserPublicKey; - private readonly randomness: Randomness; - private readonly ciphertext: Buffer; - private readonly mac: Buffer; - private readonly kdfparams: ScryptKeyDerivationParams; + private readonly encryptedData: EncryptedData; /** * Copied from: https://github.com/ElrondNetwork/elrond-core-js/blob/v1.28.0/src/account.js#L76 @@ -30,21 +18,9 @@ export class UserWallet { * passed through a password-based key derivation function (kdf). */ constructor(secretKey: UserSecretKey, password: string, randomness: Randomness = new Randomness()) { - const kdParams = new ScryptKeyDerivationParams(); - const derivedKey = UserWallet.generateDerivedKey(Buffer.from(password), randomness.salt, kdParams); - const derivedKeyFirstHalf = derivedKey.slice(0, 16); - const derivedKeySecondHalf = derivedKey.slice(16, 32); - const cipher = crypto.createCipheriv(CipherAlgorithm, derivedKeyFirstHalf, randomness.iv); - const text = Buffer.concat([secretKey.valueOf(), secretKey.generatePublicKey().valueOf()]); - const ciphertext = Buffer.concat([cipher.update(text), cipher.final()]); - const mac = crypto.createHmac(DigestAlgorithm, derivedKeySecondHalf).update(ciphertext).digest(); - + this.encryptedData = Encryptor.encrypt(text, password, randomness); this.publicKey = secretKey.generatePublicKey(); - this.randomness = randomness; - this.ciphertext = ciphertext; - this.mac = mac; - this.kdfparams = kdParams; } /** @@ -58,24 +34,9 @@ export class UserWallet { * From an encrypted keyfile, given the password, loads the secret key and the public key. */ static decryptSecretKey(keyFileObject: any, password: string): UserSecretKey { - const kdfparams = keyFileObject.crypto.kdfparams; - const salt = Buffer.from(kdfparams.salt, "hex"); - const iv = Buffer.from(keyFileObject.crypto.cipherparams.iv, "hex"); - const ciphertext = Buffer.from(keyFileObject.crypto.ciphertext, "hex"); - const derivedKey = UserWallet.generateDerivedKey(Buffer.from(password), salt, kdfparams); - const derivedKeyFirstHalf = derivedKey.slice(0, 16); - const derivedKeySecondHalf = derivedKey.slice(16, 32); - - const computedMAC = crypto.createHmac(DigestAlgorithm, derivedKeySecondHalf).update(ciphertext).digest(); - const actualMAC = keyFileObject.crypto.mac; - - if (computedMAC.toString("hex") !== actualMAC) { - throw new errors.ErrWallet("MAC mismatch, possibly wrong password"); - } - - const decipher = crypto.createDecipheriv(keyFileObject.crypto.cipher, derivedKeyFirstHalf, iv); + const encryptedData = UserWallet.edFromJSON(keyFileObject) - let text = Buffer.concat([decipher.update(ciphertext), decipher.final()]); + let text = Decryptor.decrypt(encryptedData, password); while (text.length < 32) { let zeroPadding = Buffer.from([0x00]); text = Buffer.concat([zeroPadding, text]); @@ -85,14 +46,23 @@ export class UserWallet { return new UserSecretKey(seed); } - /** - * Will take about: - * - 80-90 ms in Node.js, on a i3-8100 CPU @ 3.60GHz - * - 350-360 ms in browser (Firefox), on a i3-8100 CPU @ 3.60GHz - */ - private static generateDerivedKey(password: Buffer, salt: Buffer, kdfparams: ScryptKeyDerivationParams): Buffer { - const derivedKey = scryptsy(password, salt, kdfparams.n, kdfparams.r, kdfparams.p, kdfparams.dklen); - return derivedKey; + static edFromJSON(keyfileObject: any): EncryptedData { + return new EncryptedData({ + version: Version, + id: keyfileObject.id, + cipher: keyfileObject.crypto.cipher, + ciphertext: keyfileObject.crypto.ciphertext, + iv: keyfileObject.crypto.cipherparams.iv, + kdf: keyfileObject.crypto.kdf, + kdfparams: new ScryptKeyDerivationParams( + keyfileObject.crypto.kdfparams.n, + keyfileObject.crypto.kdfparams.r, + keyfileObject.crypto.kdfparams.p, + keyfileObject.crypto.kdfparams.dklen + ), + salt: keyfileObject.crypto.kdfparams.salt, + mac: keyfileObject.crypto.mac, + }); } /** @@ -101,54 +71,23 @@ export class UserWallet { toJSON(): any { return { version: Version, - id: this.randomness.id, + id: this.encryptedData.id, address: this.publicKey.hex(), bech32: this.publicKey.toAddress().toString(), crypto: { - ciphertext: this.ciphertext.toString("hex"), - cipherparams: { iv: this.randomness.iv.toString("hex") }, + ciphertext: this.encryptedData.ciphertext, + cipherparams: { iv: this.encryptedData.iv }, cipher: CipherAlgorithm, kdf: KeyDerivationFunction, kdfparams: { - dklen: this.kdfparams.dklen, - salt: this.randomness.salt.toString("hex"), - n: this.kdfparams.n, - r: this.kdfparams.r, - p: this.kdfparams.p + dklen: this.encryptedData.kdfparams.dklen, + salt: this.encryptedData.salt, + n: this.encryptedData.kdfparams.n, + r: this.encryptedData.kdfparams.r, + p: this.encryptedData.kdfparams.p }, - mac: this.mac.toString("hex"), + mac: this.encryptedData.mac, } }; } } - -class ScryptKeyDerivationParams { - /** - * numIterations - */ - n = 4096; - - /** - * memFactor - */ - r = 8; - - /** - * pFactor - */ - p = 1; - - dklen = 32; -} - -export class Randomness { - salt: Buffer; - iv: Buffer; - id: string; - - constructor(init?: Partial) { - this.salt = init?.salt || Buffer.from(nacl.randomBytes(32)); - this.iv = init?.iv || Buffer.from(nacl.randomBytes(16)); - this.id = init?.id || uuidv4({ random: crypto.randomBytes(16) }); - } -} diff --git a/src-network-wallet/users.spec.ts b/src-network-wallet/users.spec.ts index 6ffff61f..27b64670 100644 --- a/src-network-wallet/users.spec.ts +++ b/src-network-wallet/users.spec.ts @@ -3,14 +3,17 @@ import { assert } from "chai"; import { loadMnemonic, loadPassword, loadTestWallets, TestWallet } from "../testutils"; import { UserSecretKey } from "./userKeys"; import { Mnemonic } from "./mnemonic"; -import { UserWallet, Randomness } from "./userWallet"; +import { UserWallet } from "./userWallet"; +import { Randomness } from "../crypto"; import { Address } from "../address"; import { UserSigner } from "./userSigner"; import { Transaction } from "../transaction"; import { Nonce } from "../nonce"; import { Balance } from "../balance"; -import { ChainID, GasLimit, GasPrice, TransactionVersion } from "../networkParams"; +import { ChainID, GasLimit, GasPrice } from "../networkParams"; import { TransactionPayload } from "../transactionPayload"; +import { UserVerifier } from "./userVerifier"; +import { SignableMessage } from "../signableMessage"; describe("test user wallets", () => { let alice: TestWallet, bob: TestWallet, carol: TestWallet; @@ -120,6 +123,7 @@ describe("test user wallets", () => { it("should sign transactions", async () => { let signer = new UserSigner(UserSecretKey.fromString("1a927e2af5306a9bb2ea777f73e06ecc0ac9aaa72fb4ea3fecf659451394cccf")); + let verifier = new UserVerifier(UserSecretKey.fromString("1a927e2af5306a9bb2ea777f73e06ecc0ac9aaa72fb4ea3fecf659451394cccf").generatePublicKey()); let sender = new Address("erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz"); let receiver = new Address("erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r"); @@ -139,7 +143,7 @@ describe("test user wallets", () => { assert.equal(serialized, `{"nonce":0,"value":"0","receiver":"erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r","sender":"erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz","gasPrice":1000000000,"gasLimit":50000,"data":"Zm9v","chainID":"1","version":1}`); assert.equal(transaction.getSignature().hex(), "b5fddb8c16fa7f6123cb32edc854f1e760a3eb62c6dc420b5a4c0473c58befd45b621b31a448c5b59e21428f2bc128c80d0ee1caa4f2bf05a12be857ad451b00"); - + assert.isTrue(verifier.verify(transaction)); // Without data field transaction = new Transaction({ nonce: new Nonce(8), @@ -173,4 +177,16 @@ describe("test user wallets", () => { await signer.sign(transaction); assert.equal(transaction.getSignature().hex(), "c0bd2b3b33a07b9cc5ee7435228acb0936b3829c7008aacabceea35163e555e19a34def2c03a895cf36b0bcec30a7e11215c11efc0da29294a11234eb2b3b906"); }); + + it("signs a general message", function () { + let signer = new UserSigner(UserSecretKey.fromString("1a927e2af5306a9bb2ea777f73e06ecc0ac9aaa72fb4ea3fecf659451394cccf")); + let verifier = new UserVerifier(UserSecretKey.fromString("1a927e2af5306a9bb2ea777f73e06ecc0ac9aaa72fb4ea3fecf659451394cccf").generatePublicKey()); + const message = new SignableMessage({ + message: Buffer.from("hello world") + }); + + signer.sign(message); + assert.isNotEmpty(message.signature); + assert.isTrue(verifier.verify(message)); + }); });