Cryptography for JavaScript Developers – Nakov @ JS.Talks() 2018
Today I had a talk about cryptography in the JavaScript ecosystem at the js.talks() 2018 conference. I was happy to share my knowledge about most popular crypto algorithms used in the practice. As usually, I recorded a video, that I want to share with you.
Cryptography for JS Devs – Video
Cryptography for JS Devs – Slides
Cryptography for JS Devs – About the Talk
Most developers believe they know cryptography, just because they store their passwords hashed instead of in plaintext and because have once configured SSL. In this talk the speaker fills the gaps by explaining some cryptographic concepts with examples in JavaScript.
The talk covers:
- Hashes, HMAC and key derivation functions (Scrypt, Argon2) with examples in JavaScript
- Encrypting passwords: from plain text to Argon2
- Symmetric encryption at the client-side: AES, block modes, CTR mode, KDF, HMAC, examples in JavaScript (AES-256-CTR-Argon2-HMAC-SHA-256)
- Digital signatures, ECC, ECDSA, EdDSA, secp256k1, ed25519, signing messages, verifying signatures, examples in JavaScript
- Why client-side JavaScript cryptography might not be safe? Man-in-the-browser attacks, Cross-Site Scripting (XSS) / JavaScript injection, etc.
Cryptography for JS Devs – Code Examples
const aes = require("aes-js"); | |
const argon2 = require("argon2"); | |
const crypto = require("crypto"); | |
const cryptoJS = require("crypto-js"); | |
// Encrypt using AES-256-CTR-Argon2-HMAC-SHA-256 | |
async function aes256ctrEncrypt(plaintext, password) { | |
let argon2salt = crypto.randomBytes(16); // 128-bit salt for argon2 | |
let argon2Settings = { type: argon2.argon2di, raw: true, | |
timeCost: 8, memoryCost: 2 ** 15, parallelism: 2, | |
hashLength: 32, salt: argon2salt }; | |
let secretKey = await argon2.hash(password, argon2Settings); | |
console.log("Derived Argon2 encryption key:", secretKey.toString('hex')); | |
let plainTextBytes = aes.utils.utf8.toBytes(plaintext); | |
let aesIV = crypto.randomBytes(16); // 128-bit initial vector (salt) | |
let aesCTR = new aes.ModeOfOperation.ctr(secretKey, new aes.Counter(aesIV)); | |
let ciphertextBytes = aesCTR.encrypt(plainTextBytes); | |
let ciphertextHex = aes.utils.hex.fromBytes(ciphertextBytes); | |
let hmac = cryptoJS.HmacSHA256(plaintext, secretKey.toString('hex')); | |
return { | |
kdf: 'argon2', kdfSettings: { salt: argon2salt.toString('hex') }, | |
cipher: 'aes-256-ctr', cipherSettings: {iv: aesIV.toString('hex') }, | |
ciphertext: ciphertextHex, mac: hmac.toString() | |
} | |
} | |
// Decrypt using AES-256-CTR-Argon2-HMAC-SHA-256 | |
async function aes256ctrDecrypt(encryptedMsg, password) { | |
let saltBytes = Buffer.from(encryptedMsg.kdfSettings.salt, 'hex'); | |
let argon2Settings = { type: argon2.argon2di, raw: true, | |
timeCost: 8, memoryCost: 2 ** 15, parallelism: 2, | |
hashLength: 32, salt: saltBytes }; | |
let secretKey = await argon2.hash(password, argon2Settings); | |
console.log("Derived Argon2 decryption key:", secretKey.toString('hex')); | |
let aesCTR = new aes.ModeOfOperation.ctr(secretKey, | |
new aes.Counter(Buffer.from(encryptedMsg.cipherSettings.iv, 'hex'))); | |
let decryptedBytes = aesCTR.decrypt( | |
Buffer.from(encryptedMsg.ciphertext, 'hex')); | |
let decryptedPlaintext = aes.utils.utf8.fromBytes(decryptedBytes); | |
let hmac = cryptoJS.HmacSHA256(decryptedPlaintext, secretKey.toString('hex')); | |
if (hmac != encryptedMsg.mac) | |
throw new Error('MAC does not match: maybe wrong password'); | |
return decryptedPlaintext; | |
} | |
(async () => { | |
let encryptedMsg = await aes256ctrEncrypt("some text", "pass@123"); | |
console.log("Encrypted msg:", encryptedMsg); | |
let decryptedPlainText = await aes256ctrDecrypt(encryptedMsg, "pass@123"); | |
console.log("Successfully decrypted:", decryptedPlainText); | |
try { | |
await aes256ctrDecrypt(encryptedMsg, "wrong!Pass"); | |
} catch (error) { | |
console.log(error.message); | |
} | |
})(); | |
// Output: | |
// Derived Argon2 encryption key: 2c695b63f7ae8cfc1701910694fb0d087bd30810dcfe1692d15f6d61d27716a8 | |
// Encrypted msg: { kdf: 'argon2', | |
// kdfSettings: { salt: 'e484d60ea0a365c257e0a78a928d130f' }, | |
// cipher: 'aes-256-ctr', | |
// cipherSettings: { iv: '1c2b12fc5e50014cba8751d48299e92f' }, | |
// ciphertext: 'f4128e2de7ab6d5d26', | |
// mac: 'ea9393dec2aab69a6724904eb725846f532d4fb469fdb0ecc75b605128fbe34d' } | |
// Derived Argon2 decryption key: 2c695b63f7ae8cfc1701910694fb0d087bd30810dcfe1692d15f6d61d27716a8 | |
// Successfully decrypted: some text | |
// Derived Argon2 decryption key: fe5daf262e59124021c6ecb3ed915d9c143aca442f5bfec9ea6108a15a5d06be | |
// MAC does not match: maybe wrong password |
argon2 = require("argon2"); | |
(async () => { | |
let settings = {raw: true, type: argon2.argon2di, timeCost: 16, | |
memoryCost: 2 ** 15, parallelism: 2, hashLength: 32, | |
salt: Buffer("some salt")}; | |
let argon2Key = await argon2.hash("password", settings); | |
console.log("Argon2 derive key:", argon2Key.toString('hex')); | |
settings = {type: argon2.argon2di, timeCost: 16, | |
memoryCost: 2 ** 15, parallelism: 2}; // salt will be random | |
let argon2Hash = await argon2.hash("password", settings); | |
console.log("Argon2 hash (random salt):", argon2Hash); | |
console.log("Password 'password' correct?", | |
await argon2.verify(argon2Hash, "password")); | |
console.log("Password 'wrong123' correct?", | |
await argon2.verify(argon2Hash, "wrong123")); | |
})(); | |
// Output: | |
// Argon2 derive key: 1ed3694706b2a49b8031836fd501152c386495f9a669481b74ad30732f55423b | |
// Argon2 hash (random salt): $argon2d$v=19$m=32768,t=16,p=2$Efm/rWF3bxnSlQg6sRw28Q$JLgwVXNMcP3u3P4bBDB1ujERgvxkibmsHPiqO/2FV/I | |
// Password 'password' correct? true | |
// Password 'wrong123' correct? false |
const EC = require('elliptic').ec; | |
const ec = new EC('secp256k1'); // 256-bit curve: `secp256k1` | |
const cryptoJS = require("crypto-js"); | |
// Generate keys | |
let keyPair = ec.genKeyPair(); | |
console.log("Private key (256 bits):", keyPair.getPrivate("hex")); | |
console.log("Public key (512 bits): ", keyPair.getPublic("hex")); | |
console.log("Public key (compressed, 257 bits):", | |
keyPair.getPublic().encodeCompressed("hex")); | |
// Sign message | |
let msg = "Msg to be signed"; | |
let msgHash = cryptoJS.SHA256(msg).toString(); | |
let signature = | |
ec.sign(msgHash, keyPair.getPrivate(), "hex", {canonical: true}); | |
console.log(`(r=${signature.r}, s=${signature.s}, v=${signature.recoveryParam})`); | |
// Verify signature | |
let validSig = ec.verify( | |
msgHash, signature, keyPair.getPublic()); | |
console.log("Signature valid (correct key)?", validSig); | |
let validSigWrongKey = ec.verify( | |
msgHash, signature, ec.genKeyPair().getPublic()); | |
console.log("Signature valid (wrong key)?", validSigWrongKey); | |
let hexToDecimal = (x) => ec.keyFromPrivate(x, "hex") | |
.getPrivate().toString(10); | |
let pubKeyRecovered = ec.recoverPubKey( | |
hexToDecimal(msgHash), signature, | |
signature.recoveryParam, "hex"); | |
console.log("Recovered pubKey:", | |
pubKeyRecovered.encodeCompressed("hex")); | |
let validSigPK = ec.verify( | |
msgHash, signature, pubKeyRecovered); | |
console.log("Signature valid (recovered key)?", validSigPK); | |
// Output: | |
// Private key (256 bits): 166cbcb63f613645df7c773a8a7b76103f329943a1a49c1fe10de694db109557 | |
// Public key (512 bits): 04f59002e6697ec5ef794e6d03c73b23503e2bb43cde9e089468773a4eb9d85efb92a6847fa114077733389fa5a39402ca2a4573ce2a91998b0f6273c767c2ab84 | |
// Public key (compressed, 257 bits): 02f59002e6697ec5ef794e6d03c73b23503e2bb43cde9e089468773a4eb9d85efb | |
// (r=1897062889226951357652590472390874116440953950286792279211170127401533397136, s=22676265354218592780856375379860805109776725255256408551810079880452835078820, v=0) | |
// Signature valid (correct key)? true | |
// Signature valid (wrong key)? false | |
// Recovered pubKey: 02f59002e6697ec5ef794e6d03c73b23503e2bb43cde9e089468773a4eb9d85efb | |
// Signature valid (recovered key)? true |
const EC = require('elliptic').ec; | |
const ec = new EC('ed25519'); // 256-bit curve: `secp256k1` | |
const cryptoJS = require("crypto-js"); | |
// Generate keys | |
let keyPair = ec.genKeyPair(); | |
console.log("Private key (256 bits):", keyPair.getPrivate("hex")); | |
console.log("Public key (compressed, 256 bits):", | |
keyPair.getPublic().encodeCompressed("hex")); | |
// Sign message | |
let msg = "Msg to be signed"; | |
let msgHash = cryptoJS.SHA256(msg).toString(); | |
let signature = | |
ec.sign(msgHash, keyPair.getPrivate(), "hex"); | |
console.log(`(r=${signature.r}, s=${signature.s})`); | |
// Verify signature | |
let validSig = ec.verify( | |
msgHash, signature, keyPair.getPublic()); | |
console.log("Signature valid?", validSig); | |
// Output: | |
// Private key (256 bits): 08a176cb7e38f36679c798d1a24feff766a78a9b5019006d922758b13e684d5e | |
// Public key (compressed, 256 bits): 0305c38d4f5cb96f83eb45224ceac74aed2a19eaf005d54519de77b8681c35a2a6 | |
// (r=488190748947805157450628917774386284742507234334278971094831553541518954229, s=7202979389581348753339390193733324799507876820345609590903975111351095577667) | |
// Signature valid? true |
const cryptoJS = require("crypto-js"); | |
console.log("SHA-256:", cryptoJS.SHA256("hello").toString()); | |
console.log("Keccak-256:", cryptoJS.SHA3("hello", {outputLength: 256}).toString()); | |
console.log("RIPEMD-160:", cryptoJS.RIPEMD160("hello").toString()); | |
console.log("Keccak-512:", cryptoJS.SHA3("hello").toString()); | |
// Output: | |
// SHA-256: 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 | |
// Keccak-256: 1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8 | |
// RIPEMD-160: 108f07b8382412612c048d07d13f814118445acd | |
// Keccak-512: 52fa80662e64c128f8389c9ea6c73d4c02368004bf4463491900d11aaadca39d47de1b01361f207c512cfa79f0f92c3395c67ff7928e3f5ce3e3c852b392f976 |
const cryptoJS = require("crypto-js"); | |
console.log("HMAC-SHA-256:", | |
cryptoJS.HmacSHA256("hello", "key").toString()); | |
// Output: | |
// HMAC-SHA-256: 9307b3b915efb5171ff14d8cb55fbcc798c6c0ef1456d66ded1a6aa723a58b7b |
const cryptoJS = require("crypto-js"); | |
const kdfParams = { keySize: 128 / 32, | |
hasher: cryptoJS.algo.SHA256, iterations: 1000 }; | |
console.log("PBKDF2 (128-bit):", | |
cryptoJS.PBKDF2("password", "salt", kdfParams).toString()); | |
// Output: | |
// PBKDF2 (128-bit): 632c2812e46d4604102ba7618e9d6d7d |
const scrypt = require("scrypt-async"); | |
scrypt('password', 'salt', { | |
N: 16384, // iterations | |
r: 8, // block size | |
p: 1, // parallelism | |
dkLen: 16, // 128-bit key | |
encoding: 'hex' | |
}, console.log); | |
// Output: | |
// 745731af4484f323968969eda289aeee |