fune/browser/components/loop/content/shared/js/crypto.js

243 lines
7.9 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
/* global Components */
var loop = loop || {};
var inChrome = typeof Components != "undefined" && "utils" in Components;
(function(rootObject) {
"use strict";
var sharedUtils;
if (inChrome) {
this.EXPORTED_SYMBOLS = ["LoopCrypto"];
var Cu = Components.utils;
Cu.importGlobalProperties(["crypto"]);
rootObject = {
crypto: crypto
};
sharedUtils = Cu.import("resource:///modules/loop/utils.js", {}).utils;
} else {
sharedUtils = this.shared.utils;
}
var ALGORITHM = "AES-GCM";
var KEY_LENGTH = 128;
// We use JSON web key formats for the generated keys.
// https://tools.ietf.org/html/draft-ietf-jose-json-web-key-41
var KEY_FORMAT = "jwk";
// This is the JSON web key type from the generateKey algorithm.
var KEY_TYPE = "oct";
var ENCRYPT_TAG_LENGTH = 128;
var INITIALIZATION_VECTOR_LENGTH = 12;
/**
* Sets a new root object. This is useful for testing crypto not supported as
* it allows us to fake crypto not being present.
* In beforeEach(), loop.crypto.setRootObject is used to
* substitute a fake window, and in afterEach(), the real window object is
* replaced.
*
* @param {Object}
*/
function setRootObject(obj) {
rootObject = obj;
}
/**
* Determines if Web Crypto is supported by this browser.
*
* @return {Boolean} True if Web Crypto is supported
*/
function isSupported() {
return "crypto" in rootObject;
}
/**
* Generates a random key using the Web Crypto libraries.
*
* @return {Promise} A promise which is rejected on failure, or resolved
* with a string that is in the JSON web key format.
*/
function generateKey() {
if (!isSupported()) {
throw new Error("Web Crypto is not supported");
}
return new Promise(function(resolve, reject) {
// First get a crypto key.
rootObject.crypto.subtle.generateKey({name: ALGORITHM, length: KEY_LENGTH },
// `true` means that the key can be extracted from the CryptoKey object.
true,
// Usages for the key.
["encrypt", "decrypt"]
).then(function(cryptoKey) {
// Now extract the key in the JSON web key format.
return rootObject.crypto.subtle.exportKey(KEY_FORMAT, cryptoKey);
}).then(function(exportedKey) {
// Lastly resolve the promise with the new key.
resolve(exportedKey.k);
}).catch(function(error) {
reject(error);
});
});
}
/**
* Encrypts an object using the specified key.
*
* @param {String} key The key to use for encryption. This should have
* been generated by generateKey.
* @param {String} data The string to be encrypted.
*
* @return {Promise} A promise which is rejected on failure, or resolved
* with a string that is the encrypted context.
*/
function encryptBytes(key, data) {
if (!isSupported()) {
throw new Error("Web Crypto is not supported");
}
var iv = new Uint8Array(INITIALIZATION_VECTOR_LENGTH);
return new Promise(function(resolve, reject) {
// First import the key to a format we can use.
rootObject.crypto.subtle.importKey(KEY_FORMAT,
{k: key, kty: KEY_TYPE},
ALGORITHM,
// If the key is extractable.
true,
// What we're using it for.
["encrypt"]
).then(function(cryptoKey) {
// Now we've got the cryptoKey, we can do the actual encryption.
// First get the data into the format we need.
var dataBuffer = sharedUtils.strToUint8Array(data);
// It is critically important to change the IV any time the
// encrypted information is updated.
rootObject.crypto.getRandomValues(iv);
return rootObject.crypto.subtle.encrypt({
name: ALGORITHM,
iv: iv,
tagLength: ENCRYPT_TAG_LENGTH
}, cryptoKey,
dataBuffer);
}).then(function(cipherText) {
// Join the initialization vector and context for returning.
var joinedData = _mergeIVandCipherText(iv, new DataView(cipherText));
// Now convert to a string and base-64 encode.
var encryptedData = sharedUtils.btoa(joinedData);
resolve(encryptedData);
}).catch(function(error) {
reject(error);
});
});
}
/**
* Decrypts an object using the specified key.
*
* @param {String} key The key to use for encryption. This should have
* been generated by generateKey.
* @param {String} encryptedData The encrypted context.
* @return {Promise} A promise which is rejected on failure, or resolved
* with a string that is the decrypted context.
*/
function decryptBytes(key, encryptedData) {
if (!isSupported()) {
throw new Error("Web Crypto is not supported");
}
return new Promise(function(resolve, reject) {
// First import the key to a format we can use.
rootObject.crypto.subtle.importKey(KEY_FORMAT,
{k: key, kty: KEY_TYPE},
ALGORITHM,
// If the key is extractable.
true,
// What we're using it for.
["decrypt"]
).then(function(cryptoKey) {
// Now we've got the key, start the decryption.
var splitData = _splitIVandCipherText(encryptedData);
return rootObject.crypto.subtle.decrypt({
name: ALGORITHM,
iv: splitData.iv,
tagLength: ENCRYPT_TAG_LENGTH
}, cryptoKey, splitData.cipherText);
}).then(function(plainText) {
// Now we just turn it back into a string and then an object.
resolve(sharedUtils.Uint8ArrayToStr(new Uint8Array(plainText)));
}).catch(function(error) {
reject(error);
});
});
}
/**
* Appends the cipher text to the end of the initialization vector and
* returns the result.
*
* @param {Uint8Array} ivArray The array of initialization vector values.
* @param {DataView} cipherTextDataView The cipherText in data view format.
* @return {Uint8Array} An array of the IV and cipherText.
*/
function _mergeIVandCipherText(ivArray, cipherTextDataView) {
// First we translate the data view to an array so we can get
// the length.
var cipherText = new Uint8Array(cipherTextDataView.buffer);
var cipherTextLength = cipherText.length;
var joinedContext = new Uint8Array(INITIALIZATION_VECTOR_LENGTH + cipherTextLength);
var i;
for (i = 0; i < INITIALIZATION_VECTOR_LENGTH; i++) {
joinedContext[i] = ivArray[i];
}
for (i = 0; i < cipherTextLength; i++) {
joinedContext[i + INITIALIZATION_VECTOR_LENGTH] = cipherText[i];
}
return joinedContext;
}
/**
* Takes the IV from the start of the passed in array and separates
* out the cipher text.
*
* @param {String} encryptedData Encrypted data in base64 format.
* @return {Object} An object consisting of two items: iv and cipherText,
* both are Uint8Arrays.
*/
function _splitIVandCipherText(encryptedData) {
// Convert into byte arrays.
var encryptedDataArray = sharedUtils.atob(encryptedData);
// Now split out the initialization vector and the cipherText.
var iv = encryptedDataArray.slice(0, INITIALIZATION_VECTOR_LENGTH);
var cipherText = encryptedDataArray.slice(INITIALIZATION_VECTOR_LENGTH,
encryptedDataArray.length);
return {
iv: iv,
cipherText: cipherText
};
}
this[inChrome ? "LoopCrypto" : "crypto"] = {
decryptBytes: decryptBytes,
encryptBytes: encryptBytes,
generateKey: generateKey,
isSupported: isSupported,
setRootObject: setRootObject
};
}).call(inChrome ? this : loop, this);