Last active
June 5, 2020 01:34
-
-
Save kaizhu256/229bb3b3b8dc39154369d083134723b1 to your computer and use it in GitHub Desktop.
Example JWE Using AES Key Wrap and AES_128_CBC_HMAC_SHA_256
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// https://tools.ietf.org/html/rfc7516#appendix-A.3 | |
(async function () { | |
"use strict"; | |
let aad; | |
let aal; | |
let assertJsonEqual; | |
let base64urlFromBuffer; | |
let base64urlToBuffer; | |
let cek; | |
let ciphertext; | |
let crypto; | |
let enc_key; | |
let header; | |
let ii; | |
let iv; | |
let jj; | |
let jwe; | |
let jwk; | |
let mac_key; | |
let plaintext; | |
let tag; | |
globalThis.debugInline = function (...argList) { | |
/* | |
* this function will both print <argList> to stderr | |
* and return <argList>[0] | |
*/ | |
console.error("\n\ndebugInline"); | |
console.error(...argList); | |
console.error("\n"); | |
return argList[0]; | |
}; | |
assertJsonEqual = function (aa, bb) { | |
/* | |
* this function will assert JSON.stringify(<aa>) === JSON.stringify(<bb>) | |
*/ | |
aa = JSON.stringify(aa); | |
bb = JSON.stringify(bb); | |
if (aa !== bb) { | |
throw new Error(JSON.stringify(aa) + " !== " + JSON.stringify(bb)); | |
} | |
}; | |
base64urlFromBuffer = function (buf) { | |
let base64url; | |
base64url = ""; | |
ii = 0; | |
while (ii < buf.byteLength) { | |
base64url += String.fromCharCode(buf[ii]); | |
ii += 1; | |
} | |
return globalThis.btoa(base64url).replace(( | |
/\+/g | |
), "-").replace(( | |
/\//g | |
), "_").replace(( | |
/\=*?$/ | |
), ""); | |
}; | |
base64urlToBuffer = function (base64url) { | |
return Uint8Array.from(globalThis.atob(base64url.replace(( | |
/-/g | |
), "+").replace(( | |
/_/g | |
), "/").replace(( | |
/\=*?$/ | |
), "")), function (chr) { | |
return chr.charCodeAt(0); | |
}); | |
}; | |
crypto = globalThis.crypto; | |
// https://tools.ietf.org/html/rfc7516#appendix-A.3 | |
// A.3. Example JWE Using AES Key Wrap and AES_128_CBC_HMAC_SHA_256 | |
plaintext = "Live long and prosper."; | |
plaintext = await new TextEncoder().encode(plaintext); | |
assertJsonEqual(Array.from(plaintext), [ | |
76, 105, 118, 101, 32, 108, 111, 110, 103, 32, 97, 110, 100, 32, | |
112, 114, 111, 115, 112, 101, 114, 46 | |
]); | |
// A.3.1. JOSE Header | |
header = "{\"alg\":\"A128KW\",\"enc\":\"A128CBC-HS256\"}"; | |
header = globalThis.btoa(header).replace(( | |
/\+/g | |
), "-").replace(( | |
/\//g | |
), "_").replace(( | |
/\=*?$/ | |
), ""); | |
assertJsonEqual( | |
header, | |
"eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0" | |
); | |
// A.3.2. Content Encryption Key (CEK) | |
cek = Uint8Array.from([ | |
4, 211, 31, 197, 84, 157, 252, 254, 11, 100, 157, 250, 63, 170, 106, | |
206, 107, 124, 212, 45, 111, 107, 9, 219, 200, 177, 0, 240, 143, 156, | |
44, 207 | |
]); | |
mac_key = cek.slice(0, 16); | |
enc_key = cek.slice(16); | |
cek = base64urlFromBuffer(cek); | |
cek = { | |
"k": cek, | |
"kty": "oct" | |
}; | |
assertJsonEqual(cek, { | |
"k": "BNMfxVSd_P4LZJ36P6pqzmt81C1vawnbyLEA8I-cLM8", | |
"kty": "oct" | |
}); | |
// A.3.3. Key Encryption | |
jwk = { | |
"alg": "A128KW", | |
"k": "GawgguFyGrWKav7AX4VKUg", | |
"kty": "oct" | |
}; | |
jwk = await crypto.subtle.importKey("jwk", jwk, { | |
name: "AES-KW" | |
}, false, [ | |
"unwrapKey", "wrapKey" | |
]); | |
cek = await crypto.subtle.importKey("jwk", cek, { | |
name: "AES-CBC" | |
}, true, [ | |
"encrypt" | |
]); | |
cek = await crypto.subtle.wrapKey("raw", cek, jwk, "AES-KW"); | |
cek = base64urlFromBuffer(new Uint8Array(cek)); | |
assertJsonEqual( | |
cek, | |
"6KB707dM9YTIgHtLvtgWQ8mKwboJW3of9locizkDTHzBC2IlrT1oOQ" | |
); | |
// A.3.4. Initialization Vector | |
iv = base64urlToBuffer("AxY8DCtDaGlsbGljb3RoZQ"); | |
assertJsonEqual(Array.from(iv), [ | |
3, 22, 60, 12, 43, 67, 104, 105, 108, 108, 105, 99, 111, 116, 104, | |
101 | |
]); | |
// A.3.5. Additional Authenticated Data | |
aad = new TextEncoder().encode(header); | |
assertJsonEqual(Array.from(aad), [ | |
101, 121, 74, 104, 98, 71, 99, 105, 79, 105, 74, 66, 77, 84, 73, 52, | |
83, 49, 99, 105, 76, 67, 74, 108, 98, 109, 77, 105, 79, 105, 74, 66, | |
77, 84, 73, 52, 81, 48, 74, 68, 76, 85, 104, 84, 77, 106, 85, 50, 73, | |
110, 48 | |
]); | |
// A.3.6. Content Encryption | |
// Appendix B. Example AES_128_CBC_HMAC_SHA_256 Computation | |
// B.1. Extract MAC_KEY and ENC_KEY from Key | |
assertJsonEqual(Array.from(mac_key), [ | |
4, 211, 31, 197, 84, 157, 252, 254, 11, 100, 157, 250, 63, 170, 106, | |
206 | |
]); | |
assertJsonEqual(Array.from(enc_key), [ | |
107, 124, 212, 45, 111, 107, 9, 219, 200, 177, 0, 240, 143, 156, 44, | |
207 | |
]); | |
// B.2. Encrypt Plaintext to Create Ciphertext | |
enc_key = await crypto.subtle.importKey("raw", enc_key, "AES-CBC", true, [ | |
"encrypt" | |
]); | |
ciphertext = new Uint8Array(await crypto.subtle.encrypt({ | |
iv, | |
name: "AES-CBC" | |
}, enc_key, plaintext)); | |
assertJsonEqual(Array.from(ciphertext), [ | |
40, 57, 83, 181, 119, 33, 133, 148, 198, 185, 243, 24, 152, 230, 6, | |
75, 129, 223, 127, 19, 210, 82, 183, 230, 168, 33, 215, 104, 143, | |
112, 56, 102 | |
]); | |
// B.3. 64-Bit Big-Endian Representation of AAD Length | |
aal = Uint8Array.from([ | |
0, 0, 0, 0, | |
(aad.length >>> 21) & 0xff, | |
(aad.length >> 13) & 0xff, | |
(aad.length >> 5) & 0xff, | |
(8 * aad.length) & 0xff | |
]); | |
assertJsonEqual(Array.from(aal), [ | |
0, 0, 0, 0, 0, 0, 1, 152 | |
]); | |
// B.4. Initialization Vector Value | |
assertJsonEqual(Array.from(iv), [ | |
3, 22, 60, 12, 43, 67, 104, 105, 108, 108, 105, 99, 111, 116, 104, | |
101 | |
]); | |
// B.5. Create Input to HMAC Computation | |
tag = new Uint8Array(aad.length + iv.length + ciphertext.length + 8); | |
ii = 0; | |
[ | |
aad, iv, ciphertext, [ | |
// 64-bit length of aad | |
0, 0, 0, 0, | |
(aad.length >>> 21) & 0xff, | |
(aad.length >> 13) & 0xff, | |
(aad.length >> 5) & 0xff, | |
(8 * aad.length) & 0xff | |
] | |
].forEach(function (elem) { | |
jj = 0; | |
while (jj < elem.length) { | |
tag[ii] = elem[jj]; | |
ii += 1; | |
jj += 1; | |
} | |
}); | |
assertJsonEqual(Array.from(tag), [ | |
101, 121, 74, 104, 98, 71, 99, 105, 79, 105, 74, 66, 77, 84, 73, 52, | |
83, 49, 99, 105, 76, 67, 74, 108, 98, 109, 77, 105, 79, 105, 74, 66, | |
77, 84, 73, 52, 81, 48, 74, 68, 76, 85, 104, 84, 77, 106, 85, 50, 73, | |
110, 48, 3, 22, 60, 12, 43, 67, 104, 105, 108, 108, 105, 99, 111, | |
116, 104, 101, 40, 57, 83, 181, 119, 33, 133, 148, 198, 185, 243, 24, | |
152, 230, 6, 75, 129, 223, 127, 19, 210, 82, 183, 230, 168, 33, 215, | |
104, 143, 112, 56, 102, 0, 0, 0, 0, 0, 0, 1, 152 | |
]); | |
// B.6. Compute HMAC Value | |
mac_key = await crypto.subtle.importKey("raw", mac_key, { | |
hash: "SHA-256", | |
name: "HMAC" | |
}, false, [ | |
"sign" | |
]); | |
tag = new Uint8Array(await crypto.subtle.sign({ | |
name: "HMAC" | |
}, mac_key, tag)); | |
assertJsonEqual(Array.from(tag), [ | |
83, 73, 191, 98, 104, 205, 211, 128, 201, 189, 199, 133, 32, 38, | |
194, 85, 9, 84, 229, 201, 219, 135, 44, 252, 145, 102, 179, 140, 105, | |
86, 229, 116 | |
]); | |
// B.7. Truncate HMAC Value to Create Authentication Tag | |
tag = tag.slice(0, 16); | |
assertJsonEqual(Array.from(tag), [ | |
83, 73, 191, 98, 104, 205, 211, 128, 201, 189, 199, 133, 32, 38, | |
194, 85 | |
]); | |
tag = base64urlFromBuffer(tag); | |
ciphertext = base64urlFromBuffer(ciphertext); | |
assertJsonEqual({ | |
ciphertext, | |
tag | |
}, { | |
ciphertext: "KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY", | |
tag: "U0m_YmjN04DJvceFICbCVQ" | |
}); | |
// A.3.7. Complete Representation | |
jwe = ( | |
header + "." | |
+ cek + "." | |
+ base64urlFromBuffer(iv) + "." | |
+ ciphertext + "." | |
+ tag | |
); | |
assertJsonEqual(jwe, ( | |
// header | |
"eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0." | |
// cek | |
+ "6KB707dM9YTIgHtLvtgWQ8mKwboJW3of9locizkDTHzBC2IlrT1oOQ." | |
// iv | |
+ "AxY8DCtDaGlsbGljb3RoZQ." | |
// ciphertext | |
+ "KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY." | |
// tag | |
+ "U0m_YmjN04DJvceFICbCVQ" | |
)); | |
console.error("jwe - " + jwe); | |
// A.3.8. Validation | |
[ | |
header, cek, iv, ciphertext | |
] = jwe.split("."); | |
header = new TextEncoder().encode(header); | |
cek = await crypto.subtle.unwrapKey("raw", base64urlToBuffer( | |
cek | |
), jwk, "AES-KW", "AES-CBC", true, [ | |
"decrypt" | |
]); | |
cek = new Uint8Array(await crypto.subtle.exportKey("raw", cek)); | |
iv = base64urlToBuffer(iv); | |
ciphertext = base64urlToBuffer(ciphertext); | |
// B.5. Create Input to HMAC Computation | |
tag = new Uint8Array(header.length + iv.length + ciphertext.length + 8); | |
ii = 0; | |
[ | |
header, iv, ciphertext, [ | |
// 64-bit length of header | |
0, 0, 0, 0, | |
(header.length >>> 21) & 0xff, | |
(header.length >> 13) & 0xff, | |
(header.length >> 5) & 0xff, | |
(header.length << 3) & 0xff | |
] | |
].forEach(function (elem) { | |
jj = 0; | |
while (jj < elem.length) { | |
tag[ii] = elem[jj]; | |
ii += 1; | |
jj += 1; | |
} | |
}); | |
// B.6. Compute HMAC Value | |
jwk = await crypto.subtle.importKey("raw", cek.slice(0, 16), { | |
hash: "SHA-256", | |
name: "HMAC" | |
}, false, [ | |
"sign" | |
]); | |
tag = new Uint8Array(await crypto.subtle.sign({ | |
name: "HMAC" | |
}, jwk, tag)); | |
// B.7. Truncate HMAC Value to Create Authentication Tag | |
tag = tag.slice(0, 16); | |
tag = base64urlFromBuffer(tag); | |
assertJsonEqual(tag, "U0m_YmjN04DJvceFICbCVQ"); | |
// decrypt | |
jwk = await crypto.subtle.importKey("raw", cek.slice(16), "AES-CBC", true, [ | |
"decrypt" | |
]); | |
plaintext = await crypto.subtle.decrypt({ | |
iv, | |
name: "AES-CBC" | |
}, jwk, ciphertext); | |
plaintext = new TextDecoder().decode(plaintext); | |
assertJsonEqual(plaintext, "Live long and prosper."); | |
console.error("plaintext - " + plaintext); | |
}()); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment