Web Crypto

Demystifying cryptographic operations in the browser

Robert Kawecki (speaker)

Robert Kawecki

"Web Crypto: demystifying cryptographic operations in the browser"

2020-02-12

github.com/rkaw92

This presentation is interactive

A copy of these slides can be found at:

webcrypto.pl

Alternate link: https://rkaw92.github.io/warsawjs-web-crypto-api/

Copy the code samples and paste them into DevTools.

Security tools...

Web threat model

When to use Web Crypto

When not to use Web Crypto

CLI: key generation


# Generate private key PEM
openssl genrsa -out private.pem 4096
# Derive SPKI PEM
openssl rsa -in private.pem \
    -pubout -out public.pem -outform PEM
# Convert to PKCS#8 for Web Crypto
openssl pkcs8 -inform PEM -outform PEM \
    -in private.pem -out private-pkcs8.pem \
    -nocrypt -topk8
		

Encrypting a message (PKI)

Encrypting a message (PKI) - code


const encKeyDER = extractDER(publicPEM, 'public');
const encKey = await window.crypto.subtle.importKey(
    'spki',
    encKeyDER,
    { name: 'RSA-OAEP', hash: 'SHA-256' },
    false,
    [ 'encrypt' ]
);
const cryptotext = await window.crypto.subtle.encrypt(
    { name: 'RSA-OAEP' },
    encKey,
    (new TextEncoder()).encode('Hello, world!')
);
		

Receiving a message (PKI)

Receiving a message (PKI) - code


const decKeyDER = extractDER(privatePEM, 'private');
const decKey = await window.crypto.subtle.importKey(
    'pkcs8', // Make sure it's really PKCS#8
    decKeyDER,
    { name: 'RSA-OAEP', hash: 'SHA-256' },
    false,
    [ 'decrypt' ]
);
const plaintext = await window.crypto.subtle.decrypt(
    { name: 'RSA-OAEP' },
    decKey,
    cryptotext
);
console.log((new TextDecoder()).decode(plaintext));
		

CLI: decrypting RSA message


openssl pkeyutl -inkey keys/private.pem -in encrypted.raw \
    -pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256 \
    -decrypt
		

Note: openssl rsautl is deprecated and will not work for OAEP.

Big messages / key wrapping

Key wrapping - code 1/2


const messageKey = await window.crypto.subtle.generateKey(
    { name: 'AES-GCM', length: 256 },
    true, // extractable - makes it wrappable
    [ 'encrypt' ]
);
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const cryptotext2 = await window.crypto.subtle.encrypt(
    { name: 'AES-GCM', iv: iv },
    messageKey,
    (new TextEncoder()).encode('Hello, GCM!')
);
		

Key wrapping - code 2/2


const wrappingKey = await window.crypto.subtle.importKey(
    'spki',
    encKeyDER, // recipient's public key
    { name: 'RSA-OAEP', hash: 'SHA-256' },
    false,
    [ 'wrapKey' ]
);
const wrappedMessageKey = await crypto.subtle.wrapKey(
    'raw', // format - we pick the smallest one
    messageKey,
    wrappingKey,
    { name: 'RSA-OAEP' }
);
		

Key unwrapping - code 1/2


const unwrappingKey = await window.crypto.subtle.importKey(
    'pkcs8',
    decKeyDER, // recipient's private RSA key
    { name: 'RSA-OAEP', hash: 'SHA-256' },
    false,
    [ 'unwrapKey' ]
);
const unwrappedMessageKey = await crypto.subtle.unwrapKey(
    'raw',
    wrappedMessageKey,
    unwrappingKey,
    { name: 'RSA-OAEP' },
    { name: 'AES-GCM' },
    false, // no need for extractable
    [ 'decrypt' ]
);
		

Key unwrapping - code 2/2


const plaintext2 = await window.crypto.subtle.decrypt(
    { name: 'AES-GCM', iv: iv }, // remember to send IV!
    unwrappedMessageKey,
    cryptotext2
);
console.log((new TextDecoder()).decode(plaintext2));
		

Key unwrapping in Node.js - code


const payload = buf.slice(0, buf.byteLength - 16);
const authTag = buf.slice(-16);
const unwrappedMessageKey = crypto.privateDecrypt({
    key: privateKeyRaw,
    padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
    oaepHash: 'sha256'
});
const decipher = crypto.createDecipheriv(
    'aes-256-gcm',
    unwrappedMessageKey,
    iv
);
decipher.setAuthTag(authTag);
const buf1 = decipher.update(content);
// Always need to call .final() to check the auth tag:
const buf2 = decipher.final();
const decryptedContent = Buffer.concat([ buf1, buf2 ]);
		

Signing - code


const signingKeyDER = extractDER(privatePEM, 'private');
const signingKey = await window.crypto.subtle.importKey(
    'pkcs8', // Make sure it's really PKCS#8
    signingKeyDER,
    { name: 'RSA-PSS', hash: 'SHA-256' },
    false,
    [ 'sign' ]
);
const dataToSign = (new TextEncoder()).encode('Hello, sigs!');
const textSignature = await window.crypto.subtle.sign(
    { name: 'RSA-PSS', saltLength: 32 },
    signingKey,
    dataToSign
);
		

Verifying - code


const verifyKeyDER = extractDER(publicPEM, 'public');
const verifyKey = await window.crypto.subtle.importKey(
    'spki',
    verifyKeyDER,
    { name: 'RSA-PSS', hash: 'SHA-256' },
    false,
    [ 'verify' ]
);
const isLegitimate = await window.crypto.subtle.verify(
    { name: 'RSA-PSS', saltLength: 32 },
    verifyKey,
    textSignature,
    dataToSign // ArrayBuffer with "Hello, signatures!"
);
		

Addendum: sign + encrypt

Key takeaways

Questions?