diff -r 000000000000 -r 6474c204b198 services/crypto/modules/WeaveCrypto.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/services/crypto/modules/WeaveCrypto.js Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,760 @@ +/* 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/. */ + +this.EXPORTED_SYMBOLS = ["WeaveCrypto"]; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource://gre/modules/ctypes.jsm"); + +/** + * Shortcuts for some algorithm SEC OIDs. Full list available here: + * http://lxr.mozilla.org/seamonkey/source/security/nss/lib/util/secoidt.h + */ +const DES_EDE3_CBC = 156; +const AES_128_CBC = 184; +const AES_192_CBC = 186; +const AES_256_CBC = 188; + +const ALGORITHM = AES_256_CBC; +const KEYSIZE_AES_256 = 32; +const KEY_DERIVATION_ITERATIONS = 4096; // PKCS#5 recommends at least 1000. +const INITIAL_BUFFER_SIZE = 1024; + +this.WeaveCrypto = function WeaveCrypto() { + this.init(); +} + +WeaveCrypto.prototype = { + prefBranch : null, + debug : true, // services.sync.log.cryptoDebug + nss : null, + nss_t : null, + + observer : { + _self : null, + + QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsISupportsWeakReference]), + + observe : function (subject, topic, data) { + let self = this._self; + self.log("Observed " + topic + " topic."); + if (topic == "nsPref:changed") { + self.debug = self.prefBranch.getBoolPref("cryptoDebug"); + } + } + }, + + init : function() { + try { + // Preferences. Add observer so we get notified of changes. + this.prefBranch = Services.prefs.getBranch("services.sync.log."); + this.prefBranch.addObserver("cryptoDebug", this.observer, false); + this.observer._self = this; + try { + this.debug = this.prefBranch.getBoolPref("cryptoDebug"); + } catch (x) { + this.debug = false; + } + + this.initNSS(); + this.initAlgorithmSettings(); // Depends on NSS. + this.initIVSECItem(); + this.initSharedInts(); + this.initBuffers(INITIAL_BUFFER_SIZE); + } catch (e) { + this.log("init failed: " + e); + throw e; + } + }, + + // Avoid allocating new temporary ints on every run of _commonCrypt. + _commonCryptSignedOutputSize: null, + _commonCryptSignedOutputSizeAddr: null, + _commonCryptUnsignedOutputSize: null, + _commonCryptUnsignedOutputSizeAddr: null, + + initSharedInts: function initSharedInts() { + let signed = new ctypes.int(); + let unsigned = new ctypes.unsigned_int(); + this._commonCryptSignedOutputSize = signed; + this._commonCryptUnsignedOutputSize = unsigned; + this._commonCryptSignedOutputSizeAddr = signed.address(); + this._commonCryptUnsignedOutputSizeAddr = unsigned.address(); + }, + + /** + * Set a bunch of NSS values once, at init-time. These are: + * - .blockSize + * - .mechanism + * - .keygenMechanism + * - .padMechanism + * - .keySize + * + * See also the constant ALGORITHM. + */ + initAlgorithmSettings: function() { + this.mechanism = this.nss.PK11_AlgtagToMechanism(ALGORITHM); + this.blockSize = this.nss.PK11_GetBlockSize(this.mechanism, null); + this.ivLength = this.nss.PK11_GetIVLength(this.mechanism); + this.keySize = KEYSIZE_AES_256; + this.keygenMechanism = this.nss.CKM_AES_KEY_GEN; // Always the same! + + // Determine which (padded) PKCS#11 mechanism to use. + // E.g., AES_256_CBC --> CKM_AES_CBC --> CKM_AES_CBC_PAD + this.padMechanism = this.nss.PK11_GetPadMechanism(this.mechanism); + if (this.padMechanism == this.nss.CKM_INVALID_MECHANISM) + throw Components.Exception("invalid algorithm (can't pad)", Cr.NS_ERROR_FAILURE); + }, + + log : function (message) { + if (!this.debug) + return; + dump("WeaveCrypto: " + message + "\n"); + Services.console.logStringMessage("WeaveCrypto: " + message); + }, + + initNSS : function() { + // We use NSS for the crypto ops, which needs to be initialized before + // use. By convention, PSM is required to be the module that + // initializes NSS. So, make sure PSM is initialized in order to + // implicitly initialize NSS. + Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports); + + // Open the NSS library. + let path = ctypes.libraryName("nss3"); + + // XXX really want to be able to pass specific dlopen flags here. + var nsslib; + try { + this.log("Trying NSS library without path"); + nsslib = ctypes.open(path); + } catch(e) { + // In case opening the library without a full path fails, + // try again with a full path. + let file = Services.dirsvc.get("GreD", Ci.nsILocalFile); + file.append(path); + this.log("Trying again with path " + file.path); + nsslib = ctypes.open(file.path); + } + + this.log("Initializing NSS types and function declarations..."); + + this.nss = {}; + this.nss_t = {}; + + // nsprpub/pr/include/prtypes.h#435 + // typedef PRIntn PRBool; --> int + this.nss_t.PRBool = ctypes.int; + // security/nss/lib/util/seccomon.h#91 + // typedef enum + this.nss_t.SECStatus = ctypes.int; + // security/nss/lib/softoken/secmodt.h#59 + // typedef struct PK11SlotInfoStr PK11SlotInfo; (defined in secmodti.h) + this.nss_t.PK11SlotInfo = ctypes.void_t; + // security/nss/lib/util/pkcs11t.h + this.nss_t.CK_MECHANISM_TYPE = ctypes.unsigned_long; + this.nss_t.CK_ATTRIBUTE_TYPE = ctypes.unsigned_long; + this.nss_t.CK_KEY_TYPE = ctypes.unsigned_long; + this.nss_t.CK_OBJECT_HANDLE = ctypes.unsigned_long; + // security/nss/lib/softoken/secmodt.h#359 + // typedef enum PK11Origin + this.nss_t.PK11Origin = ctypes.int; + // PK11Origin enum values... + this.nss.PK11_OriginUnwrap = 4; + // security/nss/lib/softoken/secmodt.h#61 + // typedef struct PK11SymKeyStr PK11SymKey; (defined in secmodti.h) + this.nss_t.PK11SymKey = ctypes.void_t; + // security/nss/lib/util/secoidt.h#454 + // typedef enum + this.nss_t.SECOidTag = ctypes.int; + // security/nss/lib/util/seccomon.h#64 + // typedef enum + this.nss_t.SECItemType = ctypes.int; + // SECItemType enum values... + this.nss.SIBUFFER = 0; + // security/nss/lib/softoken/secmodt.h#62 (defined in secmodti.h) + // typedef struct PK11ContextStr PK11Context; + this.nss_t.PK11Context = ctypes.void_t; + // Needed for SECKEYPrivateKey struct def'n, but I don't think we need to actually access it. + this.nss_t.PLArenaPool = ctypes.void_t; + // security/nss/lib/cryptohi/keythi.h#45 + // typedef enum + this.nss_t.KeyType = ctypes.int; + // security/nss/lib/softoken/secmodt.h#201 + // typedef PRUint32 PK11AttrFlags; + this.nss_t.PK11AttrFlags = ctypes.unsigned_int; + // security/nss/lib/util/seccomon.h#83 + // typedef struct SECItemStr SECItem; --> SECItemStr defined right below it + this.nss_t.SECItem = ctypes.StructType( + "SECItem", [{ type: this.nss_t.SECItemType }, + { data: ctypes.unsigned_char.ptr }, + { len : ctypes.int }]); + // security/nss/lib/util/secoidt.h#52 + // typedef struct SECAlgorithmIDStr --> def'n right below it + this.nss_t.SECAlgorithmID = ctypes.StructType( + "SECAlgorithmID", [{ algorithm: this.nss_t.SECItem }, + { parameters: this.nss_t.SECItem }]); + + + // security/nss/lib/util/pkcs11t.h + this.nss.CKK_RSA = 0x0; + this.nss.CKM_RSA_PKCS_KEY_PAIR_GEN = 0x0000; + this.nss.CKM_AES_KEY_GEN = 0x1080; + this.nss.CKA_ENCRYPT = 0x104; + this.nss.CKA_DECRYPT = 0x105; + + // security/nss/lib/softoken/secmodt.h + this.nss.PK11_ATTR_SESSION = 0x02; + this.nss.PK11_ATTR_PUBLIC = 0x08; + this.nss.PK11_ATTR_SENSITIVE = 0x40; + + // security/nss/lib/util/secoidt.h + this.nss.SEC_OID_PKCS5_PBKDF2 = 291; + this.nss.SEC_OID_HMAC_SHA1 = 294; + this.nss.SEC_OID_PKCS1_RSA_ENCRYPTION = 16; + + + // security/nss/lib/pk11wrap/pk11pub.h#286 + // SECStatus PK11_GenerateRandom(unsigned char *data,int len); + this.nss.PK11_GenerateRandom = nsslib.declare("PK11_GenerateRandom", + ctypes.default_abi, this.nss_t.SECStatus, + ctypes.unsigned_char.ptr, ctypes.int); + // security/nss/lib/pk11wrap/pk11pub.h#74 + // PK11SlotInfo *PK11_GetInternalSlot(void); + this.nss.PK11_GetInternalSlot = nsslib.declare("PK11_GetInternalSlot", + ctypes.default_abi, this.nss_t.PK11SlotInfo.ptr); + // security/nss/lib/pk11wrap/pk11pub.h#73 + // PK11SlotInfo *PK11_GetInternalKeySlot(void); + this.nss.PK11_GetInternalKeySlot = nsslib.declare("PK11_GetInternalKeySlot", + ctypes.default_abi, this.nss_t.PK11SlotInfo.ptr); + // security/nss/lib/pk11wrap/pk11pub.h#328 + // PK11SymKey *PK11_KeyGen(PK11SlotInfo *slot,CK_MECHANISM_TYPE type, SECItem *param, int keySize,void *wincx); + this.nss.PK11_KeyGen = nsslib.declare("PK11_KeyGen", + ctypes.default_abi, this.nss_t.PK11SymKey.ptr, + this.nss_t.PK11SlotInfo.ptr, this.nss_t.CK_MECHANISM_TYPE, + this.nss_t.SECItem.ptr, ctypes.int, ctypes.voidptr_t); + // security/nss/lib/pk11wrap/pk11pub.h#477 + // SECStatus PK11_ExtractKeyValue(PK11SymKey *symKey); + this.nss.PK11_ExtractKeyValue = nsslib.declare("PK11_ExtractKeyValue", + ctypes.default_abi, this.nss_t.SECStatus, + this.nss_t.PK11SymKey.ptr); + // security/nss/lib/pk11wrap/pk11pub.h#478 + // SECItem * PK11_GetKeyData(PK11SymKey *symKey); + this.nss.PK11_GetKeyData = nsslib.declare("PK11_GetKeyData", + ctypes.default_abi, this.nss_t.SECItem.ptr, + this.nss_t.PK11SymKey.ptr); + // security/nss/lib/pk11wrap/pk11pub.h#278 + // CK_MECHANISM_TYPE PK11_AlgtagToMechanism(SECOidTag algTag); + this.nss.PK11_AlgtagToMechanism = nsslib.declare("PK11_AlgtagToMechanism", + ctypes.default_abi, this.nss_t.CK_MECHANISM_TYPE, + this.nss_t.SECOidTag); + // security/nss/lib/pk11wrap/pk11pub.h#270 + // int PK11_GetIVLength(CK_MECHANISM_TYPE type); + this.nss.PK11_GetIVLength = nsslib.declare("PK11_GetIVLength", + ctypes.default_abi, ctypes.int, + this.nss_t.CK_MECHANISM_TYPE); + // security/nss/lib/pk11wrap/pk11pub.h#269 + // int PK11_GetBlockSize(CK_MECHANISM_TYPE type,SECItem *params); + this.nss.PK11_GetBlockSize = nsslib.declare("PK11_GetBlockSize", + ctypes.default_abi, ctypes.int, + this.nss_t.CK_MECHANISM_TYPE, this.nss_t.SECItem.ptr); + // security/nss/lib/pk11wrap/pk11pub.h#293 + // CK_MECHANISM_TYPE PK11_GetPadMechanism(CK_MECHANISM_TYPE); + this.nss.PK11_GetPadMechanism = nsslib.declare("PK11_GetPadMechanism", + ctypes.default_abi, this.nss_t.CK_MECHANISM_TYPE, + this.nss_t.CK_MECHANISM_TYPE); + // security/nss/lib/pk11wrap/pk11pub.h#271 + // SECItem *PK11_ParamFromIV(CK_MECHANISM_TYPE type,SECItem *iv); + this.nss.PK11_ParamFromIV = nsslib.declare("PK11_ParamFromIV", + ctypes.default_abi, this.nss_t.SECItem.ptr, + this.nss_t.CK_MECHANISM_TYPE, this.nss_t.SECItem.ptr); + // security/nss/lib/pk11wrap/pk11pub.h#301 + // PK11SymKey *PK11_ImportSymKey(PK11SlotInfo *slot, CK_MECHANISM_TYPE type, PK11Origin origin, + // CK_ATTRIBUTE_TYPE operation, SECItem *key, void *wincx); + this.nss.PK11_ImportSymKey = nsslib.declare("PK11_ImportSymKey", + ctypes.default_abi, this.nss_t.PK11SymKey.ptr, + this.nss_t.PK11SlotInfo.ptr, this.nss_t.CK_MECHANISM_TYPE, this.nss_t.PK11Origin, + this.nss_t.CK_ATTRIBUTE_TYPE, this.nss_t.SECItem.ptr, ctypes.voidptr_t); + // security/nss/lib/pk11wrap/pk11pub.h#672 + // PK11Context *PK11_CreateContextBySymKey(CK_MECHANISM_TYPE type, CK_ATTRIBUTE_TYPE operation, + // PK11SymKey *symKey, SECItem *param); + this.nss.PK11_CreateContextBySymKey = nsslib.declare("PK11_CreateContextBySymKey", + ctypes.default_abi, this.nss_t.PK11Context.ptr, + this.nss_t.CK_MECHANISM_TYPE, this.nss_t.CK_ATTRIBUTE_TYPE, + this.nss_t.PK11SymKey.ptr, this.nss_t.SECItem.ptr); + // security/nss/lib/pk11wrap/pk11pub.h#685 + // SECStatus PK11_CipherOp(PK11Context *context, unsigned char *out + // int *outlen, int maxout, unsigned char *in, int inlen); + this.nss.PK11_CipherOp = nsslib.declare("PK11_CipherOp", + ctypes.default_abi, this.nss_t.SECStatus, + this.nss_t.PK11Context.ptr, ctypes.unsigned_char.ptr, + ctypes.int.ptr, ctypes.int, ctypes.unsigned_char.ptr, ctypes.int); + // security/nss/lib/pk11wrap/pk11pub.h#688 + // SECStatus PK11_DigestFinal(PK11Context *context, unsigned char *data, + // unsigned int *outLen, unsigned int length); + this.nss.PK11_DigestFinal = nsslib.declare("PK11_DigestFinal", + ctypes.default_abi, this.nss_t.SECStatus, + this.nss_t.PK11Context.ptr, ctypes.unsigned_char.ptr, + ctypes.unsigned_int.ptr, ctypes.unsigned_int); + // security/nss/lib/pk11wrap/pk11pub.h#731 + // SECAlgorithmID * PK11_CreatePBEV2AlgorithmID(SECOidTag pbeAlgTag, SECOidTag cipherAlgTag, + // SECOidTag prfAlgTag, int keyLength, int iteration, + // SECItem *salt); + this.nss.PK11_CreatePBEV2AlgorithmID = nsslib.declare("PK11_CreatePBEV2AlgorithmID", + ctypes.default_abi, this.nss_t.SECAlgorithmID.ptr, + this.nss_t.SECOidTag, this.nss_t.SECOidTag, this.nss_t.SECOidTag, + ctypes.int, ctypes.int, this.nss_t.SECItem.ptr); + // security/nss/lib/pk11wrap/pk11pub.h#736 + // PK11SymKey * PK11_PBEKeyGen(PK11SlotInfo *slot, SECAlgorithmID *algid, SECItem *pwitem, PRBool faulty3DES, void *wincx); + this.nss.PK11_PBEKeyGen = nsslib.declare("PK11_PBEKeyGen", + ctypes.default_abi, this.nss_t.PK11SymKey.ptr, + this.nss_t.PK11SlotInfo.ptr, this.nss_t.SECAlgorithmID.ptr, + this.nss_t.SECItem.ptr, this.nss_t.PRBool, ctypes.voidptr_t); + // security/nss/lib/pk11wrap/pk11pub.h#675 + // void PK11_DestroyContext(PK11Context *context, PRBool freeit); + this.nss.PK11_DestroyContext = nsslib.declare("PK11_DestroyContext", + ctypes.default_abi, ctypes.void_t, + this.nss_t.PK11Context.ptr, this.nss_t.PRBool); + // security/nss/lib/pk11wrap/pk11pub.h#299 + // void PK11_FreeSymKey(PK11SymKey *key); + this.nss.PK11_FreeSymKey = nsslib.declare("PK11_FreeSymKey", + ctypes.default_abi, ctypes.void_t, + this.nss_t.PK11SymKey.ptr); + // security/nss/lib/pk11wrap/pk11pub.h#70 + // void PK11_FreeSlot(PK11SlotInfo *slot); + this.nss.PK11_FreeSlot = nsslib.declare("PK11_FreeSlot", + ctypes.default_abi, ctypes.void_t, + this.nss_t.PK11SlotInfo.ptr); + // security/nss/lib/util/secitem.h#49 + // extern SECItem *SECITEM_AllocItem(PRArenaPool *arena, SECItem *item, unsigned int len); + this.nss.SECITEM_AllocItem = nsslib.declare("SECITEM_AllocItem", + ctypes.default_abi, this.nss_t.SECItem.ptr, + this.nss_t.PLArenaPool.ptr, // Not used. + this.nss_t.SECItem.ptr, ctypes.unsigned_int); + // security/nss/lib/util/secitem.h#274 + // extern void SECITEM_ZfreeItem(SECItem *zap, PRBool freeit); + this.nss.SECITEM_ZfreeItem = nsslib.declare("SECITEM_ZfreeItem", + ctypes.default_abi, ctypes.void_t, + this.nss_t.SECItem.ptr, this.nss_t.PRBool); + // security/nss/lib/util/secitem.h#114 + // extern void SECITEM_FreeItem(SECItem *zap, PRBool freeit); + this.nss.SECITEM_FreeItem = nsslib.declare("SECITEM_FreeItem", + ctypes.default_abi, ctypes.void_t, + this.nss_t.SECItem.ptr, this.nss_t.PRBool); + // security/nss/lib/util/secoid.h#103 + // extern void SECOID_DestroyAlgorithmID(SECAlgorithmID *aid, PRBool freeit); + this.nss.SECOID_DestroyAlgorithmID = nsslib.declare("SECOID_DestroyAlgorithmID", + ctypes.default_abi, ctypes.void_t, + this.nss_t.SECAlgorithmID.ptr, this.nss_t.PRBool); + }, + + + _sharedInputBuffer: null, + _sharedInputBufferInts: null, + _sharedInputBufferSize: 0, + _sharedOutputBuffer: null, + _sharedOutputBufferSize: 0, + _randomByteBuffer: null, + _randomByteBufferAddr: null, + _randomByteBufferSize: 0, + + _getInputBuffer: function _getInputBuffer(size) { + if (size > this._sharedInputBufferSize) { + let b = new ctypes.ArrayType(ctypes.unsigned_char, size)(); + this._sharedInputBuffer = b; + this._sharedInputBufferInts = ctypes.cast(b, ctypes.uint8_t.array(size)); + this._sharedInputBufferSize = size; + } + return this._sharedInputBuffer; + }, + + _getOutputBuffer: function _getOutputBuffer(size) { + if (size > this._sharedOutputBufferSize) { + let b = new ctypes.ArrayType(ctypes.unsigned_char, size)(); + this._sharedOutputBuffer = b; + this._sharedOutputBufferSize = size; + } + return this._sharedOutputBuffer; + }, + + _getRandomByteBuffer: function _getRandomByteBuffer(size) { + if (size > this._randomByteBufferSize) { + let b = new ctypes.ArrayType(ctypes.unsigned_char, size)(); + this._randomByteBuffer = b; + this._randomByteBufferAddr = b.address(); + this._randomByteBufferSize = size; + } + return this._randomByteBuffer; + }, + + initBuffers: function initBuffers(initialSize) { + this._getInputBuffer(initialSize); + this._getOutputBuffer(initialSize); + + this._getRandomByteBuffer(this.ivLength); + }, + + encrypt : function(clearTextUCS2, symmetricKey, iv) { + this.log("encrypt() called"); + + // js-ctypes autoconverts to a UTF8 buffer, but also includes a null + // at the end which we don't want. Decrement length to skip it. + let inputBuffer = new ctypes.ArrayType(ctypes.unsigned_char)(clearTextUCS2); + let inputBufferSize = inputBuffer.length - 1; + + // When using CBC padding, the output size is the input size rounded + // up to the nearest block. If the input size is exactly on a block + // boundary, the output is 1 extra block long. + let outputBufferSize = inputBufferSize + this.blockSize; + let outputBuffer = this._getOutputBuffer(outputBufferSize); + + outputBuffer = this._commonCrypt(inputBuffer, inputBufferSize, + outputBuffer, outputBufferSize, + symmetricKey, iv, this.nss.CKA_ENCRYPT); + + return this.encodeBase64(outputBuffer.address(), outputBuffer.length); + }, + + + decrypt : function(cipherText, symmetricKey, iv) { + this.log("decrypt() called"); + + let inputUCS2 = ""; + if (cipherText.length) + inputUCS2 = atob(cipherText); + + // We can't have js-ctypes create the buffer directly from the string + // (as in encrypt()), because we do _not_ want it to do UTF8 + // conversion... We've got random binary data in the input's low byte. + // + // Compress a JS string (2-byte chars) into a normal C string (1-byte chars). + let len = inputUCS2.length; + let input = this._getInputBuffer(len); + this.byteCompressInts(inputUCS2, this._sharedInputBufferInts, len); + + let outputBuffer = this._commonCrypt(input, len, + this._getOutputBuffer(len), len, + symmetricKey, iv, this.nss.CKA_DECRYPT); + + // outputBuffer contains UTF-8 data, let js-ctypes autoconvert that to a JS string. + // XXX Bug 573842: wrap the string from ctypes to get a new string, so + // we don't hit bug 573841. + return "" + outputBuffer.readString() + ""; + }, + + _commonCrypt : function (input, inputLength, output, outputLength, symmetricKey, iv, operation) { + this.log("_commonCrypt() called"); + iv = atob(iv); + + // We never want an IV longer than the block size, which is 16 bytes + // for AES. Neither do we want one smaller; throw in that case. + if (iv.length < this.blockSize) + throw "IV too short; must be " + this.blockSize + " bytes."; + if (iv.length > this.blockSize) + iv = iv.slice(0, this.blockSize); + + // We use a single IV SECItem for the sake of efficiency. Fill it here. + this.byteCompressInts(iv, this._ivSECItemContents, iv.length); + + let ctx, symKey, ivParam; + try { + ivParam = this.nss.PK11_ParamFromIV(this.padMechanism, this._ivSECItem); + if (ivParam.isNull()) + throw Components.Exception("can't convert IV to param", Cr.NS_ERROR_FAILURE); + + symKey = this.importSymKey(symmetricKey, operation); + ctx = this.nss.PK11_CreateContextBySymKey(this.padMechanism, operation, symKey, ivParam); + if (ctx.isNull()) + throw Components.Exception("couldn't create context for symkey", Cr.NS_ERROR_FAILURE); + + let maxOutputSize = outputLength; + if (this.nss.PK11_CipherOp(ctx, output, this._commonCryptSignedOutputSize.address(), maxOutputSize, input, inputLength)) + throw Components.Exception("cipher operation failed", Cr.NS_ERROR_FAILURE); + + let actualOutputSize = this._commonCryptSignedOutputSize.value; + let finalOutput = output.addressOfElement(actualOutputSize); + maxOutputSize -= actualOutputSize; + + // PK11_DigestFinal sure sounds like the last step for *hashing*, but it + // just seems to be an odd name -- NSS uses this to finish the current + // cipher operation. You'd think it would be called PK11_CipherOpFinal... + if (this.nss.PK11_DigestFinal(ctx, finalOutput, this._commonCryptUnsignedOutputSizeAddr, maxOutputSize)) + throw Components.Exception("cipher finalize failed", Cr.NS_ERROR_FAILURE); + + actualOutputSize += this._commonCryptUnsignedOutputSize.value; + let newOutput = ctypes.cast(output, ctypes.unsigned_char.array(actualOutputSize)); + return newOutput; + } catch (e) { + this.log("_commonCrypt: failed: " + e); + throw e; + } finally { + if (ctx && !ctx.isNull()) + this.nss.PK11_DestroyContext(ctx, true); + if (ivParam && !ivParam.isNull()) + this.nss.SECITEM_FreeItem(ivParam, true); + + // Note that we do not free the IV SECItem; we reuse it. + // Neither do we free the symKey, because that's memoized. + } + }, + + + generateRandomKey : function() { + this.log("generateRandomKey() called"); + let slot, randKey, keydata; + try { + slot = this.nss.PK11_GetInternalSlot(); + if (slot.isNull()) + throw Components.Exception("couldn't get internal slot", Cr.NS_ERROR_FAILURE); + + randKey = this.nss.PK11_KeyGen(slot, this.keygenMechanism, null, this.keySize, null); + if (randKey.isNull()) + throw Components.Exception("PK11_KeyGen failed.", Cr.NS_ERROR_FAILURE); + + // Slightly odd API, this call just prepares the key value for + // extraction, we get the actual bits from the call to PK11_GetKeyData(). + if (this.nss.PK11_ExtractKeyValue(randKey)) + throw Components.Exception("PK11_ExtractKeyValue failed.", Cr.NS_ERROR_FAILURE); + + keydata = this.nss.PK11_GetKeyData(randKey); + if (keydata.isNull()) + throw Components.Exception("PK11_GetKeyData failed.", Cr.NS_ERROR_FAILURE); + + return this.encodeBase64(keydata.contents.data, keydata.contents.len); + } catch (e) { + this.log("generateRandomKey: failed: " + e); + throw e; + } finally { + if (randKey && !randKey.isNull()) + this.nss.PK11_FreeSymKey(randKey); + if (slot && !slot.isNull()) + this.nss.PK11_FreeSlot(slot); + } + }, + + generateRandomIV : function() this.generateRandomBytes(this.ivLength), + + generateRandomBytes : function(byteCount) { + this.log("generateRandomBytes() called"); + + // Temporary buffer to hold the generated data. + let scratch = this._getRandomByteBuffer(byteCount); + if (this.nss.PK11_GenerateRandom(scratch, byteCount)) + throw Components.Exception("PK11_GenrateRandom failed", Cr.NS_ERROR_FAILURE); + + return this.encodeBase64(this._randomByteBufferAddr, byteCount); + }, + + // + // PK11SymKey memoization. + // + + // Memoize the lookup of symmetric keys. We do this by using the base64 + // string itself as a key -- the overhead of SECItem creation during the + // initial population is negligible, so that phase is not memoized. + _encryptionSymKeyMemo: {}, + _decryptionSymKeyMemo: {}, + importSymKey: function importSymKey(encodedKeyString, operation) { + let memo; + + // We use two separate memos for thoroughness: operation is an input to + // key import. + switch (operation) { + case this.nss.CKA_ENCRYPT: + memo = this._encryptionSymKeyMemo; + break; + case this.nss.CKA_DECRYPT: + memo = this._decryptionSymKeyMemo; + break; + default: + throw "Unsupported operation in importSymKey."; + } + + if (encodedKeyString in memo) + return memo[encodedKeyString]; + + let keyItem, slot; + try { + keyItem = this.makeSECItem(encodedKeyString, true); + slot = this.nss.PK11_GetInternalKeySlot(); + if (slot.isNull()) + throw Components.Exception("can't get internal key slot", + Cr.NS_ERROR_FAILURE); + + let symKey = this.nss.PK11_ImportSymKey(slot, this.padMechanism, + this.nss.PK11_OriginUnwrap, + operation, keyItem, null); + if (!symKey || symKey.isNull()) + throw Components.Exception("symkey import failed", + Cr.NS_ERROR_FAILURE); + + return memo[encodedKeyString] = symKey; + } finally { + if (slot && !slot.isNull()) + this.nss.PK11_FreeSlot(slot); + this.freeSECItem(keyItem); + } + }, + + + // + // Utility functions + // + + /** + * Compress a JS string into a C uint8 array. count is the number of + * elements in the destination array. If the array is smaller than the + * string, the string is effectively truncated. If the string is smaller + * than the array, the array is not 0-padded. + */ + byteCompressInts : function byteCompressInts (jsString, intArray, count) { + let len = jsString.length; + let end = Math.min(len, count); + for (let i = 0; i < end; i++) + intArray[i] = jsString.charCodeAt(i) & 0xFF; // convert to bytes. + }, + + // Expand a normal C string (1-byte chars) into a JS string (2-byte chars) + // EG, for "ABC", 0x41, 0x42, 0x43 --> 0x0041, 0x0042, 0x0043 + byteExpand : function (charArray) { + let expanded = ""; + let len = charArray.length; + let intData = ctypes.cast(charArray, ctypes.uint8_t.array(len)); + for (let i = 0; i < len; i++) + expanded += String.fromCharCode(intData[i]); + return expanded; + }, + + expandData : function expandData(data, len) { + // Byte-expand the buffer, so we can treat it as a UCS-2 string + // consisting of u0000 - u00FF. + let expanded = ""; + let intData = ctypes.cast(data, ctypes.uint8_t.array(len).ptr).contents; + for (let i = 0; i < len; i++) + expanded += String.fromCharCode(intData[i]); + return expanded; + }, + + encodeBase64 : function (data, len) { + return btoa(this.expandData(data, len)); + }, + + // Returns a filled SECItem *, as returned by SECITEM_AllocItem. + // + // Note that this must be released with freeSECItem, which will also + // deallocate the internal buffer. + makeSECItem : function(input, isEncoded) { + if (isEncoded) + input = atob(input); + + let len = input.length; + let item = this.nss.SECITEM_AllocItem(null, null, len); + if (item.isNull()) + throw "SECITEM_AllocItem failed."; + + let ptr = ctypes.cast(item.contents.data, + ctypes.unsigned_char.array(len).ptr); + let dest = ctypes.cast(ptr.contents, ctypes.uint8_t.array(len)); + this.byteCompressInts(input, dest, len); + return item; + }, + + freeSECItem : function(zap) { + if (zap && !zap.isNull()) + this.nss.SECITEM_ZfreeItem(zap, true); + }, + + // We only ever handle one IV at a time, and they're always different. + // Consequently, we maintain a single SECItem, and a handy pointer into its + // contents to avoid repetitive and expensive casts. + _ivSECItem: null, + _ivSECItemContents: null, + + initIVSECItem: function initIVSECItem() { + if (this._ivSECItem) { + this._ivSECItemContents = null; + this.freeSECItem(this._ivSECItem); + } + + let item = this.nss.SECITEM_AllocItem(null, null, this.blockSize); + if (item.isNull()) + throw "SECITEM_AllocItem failed."; + + let ptr = ctypes.cast(item.contents.data, + ctypes.unsigned_char.array(this.blockSize).ptr); + let contents = ctypes.cast(ptr.contents, + ctypes.uint8_t.array(this.blockSize)); + this._ivSECItem = item; + this._ivSECItemContents = contents; + }, + + /** + * Returns the expanded data string for the derived key. + */ + deriveKeyFromPassphrase : function deriveKeyFromPassphrase(passphrase, salt, keyLength) { + this.log("deriveKeyFromPassphrase() called."); + let passItem = this.makeSECItem(passphrase, false); + let saltItem = this.makeSECItem(salt, true); + + let pbeAlg = ALGORITHM; + let cipherAlg = ALGORITHM; // Ignored by callee when pbeAlg != a pkcs5 mech. + + // Callee picks if SEC_OID_UNKNOWN, but only SHA1 is supported. + let prfAlg = this.nss.SEC_OID_HMAC_SHA1; + + let keyLength = keyLength || 0; // 0 = Callee will pick. + let iterations = KEY_DERIVATION_ITERATIONS; + + let algid, slot, symKey, keyData; + try { + algid = this.nss.PK11_CreatePBEV2AlgorithmID(pbeAlg, cipherAlg, prfAlg, + keyLength, iterations, + saltItem); + if (algid.isNull()) + throw Components.Exception("PK11_CreatePBEV2AlgorithmID failed", Cr.NS_ERROR_FAILURE); + + slot = this.nss.PK11_GetInternalSlot(); + if (slot.isNull()) + throw Components.Exception("couldn't get internal slot", Cr.NS_ERROR_FAILURE); + + symKey = this.nss.PK11_PBEKeyGen(slot, algid, passItem, false, null); + if (symKey.isNull()) + throw Components.Exception("PK11_PBEKeyGen failed", Cr.NS_ERROR_FAILURE); + + // Take the PK11SymKeyStr, returning the extracted key data. + if (this.nss.PK11_ExtractKeyValue(symKey)) { + throw this.makeException("PK11_ExtractKeyValue failed.", Cr.NS_ERROR_FAILURE); + } + + keyData = this.nss.PK11_GetKeyData(symKey); + + if (keyData.isNull()) + throw Components.Exception("PK11_GetKeyData failed", Cr.NS_ERROR_FAILURE); + + // This copies the key contents into a JS string, so we don't leak. + // The `finally` block below will clean up. + return this.expandData(keyData.contents.data, keyData.contents.len); + + } catch (e) { + this.log("deriveKeyFromPassphrase: failed: " + e); + throw e; + } finally { + if (algid && !algid.isNull()) + this.nss.SECOID_DestroyAlgorithmID(algid, true); + if (slot && !slot.isNull()) + this.nss.PK11_FreeSlot(slot); + if (symKey && !symKey.isNull()) + this.nss.PK11_FreeSymKey(symKey); + + this.freeSECItem(passItem); + this.freeSECItem(saltItem); + } + }, +};