Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Encryption component #11

Merged
merged 14 commits into from
Jul 1, 2021
5 changes: 5 additions & 0 deletions src/crypto/constants.ts
Original file line number Diff line number Diff line change
@@ -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";
27 changes: 27 additions & 0 deletions src/crypto/decryptor.ts
Original file line number Diff line number Diff line change
@@ -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()]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, the padding is still done in userWallet.ts.

I can confirm there's no change in logic - only refactoring ✔️

}
}
36 changes: 36 additions & 0 deletions src/crypto/derivationParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import scryptsy from "scryptsy";

export class ScryptKeyDerivationParams {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I confirm there's no change in logic - only movement & refactoring ✔️ (better cohesion now).

/**
* 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);
}
}
22 changes: 22 additions & 0 deletions src/crypto/encrypt.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { assert } from "chai";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 for this test.

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");
});
});
65 changes: 65 additions & 0 deletions src/crypto/encryptedData.ts
Original file line number Diff line number Diff line change
@@ -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<EncryptedData, "toJSON">) {
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,
});
}
}
30 changes: 30 additions & 0 deletions src/crypto/encryptor.ts
Original file line number Diff line number Diff line change
@@ -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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I confirm the login here is the one from the previous constructor of UserWallet ✔️

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')
});
}
}
5 changes: 5 additions & 0 deletions src/crypto/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from "./constants";
export * from "./encryptor";
export * from "./decryptor";
export * from "./encryptedData";
export * from "./randomness";
15 changes: 15 additions & 0 deletions src/crypto/randomness.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import nacl from "tweetnacl";
import {v4 as uuidv4} from "uuid";
const crypto = require("crypto");

export class Randomness {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✔️ I confirm there's no change in the logic of this class (only move / refactor).

salt: Buffer;
iv: Buffer;
id: string;

constructor(init?: Partial<Randomness>) {
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) });
}
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export * from "./apiProvider";
export * from "./scArgumentsParser";
export * from "./esdtHelpers";

export * from "./crypto";
export * from "./walletcore";
export * from "./nullSigner";

Expand Down
125 changes: 32 additions & 93 deletions src/walletcore/userWallet.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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;
}

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

/**
Expand All @@ -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<Randomness>) {
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) });
}
}
Loading