-
Notifications
You must be signed in to change notification settings - Fork 37
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
Changes from all commits
c8d7e8d
bb926b0
5b41280
96f1f17
3b968d2
da35036
0ddb68b
5c10d7d
83ecaef
b5ca90a
49108de
00c99bb
2fb57a8
711ba5e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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"; |
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()]); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import scryptsy from "scryptsy"; | ||
|
||
export class ScryptKeyDerivationParams { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { assert } from "chai"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"); | ||
}); | ||
}); |
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, | ||
}); | ||
} | ||
} |
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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
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') | ||
}); | ||
} | ||
} |
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"; |
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) }); | ||
} | ||
} |
There was a problem hiding this comment.
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 ✔️