Web Crypto API

Sung
Monomax Software
https://monomax.sh

Web Crypto API

  1. Why does it matter for node.js?
  2. Web Crypto API Examples

1. Why does it matter for node.js?

Web Crypto API

A standardized JavaScript interface for accessing cryptographic primitives

W3C published a recommendation in January 2017

All major browsers implement it

Browser crypto implementations

  • Firefox - NSS (Network Security Services)
  • Chrome - BoringSSL

An interface for the underlying implementations

window.crypto.subtle.generateKey(
    {
        name: "HMAC",
        hash: {name: "SHA-256"},
    },
    false,
    ["sign", "verify"]
)
.then(function(key){
    console.log(key);
})
.catch(function(err){
    console.error(err);
});
window.crypto.subtle.sign(
    {
        name: "HMAC",
    },
    key,
    data // ArrayBuffer of data you want to sign
)
.then(function(signature){
    //an ArrayBuffer containing the signature
    console.log(new Uint8Array(signature));
})
.catch(function(err){
    console.error(err);
});

Node.js has its own crypto API

crypto module

  • first documented in v0.1.94 release (August 2011)
  • relied on the host machine's OpenSSL library.
var secret = '/* some secret string */';

hmac = crypto.createHmac('sha-256', secret);
hmac.update(data);
hash = hmac.digest('hex');

console.log("result:", hash);

Problem

Harder to write code that targets both browsers and non-browsers

Possible solution #1

Write shims or wrappers

Abstraction is where bugs live

  • It is hard to audit and maintain shim and polyfills
  • Should never be users' responsibility.

Possible solution #2

Use polyfill or npm modules

#1 rule of cryptography

NEVER EVER implement your own cryptography.

Why not polyfill or package?

  • Users should NEVER EVER implement crypto
  • packages can be compromised
  • Native modules face compilation issues
  • Pure-JavaScript implementation would be slow

Should node.js implement Web Crypto API?

Trade-off

Favor

  • Write once to target browsers and non-browsers
  • Rely on the standard library
    • No package to be compromised
    • No native module cross platform issues
    • Easier security audit

Favor - counter-arguments

  • How often do we have to write universal crypto code?
  • Core does not mean bug-free

Against

  • Can't just add stuff to core for convenience
  • Duplicate functionalities
  • Deprecation of crypto

Against - counter-arguments

  • It's more than convenience
  • Software evolve all the time

2. Web Crypto API Examples

Dnote

An open source, encrypted notebook that respects your privacy

https://dnote.io

Design goal

  • Clients can write encrypted notes on the server
  • Server cannot decrypt the notes

My solution (registration)

  • User enters email and password
  • Derive k <- pbkdf2(password: password, salt: email, alg: 'sha256', iteration: 10000)
  • Derive k0 <- hkdf(alg: 'sha256', secret: k, salt: email, info: 'enc') (used as encryption key)
  • Derive k1 <- hkdf(alg: 'sha256', secret: k, salt: email, info: 'auth') (sent to server for authentication)
  • POST to '/register': (email, k1, kdf_iteration)
  • Generate salt <- 256bit random string
  • Derive k2 <- pbkdf2(password: k1, salt: salt, alg: 'sha256', iteration: 20000)
  • Create a user with (hashed_password, salt) = (k2, salt)

Crash course in cryptography

Key-derivation function

A function to derive multiple keys from a single source key

Pseudo-random function

Defined over space (K, X, Y)

K x X -> Y such that there exists "efficient" algorithm to evaluate F(k,x)

Problem with PRF

  • Source key is not uniform (e.g. passwords are often dictionaries)
  • PRF is not psuedo-random if key is not uniform

Extract-then-expand

  1. Extract: extract a psuedo-random key, k, from the source key
  2. Expand: Expand k using the pseudo-random function

HKDF (KDF from HMAC)

  • Extract: k <- HMAC(salt, SK)
  • Expand using HMAC as PRF using k

PBKDF (Password-based KDF)

  • Defends against dictionary attack
  • Salt, slow hash function
  • bcrypt, scrypt, argon, ...

