Demystifying cryptographic operations in the browser
A copy of these slides can be found at:
Alternate link: https://rkaw92.github.io/warsawjs-web-crypto-api/
Copy the code samples and paste them into DevTools.
# 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
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!')
);
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));
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.
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!')
);
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' }
);
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' ]
);
const plaintext2 = await window.crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: iv }, // remember to send IV!
unwrappedMessageKey,
cryptotext2
);
console.log((new TextDecoder()).decode(plaintext2));
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 ]);
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
);
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!"
);