forked from mirrors/gecko-dev
244 lines
7.9 KiB
JavaScript
244 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 loop:true */
|
|
|
|
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) {
|
|
console.log("loop.crpyto.mixins: rootObject set to " + 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);
|