Last active
August 20, 2019 16:38
-
-
Save perry-mitchell/e51ddd959b1c26dedf0f92fb3d26e3e9 to your computer and use it in GitHub Desktop.
Encryption (minus derivation) in NodeJS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const crypto = require("crypto"); | |
const DERIVATION_ROUNDS = 200000; | |
function constantTimeCompare(val1, val2) { | |
let sentinel; | |
if (val1.length !== val2.length) { | |
return false; | |
} | |
for (let i = 0; i <= val1.length - 1; i += 1) { | |
sentinel |= val1.charCodeAt(i) ^ val2.charCodeAt(i); | |
} | |
return sentinel === 0; | |
} | |
function generateIV() { | |
return Promise.resolve(new Buffer(crypto.randomBytes(16))); | |
} | |
function generateSalt(length) { | |
if (length <= 0) { | |
return Promise.reject( | |
new Error(`Failed generating salt: Invalid length supplied: ${length}`) | |
); | |
} | |
let output = ""; | |
while (output.length < length) { | |
output += crypto.randomBytes(3).toString("base64"); | |
if (output.length > length) { | |
output = output.substr(0, length); | |
} | |
} | |
return Promise.resolve(output); | |
} | |
function encryptText(text, password) { | |
return generateSalt(12) | |
.then(salt => Promise.all([ | |
generateIV(), | |
Promise.resolve(salt), | |
deriveFromPassword(password, salt, DERIVATION_ROUNDS) | |
])) | |
.then(([iv, salt, derivedKey]) => { | |
const ivHex = iv.toString("hex"); | |
const encryptTool = crypto.createCipheriv("aes-256-cbc", derivedKey.key, iv); | |
const hmacTool = crypto.createHmac("sha256", derivedKey.hmac); | |
// Perform encryption | |
let encryptedContent = encryptTool.update(text, "utf8", "base64"); | |
encryptedContent += encryptTool.final("base64"); | |
// Generate hmac | |
hmacTool.update(encryptedContent); | |
hmacTool.update(ivHex); | |
hmacTool.update(salt); | |
const hmacHex = hmacTool.digest("hex"); | |
// Output encrypted components | |
const components = { | |
m: "cbc", | |
h: hmacHex, | |
i: ivHex, | |
s: salt, | |
r: DERIVATION_ROUNDS | |
}; | |
return packageComponents(encryptedContent, components); | |
}); | |
} | |
function decryptText(encryptedString, password) { | |
let encryptedComponents; | |
return Promise | |
.resolve() | |
.then(() => { | |
encryptedComponents = unpackageComponents(encryptedString); | |
return deriveFromPassword(password, encryptedComponents.s, encryptedComponents.r); | |
}) | |
.then(derivedKey => { | |
const iv = new Buffer(encryptedComponents.i, "hex"); | |
const hmacData = encryptedComponents.h; | |
// Get HMAC tool | |
const hmacTool = crypto.createHmac("sha256", derivedKey.hmac); | |
// Generate the HMAC | |
hmacTool.update(encryptedComponents.encryptedContent); | |
hmacTool.update(encryptedComponents.i); | |
hmacTool.update(encryptedComponents.s); | |
const newHmaxHex = hmacTool.digest("hex"); | |
// Check hmac for tampering | |
if (constantTimeCompare(hmacData, newHmaxHex) !== true) { | |
throw new Error("Authentication failed while decrypting content"); | |
} | |
// Decrypt | |
const decryptTool = crypto.createDecipheriv("aes-256-cbc", derivedKey.key, iv); | |
const decryptedText = decryptTool.update(encryptedComponents.encryptedContent, "base64", "utf8"); | |
return `${decryptedText}${decryptTool.final("utf8")}`; | |
}); | |
} | |
function packageComponents(encryptedContent, components) { | |
return `$myencryption$${Object.keys(components).map(key => `${key}=${components[key]}`).join(",")}$${encryptedContent}`; | |
} | |
function unpackageComponents(payload) { | |
const [, encryptor, componentsStr, encryptedContent] = payload.split("$"); | |
if (encryptor !== "myencryption") { | |
throw new Error("Failed decrypting: unrecognised encrypted payload"); | |
} | |
const components = componentsStr.split(",").reduce((output, item) => { | |
const [key, value] = item.split("="); | |
return Object.assign(output, { | |
[key]: value | |
}); | |
}, {}); | |
components.r = parseInt(components.r, 10); | |
components.encryptedContent = encryptedContent; | |
return components; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment