From 95b3238c64924c83c403d431b2b6ea042c557797 Mon Sep 17 00:00:00 2001 From: Alok Menghrajani Date: Fri, 6 Feb 2015 11:30:29 -0800 Subject: [PATCH] Adds partial support for user defined headers. This is partial support because we don't fetch jku keys or zip/deflate the plaintext. --- README.md | 10 +-- dist/jose-jwe.js | 140 +++++++++++++++++++------------- dist/jose-jwe.min.js | 2 +- examples/jose-jwe-example1.html | 7 +- examples/jose-jwe-example2.html | 6 +- lib/jose-jwe-decrypt.js | 64 ++++++++------- lib/jose-jwe-encrypt.js | 76 ++++++++++------- package.json | 2 +- test/jose-jwe-test.html | 73 ++++++++++++----- 9 files changed, 233 insertions(+), 147 deletions(-) diff --git a/README.md b/README.md index 924abb6..b0445db 100644 --- a/README.md +++ b/README.md @@ -7,15 +7,15 @@ JavaScript library to encrypt/decrypt data in JSON Web Encryption (JWE) format. This library is designed to work in the browser (tested in Chrome 38). It can do RSA-based public/private crypto as well as shared key. -Extra headers aren't yet implemented. Some headers can be trivially supported -('jku', jwk', kid', 'x5u', 'x5c', 'x5t', 'x5t#S256', 'typ', 'cty', 'crit') by -exposing a setHeaders() function. The zip header might be more challenging. -Interpreting the headers at decryption time can be hard, which is why I punted -on them for now. +JWE is an encapsulation format which makes it easy to share ciphertext between +different platforms: data encrypted in a browser can be decrypted in Go, Java, +etc. The library uses compact representation. There is therefore no support for multiple recipients. It should be easy to add that if needed. +The library partially supports extra headers. + The library uses the Web Crypto API, which is available in recent browsers (http://caniuse.com/#feat=cryptographyexists). As of Nov 2014, it seems ~50% of users have some form of Web Crypto support. diff --git a/dist/jose-jwe.js b/dist/jose-jwe.js index c5a4e4a..e07571b 100644 --- a/dist/jose-jwe.js +++ b/dist/jose-jwe.js @@ -725,28 +725,41 @@ Utils.Base64Url.decodeArray = function(str) { */ /** - * Performs encryption + * Handles encryption. * - * @param key_promise Promise, either RSA or shared key - * @param plain_text string to encrypt - * @return Promise + * @param cryptographer an instance of WebCryptographer (or equivalent). + * @param key_promise Promise, either RSA or shared key */ -JoseJWE.encrypt = function(cryptographer, key_promise, plain_text) { - /** - * Encrypts the CEK - * - * @param key_promise Promise - * @param cek_promise Promise - * @return Promise - */ - var encryptCek = function(key_promise, cek_promise) { - return Promise.all([key_promise, cek_promise]).then(function(all) { - var key = all[0]; - var cek = all[1]; - return cryptographer.wrapCek(cek, key); - }); - }; +JoseJWE.Encrypter = function(cryptographer, key_promise) { + this.cryptographer = cryptographer; + this.key_promise = key_promise; + this.userHeaders = {}; +}; +/** + * Adds a key/value pair which will be included in the header. + * + * The data lives in plaintext (an attacker can read the header) but is tamper + * proof (an attacker cannot modify the header). + * + * Note: some headers have semantic implications. E.g. if you set the "zip" + * header, you are responsible for properly compressing plain_text before + * calling encrypt(). + * + * @param k String + * @param v String + */ +JoseJWE.Encrypter.prototype.addHeader = function(k, v) { + this.userHeaders[k] = v; +}; + +/** + * Performs encryption. + * + * @param plain_text String + * @return Promise + */ +JoseJWE.Encrypter.prototype.encrypt = function(plain_text) { /** * Encrypts plain_text with CEK. * @@ -756,19 +769,22 @@ JoseJWE.encrypt = function(cryptographer, key_promise, plain_text) { */ var encryptPlainText = function(cek_promise, plain_text) { // Create header - var jwe_protected_header = Utils.Base64Url.encode(JSON.stringify({ - "alg": cryptographer.getKeyEncryptionAlgorithm(), - "enc": cryptographer.getContentEncryptionAlgorithm() - })); + var headers = {}; + for (var i in this.userHeaders) { + headers[i] = this.userHeaders[i]; + } + headers.alg = this.cryptographer.getKeyEncryptionAlgorithm(); + headers.enc = this.cryptographer.getContentEncryptionAlgorithm(); + var jwe_protected_header = Utils.Base64Url.encode(JSON.stringify(headers)); // Create the IV - var iv = cryptographer.createIV(); + var iv = this.cryptographer.createIV(); // Create the AAD var aad = Utils.arrayFromString(jwe_protected_header); plain_text = Utils.arrayFromString(plain_text); - return cryptographer.encrypt(iv, aad, cek_promise, plain_text).then(function(r) { + return this.cryptographer.encrypt(iv, aad, cek_promise, plain_text).then(function(r) { r.header = jwe_protected_header; r.iv = iv; return r; @@ -776,13 +792,17 @@ JoseJWE.encrypt = function(cryptographer, key_promise, plain_text) { }; // Create a CEK key - var cek_promise = cryptographer.createCek(); + var cek_promise = this.cryptographer.createCek(); // Key & Cek allows us to create the encrypted_cek - var encrypted_cek = encryptCek(key_promise, cek_promise); + var encrypted_cek = Promise.all([this.key_promise, cek_promise]).then(function(all) { + var key = all[0]; + var cek = all[1]; + return this.cryptographer.wrapCek(cek, key); + }.bind(this)); // Cek allows us to encrypy the plain text - var enc_promise = encryptPlainText(cek_promise, plain_text); + var enc_promise = encryptPlainText.bind(this, cek_promise, plain_text)(); // Once we have all the promises, we can base64 encode all the pieces. return Promise.all([encrypted_cek, enc_promise]).then(function(all) { @@ -813,29 +833,29 @@ JoseJWE.encrypt = function(cryptographer, key_promise, plain_text) { */ /** - * Performs decryption + * Handles decryption. * - * @param key_promise Promise, either RSA private key or shared key - * @param plain_text string to decrypt - * @return Promise + * @param cryptographer an instance of WebCryptographer (or equivalent). Keep + * in mind that decryption mutates the cryptographer. + * @param key_promise Promise, either RSA or shared key */ -JoseJWE.decrypt = function(cryptographer, key_promise, cipher_text) { - /** - * Decrypts the encrypted CEK. If decryption fails, we create a random CEK. - * - * In some modes (e.g. RSA-PKCS1v1.5), you myst take precautions to prevent - * chosen-ciphertext attacks as described in RFC 3218, "Preventing - * the Million Message Attack on Cryptographic Message Syntax". We currently - * only support RSA-OAEP, so we don't generate a key if unwrapping fails. - * - * return Promise - */ - var decryptCek = function(key_promise, encrypted_cek) { - return key_promise.then(function(key) { - return cryptographer.unwrapCek(encrypted_cek, key); - }); - }; +JoseJWE.Decrypter = function(cryptographer, key_promise) { + this.cryptographer = cryptographer; + this.key_promise = key_promise; + this.headers = {}; +}; + +JoseJWE.Decrypter.prototype.getHeaders = function() { + return this.headers; +}; +/** + * Performs decryption. + * + * @param cipher_text String + * @return Promise + */ +JoseJWE.Decrypter.prototype.decrypt = function(cipher_text) { // Split cipher_text in 5 parts var parts = cipher_text.split("."); if (parts.length != 5) { @@ -843,27 +863,33 @@ JoseJWE.decrypt = function(cryptographer, key_promise, cipher_text) { } // part 1: header - header = JSON.parse(Utils.Base64Url.decode(parts[0])); - if (!header.alg) { + this.headers = JSON.parse(Utils.Base64Url.decode(parts[0])); + if (!this.headers.alg) { return Promise.reject(Error("decrypt: missing alg")); } - cryptographer.setKeyEncryptionAlgorithm(header.alg); - - if (!header.enc) { + if (!this.headers.enc) { return Promise.reject(Error("decrypt: missing enc")); } - cryptographer.setContentEncryptionAlgorithm(header.enc); + this.cryptographer.setKeyEncryptionAlgorithm(this.headers.alg); + this.cryptographer.setContentEncryptionAlgorithm(this.headers.enc); - if (header.crit) { + if (this.headers.crit) { // We don't support the crit header return Promise.reject(Error("decrypt: crit is not supported")); } // part 2: decrypt the CEK - var cek_promise = decryptCek(key_promise, Utils.Base64Url.decodeArray(parts[1])); + // In some modes (e.g. RSA-PKCS1v1.5), you must take precautions to prevent + // chosen-ciphertext attacks as described in RFC 3218, "Preventing + // the Million Message Attack on Cryptographic Message Syntax". We currently + // only support RSA-OAEP, so we don't generate a key if unwrapping fails. + var encrypted_cek = Utils.Base64Url.decodeArray(parts[1]); + var cek_promise = this.key_promise.then(function(key) { + return this.cryptographer.unwrapCek(encrypted_cek, key); + }.bind(this)); // part 3: decrypt the cipher text - var plain_text_promise = cryptographer.decrypt( + var plain_text_promise = this.cryptographer.decrypt( cek_promise, Utils.arrayFromString(parts[0]), Utils.Base64Url.decodeArray(parts[2]), diff --git a/dist/jose-jwe.min.js b/dist/jose-jwe.min.js index 7588dda..8174309 100644 --- a/dist/jose-jwe.min.js +++ b/dist/jose-jwe.min.js @@ -1 +1 @@ -!function(){JoseJWE={},JoseJWE.assert=function(a,b){if(!a)throw new Error(b)},JoseJWE.WebCryptographer=function(){this.setKeyEncryptionAlgorithm("RSA-OAEP"),this.setContentEncryptionAlgorithm("A256GCM")},JoseJWE.WebCryptographer.prototype.setKeyEncryptionAlgorithm=function(a){this.key_encryption=d(a)},JoseJWE.WebCryptographer.prototype.getKeyEncryptionAlgorithm=function(){return this.key_encryption.jwe_name},JoseJWE.WebCryptographer.prototype.setContentEncryptionAlgorithm=function(a){this.content_encryption=d(a)},JoseJWE.WebCryptographer.prototype.getContentEncryptionAlgorithm=function(){return this.content_encryption.jwe_name},JoseJWE.WebCryptographer.prototype.createIV=function(){var a=new Uint8Array(new Array(this.content_encryption.iv_bytes)),b=crypto.getRandomValues(a);return b},JoseJWE.WebCryptographer.prototype.createCek=function(){var b=a(this.content_encryption);return crypto.subtle.generateKey(b.id,!0,b.enc_op)},JoseJWE.WebCryptographer.prototype.wrapCek=function(a,b){return crypto.subtle.wrapKey("raw",a,b,this.key_encryption.id)},JoseJWE.WebCryptographer.prototype.unwrapCek=function(b,c){var d=a(this.content_encryption),e=this.content_encryption.specific_cek_bytes>0,f=this.key_encryption.id;return crypto.subtle.unwrapKey("raw",b,c,f,d.id,e,d.dec_op)};var a=function(a){var b=a.specific_cek_bytes;if(b){if(16==b)return{id:{name:"AES-CBC",length:128},enc_op:["encrypt"],dec_op:["decrypt"]};if(32==b)return{id:{name:"AES-CBC",length:256},enc_op:["encrypt"],dec_op:["decrypt"]};if(64==b)return{id:{name:"HMAC",hash:{name:"SHA-256"}},enc_op:["sign"],dec_op:["verify"]};if(128==b)return{id:{name:"HMAC",hash:{name:"SHA-384"}},enc_op:["sign"],dec_op:["verify"]};JoseJWE.assert(!1,"getCekWorkaround: invalid len")}return{id:a.id,enc_op:["encrypt"],dec_op:["decrypt"]}};JoseJWE.WebCryptographer.prototype.encrypt=function(a,d,e,f){var g=this.content_encryption;if(a.length!=g.iv_bytes)return Promise.reject(Error("invalid IV length"));if(g.auth.aead){var h=g.auth.tag_bytes,i={name:g.id.name,iv:a,additionalData:d,tagLength:8*h};return e.then(function(a){return crypto.subtle.encrypt(i,a,f).then(function(a){var b=a.byteLength-h;return{cipher:a.slice(0,b),tag:a.slice(b)}})})}var j=b(g,e,["encrypt"]),k=j[0],l=j[1],m=l.then(function(b){var c={name:g.id.name,iv:a};return crypto.subtle.encrypt(c,b,f)}),n=m.then(function(b){return c(g,k,d,a,b)});return Promise.all([m,n]).then(function(a){var b=a[0],c=a[1];return{cipher:b,tag:c}})},JoseJWE.WebCryptographer.prototype.decrypt=function(a,d,f,g,h){var i=function(a,b,c,d){return JoseJWE.assert(c instanceof Uint8Array,"compare: invalid input"),JoseJWE.assert(d instanceof Uint8Array,"compare: invalid input"),b.then(function(b){var e=crypto.subtle.sign(a.auth.id,b,c),f=crypto.subtle.sign(a.auth.id,b,d);return Promise.all([e,f]).then(function(a){var b=new Uint8Array(a[0]),c=new Uint8Array(a[1]);if(b.length!=c.length)throw new Error("compare failed");for(var d=0;d0&&JoseJWE.assert(!1,"convertRsaKey: Was expecting "+d.join()),void 0!==a.kty&&JoseJWE.assert("RSA"==a.kty,"convertRsaKey: expecting rsa_key['kty'] to be 'RSA'"),c.kty="RSA",void 0!==a.alg&&JoseJWE.assert("RSA-OAEP"==a.alg,"convertRsaKey: expecting rsa_key['alg'] to be 'RSA-OAEP'"),c.alg="RSA-OAEP";for(var f=function(a){return parseInt(a,16)},g=0;gd;d++)c[d]=b[3-d];return c.buffer},e.arrayBufferConcat=function(){for(var a=[],b=0,c=0;c0,f=this.key_encryption.id;return crypto.subtle.unwrapKey("raw",b,c,f,d.id,e,d.dec_op)};var a=function(a){var b=a.specific_cek_bytes;if(b){if(16==b)return{id:{name:"AES-CBC",length:128},enc_op:["encrypt"],dec_op:["decrypt"]};if(32==b)return{id:{name:"AES-CBC",length:256},enc_op:["encrypt"],dec_op:["decrypt"]};if(64==b)return{id:{name:"HMAC",hash:{name:"SHA-256"}},enc_op:["sign"],dec_op:["verify"]};if(128==b)return{id:{name:"HMAC",hash:{name:"SHA-384"}},enc_op:["sign"],dec_op:["verify"]};JoseJWE.assert(!1,"getCekWorkaround: invalid len")}return{id:a.id,enc_op:["encrypt"],dec_op:["decrypt"]}};JoseJWE.WebCryptographer.prototype.encrypt=function(a,d,e,f){var g=this.content_encryption;if(a.length!=g.iv_bytes)return Promise.reject(Error("invalid IV length"));if(g.auth.aead){var h=g.auth.tag_bytes,i={name:g.id.name,iv:a,additionalData:d,tagLength:8*h};return e.then(function(a){return crypto.subtle.encrypt(i,a,f).then(function(a){var b=a.byteLength-h;return{cipher:a.slice(0,b),tag:a.slice(b)}})})}var j=b(g,e,["encrypt"]),k=j[0],l=j[1],m=l.then(function(b){var c={name:g.id.name,iv:a};return crypto.subtle.encrypt(c,b,f)}),n=m.then(function(b){return c(g,k,d,a,b)});return Promise.all([m,n]).then(function(a){var b=a[0],c=a[1];return{cipher:b,tag:c}})},JoseJWE.WebCryptographer.prototype.decrypt=function(a,d,f,g,h){var i=function(a,b,c,d){return JoseJWE.assert(c instanceof Uint8Array,"compare: invalid input"),JoseJWE.assert(d instanceof Uint8Array,"compare: invalid input"),b.then(function(b){var e=crypto.subtle.sign(a.auth.id,b,c),f=crypto.subtle.sign(a.auth.id,b,d);return Promise.all([e,f]).then(function(a){var b=new Uint8Array(a[0]),c=new Uint8Array(a[1]);if(b.length!=c.length)throw new Error("compare failed");for(var d=0;d0&&JoseJWE.assert(!1,"convertRsaKey: Was expecting "+d.join()),void 0!==a.kty&&JoseJWE.assert("RSA"==a.kty,"convertRsaKey: expecting rsa_key['kty'] to be 'RSA'"),c.kty="RSA",void 0!==a.alg&&JoseJWE.assert("RSA-OAEP"==a.alg,"convertRsaKey: expecting rsa_key['alg'] to be 'RSA-OAEP'"),c.alg="RSA-OAEP";for(var f=function(a){return parseInt(a,16)},g=0;gd;d++)c[d]=b[3-d];return c.buffer},e.arrayBufferConcat=function(){for(var a=[],b=0,c=0;c, either RSA private key or shared key - * @param plain_text string to decrypt - * @return Promise + * @param cryptographer an instance of WebCryptographer (or equivalent). Keep + * in mind that decryption mutates the cryptographer. + * @param key_promise Promise, either RSA or shared key */ -JoseJWE.decrypt = function(cryptographer, key_promise, cipher_text) { - /** - * Decrypts the encrypted CEK. If decryption fails, we create a random CEK. - * - * In some modes (e.g. RSA-PKCS1v1.5), you myst take precautions to prevent - * chosen-ciphertext attacks as described in RFC 3218, "Preventing - * the Million Message Attack on Cryptographic Message Syntax". We currently - * only support RSA-OAEP, so we don't generate a key if unwrapping fails. - * - * return Promise - */ - var decryptCek = function(key_promise, encrypted_cek) { - return key_promise.then(function(key) { - return cryptographer.unwrapCek(encrypted_cek, key); - }); - }; +JoseJWE.Decrypter = function(cryptographer, key_promise) { + this.cryptographer = cryptographer; + this.key_promise = key_promise; + this.headers = {}; +}; + +JoseJWE.Decrypter.prototype.getHeaders = function() { + return this.headers; +}; +/** + * Performs decryption. + * + * @param cipher_text String + * @return Promise + */ +JoseJWE.Decrypter.prototype.decrypt = function(cipher_text) { // Split cipher_text in 5 parts var parts = cipher_text.split("."); if (parts.length != 5) { @@ -45,27 +45,33 @@ JoseJWE.decrypt = function(cryptographer, key_promise, cipher_text) { } // part 1: header - header = JSON.parse(Utils.Base64Url.decode(parts[0])); - if (!header.alg) { + this.headers = JSON.parse(Utils.Base64Url.decode(parts[0])); + if (!this.headers.alg) { return Promise.reject(Error("decrypt: missing alg")); } - cryptographer.setKeyEncryptionAlgorithm(header.alg); - - if (!header.enc) { + if (!this.headers.enc) { return Promise.reject(Error("decrypt: missing enc")); } - cryptographer.setContentEncryptionAlgorithm(header.enc); + this.cryptographer.setKeyEncryptionAlgorithm(this.headers.alg); + this.cryptographer.setContentEncryptionAlgorithm(this.headers.enc); - if (header.crit) { + if (this.headers.crit) { // We don't support the crit header return Promise.reject(Error("decrypt: crit is not supported")); } // part 2: decrypt the CEK - var cek_promise = decryptCek(key_promise, Utils.Base64Url.decodeArray(parts[1])); + // In some modes (e.g. RSA-PKCS1v1.5), you must take precautions to prevent + // chosen-ciphertext attacks as described in RFC 3218, "Preventing + // the Million Message Attack on Cryptographic Message Syntax". We currently + // only support RSA-OAEP, so we don't generate a key if unwrapping fails. + var encrypted_cek = Utils.Base64Url.decodeArray(parts[1]); + var cek_promise = this.key_promise.then(function(key) { + return this.cryptographer.unwrapCek(encrypted_cek, key); + }.bind(this)); // part 3: decrypt the cipher text - var plain_text_promise = cryptographer.decrypt( + var plain_text_promise = this.cryptographer.decrypt( cek_promise, Utils.arrayFromString(parts[0]), Utils.Base64Url.decodeArray(parts[2]), diff --git a/lib/jose-jwe-encrypt.js b/lib/jose-jwe-encrypt.js index 14a4d80..7b53315 100644 --- a/lib/jose-jwe-encrypt.js +++ b/lib/jose-jwe-encrypt.js @@ -15,28 +15,41 @@ */ /** - * Performs encryption + * Handles encryption. * - * @param key_promise Promise, either RSA or shared key - * @param plain_text string to encrypt - * @return Promise + * @param cryptographer an instance of WebCryptographer (or equivalent). + * @param key_promise Promise, either RSA or shared key */ -JoseJWE.encrypt = function(cryptographer, key_promise, plain_text) { - /** - * Encrypts the CEK - * - * @param key_promise Promise - * @param cek_promise Promise - * @return Promise - */ - var encryptCek = function(key_promise, cek_promise) { - return Promise.all([key_promise, cek_promise]).then(function(all) { - var key = all[0]; - var cek = all[1]; - return cryptographer.wrapCek(cek, key); - }); - }; +JoseJWE.Encrypter = function(cryptographer, key_promise) { + this.cryptographer = cryptographer; + this.key_promise = key_promise; + this.userHeaders = {}; +}; +/** + * Adds a key/value pair which will be included in the header. + * + * The data lives in plaintext (an attacker can read the header) but is tamper + * proof (an attacker cannot modify the header). + * + * Note: some headers have semantic implications. E.g. if you set the "zip" + * header, you are responsible for properly compressing plain_text before + * calling encrypt(). + * + * @param k String + * @param v String + */ +JoseJWE.Encrypter.prototype.addHeader = function(k, v) { + this.userHeaders[k] = v; +}; + +/** + * Performs encryption. + * + * @param plain_text String + * @return Promise + */ +JoseJWE.Encrypter.prototype.encrypt = function(plain_text) { /** * Encrypts plain_text with CEK. * @@ -46,19 +59,22 @@ JoseJWE.encrypt = function(cryptographer, key_promise, plain_text) { */ var encryptPlainText = function(cek_promise, plain_text) { // Create header - var jwe_protected_header = Utils.Base64Url.encode(JSON.stringify({ - "alg": cryptographer.getKeyEncryptionAlgorithm(), - "enc": cryptographer.getContentEncryptionAlgorithm() - })); + var headers = {}; + for (var i in this.userHeaders) { + headers[i] = this.userHeaders[i]; + } + headers.alg = this.cryptographer.getKeyEncryptionAlgorithm(); + headers.enc = this.cryptographer.getContentEncryptionAlgorithm(); + var jwe_protected_header = Utils.Base64Url.encode(JSON.stringify(headers)); // Create the IV - var iv = cryptographer.createIV(); + var iv = this.cryptographer.createIV(); // Create the AAD var aad = Utils.arrayFromString(jwe_protected_header); plain_text = Utils.arrayFromString(plain_text); - return cryptographer.encrypt(iv, aad, cek_promise, plain_text).then(function(r) { + return this.cryptographer.encrypt(iv, aad, cek_promise, plain_text).then(function(r) { r.header = jwe_protected_header; r.iv = iv; return r; @@ -66,13 +82,17 @@ JoseJWE.encrypt = function(cryptographer, key_promise, plain_text) { }; // Create a CEK key - var cek_promise = cryptographer.createCek(); + var cek_promise = this.cryptographer.createCek(); // Key & Cek allows us to create the encrypted_cek - var encrypted_cek = encryptCek(key_promise, cek_promise); + var encrypted_cek = Promise.all([this.key_promise, cek_promise]).then(function(all) { + var key = all[0]; + var cek = all[1]; + return this.cryptographer.wrapCek(cek, key); + }.bind(this)); // Cek allows us to encrypy the plain text - var enc_promise = encryptPlainText(cek_promise, plain_text); + var enc_promise = encryptPlainText.bind(this, cek_promise, plain_text)(); // Once we have all the promises, we can base64 encode all the pieces. return Promise.all([encrypted_cek, enc_promise]).then(function(all) { diff --git a/package.json b/package.json index c45c82e..1a3020e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jose-jwe", - "version": "0.0.1", + "version": "0.0.2", "description": "Library to encrypt and decrypt data in JSON Web Encryption (JWE) format.", "keywords": ["crypto"], "license": "Apache-2.0", diff --git a/test/jose-jwe-test.html b/test/jose-jwe-test.html index 9cf197d..14fd155 100644 --- a/test/jose-jwe-test.html +++ b/test/jose-jwe-test.html @@ -51,7 +51,8 @@ }; var public_rsa_key = JoseJWE.Utils.importRsaPublicKey(rsa_key); var plain_text = "The true sign of intelligence is not knowledge but imagination."; - var cipher_text_promise = JoseJWE.encrypt(cryptographer, public_rsa_key, plain_text); + var encrypter = new JoseJWE.Encrypter(cryptographer, public_rsa_key); + var cipher_text_promise = encrypter.encrypt(plain_text); assert.willEqual(cipher_text_promise.then(function(result){return result.split('.').length}), 5, "got right number of components"); @@ -73,8 +74,9 @@ "got expected tag"); var private_rsa_key = JoseJWE.Utils.importRsaPrivateKey(rsa_key); + var decrypter = new JoseJWE.Decrypter(cryptographer, private_rsa_key); var decrypted_plain_text_promise = cipher_text_promise.then(function(cipher_text){ - return JoseJWE.decrypt(cryptographer, private_rsa_key, cipher_text); + return decrypter.decrypt(cipher_text); }); assert.willEqual(decrypted_plain_text_promise, plain_text, "Error: got expected decrypted plain text"); @@ -83,13 +85,12 @@ var cipher_text = cipher_text.split('.'); cipher_text.pop(); cipher_text.push("WFBoMYUZodetZdvTiFvSkQ"); - return JoseJWE.decrypt(cryptographer, private_rsa_key, cipher_text.join('.')); + return decrypter.decrypt(cipher_text.join('.')); }); assert.wontEqual(mac_failure, "OperationError: ", "invalid tag did not cause failure"); }); // We can't test appendix-A.2 because Chrome dropped support for RSAES-PKCS1-V1_5. - test("encryption using AES Key Wrap and AES_128_CBC_HMAC_SHA_256 (keys & IV from appendix-A.3)", function(assert) { var cryptographer = new JoseJWE.WebCryptographer(); cryptographer.setKeyEncryptionAlgorithm("A128KW"); @@ -106,7 +107,8 @@ shared_key = crypto.subtle.importKey("jwk", shared_key, {name: "AES-KW"}, true, ["wrapKey", "unwrapKey"]); var plain_text = "Live long and prosper."; - var cipher_text_promise = JoseJWE.encrypt(cryptographer, shared_key, plain_text); + var encrypter = new JoseJWE.Encrypter(cryptographer, shared_key); + var cipher_text_promise = encrypter.encrypt(plain_text); assert.willEqual(cipher_text_promise.then(function(result){return result.split('.').length}), 5, "got right number of components"); @@ -131,8 +133,9 @@ "U0m_YmjN04DJvceFICbCVQ", "got expected tag"); + var decrypter = new JoseJWE.Decrypter(cryptographer, shared_key); var decrypted_plain_text_promise = cipher_text_promise.then(function(cipher_text){ - return JoseJWE.decrypt(cryptographer, shared_key, cipher_text); + return decrypter.decrypt(cipher_text); }); assert.willEqual(decrypted_plain_text_promise, plain_text, "got expected decrypted plain text"); @@ -141,7 +144,7 @@ var cipher_text = cipher_text.split('.'); cipher_text.pop(); cipher_text.push("WEm_YmjN04DJvceFICbCVQ"); - return JoseJWE.decrypt(cryptographer, shared_key, cipher_text.join('.')); + return decrypter.decrypt(cipher_text.join('.')); }); assert.wontEqual(mac_failure, "Error: decryptCiphertext: MAC failed.", "invalid tag did not cause failure"); }); @@ -163,16 +166,36 @@ }; var public_rsa_key = JoseJWE.Utils.importRsaPublicKey(rsa_key); - var plain_text = "Look deep into nature, and then you will understand everything better." - var cipher_text_promise = JoseJWE.encrypt(cryptographer, public_rsa_key, plain_text); + var plain_text = "Look deep into nature, and then you will understand everything better. --Albert Einstein" + var encrypter = new JoseJWE.Encrypter(cryptographer, public_rsa_key); + var cipher_text_promise = encrypter.encrypt(plain_text); var private_rsa_key = JoseJWE.Utils.importRsaPrivateKey(rsa_key); var decrypted_plain_text_promise = cipher_text_promise.then(function(cipher_text){ - return JoseJWE.decrypt(cryptographer, private_rsa_key, cipher_text); + var decrypter = new JoseJWE.Decrypter(cryptographer, private_rsa_key); + return decrypter.decrypt(cipher_text); }); assert.willEqual(decrypted_plain_text_promise, plain_text, "got expected decrypted plain text"); }); + test("extra headers", function(assert) { + var cryptographer = new JoseJWE.WebCryptographer(); + cryptographer.setKeyEncryptionAlgorithm("A128KW"); + var shared_key = {"kty":"oct", "k":"GawgguFyGrWKav7AX4VKUg"}; + shared_key = crypto.subtle.importKey("jwk", shared_key, cryptographer.key_encryption.id, true, ["wrapKey", "unwrapKey"]); + var plain_text = "I only went out for a walk and finally concluded to stay out till sundown, for going out, I found, was really going in. --John Muir"; + + var encrypter = new JoseJWE.Encrypter(cryptographer, shared_key); + encrypter.addHeader("cty", "text/plain"); + var cipher_text_promise = encrypter.encrypt(plain_text); + + var decrypter = new JoseJWE.Decrypter(cryptographer, shared_key); + var decrypted_plain_text_promise = cipher_text_promise.then(function(cipher_text){ + return decrypter.decrypt(cipher_text); + }); + assert.willEqual(decrypted_plain_text_promise.then(function(_){return decrypter.getHeaders()['cty']}), "text/plain", "got expected header"); + }); + test("RSA-OAEP-256 with A256CBC-HS512", function(assert) { var cryptographer = new JoseJWE.WebCryptographer(); cryptographer.setKeyEncryptionAlgorithm("RSA-OAEP-256"); @@ -185,8 +208,10 @@ var plain_text = "Always remember that you are absolutely unique. Just like everyone else. --Margaret Mead"; var decrypted_plain_text_promise = key_promise.then(function(key) { - return JoseJWE.encrypt(cryptographer, Promise.resolve(key.publicKey), plain_text).then(function(cipher_text) { - return JoseJWE.decrypt(cryptographer, Promise.resolve(key.privateKey), cipher_text); + var encrypter = new JoseJWE.Encrypter(cryptographer, Promise.resolve(key.publicKey)); + return encrypter.encrypt(plain_text).then(function(cipher_text) { + var decrypter = new JoseJWE.Decrypter(cryptographer, Promise.resolve(key.privateKey)); + return decrypter.decrypt(cipher_text); }); }); assert.willEqual(decrypted_plain_text_promise, plain_text, "got expected decrypted plain text"); @@ -201,11 +226,12 @@ shared_key = crypto.subtle.importKey("jwk", shared_key, cryptographer.key_encryption.id, true, ["wrapKey", "unwrapKey"]); var plain_text = "All generalizations are false, including this one. --Mark Twain"; - - var cipher_text_promise = JoseJWE.encrypt(cryptographer, shared_key, plain_text); + var encrypter = new JoseJWE.Encrypter(cryptographer, shared_key); + var cipher_text_promise = encrypter.encrypt(plain_text); var decrypted_plain_text_promise = cipher_text_promise.then(function(cipher_text){ - return JoseJWE.decrypt(cryptographer, shared_key, cipher_text) + var decrypter = new JoseJWE.Decrypter(cryptographer, shared_key); + return decrypter.decrypt(cipher_text) }); assert.willEqual(decrypted_plain_text_promise, plain_text, "got expected decrypted plain text"); }); @@ -243,46 +269,49 @@ shared_key = crypto.subtle.importKey("jwk", shared_key, {name: "AES-KW"}, true, ["wrapKey", "unwrapKey"]); var plain_text = "A yawn is a silent scream for coffee. --unknown"; - var cipher_text_promise = JoseJWE.encrypt(cryptographer, shared_key, plain_text); + var encrypter = new JoseJWE.Encrypter(cryptographer, shared_key); + var cipher_text_promise = encrypter.encrypt(plain_text); + var decrypter = new JoseJWE.Decrypter(cryptographer, shared_key); var decrypt_truncated_input_promise = cipher_text_promise.then(function(cipher_text){ cipher_text = cipher_text.split('.').slice(1).join('.'); - return JoseJWE.decrypt(cryptographer, shared_key, cipher_text) + return decrypter.decrypt(cipher_text); }); assert.wontEqual(decrypt_truncated_input_promise, "Error: decrypt: invalid input", "truncated input did not cause failure"); var decrypt_missing_alg_promise = cipher_text_promise.then(function(cipher_text){ cipher_text = cipher_text.split('.'); cipher_text[0] = 'eyJlbmMiOiJBMjU2R0NNIn0='; - return JoseJWE.decrypt(cryptographer, shared_key, cipher_text.join('.')); + return decrypter.decrypt(cipher_text.join('.')); }); assert.wontEqual(decrypt_missing_alg_promise, "Error: decrypt: missing alg", "missing alg in header did not cause failure"); var decrypt_missing_enc_promise = cipher_text_promise.then(function(cipher_text){ cipher_text = cipher_text.split('.'); cipher_text[0] = 'eyJhbGciOiJBMTI4S1cifQ=='; - return JoseJWE.decrypt(cryptographer, shared_key, cipher_text.join('.')); + return decrypter.decrypt(cipher_text.join('.')); }); assert.wontEqual(decrypt_missing_enc_promise, "Error: decrypt: missing enc", "missing enc in header did not cause failure"); var decrypt_crit_header_promise = cipher_text_promise.then(function(cipher_text){ cipher_text = cipher_text.split('.'); cipher_text[0] = 'eyJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiQTEyOEtXIiwiY3JpdCI6ImJsYWgifQ=='; - return JoseJWE.decrypt(cryptographer, shared_key, cipher_text.join('.')); + return decrypter.decrypt(cipher_text.join('.')); }); assert.wontEqual(decrypt_crit_header_promise, "Error: decrypt: crit is not supported", "crit in header did not cause failure"); var decrypt_invalid_iv_promise = cipher_text_promise.then(function(cipher_text){ cipher_text = cipher_text.split('.'); cipher_text[2] = 'w4kHDJEum_fHW-U'; - return JoseJWE.decrypt(cryptographer, shared_key, cipher_text.join('.')); + return decrypter.decrypt(cipher_text.join('.')); }); assert.wontEqual(decrypt_invalid_iv_promise, "Error: decryptCiphertext: invalid IV", "invalid IV did not cause failure"); var cryptographer2 = new JoseJWE.WebCryptographer(); cryptographer2.setKeyEncryptionAlgorithm("A128KW"); cryptographer2.createIV = function() { return new Uint8Array(new Array(11)); }; - assert.wontEqual(JoseJWE.encrypt(cryptographer2, shared_key, plain_text), "Error: invalid IV length", "fails to encrypt when we don't have the right IV length"); + var encrypter2 = new JoseJWE.Encrypter(cryptographer2, shared_key); + assert.wontEqual(encrypter2.encrypt(plain_text), "Error: invalid IV length", "fails to encrypt when we don't have the right IV length"); });