Registering on dnote.io

  • User enters email and password
  • Derive k <- pbkdf2(password: password, salt: email, alg: 'sha256', iteration: 10000)
  • Derive k0 <- hkdf(alg: 'sha256', secret: k, salt: email, info: 'enc') (used as encryption key)
  • Derive k1 <- hkdf(alg: 'sha256', secret: k, salt: email, info: 'auth') (sent to server for authentication)
  • POST to '/register': (email, k1, kdf_iteration)

  • Generate salt <- 256bit random string
  • Derive k2 <- pbkdf2(password: k1, salt: salt, alg: 'sha256', iteration: 20000)
  • Create a user with (hashed_password, salt) = (k2, salt)
function pbkdf2(secret, salt, iterations) {
  const secretBuf = strToBuffer(secret);
  const saltBuf = strToBuffer(salt);

  const key = await window.crypto.subtle.importKey(
    'raw',
    secretBuf,
    { name: 'PBKDF2' },
    false,
    ['deriveBits']
  );

  return window.crypto.subtle.deriveBits(
    {
      name: 'PBKDF2',
      salt: saltBuf,
      iterations,
      hash: { name: SHA256 }
    },
    key,
    256
  );
}
function strToBuffer(str) {
  // convert to utf8 encoding
  const strUtf8 = unescape(encodeURIComponent(str));

  const buf = new ArrayBuffer(strUtf8.length);
  const bufView = new Uint8Array(buf);
  for (let i = 0; i < strUtf8.length; i++) {
    bufView[i] = strUtf8.charCodeAt(i);
  }

  return buf;
}

What is ArrayBuffer?

Fixed-size raw binary data buffer

// create an ArrayBuffer with a size in bytes
var buffer = new ArrayBuffer(8);

console.log(buffer.byteLength);
// expected output: 8

Viewed and manipulated using a mask

function bufToB64(buf) {
  let binary = '';
  const bytes = new Uint8Array(buf);
  const len = bytes.byteLength;

  for (let i = 0; i < len; i++) {
    binary += String.fromCharCode(bytes[i]);
  }

  return window.btoa(binary);
}
function pbkdf2(secret, salt, iterations) {
  const secretBuf = strToBuffer(secret);
  const saltBuf = strToBuffer(salt);

  const key = await window.crypto.subtle.importKey(
    'raw',
    secretBuf,
    { name: 'PBKDF2' },
    false,
    ['deriveBits']
  );

  return window.crypto.subtle.deriveBits(
    {
      name: 'PBKDF2',
      salt: saltBuf,
      iterations,
      hash: { name: SHA256 }
    },
    key,
    256
  );
}
async function hkdf(secret, salt, info, algorithm, dkLen) {
  let secretBuf;
  if (typeof secret === 'string') {
    secretBuf = strToBuffer(secret);
  } else {
    secretBuf = secret;
  }

  const saltBuf = strToBuffer(salt);
  const infoBuf = strToBuffer(info);

  const key = await window.crypto.subtle.importKey(
    'raw',
    secretBuf,
    {
      name: 'HKDF'
    },
    false,
    ['deriveBits']
  );

  return window.crypto.subtle.deriveBits(
    {
      name: HKDF,
      hash: algorithm,
      salt: saltBuf,
      info: infoBuf
    },
    key,
    dkLen
  );
}
async function makeKeys(email, password, iteration) {
  const masterKey = await pbkdf2(password, email, iteration);
  const encKeyBits = await hkdf(
    masterKey, email, 'enc', SHA256, 256
  );
  const authKeyBits = await hkdf(
    masterKey, email, 'auth', SHA256, 256
  );

  return {
    encKey: bufToB64(encKeyBits),
    authKey: bufToB64(authKeyBits)
  };
}

...And more

symmetric block cipher using AES256 in GCM mode

Open source

https://dnote.io

Wrap up - Web Crypto API

  1. Why does it matter for node.js?
  2. Web Crypto API Examples

Conclusion

  • Node.js has crypto module
  • Browsers have Web Crypto API
  • Should node.js implement Web Crypto API? Depends.

Thanks

Sung
Monomax Software
sung@monomax.sh
https://monomax.sh