Skip to content

Instantly share code, notes, and snippets.

@rfk
Created September 19, 2018 01:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rfk/9a0c6698235de7ac122ffd84895041a8 to your computer and use it in GitHub Desktop.
Save rfk/9a0c6698235de7ac122ffd84895041a8 to your computer and use it in GitHub Desktop.
<html>
<head>
<script src="http://code.jquery.com/jquery-3.2.1.min.js"
integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="
crossorigin="anonymous"></script>
<script type="text/javascript">
// UI code to hook up buttons etc.
$(function() {
var keypair = {}
$("#gen-keypair").on("click", function () {
generateEphemeralKeypair().then(function(k) {
keypair = k
$("#pubkey").val(JSON.stringify(keypair.publicJWK, null, 2))
}, function(error) {
$("#pubkey").val("ERROR: " + error);
})
})
$("#pubkey").on("change", function () {
try {
keypair = {
publicJWK: JSON.parse($("#pubkey").val())
}
} catch (error) {
keypair = {};
}
})
$("#encrypt-message").on("click", function () {
encryptJWE(keypair.publicJWK, $("#message").val()).then(function(jwe) {
$("#jwe").val(jwe);
}, function(error) {
$("#jwe").val("ERROR: " + error);
})
})
$("#decrypt-message").on("click", function () {
if (!keypair.privateJWK) {
$("#message").val("ERROR: private key not available")
} else {
decryptJWE(keypair.privateJWK, $("#jwe").val()).then(function(plaintext) {
$("#message").val(plaintext);
}, function(error) {
$("#message").val("ERROR: " + error);
})
}
})
$("#pubkey").val("");
$("#message").val("");
$("#jwe").val("");
})
// ---- Crypto Stuff Begins Here ---- //
//
// Obviously a real-world app would use a library for much of this,
// like the fine https://github.com/cisco/node-jose/ project.
// But I wanted to do a minimal implementation of my own to ensure
// I understand all the details of the flow, and to ensure we're
// not accidentally depending on some private implementation detail
// of a support library.
const ECDH_PARAMS = {
name: "ECDH",
namedCurve: "P-256",
}
const AES_PARAMS = {
name: "AES-GCM",
length: 256
}
// Makes an ephemeral ECDH keypair, as JWKs.
//
function generateEphemeralKeypair() {
return crypto.subtle.generateKey(ECDH_PARAMS, true, ["deriveKey"]).then(function(keypair) {
return crypto.subtle.exportKey("jwk", keypair.publicKey).then(function(publicJWK) {
return crypto.subtle.exportKey("jwk", keypair.privateKey).then(function(privateJWK) {
delete publicJWK.key_ops;
return {
publicJWK: publicJWK,
privateJWK: privateJWK
}
})
})
})
}
// Encrypt a JWE.
// This is hugely special-cased to the precise JWE format required by FxA.
// Use a library for this in production code, seriously.
//
function encryptJWE(publicJWK, contents) {
// Generate an ephemeral key to use just for this encryption.
return generateEphemeralKeypair().then(function(epk) {
// Do ECDH agreement to get the content encryption key.
return deriveECDHSharedAESKey(epk.privateJWK, publicJWK, ["encrypt"]).then(function(contentKey) {
// Encrypt the JWE contents with the content-key,
// passing the JWE header as additional authenticated data.
var iv = crypto.getRandomValues(new Uint8Array(12))
var protectedHeader = str2buf(b64Encode(JSON.stringify({
alg: 'ECDH-ES',
enc: 'A256GCM',
epk: epk.publicJWK
})))
return crypto.subtle.encrypt({
name: "AES-GCM",
iv: iv,
additionalData: protectedHeader,
tagLength: 128
}, contentKey, str2buf(contents)).then(function(ciphertextWithTag) {
ciphertextWithTag = new Uint8Array(ciphertextWithTag)
var ciphertext = ciphertextWithTag.slice(0, -128 / 8)
var tag = ciphertextWithTag.slice(-128 / 8)
return serializeJWE(protectedHeader, new Uint8Array(0), iv, ciphertext, tag)
})
})
})
}
// Decrypt a JWE.
// This is hugely special-cased to the precise JWE format produced by FxA.
// Use a library for this in production code, seriously.
//
function decryptJWE(privateJWK, jwe) {
jwe = parseJWE(jwe)
if (jwe.header.alg !== 'ECDH-ES') { throw new Error('unexpected jwe alg') }
if (jwe.header.enc !== 'A256GCM') { throw new Error('unexpected jwe alg') }
if (jwe.header.epk.kty !== 'EC') { throw new Error('unexpected jwe epk.kty') }
if (jwe.header.epk.crv !== 'P-256') { throw new Error('unexpected jwe epk.crv') }
if (jwe.contentKey.length !== 0) { throw new Error('unexpected jwe contentKey') }
// Do ECDH agreement to get the content encryption key.
return deriveECDHSharedAESKey(privateJWK, jwe.header.epk, ["decrypt"]).then(function(contentKey) {
// Decrypt the JWE contents with the content-key,
// authenticating the JWE header in the process.
var ciphertextWithTag = new Uint8Array(jwe.ciphertext.length + jwe.tag.length)
ciphertextWithTag.set(jwe.ciphertext)
ciphertextWithTag.set(jwe.tag, jwe.ciphertext.length)
return crypto.subtle.decrypt({
name: "AES-GCM",
iv: jwe.iv,
additionalData: jwe.protectedHeader,
tagLength: 128
}, contentKey, ciphertextWithTag).then(function(plaintext) {
return buf2str(new Uint8Array(plaintext))
})
})
}
function parseJWE(jwe) {
let jweParts = jwe.split(".")
if (jweParts.length !== 5) { throw new Error('invalid JWE') }
return {
protectedHeader: str2buf(jweParts[0]),
header: JSON.parse(b64Decode(jweParts[0])),
contentKey: str2buf(b64Decode(jweParts[1])),
iv: str2buf(b64Decode(jweParts[2])),
ciphertext: str2buf(b64Decode(jweParts[3])),
tag: str2buf(b64Decode(jweParts[4]))
}
}
function serializeJWE(protectedHeader, contentKey, iv, ciphertext, tag) {
return [
buf2str(protectedHeader),
b64Encode(contentKey),
b64Encode(iv),
b64Encode(ciphertext),
b64Encode(tag)
].join(".")
}
// Do ECDH agreement between a public and private key,
// returning the derived encryption key as specced by
// JWA RFC. The spec includes a very precise way to
// derive a purpose-specific key from the raw ECDH secret.
//
function deriveECDHSharedAESKey(privateJWK, publicJWK, operations) {
return Promise.resolve().then(function() {
// Import the two keys and do raw ECDH agreement.
return crypto.subtle.importKey("jwk", privateJWK, ECDH_PARAMS, false, ["deriveKey"]).then(function(privateKey) {
return crypto.subtle.importKey("jwk", publicJWK, ECDH_PARAMS, false, ["deriveKey"]).then(function(publicKey) {
var params = Object.assign({}, ECDH_PARAMS, { public: publicKey })
return crypto.subtle.deriveKey(params, privateKey, AES_PARAMS, true, operations);
})
})
}).then(function(sharedKey) {
// We can't use the raw shared secret from the ECDH agreement,
// we have to hash the bytes into a purpose-specific key.
// This is the NIST Concat KDF specialized to a specific set of parameters,
// which basically turn it into a single application of SHA256.
// The details are from the JWA RFC.
return crypto.subtle.exportKey("raw", sharedKey).then(function(sharedKeyBytes) {
sharedKeyBytes = new Uint8Array(sharedKeyBytes);
var info = [
"\x00\x00\x00\x07A256GCM", // 7-byte algorithm identifier
"\x00\x00\x00\x00", // empty PartyUInfo
"\x00\x00\x00\x00", // empty PartyVInfo
"\x00\x00\x01\x00" // keylen == 256
].join("")
return sha256("\x00\x00\x00\x01" + buf2str(sharedKeyBytes) + info)
})
}).then(function (derivedKeyBytes) {
// Re-import the derived bytes so we can use them as live keys for encryption/decryption.
return crypto.subtle.importKey("raw", derivedKeyBytes, AES_PARAMS, false, operations)
})
}
function randomString(len) {
let buf = new Uint8Array(len)
return b64Encode(crypto.getRandomValues(buf)).substr(0, len)
}
function b64Encode(str) {
if (typeof str !== 'string') {
str = buf2str(str)
}
return btoa(str)
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "")
}
function b64Decode(str) {
if (typeof str !== 'string') {
str = buf2str(str)
}
return atob(str.replace(/-/g, "+").replace(/_/g, "/"))
}
function sha256(buf) {
if (typeof buf === 'string') {
buf = str2buf(buf)
}
return crypto.subtle.digest({ name: "SHA-256" }, buf).then(function(hash) {
return new Uint8Array(hash)
})
}
function str2buf(str) {
return Uint8Array.from(Array.prototype.map.call(str, function (c) { return c.charCodeAt(0) }))
}
function buf2str(buf) {
return String.fromCharCode.apply(null, buf)
}
</script>
</head>
<body>
<p>Public JWK:</p>
<textarea id="pubkey" style="width: 40em; height: 10em;"></textarea>
<input type="button" value="Generate Keypair" id="gen-keypair"/>
<p>Secret Message:</p>
<textarea id="message" style="width: 40em; height: 10em;"></textarea>
<input type="button" value="Encrypt Message" id="encrypt-message"/>
<p>JWE:</p>
<textarea id="jwe" style="width: 40em; height: 10em;"></textarea>
<input type="button" value="Decrypt Message" id="decrypt-message"/>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment