diff --git a/src/crypto/constants.ts b/src/crypto/constants.ts new file mode 100644 index 00000000..a9fe61a5 --- /dev/null +++ b/src/crypto/constants.ts @@ -0,0 +1,5 @@ +// In a future PR, improve versioning infrastructure for key-file objects in erdjs. +export const Version = 4; +export const CipherAlgorithm = "aes-128-ctr"; +export const DigestAlgorithm = "sha256"; +export const KeyDerivationFunction = "scrypt"; diff --git a/src/crypto/decryptor.ts b/src/crypto/decryptor.ts new file mode 100644 index 00000000..35baace9 --- /dev/null +++ b/src/crypto/decryptor.ts @@ -0,0 +1,27 @@ +import crypto from "crypto"; +import {EncryptedData} from "./encryptedData"; +import * as errors from "../errors"; +import { DigestAlgorithm } from "./constants"; + +export class Decryptor { + public static decrypt(data: EncryptedData, password: string): Buffer { + const kdfparams = data.kdfparams; + const salt = Buffer.from(data.salt, "hex"); + const iv = Buffer.from(data.iv, "hex"); + const ciphertext = Buffer.from(data.ciphertext, "hex"); + const derivedKey = kdfparams.generateDerivedKey(Buffer.from(password), salt); + const derivedKeyFirstHalf = derivedKey.slice(0, 16); + const derivedKeySecondHalf = derivedKey.slice(16, 32); + + const computedMAC = crypto.createHmac(DigestAlgorithm, derivedKeySecondHalf).update(ciphertext).digest(); + const actualMAC = data.mac; + + if (computedMAC.toString("hex") !== actualMAC) { + throw new errors.ErrWallet("MAC mismatch, possibly wrong password"); + } + + const decipher = crypto.createDecipheriv(data.cipher, derivedKeyFirstHalf, iv); + + return Buffer.concat([decipher.update(ciphertext), decipher.final()]); + } +} diff --git a/src/crypto/derivationParams.ts b/src/crypto/derivationParams.ts new file mode 100644 index 00000000..134a9534 --- /dev/null +++ b/src/crypto/derivationParams.ts @@ -0,0 +1,36 @@ +import scryptsy from "scryptsy"; + +export class ScryptKeyDerivationParams { + /** + * numIterations + */ + n = 4096; + + /** + * memFactor + */ + r = 8; + + /** + * pFactor + */ + p = 1; + + dklen = 32; + + constructor(n = 4096, r = 8, p = 1, dklen = 32) { + this.n = n; + this. r = r; + this.p = p; + this.dklen = dklen; + } + + /** + * 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 + */ + public generateDerivedKey(password: Buffer, salt: Buffer): Buffer { + return scryptsy(password, salt, this.n, this.r, this.p, this.dklen); + } +} diff --git a/src/crypto/encrypt.spec.ts b/src/crypto/encrypt.spec.ts new file mode 100644 index 00000000..b076b311 --- /dev/null +++ b/src/crypto/encrypt.spec.ts @@ -0,0 +1,22 @@ +import { assert } from "chai"; +import { Encryptor } from "./encryptor"; +import { Decryptor } from "./decryptor"; +import { EncryptedData } from "./encryptedData"; + +describe("test address", () => { + it("encrypts/decrypts", () => { + const sensitiveData = Buffer.from("my mnemonic"); + const encryptedData = Encryptor.encrypt(sensitiveData, "password123"); + const decryptedBuffer = Decryptor.decrypt(encryptedData, "password123"); + + assert.equal(sensitiveData.toString('hex'), decryptedBuffer.toString('hex')); + }); + + it("encodes/decodes kdfparams", () => { + const sensitiveData = Buffer.from("my mnemonic"); + const encryptedData = Encryptor.encrypt(sensitiveData, "password123"); + const decodedData = EncryptedData.fromJSON(encryptedData.toJSON()); + + assert.deepEqual(decodedData, encryptedData, "invalid decoded data"); + }); +}); diff --git a/src/crypto/encryptedData.ts b/src/crypto/encryptedData.ts new file mode 100644 index 00000000..b882a570 --- /dev/null +++ b/src/crypto/encryptedData.ts @@ -0,0 +1,65 @@ +import { ScryptKeyDerivationParams } from "./derivationParams"; + +export class EncryptedData { + id: string; + version: number; + cipher: string; + ciphertext: string; + iv: string; + kdf: string; + kdfparams: ScryptKeyDerivationParams; + salt: string; + mac: string; + + constructor(data: Omit) { + this.id = data.id; + this.version = data.version; + this.ciphertext = data.ciphertext; + this.iv = data.iv; + this.cipher = data.cipher; + this.kdf = data.kdf; + this.kdfparams = data.kdfparams; + this.mac = data.mac; + this.salt = data.salt; + } + + toJSON(): any { + return { + version: this.version, + id: this.id, + crypto: { + ciphertext: this.ciphertext, + cipherparams: { iv: this.iv }, + cipher: this.cipher, + kdf: this.kdf, + kdfparams: { + dklen: this.kdfparams.dklen, + salt: this.salt, + n: this.kdfparams.n, + r: this.kdfparams.r, + p: this.kdfparams.p + }, + mac: this.mac, + } + }; + } + + static fromJSON(data: any): EncryptedData { + return new EncryptedData({ + version: data.version, + id: data.id, + ciphertext: data.crypto.ciphertext, + iv: data.crypto.cipherparams.iv, + cipher: data.crypto.cipher, + kdf: data.crypto.kdf, + kdfparams: new ScryptKeyDerivationParams( + data.crypto.kdfparams.n, + data.crypto.kdfparams.r, + data.crypto.kdfparams.p, + data.crypto.kdfparams.dklen, + ), + salt: data.crypto.kdfparams.salt, + mac: data.crypto.mac, + }); + } +} diff --git a/src/crypto/encryptor.ts b/src/crypto/encryptor.ts new file mode 100644 index 00000000..e329dbb9 --- /dev/null +++ b/src/crypto/encryptor.ts @@ -0,0 +1,30 @@ +import crypto from "crypto"; +import { Randomness } from "./randomness"; +import { ScryptKeyDerivationParams } from "./derivationParams"; +import { CipherAlgorithm, DigestAlgorithm, Version, KeyDerivationFunction } from "./constants"; +import {EncryptedData} from "./encryptedData"; + +export class Encryptor { + public static encrypt(data: Buffer, password: string, randomness: Randomness = new Randomness()): EncryptedData { + const kdParams = new ScryptKeyDerivationParams(); + const derivedKey = kdParams.generateDerivedKey(Buffer.from(password), randomness.salt); + const derivedKeyFirstHalf = derivedKey.slice(0, 16); + const derivedKeySecondHalf = derivedKey.slice(16, 32); + const cipher = crypto.createCipheriv(CipherAlgorithm, derivedKeyFirstHalf, randomness.iv); + + const ciphertext = Buffer.concat([cipher.update(data), cipher.final()]); + const mac = crypto.createHmac(DigestAlgorithm, derivedKeySecondHalf).update(ciphertext).digest(); + + return new EncryptedData({ + version: Version, + id: randomness.id, + ciphertext: ciphertext.toString('hex'), + iv: randomness.iv.toString('hex'), + cipher: CipherAlgorithm, + kdf: KeyDerivationFunction, + kdfparams: kdParams, + mac: mac.toString('hex'), + salt: randomness.salt.toString('hex') + }); + } +} diff --git a/src/crypto/index.ts b/src/crypto/index.ts new file mode 100644 index 00000000..d674cb91 --- /dev/null +++ b/src/crypto/index.ts @@ -0,0 +1,5 @@ +export * from "./constants"; +export * from "./encryptor"; +export * from "./decryptor"; +export * from "./encryptedData"; +export * from "./randomness"; diff --git a/src/crypto/randomness.ts b/src/crypto/randomness.ts new file mode 100644 index 00000000..f045dc67 --- /dev/null +++ b/src/crypto/randomness.ts @@ -0,0 +1,15 @@ +import nacl from "tweetnacl"; +import {v4 as uuidv4} from "uuid"; +const crypto = require("crypto"); + +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/index.ts b/src/index.ts index 8114d3ff..a82257ff 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,6 +15,7 @@ export * from "./apiProvider"; export * from "./scArgumentsParser"; export * from "./esdtHelpers"; +export * from "./crypto"; export * from "./walletcore"; export * from "./nullSigner"; diff --git a/src/walletcore/userWallet.ts b/src/walletcore/userWallet.ts index d42c468a..c0f7064d 100644 --- a/src/walletcore/userWallet.ts +++ b/src/walletcore/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/walletcore/users.spec.ts b/src/walletcore/users.spec.ts index 78cfa160..4f57c11f 100644 --- a/src/walletcore/users.spec.ts +++ b/src/walletcore/users.spec.ts @@ -3,7 +3,8 @@ import { assert } from "chai"; import { TestWallets } 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";