Menu

Nakov.com logo

Thoughts on Software Engineering

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

Comments (0)

RSS feed for comments on this post. TrackBack URL

LEAVE A COMMENT