Skip to content

Commit

Permalink
Adds partial support for user defined headers.
Browse files Browse the repository at this point in the history
This is partial support because we don't fetch jku keys or zip/deflate
the plaintext.
  • Loading branch information
alokmenghrajani committed Feb 11, 2015
1 parent 7cefcde commit 95b3238
Show file tree
Hide file tree
Showing 9 changed files with 233 additions and 147 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
140 changes: 83 additions & 57 deletions dist/jose-jwe.js
Original file line number Diff line number Diff line change
Expand Up @@ -725,28 +725,41 @@ Utils.Base64Url.decodeArray = function(str) {
*/

/**
* Performs encryption
* Handles encryption.
*
* @param key_promise Promise<CryptoKey>, either RSA or shared key
* @param plain_text string to encrypt
* @return Promise<string>
* @param cryptographer an instance of WebCryptographer (or equivalent).
* @param key_promise Promise<CryptoKey>, either RSA or shared key
*/
JoseJWE.encrypt = function(cryptographer, key_promise, plain_text) {
/**
* Encrypts the CEK
*
* @param key_promise Promise<CryptoKey>
* @param cek_promise Promise<CryptoKey>
* @return Promise<ArrayBuffer>
*/
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<String>
*/
JoseJWE.Encrypter.prototype.encrypt = function(plain_text) {
/**
* Encrypts plain_text with CEK.
*
Expand All @@ -756,33 +769,40 @@ 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;
});
};

// 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) {
Expand Down Expand Up @@ -813,57 +833,63 @@ JoseJWE.encrypt = function(cryptographer, key_promise, plain_text) {
*/

/**
* Performs decryption
* Handles decryption.
*
* @param key_promise Promise<CryptoKey>, either RSA private key or shared key
* @param plain_text string to decrypt
* @return Promise<string>
* @param cryptographer an instance of WebCryptographer (or equivalent). Keep
* in mind that decryption mutates the cryptographer.
* @param key_promise Promise<CryptoKey>, 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<CryptoKey>
*/
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<String>
*/
JoseJWE.Decrypter.prototype.decrypt = function(cipher_text) {
// Split cipher_text in 5 parts
var parts = cipher_text.split(".");
if (parts.length != 5) {
return Promise.reject(Error("decrypt: invalid input"));
}

// 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]),
Expand Down
2 changes: 1 addition & 1 deletion dist/jose-jwe.min.js

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions examples/jose-jwe-example1.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,13 @@
var public_rsa_key = JoseJWE.Utils.importRsaPublicKey(rsa_key);
var private_rsa_key = JoseJWE.Utils.importRsaPrivateKey(rsa_key);

JoseJWE.encrypt(cryptographer, public_rsa_key, plaintext.textContent).then(function(result) {
var encrypter = new JoseJWE.Encrypter(cryptographer, public_rsa_key);

encrypter.encrypt(plaintext.textContent).then(function(result) {
ciphertext.textContent = result;

JoseJWE.decrypt(cryptographer, private_rsa_key, result)
var decrypter = new JoseJWE.Decrypter(cryptographer, private_rsa_key);
decrypter.decrypt(result)
.then(function(decrypted_plain_text) {
if (decrypted_plain_text != plaintext.textContent) {
error.textContent = "decryption failed!";
Expand Down
6 changes: 4 additions & 2 deletions examples/jose-jwe-example2.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@
var shared_key = {"kty":"oct", "k":"GawgguFyGrWKav7AX4VKUg"};
shared_key = crypto.subtle.importKey("jwk", shared_key, {name: "AES-KW"}, true, ["wrapKey", "unwrapKey"]);

JoseJWE.encrypt(cryptographer, shared_key, plaintext.textContent).then(function(result) {
var encrypter = new JoseJWE.Encrypter(cryptographer, shared_key);
encrypter.encrypt(plaintext.textContent).then(function(result) {
ciphertext.textContent = result;

JoseJWE.decrypt(cryptographer, shared_key, result)
var decrypter = new JoseJWE.Decrypter(cryptographer, shared_key);
decrypter.decrypt(result)
.then(function(decrypted_plain_text) {
if (decrypted_plain_text != plaintext.textContent) {
error.textContent = "decryption failed!";
Expand Down
64 changes: 35 additions & 29 deletions lib/jose-jwe-decrypt.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,57 +15,63 @@
*/

/**
* Performs decryption
* Handles decryption.
*
* @param key_promise Promise<CryptoKey>, either RSA private key or shared key
* @param plain_text string to decrypt
* @return Promise<string>
* @param cryptographer an instance of WebCryptographer (or equivalent). Keep
* in mind that decryption mutates the cryptographer.
* @param key_promise Promise<CryptoKey>, 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<CryptoKey>
*/
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<String>
*/
JoseJWE.Decrypter.prototype.decrypt = function(cipher_text) {
// Split cipher_text in 5 parts
var parts = cipher_text.split(".");
if (parts.length != 5) {
return Promise.reject(Error("decrypt: invalid input"));
}

// 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]),
Expand Down
Loading

0 comments on commit 95b3238

Please sign in to comment.