diff options
Diffstat (limited to 'services/crypto')
21 files changed, 2890 insertions, 0 deletions
diff --git a/services/crypto/component/moz.build b/services/crypto/component/moz.build new file mode 100644 index 0000000000..f251bbd570 --- /dev/null +++ b/services/crypto/component/moz.build @@ -0,0 +1,19 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +XPCSHELL_TESTS_MANIFESTS += ['tests/unit/xpcshell.ini'] + +XPIDL_SOURCES += [ + 'nsISyncJPAKE.idl', +] + +XPIDL_MODULE = 'services-crypto-component' + +SOURCES += [ + 'nsSyncJPAKE.cpp', +] + +FINAL_LIBRARY = 'xul' diff --git a/services/crypto/component/nsISyncJPAKE.idl b/services/crypto/component/nsISyncJPAKE.idl new file mode 100644 index 0000000000..864057235c --- /dev/null +++ b/services/crypto/component/nsISyncJPAKE.idl @@ -0,0 +1,103 @@ +/* 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/. */ + +#include "nsISupports.idl" + +[scriptable, uuid(5ab02a98-5122-4b90-93cd-f259c4b42e3a)] +interface nsISyncJPAKE : nsISupports +{ + /** + * Perform first round of the JPAKE exchange. + * + * @param aSignerID + * String identifying the signer. + * @param aGX1 + * Schnorr signature value g^x1, in hex representation. + * @param aGV1 + * Schnorr signature value g^v1 (v1 is a random value), in hex + * representation. + * @param aR1 + * Schnorr signature value r1 = v1 - x1 * h, in hex representation. + * @param aGX2 + * Schnorr signature value g^x2, in hex representation. + * @param aGV2 + * Schnorr signature value g^v2 (v2 is a random value), in hex + * representation. + * @param aR2 + * Schnorr signature value r2 = v2 - x2 * h, in hex representation. + */ + void round1(in ACString aSignerID, + out ACString aGX1, + out ACString aGV1, + out ACString aR1, + out ACString aGX2, + out ACString aGV2, + out ACString aR2); + + /** + * Perform second round of the JPAKE exchange. + * + * @param aPeerID + * String identifying the peer. + * @param aPIN + * String containing the weak secret (PIN). + * @param aGX3 + * Schnorr signature value g^x3, in hex representation. + * @param aGV3 + * Schnorr signature value g^v3 (v3 is a random value), in hex + * representation. + * @param aR3 + * Schnorr signature value r3 = v3 - x3 * h, in hex representation. + * @param aGX4 + * Schnorr signature value g^x4, in hex representation. + * @param aGV4 + * Schnorr signature value g^v4 (v4 is a random value), in hex + * representation. + * @param aR4 + * Schnorr signature value r4 = v4 - x4 * h, in hex representation. + * @param aA + * Schnorr signature value A, in hex representation. + * @param aGVA + * Schnorr signature value g^va (va is a random value), in hex + * representation. + * @param aRA + * Schnorr signature value ra = va - xa * h, in hex representation. + */ + void round2(in ACString aPeerID, + in ACString aPIN, + in ACString aGX3, + in ACString aGV3, + in ACString aR3, + in ACString aGX4, + in ACString aGV4, + in ACString aR4, + out ACString aA, + out ACString aGVA, + out ACString aRA); + + /** + * Perform the final step of the JPAKE exchange. This will compute + * the key and expand the key to two keys, an AES256 encryption key + * and a 256 bit HMAC key. It returns a key confirmation value + * (SHA256d of the key) and the encryption and HMAC keys. + * + * @param aB + * Schnorr signature value B, in hex representation. + * @param aGVB + * Schnorr signature value g^vb (vb is a random value), in hex + * representation. + * @param aRB + * Schnorr signature value rb = vb - xb * h, in hex representation. + * @param aAES256Key + * The AES 256 encryption key, in base64 representation. + * @param aHMAC256Key + * The 256 bit HMAC key, in base64 representation. + */ + void final(in ACString aB, + in ACString aGVB, + in ACString aRB, + in ACString aHkdfInfo, + out ACString aAES256Key, + out ACString aHMAC256Key); +}; diff --git a/services/crypto/component/nsSyncJPAKE.cpp b/services/crypto/component/nsSyncJPAKE.cpp new file mode 100644 index 0000000000..23378f56a6 --- /dev/null +++ b/services/crypto/component/nsSyncJPAKE.cpp @@ -0,0 +1,484 @@ +/* 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/. */ + +#include "nsSyncJPAKE.h" + +#include "base64.h" +#include "keyhi.h" +#include "mozilla/ModuleUtils.h" +#include "mozilla/Move.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsString.h" +#include "nscore.h" +#include "pk11pub.h" +#include "pkcs11.h" +#include "secerr.h" +#include "secmodt.h" +#include "secport.h" + +using mozilla::fallible; + +static bool +hex_from_2char(const unsigned char *c2, unsigned char *byteval) +{ + int i; + unsigned char offset; + *byteval = 0; + for (i=0; i<2; i++) { + if (c2[i] >= '0' && c2[i] <= '9') { + offset = c2[i] - '0'; + *byteval |= offset << 4*(1-i); + } else if (c2[i] >= 'a' && c2[i] <= 'f') { + offset = c2[i] - 'a'; + *byteval |= (offset + 10) << 4*(1-i); + } else if (c2[i] >= 'A' && c2[i] <= 'F') { + offset = c2[i] - 'A'; + *byteval |= (offset + 10) << 4*(1-i); + } else { + return false; + } + } + return true; +} + +static bool +fromHex(const char * str, unsigned char * p, size_t sLen) +{ + size_t i; + if (sLen & 1) + return false; + + for (i = 0; i < sLen / 2; ++i) { + if (!hex_from_2char((const unsigned char *) str + (2*i), + (unsigned char *) p + i)) { + return false; + } + } + return true; +} + +static nsresult +fromHexString(const nsACString & str, unsigned char * p, size_t pMaxLen) +{ + char * strData = (char *) str.Data(); + unsigned len = str.Length(); + NS_ENSURE_ARG(len / 2 <= pMaxLen); + if (!fromHex(strData, p, len)) { + return NS_ERROR_INVALID_ARG; + } + return NS_OK; +} + +static bool +toHexString(const unsigned char * str, unsigned len, nsACString & out) +{ + static const char digits[] = "0123456789ABCDEF"; + if (!out.SetCapacity(2 * len, fallible)) + return false; + out.SetLength(0); + for (unsigned i = 0; i < len; ++i) { + out.Append(digits[str[i] >> 4]); + out.Append(digits[str[i] & 0x0f]); + } + return true; +} + +static nsresult +mapErrno() +{ + int err = PORT_GetError(); + switch (err) { + case SEC_ERROR_NO_MEMORY: return NS_ERROR_OUT_OF_MEMORY; + default: return NS_ERROR_UNEXPECTED; + } +} + +#define NUM_ELEM(x) (sizeof(x) / sizeof (x)[0]) + +static const char p[] = + "90066455B5CFC38F9CAA4A48B4281F292C260FEEF01FD61037E56258A7795A1C" + "7AD46076982CE6BB956936C6AB4DCFE05E6784586940CA544B9B2140E1EB523F" + "009D20A7E7880E4E5BFA690F1B9004A27811CD9904AF70420EEFD6EA11EF7DA1" + "29F58835FF56B89FAA637BC9AC2EFAAB903402229F491D8D3485261CD068699B" + "6BA58A1DDBBEF6DB51E8FE34E8A78E542D7BA351C21EA8D8F1D29F5D5D159394" + "87E27F4416B0CA632C59EFD1B1EB66511A5A0FBF615B766C5862D0BD8A3FE7A0" + "E0DA0FB2FE1FCB19E8F9996A8EA0FCCDE538175238FC8B0EE6F29AF7F642773E" + "BE8CD5402415A01451A840476B2FCEB0E388D30D4B376C37FE401C2A2C2F941D" + "AD179C540C1C8CE030D460C4D983BE9AB0B20F69144C1AE13F9383EA1C08504F" + "B0BF321503EFE43488310DD8DC77EC5B8349B8BFE97C2C560EA878DE87C11E3D" + "597F1FEA742D73EEC7F37BE43949EF1A0D15C3F3E3FC0A8335617055AC91328E" + "C22B50FC15B941D3D1624CD88BC25F3E941FDDC6200689581BFEC416B4B2CB73"; +static const char q[] = + "CFA0478A54717B08CE64805B76E5B14249A77A4838469DF7F7DC987EFCCFB11D"; +static const char g[] = + "5E5CBA992E0A680D885EB903AEA78E4A45A469103D448EDE3B7ACCC54D521E37" + "F84A4BDD5B06B0970CC2D2BBB715F7B82846F9A0C393914C792E6A923E2117AB" + "805276A975AADB5261D91673EA9AAFFEECBFA6183DFCB5D3B7332AA19275AFA1" + "F8EC0B60FB6F66CC23AE4870791D5982AAD1AA9485FD8F4A60126FEB2CF05DB8" + "A7F0F09B3397F3937F2E90B9E5B9C9B6EFEF642BC48351C46FB171B9BFA9EF17" + "A961CE96C7E7A7CC3D3D03DFAD1078BA21DA425198F07D2481622BCE45969D9C" + "4D6063D72AB7A0F08B2F49A7CC6AF335E08C4720E31476B67299E231F8BD90B3" + "9AC3AE3BE0C6B6CACEF8289A2E2873D58E51E029CAFBD55E6841489AB66B5B4B" + "9BA6E2F784660896AFF387D92844CCB8B69475496DE19DA2E58259B090489AC8" + "E62363CDF82CFD8EF2A427ABCD65750B506F56DDE3B988567A88126B914D7828" + "E2B63A6D7ED0747EC59E0E0A23CE7D8A74C1D2C2A7AFB6A29799620F00E11C33" + "787F7DED3B30E1A22D09F1FBDA1ABBBFBF25CAE05A13F812E34563F99410E73B"; + +NS_IMETHODIMP nsSyncJPAKE::Round1(const nsACString & aSignerID, + nsACString & aGX1, + nsACString & aGV1, + nsACString & aR1, + nsACString & aGX2, + nsACString & aGV2, + nsACString & aR2) +{ + nsNSSShutDownPreventionLock locker; + if (isAlreadyShutDown()) { + return NS_ERROR_NOT_AVAILABLE; + } + + NS_ENSURE_STATE(round == JPAKENotStarted); + NS_ENSURE_STATE(key == nullptr); + + static CK_MECHANISM_TYPE mechanisms[] = { + CKM_NSS_JPAKE_ROUND1_SHA256, + CKM_NSS_JPAKE_ROUND2_SHA256, + CKM_NSS_JPAKE_FINAL_SHA256 + }; + + UniquePK11SlotInfo slot(PK11_GetBestSlotMultiple(mechanisms, + NUM_ELEM(mechanisms), + nullptr)); + NS_ENSURE_STATE(slot != nullptr); + + CK_BYTE pBuf[(NUM_ELEM(p) - 1) / 2]; + CK_BYTE qBuf[(NUM_ELEM(q) - 1) / 2]; + CK_BYTE gBuf[(NUM_ELEM(g) - 1) / 2]; + + CK_KEY_TYPE keyType = CKK_NSS_JPAKE_ROUND1; + NS_ENSURE_STATE(fromHex(p, pBuf, (NUM_ELEM(p) - 1))); + NS_ENSURE_STATE(fromHex(q, qBuf, (NUM_ELEM(q) - 1))); + NS_ENSURE_STATE(fromHex(g, gBuf, (NUM_ELEM(g) - 1))); + CK_ATTRIBUTE keyTemplate[] = { + { CKA_NSS_JPAKE_SIGNERID, (CK_BYTE *) aSignerID.Data(), + aSignerID.Length() }, + { CKA_KEY_TYPE, &keyType, sizeof keyType }, + { CKA_PRIME, pBuf, sizeof pBuf }, + { CKA_SUBPRIME, qBuf, sizeof qBuf }, + { CKA_BASE, gBuf, sizeof gBuf } + }; + + CK_BYTE gx1Buf[NUM_ELEM(p) / 2]; + CK_BYTE gv1Buf[NUM_ELEM(p) / 2]; + CK_BYTE r1Buf [NUM_ELEM(p) / 2]; + CK_BYTE gx2Buf[NUM_ELEM(p) / 2]; + CK_BYTE gv2Buf[NUM_ELEM(p) / 2]; + CK_BYTE r2Buf [NUM_ELEM(p) / 2]; + CK_NSS_JPAKERound1Params rp = { + { gx1Buf, sizeof gx1Buf, gv1Buf, sizeof gv1Buf, r1Buf, sizeof r1Buf }, + { gx2Buf, sizeof gx2Buf, gv2Buf, sizeof gv2Buf, r2Buf, sizeof r2Buf } + }; + SECItem paramsItem; + paramsItem.data = (unsigned char *) &rp; + paramsItem.len = sizeof rp; + key = UniquePK11SymKey( + PK11_KeyGenWithTemplate(slot.get(), CKM_NSS_JPAKE_ROUND1_SHA256, + CKM_NSS_JPAKE_ROUND1_SHA256, ¶msItem, + keyTemplate, NUM_ELEM(keyTemplate), nullptr)); + nsresult rv = key != nullptr + ? NS_OK + : mapErrno(); + if (rv == NS_OK) { + NS_ENSURE_TRUE(toHexString(rp.gx1.pGX, rp.gx1.ulGXLen, aGX1) && + toHexString(rp.gx1.pGV, rp.gx1.ulGVLen, aGV1) && + toHexString(rp.gx1.pR, rp.gx1.ulRLen, aR1) && + toHexString(rp.gx2.pGX, rp.gx2.ulGXLen, aGX2) && + toHexString(rp.gx2.pGV, rp.gx2.ulGVLen, aGV2) && + toHexString(rp.gx2.pR, rp.gx2.ulRLen, aR2), + NS_ERROR_OUT_OF_MEMORY); + round = JPAKEBeforeRound2; + } + return rv; +} + +NS_IMETHODIMP nsSyncJPAKE::Round2(const nsACString & aPeerID, + const nsACString & aPIN, + const nsACString & aGX3, + const nsACString & aGV3, + const nsACString & aR3, + const nsACString & aGX4, + const nsACString & aGV4, + const nsACString & aR4, + nsACString & aA, + nsACString & aGVA, + nsACString & aRA) +{ + nsNSSShutDownPreventionLock locker; + if (isAlreadyShutDown()) { + return NS_ERROR_NOT_AVAILABLE; + } + + NS_ENSURE_STATE(round == JPAKEBeforeRound2); + NS_ENSURE_STATE(key != nullptr); + NS_ENSURE_ARG(!aPeerID.IsEmpty()); + + /* PIN cannot be equal to zero when converted to a bignum. NSS 3.12.9 J-PAKE + assumes that the caller has already done this check. Future versions of + NSS J-PAKE will do this check internally. See Bug 609068 Comment 4 */ + bool foundNonZero = false; + for (size_t i = 0; i < aPIN.Length(); ++i) { + if (aPIN[i] != 0) { + foundNonZero = true; + break; + } + } + NS_ENSURE_ARG(foundNonZero); + + CK_BYTE gx3Buf[NUM_ELEM(p)/2], gv3Buf[NUM_ELEM(p)/2], r3Buf [NUM_ELEM(p)/2]; + CK_BYTE gx4Buf[NUM_ELEM(p)/2], gv4Buf[NUM_ELEM(p)/2], r4Buf [NUM_ELEM(p)/2]; + CK_BYTE gxABuf[NUM_ELEM(p)/2], gvABuf[NUM_ELEM(p)/2], rABuf [NUM_ELEM(p)/2]; + nsresult rv = fromHexString(aGX3, gx3Buf, sizeof gx3Buf); + if (rv == NS_OK) rv = fromHexString(aGV3, gv3Buf, sizeof gv3Buf); + if (rv == NS_OK) rv = fromHexString(aR3, r3Buf, sizeof r3Buf); + if (rv == NS_OK) rv = fromHexString(aGX4, gx4Buf, sizeof gx4Buf); + if (rv == NS_OK) rv = fromHexString(aGV4, gv4Buf, sizeof gv4Buf); + if (rv == NS_OK) rv = fromHexString(aR4, r4Buf, sizeof r4Buf); + if (rv != NS_OK) + return rv; + + CK_NSS_JPAKERound2Params rp; + rp.pSharedKey = (CK_BYTE *) aPIN.Data(); + rp.ulSharedKeyLen = aPIN.Length(); + rp.gx3.pGX = gx3Buf; rp.gx3.ulGXLen = aGX3.Length() / 2; + rp.gx3.pGV = gv3Buf; rp.gx3.ulGVLen = aGV3.Length() / 2; + rp.gx3.pR = r3Buf; rp.gx3.ulRLen = aR3 .Length() / 2; + rp.gx4.pGX = gx4Buf; rp.gx4.ulGXLen = aGX4.Length() / 2; + rp.gx4.pGV = gv4Buf; rp.gx4.ulGVLen = aGV4.Length() / 2; + rp.gx4.pR = r4Buf; rp.gx4.ulRLen = aR4 .Length() / 2; + rp.A.pGX = gxABuf; rp.A .ulGXLen = sizeof gxABuf; + rp.A.pGV = gvABuf; rp.A .ulGVLen = sizeof gxABuf; + rp.A.pR = rABuf; rp.A .ulRLen = sizeof gxABuf; + + // Bug 629090: NSS 3.12.9 J-PAKE fails to check that gx^4 != 1, so check here. + bool gx4Good = false; + for (unsigned i = 0; i < rp.gx4.ulGXLen; ++i) { + if (rp.gx4.pGX[i] > 1 || (rp.gx4.pGX[i] != 0 && i < rp.gx4.ulGXLen - 1)) { + gx4Good = true; + break; + } + } + NS_ENSURE_ARG(gx4Good); + + SECItem paramsItem; + paramsItem.data = (unsigned char *) &rp; + paramsItem.len = sizeof rp; + CK_KEY_TYPE keyType = CKK_NSS_JPAKE_ROUND2; + CK_ATTRIBUTE keyTemplate[] = { + { CKA_NSS_JPAKE_PEERID, (CK_BYTE *) aPeerID.Data(), aPeerID.Length(), }, + { CKA_KEY_TYPE, &keyType, sizeof keyType } + }; + UniquePK11SymKey newKey(PK11_DeriveWithTemplate(key.get(), + CKM_NSS_JPAKE_ROUND2_SHA256, + ¶msItem, + CKM_NSS_JPAKE_FINAL_SHA256, + CKA_DERIVE, 0, + keyTemplate, + NUM_ELEM(keyTemplate), + false)); + if (newKey != nullptr) { + if (toHexString(rp.A.pGX, rp.A.ulGXLen, aA) && + toHexString(rp.A.pGV, rp.A.ulGVLen, aGVA) && + toHexString(rp.A.pR, rp.A.ulRLen, aRA)) { + round = JPAKEAfterRound2; + key = Move(newKey); + return NS_OK; + } else { + rv = NS_ERROR_OUT_OF_MEMORY; + } + } else { + rv = mapErrno(); + } + + return rv; +} + +static nsresult +setBase64(const unsigned char * data, unsigned len, nsACString & out) +{ + nsresult rv = NS_OK; + const char * base64 = BTOA_DataToAscii(data, len); + + if (base64 != nullptr) { + size_t len = PORT_Strlen(base64); + if (out.SetCapacity(len, fallible)) { + out.SetLength(0); + out.Append(base64, len); + } else { + rv = NS_ERROR_OUT_OF_MEMORY; + } + PORT_Free((void*) base64); + } else { + rv = NS_ERROR_OUT_OF_MEMORY; + } + return rv; +} + +static nsresult +base64KeyValue(PK11SymKey * key, nsACString & keyString) +{ + nsresult rv = NS_OK; + if (PK11_ExtractKeyValue(key) == SECSuccess) { + const SECItem * value = PK11_GetKeyData(key); + rv = value != nullptr && value->data != nullptr && value->len > 0 + ? setBase64(value->data, value->len, keyString) + : NS_ERROR_UNEXPECTED; + } else { + rv = mapErrno(); + } + return rv; +} + +static nsresult +extractBase64KeyValue(UniquePK11SymKey & keyBlock, CK_ULONG bitPosition, + CK_MECHANISM_TYPE destMech, int keySize, + nsACString & keyString) +{ + SECItem paramsItem; + paramsItem.data = (CK_BYTE *) &bitPosition; + paramsItem.len = sizeof bitPosition; + PK11SymKey * key = PK11_Derive(keyBlock.get(), CKM_EXTRACT_KEY_FROM_KEY, + ¶msItem, destMech, + CKA_SIGN, keySize); + if (key == nullptr) + return mapErrno(); + nsresult rv = base64KeyValue(key, keyString); + PK11_FreeSymKey(key); + return rv; +} + + +NS_IMETHODIMP nsSyncJPAKE::Final(const nsACString & aB, + const nsACString & aGVB, + const nsACString & aRB, + const nsACString & aHKDFInfo, + nsACString & aAES256Key, + nsACString & aHMAC256Key) +{ + nsNSSShutDownPreventionLock locker; + if (isAlreadyShutDown()) { + return NS_ERROR_NOT_AVAILABLE; + } + + static const unsigned AES256_KEY_SIZE = 256 / 8; + static const unsigned HMAC_SHA256_KEY_SIZE = 256 / 8; + CK_EXTRACT_PARAMS aesBitPosition = 0; + CK_EXTRACT_PARAMS hmacBitPosition = aesBitPosition + (AES256_KEY_SIZE * 8); + + NS_ENSURE_STATE(round == JPAKEAfterRound2); + NS_ENSURE_STATE(key != nullptr); + + CK_BYTE gxBBuf[NUM_ELEM(p)/2], gvBBuf[NUM_ELEM(p)/2], rBBuf [NUM_ELEM(p)/2]; + nsresult rv = fromHexString(aB, gxBBuf, sizeof gxBBuf); + if (rv == NS_OK) rv = fromHexString(aGVB, gvBBuf, sizeof gvBBuf); + if (rv == NS_OK) rv = fromHexString(aRB, rBBuf, sizeof rBBuf); + if (rv != NS_OK) + return rv; + + CK_NSS_JPAKEFinalParams rp; + rp.B.pGX = gxBBuf; rp.B.ulGXLen = aB .Length() / 2; + rp.B.pGV = gvBBuf; rp.B.ulGVLen = aGVB.Length() / 2; + rp.B.pR = rBBuf; rp.B.ulRLen = aRB .Length() / 2; + SECItem paramsItem; + paramsItem.data = (unsigned char *) &rp; + paramsItem.len = sizeof rp; + UniquePK11SymKey keyMaterial(PK11_Derive(key.get(), CKM_NSS_JPAKE_FINAL_SHA256, + ¶msItem, CKM_NSS_HKDF_SHA256, + CKA_DERIVE, 0)); + UniquePK11SymKey keyBlock; + + if (keyMaterial == nullptr) + rv = mapErrno(); + + if (rv == NS_OK) { + CK_NSS_HKDFParams hkdfParams; + hkdfParams.bExtract = CK_TRUE; + hkdfParams.pSalt = nullptr; + hkdfParams.ulSaltLen = 0; + hkdfParams.bExpand = CK_TRUE; + hkdfParams.pInfo = (CK_BYTE *) aHKDFInfo.Data(); + hkdfParams.ulInfoLen = aHKDFInfo.Length(); + paramsItem.data = (unsigned char *) &hkdfParams; + paramsItem.len = sizeof hkdfParams; + keyBlock = UniquePK11SymKey( + PK11_Derive(keyMaterial.get(), CKM_NSS_HKDF_SHA256, ¶msItem, + CKM_EXTRACT_KEY_FROM_KEY, CKA_DERIVE, + AES256_KEY_SIZE + HMAC_SHA256_KEY_SIZE)); + if (keyBlock == nullptr) + rv = mapErrno(); + } + + if (rv == NS_OK) { + rv = extractBase64KeyValue(keyBlock, aesBitPosition, CKM_AES_CBC, + AES256_KEY_SIZE, aAES256Key); + } + if (rv == NS_OK) { + rv = extractBase64KeyValue(keyBlock, hmacBitPosition, CKM_SHA256_HMAC, + HMAC_SHA256_KEY_SIZE, aHMAC256Key); + } + + if (rv == NS_OK) { + SECStatus srv = PK11_ExtractKeyValue(keyMaterial.get()); + NS_ENSURE_TRUE(srv == SECSuccess, NS_ERROR_UNEXPECTED); + SECItem * keyMaterialBytes = PK11_GetKeyData(keyMaterial.get()); + NS_ENSURE_TRUE(keyMaterialBytes != nullptr, NS_ERROR_UNEXPECTED); + } + + return rv; +} + +NS_GENERIC_FACTORY_CONSTRUCTOR(nsSyncJPAKE) +NS_DEFINE_NAMED_CID(NS_SYNCJPAKE_CID); + +nsSyncJPAKE::nsSyncJPAKE() : round(JPAKENotStarted), key(nullptr) { } + +nsSyncJPAKE::~nsSyncJPAKE() +{ + nsNSSShutDownPreventionLock locker; + if (isAlreadyShutDown()) { + return; + } + destructorSafeDestroyNSSReference(); + shutdown(ShutdownCalledFrom::Object); +} + +void +nsSyncJPAKE::virtualDestroyNSSReference() +{ + destructorSafeDestroyNSSReference(); +} + +void +nsSyncJPAKE::destructorSafeDestroyNSSReference() +{ + key = nullptr; +} + +static const mozilla::Module::CIDEntry kServicesCryptoCIDs[] = { + { &kNS_SYNCJPAKE_CID, false, nullptr, nsSyncJPAKEConstructor }, + { nullptr } +}; + +static const mozilla::Module::ContractIDEntry kServicesCryptoContracts[] = { + { NS_SYNCJPAKE_CONTRACTID, &kNS_SYNCJPAKE_CID }, + { nullptr } +}; + +static const mozilla::Module kServicesCryptoModule = { + mozilla::Module::kVersion, + kServicesCryptoCIDs, + kServicesCryptoContracts +}; + +NSMODULE_DEFN(nsServicesCryptoModule) = &kServicesCryptoModule; diff --git a/services/crypto/component/nsSyncJPAKE.h b/services/crypto/component/nsSyncJPAKE.h new file mode 100644 index 0000000000..0c737d9979 --- /dev/null +++ b/services/crypto/component/nsSyncJPAKE.h @@ -0,0 +1,38 @@ +/* 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/. */ +#ifndef nsSyncJPAKE_h__ +#define nsSyncJPAKE_h__ + +#include "ScopedNSSTypes.h" +#include "nsISyncJPAKE.h" +#include "nsNSSShutDown.h" + +#define NS_SYNCJPAKE_CONTRACTID \ + "@mozilla.org/services-crypto/sync-jpake;1" + +#define NS_SYNCJPAKE_CID \ + {0x0b9721c0, 0x1805, 0x47c3, {0x86, 0xce, 0x68, 0x13, 0x79, 0x5a, 0x78, 0x3f}} + +using namespace mozilla; + +class nsSyncJPAKE : public nsISyncJPAKE + , public nsNSSShutDownObject +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSISYNCJPAKE + nsSyncJPAKE(); +protected: + virtual ~nsSyncJPAKE(); +private: + virtual void virtualDestroyNSSReference() override; + void destructorSafeDestroyNSSReference(); + + enum { JPAKENotStarted, JPAKEBeforeRound2, JPAKEAfterRound2 } round; + UniquePK11SymKey key; +}; + +NS_IMPL_ISUPPORTS(nsSyncJPAKE, nsISyncJPAKE) + +#endif // nsSyncJPAKE_h__ diff --git a/services/crypto/component/tests/unit/test_jpake.js b/services/crypto/component/tests/unit/test_jpake.js new file mode 100644 index 0000000000..4e9b25e1b2 --- /dev/null +++ b/services/crypto/component/tests/unit/test_jpake.js @@ -0,0 +1,289 @@ +var Cc = Components.classes; +var Ci = Components.interfaces; + +// Ensure PSM is initialized. +Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports); + +function do_check_throws(func) { + let have_error = false; + try { + func(); + } catch(ex) { + dump("Was expecting an exception. Caught: " + ex + "\n"); + have_error = true; + } + do_check_true(have_error); +} + +function test_success() { + let a = Cc["@mozilla.org/services-crypto/sync-jpake;1"] + .createInstance(Ci.nsISyncJPAKE); + let b = Cc["@mozilla.org/services-crypto/sync-jpake;1"] + .createInstance(Ci.nsISyncJPAKE); + + let a_gx1 = {}; + let a_gv1 = {}; + let a_r1 = {}; + let a_gx2 = {}; + let a_gv2 = {}; + let a_r2 = {}; + + let b_gx1 = {}; + let b_gv1 = {}; + let b_r1 = {}; + let b_gx2 = {}; + let b_gv2 = {}; + let b_r2 = {}; + + a.round1("alice", a_gx1, a_gv1, a_r1, a_gx2, a_gv2, a_r2); + b.round1("bob", b_gx1, b_gv1, b_r1, b_gx2, b_gv2, b_r2); + + let a_A = {}; + let a_gva = {}; + let a_ra = {}; + + let b_A = {}; + let b_gva = {}; + let b_ra = {}; + + a.round2("bob", "sekrit", b_gx1.value, b_gv1.value, b_r1.value, + b_gx2.value, b_gv2.value, b_r2.value, a_A, a_gva, a_ra); + b.round2("alice", "sekrit", a_gx1.value, a_gv1.value, a_r1.value, + a_gx2.value, a_gv2.value, a_r2.value, b_A, b_gva, b_ra); + + let a_aes = {}; + let a_hmac = {}; + let b_aes = {}; + let b_hmac = {}; + + a.final(b_A.value, b_gva.value, b_ra.value, "ohai", a_aes, a_hmac); + b.final(a_A.value, a_gva.value, a_ra.value, "ohai", b_aes, b_hmac); + + do_check_eq(a_aes.value, b_aes.value); + do_check_eq(a_hmac.value, b_hmac.value); +} + +function test_failure(modlen) { + let a = Cc["@mozilla.org/services-crypto/sync-jpake;1"] + .createInstance(Ci.nsISyncJPAKE); + let b = Cc["@mozilla.org/services-crypto/sync-jpake;1"] + .createInstance(Ci.nsISyncJPAKE); + + let a_gx1 = {}; + let a_gv1 = {}; + let a_r1 = {}; + let a_gx2 = {}; + let a_gv2 = {}; + let a_r2 = {}; + + let b_gx1 = {}; + let b_gv1 = {}; + let b_r1 = {}; + let b_gx2 = {}; + let b_gv2 = {}; + let b_r2 = {}; + + a.round1("alice", a_gx1, a_gv1, a_r1, a_gx2, a_gv2, a_r2); + b.round1("bob", b_gx1, b_gv1, b_r1, b_gx2, b_gv2, b_r2); + + let a_A = {}; + let a_gva = {}; + let a_ra = {}; + + let b_A = {}; + let b_gva = {}; + let b_ra = {}; + + // Note how the PINs are different (secret vs. sekrit) + a.round2("bob", "secret", b_gx1.value, b_gv1.value, b_r1.value, + b_gx2.value, b_gv2.value, b_r2.value, a_A, a_gva, a_ra); + b.round2("alice", "sekrit", a_gx1.value, a_gv1.value, a_r1.value, + a_gx2.value, a_gv2.value, a_r2.value, b_A, b_gva, b_ra); + + let a_aes = {}; + let a_hmac = {}; + let b_aes = {}; + let b_hmac = {}; + + a.final(b_A.value, b_gva.value, b_ra.value, "ohai", a_aes, a_hmac); + b.final(a_A.value, a_gva.value, a_ra.value, "ohai", b_aes, b_hmac); + + do_check_neq(a_aes.value, b_aes.value); + do_check_neq(a_hmac.value, b_hmac.value); +} + +function test_same_signerids() { + let a = Cc["@mozilla.org/services-crypto/sync-jpake;1"] + .createInstance(Ci.nsISyncJPAKE); + let b = Cc["@mozilla.org/services-crypto/sync-jpake;1"] + .createInstance(Ci.nsISyncJPAKE); + + let gx1 = {}; + let gv1 = {}; + let r1 = {}; + let gx2 = {}; + let gv2 = {}; + let r2 = {}; + + a.round1("alice", {}, {}, {}, {}, {}, {}); + b.round1("alice", gx1, gv1, r1, gx2, gv2, r2); + do_check_throws(function() { + a.round2("alice", "sekrit", gx1.value, gv1.value, r1.value, + gx2.value, gv2.value, r2.value, {}, {}, {}); + }); +} + +function test_bad_zkp() { + let a = Cc["@mozilla.org/services-crypto/sync-jpake;1"] + .createInstance(Ci.nsISyncJPAKE); + let b = Cc["@mozilla.org/services-crypto/sync-jpake;1"] + .createInstance(Ci.nsISyncJPAKE); + + let gx1 = {}; + let gv1 = {}; + let r1 = {}; + let gx2 = {}; + let gv2 = {}; + let r2 = {}; + + a.round1("alice", {}, {}, {}, {}, {}, {}); + b.round1("bob", gx1, gv1, r1, gx2, gv2, r2); + do_check_throws(function() { + a.round2("invalid", "sekrit", gx1.value, gv1.value, r1.value, + gx2.value, gv2.value, r2.value, {}, {}, {}); + }); +} + +function test_x4_zero() { + // The PKCS#11 API for J-PAKE does not allow us to choose any of the nonces. + // In order to test the defence against x4 (mod p) == 1, we had to generate + // our own signed nonces using a the FreeBL JPAKE_Sign function directly. + // To verify the signatures are accurate, pass the given value of R as the + // "testRandom" parameter to FreeBL's JPAKE_Sign, along with the given values + // for X and GX, using signerID "alice". Then verify that each GV returned + // from JPAKE_Sign matches the value specified here. + let test = function(badGX, badX_GV, badX_R) { + let a = Cc["@mozilla.org/services-crypto/sync-jpake;1"] + .createInstance(Ci.nsISyncJPAKE); + let b = Cc["@mozilla.org/services-crypto/sync-jpake;1"] + .createInstance(Ci.nsISyncJPAKE); + + let a_gx1 = {}; + let a_gv1 = {}; + let a_r1 = {}; + let a_gx2 = {}; + let a_gv2 = {}; + let a_r2 = {}; + + let b_gx1 = {}; + let b_gv1 = {}; + let b_r1 = {}; + let b_gx2 = {}; + let b_gv2 = {}; + let b_r2 = {}; + + a.round1("alice", a_gx1, a_gv1, a_r1, a_gx2, a_gv2, a_r2); + b.round1("bob", b_gx1, b_gv1, b_r1, b_gx2, b_gv2, b_r2); + + // Replace the g^x2 generated by A with the given illegal value. + a_gx2.value = badGX; + a_gv2.value = badX_GV; + a_r2.value = badX_R; + + let b_A = {}; + let b_gva = {}; + let b_ra = {}; + + do_check_throws(function() { + b.round2("alice", "secret", a_gx1.value, a_gv1.value, a_r1.value, + a_gx2.value, a_gv2.value, a_r2.value, b_A, b_gva, b_ra); + }); + }; + + // g^x is NIST 3072's p + 1, (p + 1) mod p == 1, x == 0 + test("90066455B5CFC38F9CAA4A48B4281F292C260FEEF01FD61037E56258A7795A1C" + + "7AD46076982CE6BB956936C6AB4DCFE05E6784586940CA544B9B2140E1EB523F" + + "009D20A7E7880E4E5BFA690F1B9004A27811CD9904AF70420EEFD6EA11EF7DA1" + + "29F58835FF56B89FAA637BC9AC2EFAAB903402229F491D8D3485261CD068699B" + + "6BA58A1DDBBEF6DB51E8FE34E8A78E542D7BA351C21EA8D8F1D29F5D5D159394" + + "87E27F4416B0CA632C59EFD1B1EB66511A5A0FBF615B766C5862D0BD8A3FE7A0" + + "E0DA0FB2FE1FCB19E8F9996A8EA0FCCDE538175238FC8B0EE6F29AF7F642773E" + + "BE8CD5402415A01451A840476B2FCEB0E388D30D4B376C37FE401C2A2C2F941D" + + "AD179C540C1C8CE030D460C4D983BE9AB0B20F69144C1AE13F9383EA1C08504F" + + "B0BF321503EFE43488310DD8DC77EC5B8349B8BFE97C2C560EA878DE87C11E3D" + + "597F1FEA742D73EEC7F37BE43949EF1A0D15C3F3E3FC0A8335617055AC91328E" + + "C22B50FC15B941D3D1624CD88BC25F3E941FDDC6200689581BFEC416B4B2CB74", + "5386107A0DD4A96ECF8D9BCF864BDE23AAEF13351F5550D777A32C1FEC165ED67AE51" + + "66C3876AABC1FED1A0993754F3AEE256530F529548F8FE010BC0D070175569845" + + "CF009AD24BC897A9CA1F18E1A9CE421DD54FD93AB528BC2594B47791713165276" + + "7B76903190C3DCD2076FEC1E61FFFC32D1B07273B06EA2889E66FCBFD41FE8984" + + "5FCE36056B09D1F20E58BB6BAA07A32796F11998BEF0AB3D387E2FB4FE3073FEB" + + "634BA91709010A70DA29C06F8F92D638C4F158680EAFEB5E0E323BD7DACB671C0" + + "BA3EDEEAB5CAA243CABAB28E7205AC9A0AAEAFE132635DAC7FE001C19F880A96E" + + "395C42536D694F81B4F44DC66D7D6FBE933C56ABF585837291D8751C18EB1F3FB" + + "620582E6A7B795D699E38C270863A289583CB9D07651E6BA3B82BC656B49BD09B" + + "6B8C27F370120C7CB89D0829BE51D56356EA836012E9204FF4D1CA8B1B7F9C768" + + "4BB2B0F226FD4042EEBAD931FDBD4F81F8425B305752F5E37FFA2B73BB5A034EC" + + "7EEF5AAC92EA212897E3A2B8961D2147710ECCE127B942AB2", + "05CC4DF005FE006C11111624E14806E4A904A4D1D6A53E795AC7867A960CD4FD"); + + // x == 0 implies g^x == 1 + test("01", + "488759644532FA7C53E5239F2A365D4B9189582BDD2967A1852FE56568382B65" + + "C66BDFCD9B581EAEF4BB497CAF1290ECDFA47A1D1658DC5DC9248D9A4135" + + "DC70B6A8497CDF117236841FA18500DC696A92EEF5000ABE68E9C75B37BC" + + "6A722126BE728163AA90A6B03D5585994D3403557EEF08E819C72D143BBC" + + "CDF74559645066CB3607E1B0430365356389FC8FB3D66FD2B6E2E834EC23" + + "0B0234956752D07F983C918488C8E5A124B062D50B44C5E6FB36BCB03E39" + + "0385B17CF8062B6688371E6AF5915C2B1AAA31C9294943CC6DC1B994FC09" + + "49CA31828B83F3D6DFB081B26045DFD9F10092588B63F1D6E68881A06522" + + "5A417CA9555B036DE89D349AC794A43EB28FE320F9A321F06A9364C88B54" + + "99EEF4816375B119824ACC9AA56D1340B6A49D05F855DE699B351012028C" + + "CA43001F708CC61E71CA3849935BEEBABC0D268CD41B8D2B8DCA705FDFF8" + + "1DAA772DA96EDEA0B291FD5C0C1B8EFE5318D37EBC1BFF53A9DDEC4171A6" + + "479E341438970058E25C8F2BCDA6166C8BF1B065C174", + "8B2BACE575179D762F6F2FFDBFF00B497C07766AB3EED9961447CF6F43D06A97"); +} + +function test_invalid_input_round2() { + let a = Cc["@mozilla.org/services-crypto/sync-jpake;1"] + .createInstance(Ci.nsISyncJPAKE); + + a.round1("alice", {}, {}, {}, {}, {}, {}); + do_check_throws(function() { + a.round2("invalid", "sekrit", "some", "real", "garbage", + "even", "more", "garbage", {}, {}, {}); + }); +} + +function test_invalid_input_final() { + let a = Cc["@mozilla.org/services-crypto/sync-jpake;1"] + .createInstance(Ci.nsISyncJPAKE); + let b = Cc["@mozilla.org/services-crypto/sync-jpake;1"] + .createInstance(Ci.nsISyncJPAKE); + + let gx1 = {}; + let gv1 = {}; + let r1 = {}; + let gx2 = {}; + let gv2 = {}; + let r2 = {}; + + a.round1("alice", {}, {}, {}, {}, {}, {}); + b.round1("bob", gx1, gv1, r1, gx2, gv2, r2); + a.round2("bob", "sekrit", gx1.value, gv1.value, r1.value, + gx2.value, gv2.value, r2.value, {}, {}, {}); + do_check_throws(function() { + a.final("some", "garbage", "alright", "foobar-info", {}, {}); + }); +} + +function run_test() { + test_x4_zero(); + test_success(); + test_failure(); + test_same_signerids(); + test_bad_zkp(); + test_invalid_input_round2(); + test_invalid_input_final(); +} diff --git a/services/crypto/component/tests/unit/xpcshell.ini b/services/crypto/component/tests/unit/xpcshell.ini new file mode 100644 index 0000000000..83b6158882 --- /dev/null +++ b/services/crypto/component/tests/unit/xpcshell.ini @@ -0,0 +1,6 @@ +[DEFAULT] +head = +tail = +firefox-appdir = browser + +[test_jpake.js] diff --git a/services/crypto/cryptoComponents.manifest b/services/crypto/cryptoComponents.manifest new file mode 100644 index 0000000000..f9f47bb42a --- /dev/null +++ b/services/crypto/cryptoComponents.manifest @@ -0,0 +1 @@ +resource services-crypto resource://gre/modules/services-crypto/ diff --git a/services/crypto/modules/WeaveCrypto.js b/services/crypto/modules/WeaveCrypto.js new file mode 100644 index 0000000000..c040c4f6fc --- /dev/null +++ b/services/crypto/modules/WeaveCrypto.js @@ -0,0 +1,266 @@ +/* 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"]; + +var {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://services-common/async.js"); + +Cu.importGlobalProperties(['crypto']); + +const CRYPT_ALGO = "AES-CBC"; +const CRYPT_ALGO_LENGTH = 256; +const AES_CBC_IV_SIZE = 16; +const OPERATIONS = { ENCRYPT: 0, DECRYPT: 1 }; +const UTF_LABEL = "utf-8"; + +const KEY_DERIVATION_ALGO = "PBKDF2"; +const KEY_DERIVATION_HASHING_ALGO = "SHA-1"; +const KEY_DERIVATION_ITERATIONS = 4096; // PKCS#5 recommends at least 1000. +const DERIVED_KEY_ALGO = CRYPT_ALGO; + +this.WeaveCrypto = function WeaveCrypto() { + this.init(); +}; + +WeaveCrypto.prototype = { + prefBranch : null, + debug : true, // services.sync.log.cryptoDebug + + observer : { + _self : null, + + QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsISupportsWeakReference]), + + observe(subject, topic, data) { + let self = this._self; + self.log("Observed " + topic + " topic."); + if (topic == "nsPref:changed") { + self.debug = self.prefBranch.getBoolPref("cryptoDebug"); + } + } + }, + + init() { + // 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; + } + XPCOMUtils.defineLazyGetter(this, 'encoder', () => new TextEncoder(UTF_LABEL)); + XPCOMUtils.defineLazyGetter(this, 'decoder', () => new TextDecoder(UTF_LABEL, { fatal: true })); + }, + + log(message) { + if (!this.debug) { + return; + } + dump("WeaveCrypto: " + message + "\n"); + Services.console.logStringMessage("WeaveCrypto: " + message); + }, + + // /!\ Only use this for tests! /!\ + _getCrypto() { + return crypto; + }, + + encrypt(clearTextUCS2, symmetricKey, iv) { + this.log("encrypt() called"); + let clearTextBuffer = this.encoder.encode(clearTextUCS2).buffer; + let encrypted = this._commonCrypt(clearTextBuffer, symmetricKey, iv, OPERATIONS.ENCRYPT); + return this.encodeBase64(encrypted); + }, + + decrypt(cipherText, symmetricKey, iv) { + this.log("decrypt() called"); + if (cipherText.length) { + cipherText = atob(cipherText); + } + let cipherTextBuffer = this.byteCompressInts(cipherText); + let decrypted = this._commonCrypt(cipherTextBuffer, symmetricKey, iv, OPERATIONS.DECRYPT); + return this.decoder.decode(decrypted); + }, + + /** + * _commonCrypt + * + * @args + * data: data to encrypt/decrypt (ArrayBuffer) + * symKeyStr: symmetric key (Base64 String) + * ivStr: initialization vector (Base64 String) + * operation: operation to apply (either OPERATIONS.ENCRYPT or OPERATIONS.DECRYPT) + * @returns + * the encrypted/decrypted data (ArrayBuffer) + */ + _commonCrypt(data, symKeyStr, ivStr, operation) { + this.log("_commonCrypt() called"); + ivStr = atob(ivStr); + + if (operation !== OPERATIONS.ENCRYPT && operation !== OPERATIONS.DECRYPT) { + throw new Error("Unsupported operation in _commonCrypt."); + } + // We never want an IV longer than the block size, which is 16 bytes + // for AES, neither do we want one smaller; throw in both cases. + if (ivStr.length !== AES_CBC_IV_SIZE) { + throw "Invalid IV size; must be " + AES_CBC_IV_SIZE + " bytes."; + } + + let iv = this.byteCompressInts(ivStr); + let symKey = this.importSymKey(symKeyStr, operation); + let cryptMethod = (operation === OPERATIONS.ENCRYPT + ? crypto.subtle.encrypt + : crypto.subtle.decrypt) + .bind(crypto.subtle); + let algo = { name: CRYPT_ALGO, iv: iv }; + + + return Async.promiseSpinningly( + cryptMethod(algo, symKey, data) + .then(keyBytes => new Uint8Array(keyBytes)) + ); + }, + + + generateRandomKey() { + this.log("generateRandomKey() called"); + let algo = { + name: CRYPT_ALGO, + length: CRYPT_ALGO_LENGTH + }; + return Async.promiseSpinningly( + crypto.subtle.generateKey(algo, true, []) + .then(key => crypto.subtle.exportKey("raw", key)) + .then(keyBytes => { + keyBytes = new Uint8Array(keyBytes); + return this.encodeBase64(keyBytes); + }) + ); + }, + + generateRandomIV() { + return this.generateRandomBytes(AES_CBC_IV_SIZE); + }, + + generateRandomBytes(byteCount) { + this.log("generateRandomBytes() called"); + + let randBytes = new Uint8Array(byteCount); + crypto.getRandomValues(randBytes); + + return this.encodeBase64(randBytes); + }, + + // + // SymKey CryptoKey memoization. + // + + // Memoize the import of symmetric keys. We do this by using the base64 + // string itself as a key. + _encryptionSymKeyMemo: {}, + _decryptionSymKeyMemo: {}, + importSymKey(encodedKeyString, operation) { + let memo; + + // We use two separate memos for thoroughness: operation is an input to + // key import. + switch (operation) { + case OPERATIONS.ENCRYPT: + memo = this._encryptionSymKeyMemo; + break; + case OPERATIONS.DECRYPT: + memo = this._decryptionSymKeyMemo; + break; + default: + throw "Unsupported operation in importSymKey."; + } + + if (encodedKeyString in memo) + return memo[encodedKeyString]; + + let symmetricKeyBuffer = this.makeUint8Array(encodedKeyString, true); + let algo = { name: CRYPT_ALGO }; + let usages = [operation === OPERATIONS.ENCRYPT ? "encrypt" : "decrypt"]; + + return Async.promiseSpinningly( + crypto.subtle.importKey("raw", symmetricKeyBuffer, algo, false, usages) + .then(symKey => { + memo[encodedKeyString] = symKey; + return symKey; + }) + ); + }, + + + // + // Utility functions + // + + /** + * Returns an Uint8Array filled with a JS string, + * which means we only keep utf-16 characters from 0x00 to 0xFF. + */ + byteCompressInts(str) { + let arrayBuffer = new Uint8Array(str.length); + for (let i = 0; i < str.length; i++) { + arrayBuffer[i] = str.charCodeAt(i) & 0xFF; + } + return arrayBuffer; + }, + + expandData(data) { + let expanded = ""; + for (let i = 0; i < data.length; i++) { + expanded += String.fromCharCode(data[i]); + } + return expanded; + }, + + encodeBase64(data) { + return btoa(this.expandData(data)); + }, + + makeUint8Array(input, isEncoded) { + if (isEncoded) { + input = atob(input); + } + return this.byteCompressInts(input); + }, + + /** + * Returns the expanded data string for the derived key. + */ + deriveKeyFromPassphrase(passphrase, saltStr, keyLength = 32) { + this.log("deriveKeyFromPassphrase() called."); + let keyData = this.makeUint8Array(passphrase, false); + let salt = this.makeUint8Array(saltStr, true); + let importAlgo = { name: KEY_DERIVATION_ALGO }; + let deriveAlgo = { + name: KEY_DERIVATION_ALGO, + salt: salt, + iterations: KEY_DERIVATION_ITERATIONS, + hash: { name: KEY_DERIVATION_HASHING_ALGO }, + }; + let derivedKeyType = { + name: DERIVED_KEY_ALGO, + length: keyLength * 8, + }; + return Async.promiseSpinningly( + crypto.subtle.importKey("raw", keyData, importAlgo, false, ["deriveKey"]) + .then(key => crypto.subtle.deriveKey(deriveAlgo, key, derivedKeyType, true, [])) + .then(derivedKey => crypto.subtle.exportKey("raw", derivedKey)) + .then(keyBytes => { + keyBytes = new Uint8Array(keyBytes); + return this.expandData(keyBytes); + }) + ); + }, +}; diff --git a/services/crypto/modules/utils.js b/services/crypto/modules/utils.js new file mode 100644 index 0000000000..c17f5dfa18 --- /dev/null +++ b/services/crypto/modules/utils.js @@ -0,0 +1,584 @@ +/* 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/. */ + +var {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components; + +this.EXPORTED_SYMBOLS = ["CryptoUtils"]; + +Cu.import("resource://services-common/observers.js"); +Cu.import("resource://services-common/utils.js"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +this.CryptoUtils = { + xor: function xor(a, b) { + let bytes = []; + + if (a.length != b.length) { + throw new Error("can't xor unequal length strings: "+a.length+" vs "+b.length); + } + + for (let i = 0; i < a.length; i++) { + bytes[i] = a.charCodeAt(i) ^ b.charCodeAt(i); + } + + return String.fromCharCode.apply(String, bytes); + }, + + /** + * Generate a string of random bytes. + */ + generateRandomBytes: function generateRandomBytes(length) { + let rng = Cc["@mozilla.org/security/random-generator;1"] + .createInstance(Ci.nsIRandomGenerator); + let bytes = rng.generateRandomBytes(length); + return CommonUtils.byteArrayToString(bytes); + }, + + /** + * UTF8-encode a message and hash it with the given hasher. Returns a + * string containing bytes. The hasher is reset if it's an HMAC hasher. + */ + digestUTF8: function digestUTF8(message, hasher) { + let data = this._utf8Converter.convertToByteArray(message, {}); + hasher.update(data, data.length); + let result = hasher.finish(false); + if (hasher instanceof Ci.nsICryptoHMAC) { + hasher.reset(); + } + return result; + }, + + /** + * Treat the given message as a bytes string and hash it with the given + * hasher. Returns a string containing bytes. The hasher is reset if it's + * an HMAC hasher. + */ + digestBytes: function digestBytes(message, hasher) { + // No UTF-8 encoding for you, sunshine. + let bytes = Array.prototype.slice.call(message).map(b => b.charCodeAt(0)); + hasher.update(bytes, bytes.length); + let result = hasher.finish(false); + if (hasher instanceof Ci.nsICryptoHMAC) { + hasher.reset(); + } + return result; + }, + + /** + * Encode the message into UTF-8 and feed the resulting bytes into the + * given hasher. Does not return a hash. This can be called multiple times + * with a single hasher, but eventually you must extract the result + * yourself. + */ + updateUTF8: function(message, hasher) { + let bytes = this._utf8Converter.convertToByteArray(message, {}); + hasher.update(bytes, bytes.length); + }, + + /** + * UTF-8 encode a message and perform a SHA-1 over it. + * + * @param message + * (string) Buffer to perform operation on. Should be a JS string. + * It is possible to pass in a string representing an array + * of bytes. But, you probably don't want to UTF-8 encode + * such data and thus should not be using this function. + * + * @return string + * Raw bytes constituting SHA-1 hash. Value is a JS string. Each + * character is the byte value for that offset. Returned string + * always has .length == 20. + */ + UTF8AndSHA1: function UTF8AndSHA1(message) { + let hasher = Cc["@mozilla.org/security/hash;1"] + .createInstance(Ci.nsICryptoHash); + hasher.init(hasher.SHA1); + + return CryptoUtils.digestUTF8(message, hasher); + }, + + sha1: function sha1(message) { + return CommonUtils.bytesAsHex(CryptoUtils.UTF8AndSHA1(message)); + }, + + sha1Base32: function sha1Base32(message) { + return CommonUtils.encodeBase32(CryptoUtils.UTF8AndSHA1(message)); + }, + + sha256(message) { + let hasher = Cc["@mozilla.org/security/hash;1"] + .createInstance(Ci.nsICryptoHash); + hasher.init(hasher.SHA256); + return CommonUtils.bytesAsHex(CryptoUtils.digestUTF8(message, hasher)); + }, + + /** + * Produce an HMAC key object from a key string. + */ + makeHMACKey: function makeHMACKey(str) { + return Svc.KeyFactory.keyFromString(Ci.nsIKeyObject.HMAC, str); + }, + + /** + * Produce an HMAC hasher and initialize it with the given HMAC key. + */ + makeHMACHasher: function makeHMACHasher(type, key) { + let hasher = Cc["@mozilla.org/security/hmac;1"] + .createInstance(Ci.nsICryptoHMAC); + hasher.init(type, key); + return hasher; + }, + + /** + * HMAC-based Key Derivation (RFC 5869). + */ + hkdf: function hkdf(ikm, xts, info, len) { + const BLOCKSIZE = 256 / 8; + if (typeof xts === undefined) + xts = String.fromCharCode(0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0); + let h = CryptoUtils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256, + CryptoUtils.makeHMACKey(xts)); + let prk = CryptoUtils.digestBytes(ikm, h); + return CryptoUtils.hkdfExpand(prk, info, len); + }, + + /** + * HMAC-based Key Derivation Step 2 according to RFC 5869. + */ + hkdfExpand: function hkdfExpand(prk, info, len) { + const BLOCKSIZE = 256 / 8; + let h = CryptoUtils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256, + CryptoUtils.makeHMACKey(prk)); + let T = ""; + let Tn = ""; + let iterations = Math.ceil(len/BLOCKSIZE); + for (let i = 0; i < iterations; i++) { + Tn = CryptoUtils.digestBytes(Tn + info + String.fromCharCode(i + 1), h); + T += Tn; + } + return T.slice(0, len); + }, + + /** + * PBKDF2 implementation in Javascript. + * + * The arguments to this function correspond to items in + * PKCS #5, v2.0 pp. 9-10 + * + * P: the passphrase, an octet string: e.g., "secret phrase" + * S: the salt, an octet string: e.g., "DNXPzPpiwn" + * c: the number of iterations, a positive integer: e.g., 4096 + * dkLen: the length in octets of the destination + * key, a positive integer: e.g., 16 + * hmacAlg: The algorithm to use for hmac + * hmacLen: The hmac length + * + * The default value of 20 for hmacLen is appropriate for SHA1. For SHA256, + * hmacLen should be 32. + * + * The output is an octet string of length dkLen, which you + * can encode as you wish. + */ + pbkdf2Generate : function pbkdf2Generate(P, S, c, dkLen, + hmacAlg=Ci.nsICryptoHMAC.SHA1, hmacLen=20) { + + // We don't have a default in the algo itself, as NSS does. + // Use the constant. + if (!dkLen) { + dkLen = SYNC_KEY_DECODED_LENGTH; + } + + function F(S, c, i, h) { + + function XOR(a, b, isA) { + if (a.length != b.length) { + return false; + } + + let val = []; + for (let i = 0; i < a.length; i++) { + if (isA) { + val[i] = a[i] ^ b[i]; + } else { + val[i] = a.charCodeAt(i) ^ b.charCodeAt(i); + } + } + + return val; + } + + let ret; + let U = []; + + /* Encode i into 4 octets: _INT */ + let I = []; + I[0] = String.fromCharCode((i >> 24) & 0xff); + I[1] = String.fromCharCode((i >> 16) & 0xff); + I[2] = String.fromCharCode((i >> 8) & 0xff); + I[3] = String.fromCharCode(i & 0xff); + + U[0] = CryptoUtils.digestBytes(S + I.join(''), h); + for (let j = 1; j < c; j++) { + U[j] = CryptoUtils.digestBytes(U[j - 1], h); + } + + ret = U[0]; + for (let j = 1; j < c; j++) { + ret = CommonUtils.byteArrayToString(XOR(ret, U[j])); + } + + return ret; + } + + let l = Math.ceil(dkLen / hmacLen); + let r = dkLen - ((l - 1) * hmacLen); + + // Reuse the key and the hasher. Remaking them 4096 times is 'spensive. + let h = CryptoUtils.makeHMACHasher(hmacAlg, + CryptoUtils.makeHMACKey(P)); + + let T = []; + for (let i = 0; i < l;) { + T[i] = F(S, c, ++i, h); + } + + let ret = ""; + for (let i = 0; i < l-1;) { + ret += T[i++]; + } + ret += T[l - 1].substr(0, r); + + return ret; + }, + + deriveKeyFromPassphrase: function deriveKeyFromPassphrase(passphrase, + salt, + keyLength, + forceJS) { + if (Svc.Crypto.deriveKeyFromPassphrase && !forceJS) { + return Svc.Crypto.deriveKeyFromPassphrase(passphrase, salt, keyLength); + } + else { + // Fall back to JS implementation. + // 4096 is hardcoded in WeaveCrypto, so do so here. + return CryptoUtils.pbkdf2Generate(passphrase, atob(salt), 4096, + keyLength); + } + }, + + /** + * Compute the HTTP MAC SHA-1 for an HTTP request. + * + * @param identifier + * (string) MAC Key Identifier. + * @param key + * (string) MAC Key. + * @param method + * (string) HTTP request method. + * @param URI + * (nsIURI) HTTP request URI. + * @param extra + * (object) Optional extra parameters. Valid keys are: + * nonce_bytes - How many bytes the nonce should be. This defaults + * to 8. Note that this many bytes are Base64 encoded, so the + * string length of the nonce will be longer than this value. + * ts - Timestamp to use. Should only be defined for testing. + * nonce - String nonce. Should only be defined for testing as this + * function will generate a cryptographically secure random one + * if not defined. + * ext - Extra string to be included in MAC. Per the HTTP MAC spec, + * the format is undefined and thus application specific. + * @returns + * (object) Contains results of operation and input arguments (for + * symmetry). The object has the following keys: + * + * identifier - (string) MAC Key Identifier (from arguments). + * key - (string) MAC Key (from arguments). + * method - (string) HTTP request method (from arguments). + * hostname - (string) HTTP hostname used (derived from arguments). + * port - (string) HTTP port number used (derived from arguments). + * mac - (string) Raw HMAC digest bytes. + * getHeader - (function) Call to obtain the string Authorization + * header value for this invocation. + * nonce - (string) Nonce value used. + * ts - (number) Integer seconds since Unix epoch that was used. + */ + computeHTTPMACSHA1: function computeHTTPMACSHA1(identifier, key, method, + uri, extra) { + let ts = (extra && extra.ts) ? extra.ts : Math.floor(Date.now() / 1000); + let nonce_bytes = (extra && extra.nonce_bytes > 0) ? extra.nonce_bytes : 8; + + // We are allowed to use more than the Base64 alphabet if we want. + let nonce = (extra && extra.nonce) + ? extra.nonce + : btoa(CryptoUtils.generateRandomBytes(nonce_bytes)); + + let host = uri.asciiHost; + let port; + let usedMethod = method.toUpperCase(); + + if (uri.port != -1) { + port = uri.port; + } else if (uri.scheme == "http") { + port = "80"; + } else if (uri.scheme == "https") { + port = "443"; + } else { + throw new Error("Unsupported URI scheme: " + uri.scheme); + } + + let ext = (extra && extra.ext) ? extra.ext : ""; + + let requestString = ts.toString(10) + "\n" + + nonce + "\n" + + usedMethod + "\n" + + uri.path + "\n" + + host + "\n" + + port + "\n" + + ext + "\n"; + + let hasher = CryptoUtils.makeHMACHasher(Ci.nsICryptoHMAC.SHA1, + CryptoUtils.makeHMACKey(key)); + let mac = CryptoUtils.digestBytes(requestString, hasher); + + function getHeader() { + return CryptoUtils.getHTTPMACSHA1Header(this.identifier, this.ts, + this.nonce, this.mac, this.ext); + } + + return { + identifier: identifier, + key: key, + method: usedMethod, + hostname: host, + port: port, + mac: mac, + nonce: nonce, + ts: ts, + ext: ext, + getHeader: getHeader + }; + }, + + + /** + * Obtain the HTTP MAC Authorization header value from fields. + * + * @param identifier + * (string) MAC key identifier. + * @param ts + * (number) Integer seconds since Unix epoch. + * @param nonce + * (string) Nonce value. + * @param mac + * (string) Computed HMAC digest (raw bytes). + * @param ext + * (optional) (string) Extra string content. + * @returns + * (string) Value to put in Authorization header. + */ + getHTTPMACSHA1Header: function getHTTPMACSHA1Header(identifier, ts, nonce, + mac, ext) { + let header ='MAC id="' + identifier + '", ' + + 'ts="' + ts + '", ' + + 'nonce="' + nonce + '", ' + + 'mac="' + btoa(mac) + '"'; + + if (!ext) { + return header; + } + + return header += ', ext="' + ext +'"'; + }, + + /** + * Given an HTTP header value, strip out any attributes. + */ + + stripHeaderAttributes: function(value) { + value = value || ""; + let i = value.indexOf(";"); + return value.substring(0, (i >= 0) ? i : undefined).trim().toLowerCase(); + }, + + /** + * Compute the HAWK client values (mostly the header) for an HTTP request. + * + * @param URI + * (nsIURI) HTTP request URI. + * @param method + * (string) HTTP request method. + * @param options + * (object) extra parameters (all but "credentials" are optional): + * credentials - (object, mandatory) HAWK credentials object. + * All three keys are required: + * id - (string) key identifier + * key - (string) raw key bytes + * algorithm - (string) which hash to use: "sha1" or "sha256" + * ext - (string) application-specific data, included in MAC + * localtimeOffsetMsec - (number) local clock offset (vs server) + * payload - (string) payload to include in hash, containing the + * HTTP request body. If not provided, the HAWK hash + * will not cover the request body, and the server + * should not check it either. This will be UTF-8 + * encoded into bytes before hashing. This function + * cannot handle arbitrary binary data, sorry (the + * UTF-8 encoding process will corrupt any codepoints + * between U+0080 and U+00FF). Callers must be careful + * to use an HTTP client function which encodes the + * payload exactly the same way, otherwise the hash + * will not match. + * contentType - (string) payload Content-Type. This is included + * (without any attributes like "charset=") in the + * HAWK hash. It does *not* affect interpretation + * of the "payload" property. + * hash - (base64 string) pre-calculated payload hash. If + * provided, "payload" is ignored. + * ts - (number) pre-calculated timestamp, secs since epoch + * now - (number) current time, ms-since-epoch, for tests + * nonce - (string) pre-calculated nonce. Should only be defined + * for testing as this function will generate a + * cryptographically secure random one if not defined. + * @returns + * (object) Contains results of operation. The object has the + * following keys: + * field - (string) HAWK header, to use in Authorization: header + * artifacts - (object) other generated values: + * ts - (number) timestamp, in seconds since epoch + * nonce - (string) + * method - (string) + * resource - (string) path plus querystring + * host - (string) + * port - (number) + * hash - (string) payload hash (base64) + * ext - (string) app-specific data + * MAC - (string) request MAC (base64) + */ + computeHAWK: function(uri, method, options) { + let credentials = options.credentials; + let ts = options.ts || Math.floor(((options.now || Date.now()) + + (options.localtimeOffsetMsec || 0)) + / 1000); + + let hash_algo, hmac_algo; + if (credentials.algorithm == "sha1") { + hash_algo = Ci.nsICryptoHash.SHA1; + hmac_algo = Ci.nsICryptoHMAC.SHA1; + } else if (credentials.algorithm == "sha256") { + hash_algo = Ci.nsICryptoHash.SHA256; + hmac_algo = Ci.nsICryptoHMAC.SHA256; + } else { + throw new Error("Unsupported algorithm: " + credentials.algorithm); + } + + let port; + if (uri.port != -1) { + port = uri.port; + } else if (uri.scheme == "http") { + port = 80; + } else if (uri.scheme == "https") { + port = 443; + } else { + throw new Error("Unsupported URI scheme: " + uri.scheme); + } + + let artifacts = { + ts: ts, + nonce: options.nonce || btoa(CryptoUtils.generateRandomBytes(8)), + method: method.toUpperCase(), + resource: uri.path, // This includes both path and search/queryarg. + host: uri.asciiHost.toLowerCase(), // This includes punycoding. + port: port.toString(10), + hash: options.hash, + ext: options.ext, + }; + + let contentType = CryptoUtils.stripHeaderAttributes(options.contentType); + + if (!artifacts.hash && options.hasOwnProperty("payload") + && options.payload) { + let hasher = Cc["@mozilla.org/security/hash;1"] + .createInstance(Ci.nsICryptoHash); + hasher.init(hash_algo); + CryptoUtils.updateUTF8("hawk.1.payload\n", hasher); + CryptoUtils.updateUTF8(contentType+"\n", hasher); + CryptoUtils.updateUTF8(options.payload, hasher); + CryptoUtils.updateUTF8("\n", hasher); + let hash = hasher.finish(false); + // HAWK specifies this .hash to use +/ (not _-) and include the + // trailing "==" padding. + let hash_b64 = btoa(hash); + artifacts.hash = hash_b64; + } + + let requestString = ("hawk.1.header" + "\n" + + artifacts.ts.toString(10) + "\n" + + artifacts.nonce + "\n" + + artifacts.method + "\n" + + artifacts.resource + "\n" + + artifacts.host + "\n" + + artifacts.port + "\n" + + (artifacts.hash || "") + "\n"); + if (artifacts.ext) { + requestString += artifacts.ext.replace("\\", "\\\\").replace("\n", "\\n"); + } + requestString += "\n"; + + let hasher = CryptoUtils.makeHMACHasher(hmac_algo, + CryptoUtils.makeHMACKey(credentials.key)); + artifacts.mac = btoa(CryptoUtils.digestBytes(requestString, hasher)); + // The output MAC uses "+" and "/", and padded== . + + function escape(attribute) { + // This is used for "x=y" attributes inside HTTP headers. + return attribute.replace(/\\/g, "\\\\").replace(/\"/g, '\\"'); + } + let header = ('Hawk id="' + credentials.id + '", ' + + 'ts="' + artifacts.ts + '", ' + + 'nonce="' + artifacts.nonce + '", ' + + (artifacts.hash ? ('hash="' + artifacts.hash + '", ') : "") + + (artifacts.ext ? ('ext="' + escape(artifacts.ext) + '", ') : "") + + 'mac="' + artifacts.mac + '"'); + return { + artifacts: artifacts, + field: header, + }; + }, + +}; + +XPCOMUtils.defineLazyGetter(CryptoUtils, "_utf8Converter", function() { + let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] + .createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = "UTF-8"; + + return converter; +}); + +var Svc = {}; + +XPCOMUtils.defineLazyServiceGetter(Svc, + "KeyFactory", + "@mozilla.org/security/keyobjectfactory;1", + "nsIKeyObjectFactory"); + +Svc.__defineGetter__("Crypto", function() { + let ns = {}; + Cu.import("resource://services-crypto/WeaveCrypto.js", ns); + + let wc = new ns.WeaveCrypto(); + delete Svc.Crypto; + return Svc.Crypto = wc; +}); + +Observers.add("xpcom-shutdown", function unloadServices() { + Observers.remove("xpcom-shutdown", unloadServices); + + for (let k in Svc) { + delete Svc[k]; + } +}); diff --git a/services/crypto/moz.build b/services/crypto/moz.build new file mode 100644 index 0000000000..27fd5b90bc --- /dev/null +++ b/services/crypto/moz.build @@ -0,0 +1,21 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files('**'): + BUG_COMPONENT = ('Mozilla Services', 'Firefox Sync: Crypto') + +DIRS += ['component'] + +XPCSHELL_TESTS_MANIFESTS += ['tests/unit/xpcshell.ini'] + +EXTRA_JS_MODULES['services-crypto'] += [ + 'modules/utils.js', + 'modules/WeaveCrypto.js', +] + +EXTRA_COMPONENTS += [ + 'cryptoComponents.manifest', +] diff --git a/services/crypto/tests/unit/head_helpers.js b/services/crypto/tests/unit/head_helpers.js new file mode 100644 index 0000000000..70522fc380 --- /dev/null +++ b/services/crypto/tests/unit/head_helpers.js @@ -0,0 +1,55 @@ +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cr = Components.results; +var Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +try { + // In the context of xpcshell tests, there won't be a default AppInfo + Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo); +} +catch(ex) { + +// Make sure to provide the right OS so crypto loads the right binaries +var OS = "XPCShell"; +if (mozinfo.os == "win") + OS = "WINNT"; +else if (mozinfo.os == "mac") + OS = "Darwin"; +else + OS = "Linux"; + +Cu.import("resource://testing-common/AppInfo.jsm", this); +updateAppInfo({ + name: "XPCShell", + ID: "{3e3ba16c-1675-4e88-b9c8-afef81b3d2ef}", + version: "1", + platformVersion: "", + OS: OS, +}); +} + +// Register resource alias. Normally done in SyncComponents.manifest. +function addResourceAlias() { + Cu.import("resource://gre/modules/Services.jsm"); + const resProt = Services.io.getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler); + let uri = Services.io.newURI("resource://gre/modules/services-crypto/", + null, null); + resProt.setSubstitution("services-crypto", uri); +} +addResourceAlias(); + +/** + * Print some debug message to the console. All arguments will be printed, + * separated by spaces. + * + * @param [arg0, arg1, arg2, ...] + * Any number of arguments to print out + * @usage _("Hello World") -> prints "Hello World" + * @usage _(1, 2, 3) -> prints "1 2 3" + */ +var _ = function(some, debug, text, to) { + print(Array.slice(arguments).join(" ")); +}; diff --git a/services/crypto/tests/unit/test_crypto_crypt.js b/services/crypto/tests/unit/test_crypto_crypt.js new file mode 100644 index 0000000000..fcea21d008 --- /dev/null +++ b/services/crypto/tests/unit/test_crypto_crypt.js @@ -0,0 +1,213 @@ +Cu.import("resource://services-crypto/WeaveCrypto.js"); +Cu.importGlobalProperties(['crypto']); + +var cryptoSvc = new WeaveCrypto(); + +add_task(function* test_key_memoization() { + let cryptoGlobal = cryptoSvc._getCrypto(); + let oldImport = cryptoGlobal.subtle.importKey; + if (!oldImport) { + _("Couldn't swizzle crypto.subtle.importKey; returning."); + return; + } + + let iv = cryptoSvc.generateRandomIV(); + let key = cryptoSvc.generateRandomKey(); + let c = 0; + cryptoGlobal.subtle.importKey = function(format, keyData, algo, extractable, usages) { + c++; + return oldImport.call(cryptoGlobal.subtle, format, keyData, algo, extractable, usages); + } + + // Encryption should cause a single counter increment. + do_check_eq(c, 0); + let cipherText = cryptoSvc.encrypt("Hello, world.", key, iv); + do_check_eq(c, 1); + cipherText = cryptoSvc.encrypt("Hello, world.", key, iv); + do_check_eq(c, 1); + + // ... as should decryption. + cryptoSvc.decrypt(cipherText, key, iv); + cryptoSvc.decrypt(cipherText, key, iv); + cryptoSvc.decrypt(cipherText, key, iv); + do_check_eq(c, 2); + + // Un-swizzle. + cryptoGlobal.subtle.importKey = oldImport; +}); + +// Just verify that it gets populated with the correct bytes. +add_task(function* test_makeUint8Array() { + Components.utils.import("resource://gre/modules/ctypes.jsm"); + + let item1 = cryptoSvc.makeUint8Array("abcdefghi", false); + do_check_true(item1); + for (let i = 0; i < 8; ++i) + do_check_eq(item1[i], "abcdefghi".charCodeAt(i)); +}); + +add_task(function* test_encrypt_decrypt() { + // First, do a normal run with expected usage... Generate a random key and + // iv, encrypt and decrypt a string. + var iv = cryptoSvc.generateRandomIV(); + do_check_eq(iv.length, 24); + + var key = cryptoSvc.generateRandomKey(); + do_check_eq(key.length, 44); + + var mySecret = "bacon is a vegetable"; + var cipherText = cryptoSvc.encrypt(mySecret, key, iv); + do_check_eq(cipherText.length, 44); + + var clearText = cryptoSvc.decrypt(cipherText, key, iv); + do_check_eq(clearText.length, 20); + + // Did the text survive the encryption round-trip? + do_check_eq(clearText, mySecret); + do_check_neq(cipherText, mySecret); // just to be explicit + + + // Do some more tests with a fixed key/iv, to check for reproducable results. + key = "St1tFCor7vQEJNug/465dQ=="; + iv = "oLjkfrLIOnK2bDRvW4kXYA=="; + + _("Testing small IV."); + mySecret = "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo="; + let shortiv = "YWJj"; + let err; + try { + cryptoSvc.encrypt(mySecret, key, shortiv); + } catch (ex) { + err = ex; + } + do_check_true(!!err); + + _("Testing long IV."); + let longiv = "gsgLRDaxWvIfKt75RjuvFWERt83FFsY2A0TW+0b2iVk="; + try { + cryptoSvc.encrypt(mySecret, key, longiv); + } catch (ex) { + err = ex; + } + do_check_true(!!err); + + // Test small input sizes + mySecret = ""; + cipherText = cryptoSvc.encrypt(mySecret, key, iv); + clearText = cryptoSvc.decrypt(cipherText, key, iv); + do_check_eq(cipherText, "OGQjp6mK1a3fs9k9Ml4L3w=="); + do_check_eq(clearText, mySecret); + + mySecret = "x"; + cipherText = cryptoSvc.encrypt(mySecret, key, iv); + clearText = cryptoSvc.decrypt(cipherText, key, iv); + do_check_eq(cipherText, "96iMl4vhOxFUW/lVHHzVqg=="); + do_check_eq(clearText, mySecret); + + mySecret = "xx"; + cipherText = cryptoSvc.encrypt(mySecret, key, iv); + clearText = cryptoSvc.decrypt(cipherText, key, iv); + do_check_eq(cipherText, "olpPbETRYROCSqFWcH2SWg=="); + do_check_eq(clearText, mySecret); + + mySecret = "xxx"; + cipherText = cryptoSvc.encrypt(mySecret, key, iv); + clearText = cryptoSvc.decrypt(cipherText, key, iv); + do_check_eq(cipherText, "rRbpHGyVSZizLX/x43Wm+Q=="); + do_check_eq(clearText, mySecret); + + mySecret = "xxxx"; + cipherText = cryptoSvc.encrypt(mySecret, key, iv); + clearText = cryptoSvc.decrypt(cipherText, key, iv); + do_check_eq(cipherText, "HeC7miVGDcpxae9RmiIKAw=="); + do_check_eq(clearText, mySecret); + + // Test non-ascii input + // ("testuser1" using similar-looking glyphs) + mySecret = String.fromCharCode(355, 277, 349, 357, 533, 537, 101, 345, 185); + cipherText = cryptoSvc.encrypt(mySecret, key, iv); + clearText = cryptoSvc.decrypt(cipherText, key, iv); + do_check_eq(cipherText, "Pj4ixByXoH3SU3JkOXaEKPgwRAWplAWFLQZkpJd5Kr4="); + do_check_eq(clearText, mySecret); + + // Tests input spanning a block boundary (AES block size is 16 bytes) + mySecret = "123456789012345"; + cipherText = cryptoSvc.encrypt(mySecret, key, iv); + clearText = cryptoSvc.decrypt(cipherText, key, iv); + do_check_eq(cipherText, "e6c5hwphe45/3VN/M0bMUA=="); + do_check_eq(clearText, mySecret); + + mySecret = "1234567890123456"; + cipherText = cryptoSvc.encrypt(mySecret, key, iv); + clearText = cryptoSvc.decrypt(cipherText, key, iv); + do_check_eq(cipherText, "V6aaOZw8pWlYkoIHNkhsP1JOIQF87E2vTUvBUQnyV04="); + do_check_eq(clearText, mySecret); + + mySecret = "12345678901234567"; + cipherText = cryptoSvc.encrypt(mySecret, key, iv); + clearText = cryptoSvc.decrypt(cipherText, key, iv); + do_check_eq(cipherText, "V6aaOZw8pWlYkoIHNkhsP5GvxWJ9+GIAS6lXw+5fHTI="); + do_check_eq(clearText, mySecret); + + + key = "iz35tuIMq4/H+IYw2KTgow=="; + iv = "TJYrvva2KxvkM8hvOIvWp3=="; + mySecret = "i like pie"; + + cipherText = cryptoSvc.encrypt(mySecret, key, iv); + clearText = cryptoSvc.decrypt(cipherText, key, iv); + do_check_eq(cipherText, "DLGx8BWqSCLGG7i/xwvvxg=="); + do_check_eq(clearText, mySecret); + + key = "c5hG3YG+NC61FFy8NOHQak1ZhMEWO79bwiAfar2euzI="; + iv = "gsgLRDaxWvIfKt75RjuvFW=="; + mySecret = "i like pie"; + + cipherText = cryptoSvc.encrypt(mySecret, key, iv); + clearText = cryptoSvc.decrypt(cipherText, key, iv); + do_check_eq(cipherText, "o+ADtdMd8ubzNWurS6jt0Q=="); + do_check_eq(clearText, mySecret); + + key = "St1tFCor7vQEJNug/465dQ=="; + iv = "oLjkfrLIOnK2bDRvW4kXYA=="; + mySecret = "does thunder read testcases?"; + cipherText = cryptoSvc.encrypt(mySecret, key, iv); + do_check_eq(cipherText, "T6fik9Ros+DB2ablH9zZ8FWZ0xm/szSwJjIHZu7sjPs="); + + var badkey = "badkeybadkeybadkeybadk=="; + var badiv = "badivbadivbadivbadivbad="; + var badcipher = "crapinputcrapinputcrapinputcrapinputcrapinp="; + var failure; + + try { + failure = false; + clearText = cryptoSvc.decrypt(cipherText, badkey, iv); + } catch (e) { + failure = true; + } + do_check_true(failure); + + try { + failure = false; + clearText = cryptoSvc.decrypt(cipherText, key, badiv); + } catch (e) { + failure = true; + } + do_check_true(failure); + + try { + failure = false; + clearText = cryptoSvc.decrypt(cipherText, badkey, badiv); + } catch (e) { + failure = true; + } + do_check_true(failure); + + try { + failure = false; + clearText = cryptoSvc.decrypt(badcipher, key, iv); + } catch (e) { + failure = true; + } + do_check_true(failure); +}); diff --git a/services/crypto/tests/unit/test_crypto_deriveKey.js b/services/crypto/tests/unit/test_crypto_deriveKey.js new file mode 100644 index 0000000000..00af474cb0 --- /dev/null +++ b/services/crypto/tests/unit/test_crypto_deriveKey.js @@ -0,0 +1,28 @@ +Components.utils.import("resource://services-crypto/WeaveCrypto.js"); + +function run_test() { + let cryptoSvc = new WeaveCrypto(); + // Extracted from test_utils_deriveKey. + let pp = "secret phrase"; + let salt = "RE5YUHpQcGl3bg=="; // btoa("DNXPzPpiwn") + + // 16-byte, extract key data. + let k = cryptoSvc.deriveKeyFromPassphrase(pp, salt, 16); + do_check_eq(16, k.length); + do_check_eq(btoa(k), "d2zG0d2cBfXnRwMUGyMwyg=="); + + // Test different key lengths. + k = cryptoSvc.deriveKeyFromPassphrase(pp, salt, 32); + do_check_eq(32, k.length); + do_check_eq(btoa(k), "d2zG0d2cBfXnRwMUGyMwyroRXtnrSIeLwSDvReSfcyA="); + let encKey = btoa(k); + + // Test via encryption. + let iv = cryptoSvc.generateRandomIV(); + do_check_eq(cryptoSvc.decrypt(cryptoSvc.encrypt("bacon", encKey, iv), encKey, iv), "bacon"); + + // Test default length (32). + k = cryptoSvc.deriveKeyFromPassphrase(pp, salt); + do_check_eq(32, k.length); + do_check_eq(encKey, btoa(k)); +} diff --git a/services/crypto/tests/unit/test_crypto_random.js b/services/crypto/tests/unit/test_crypto_random.js new file mode 100644 index 0000000000..46b4c7f82f --- /dev/null +++ b/services/crypto/tests/unit/test_crypto_random.js @@ -0,0 +1,58 @@ +var WeaveCryptoModule = Cu.import("resource://services-crypto/WeaveCrypto.js"); + +var cryptoSvc = new WeaveCrypto(); + +function run_test() { + if (this.gczeal) { + _("Running crypto random tests with gczeal(2)."); + gczeal(2); + } + + // Test salt generation. + var salt; + + salt = cryptoSvc.generateRandomBytes(0); + do_check_eq(salt.length, 0); + salt = cryptoSvc.generateRandomBytes(1); + do_check_eq(salt.length, 4); + salt = cryptoSvc.generateRandomBytes(2); + do_check_eq(salt.length, 4); + salt = cryptoSvc.generateRandomBytes(3); + do_check_eq(salt.length, 4); + salt = cryptoSvc.generateRandomBytes(4); + do_check_eq(salt.length, 8); + salt = cryptoSvc.generateRandomBytes(8); + do_check_eq(salt.length, 12); + + // sanity check to make sure salts seem random + var salt2 = cryptoSvc.generateRandomBytes(8); + do_check_eq(salt2.length, 12); + do_check_neq(salt, salt2); + + salt = cryptoSvc.generateRandomBytes(1024); + do_check_eq(salt.length, 1368); + salt = cryptoSvc.generateRandomBytes(16); + do_check_eq(salt.length, 24); + + + // Test random key generation + var keydata, keydata2, iv; + + keydata = cryptoSvc.generateRandomKey(); + do_check_eq(keydata.length, 44); + keydata2 = cryptoSvc.generateRandomKey(); + do_check_neq(keydata, keydata2); // sanity check for randomness + iv = cryptoSvc.generateRandomIV(); + do_check_eq(iv.length, 24); + + cryptoSvc.algorithm = WeaveCryptoModule.AES_256_CBC; + keydata = cryptoSvc.generateRandomKey(); + do_check_eq(keydata.length, 44); + keydata2 = cryptoSvc.generateRandomKey(); + do_check_neq(keydata, keydata2); // sanity check for randomness + iv = cryptoSvc.generateRandomIV(); + do_check_eq(iv.length, 24); + + if (this.gczeal) + gczeal(0); +} diff --git a/services/crypto/tests/unit/test_load_modules.js b/services/crypto/tests/unit/test_load_modules.js new file mode 100644 index 0000000000..50f5d709c5 --- /dev/null +++ b/services/crypto/tests/unit/test_load_modules.js @@ -0,0 +1,16 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const modules = [ + "utils.js", + "WeaveCrypto.js", +]; + +function run_test() { + for (let m of modules) { + let resource = "resource://services-crypto/" + m; + _("Attempting to import: " + resource); + Components.utils.import(resource, {}); + } +} + diff --git a/services/crypto/tests/unit/test_utils_hawk.js b/services/crypto/tests/unit/test_utils_hawk.js new file mode 100644 index 0000000000..0a2cf6c317 --- /dev/null +++ b/services/crypto/tests/unit/test_utils_hawk.js @@ -0,0 +1,301 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://services-common/utils.js"); +Cu.import("resource://services-crypto/utils.js"); + +function run_test() { + initTestLogging(); + + run_next_test(); +} + +add_test(function test_hawk() { + let compute = CryptoUtils.computeHAWK; + + // vectors copied from the HAWK (node.js) tests + let credentials_sha1 = { + id: "123456", + key: "2983d45yun89q", + algorithm: "sha1", + }; + + let method = "POST"; + let ts = 1353809207; + let nonce = "Ygvqdz"; + let result; + + let uri_http = CommonUtils.makeURI("http://example.net/somewhere/over/the/rainbow"); + let sha1_opts = { credentials: credentials_sha1, + ext: "Bazinga!", + ts: ts, + nonce: nonce, + payload: "something to write about", + }; + result = compute(uri_http, method, sha1_opts); + + // The HAWK spec uses non-urlsafe base64 (+/) for its output MAC string. + do_check_eq(result.field, + 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ' + + 'hash="bsvY3IfUllw6V5rvk4tStEvpBhE=", ext="Bazinga!", ' + + 'mac="qbf1ZPG/r/e06F4ht+T77LXi5vw="' + ); + do_check_eq(result.artifacts.ts, ts); + do_check_eq(result.artifacts.nonce, nonce); + do_check_eq(result.artifacts.method, method); + do_check_eq(result.artifacts.resource, "/somewhere/over/the/rainbow"); + do_check_eq(result.artifacts.host, "example.net"); + do_check_eq(result.artifacts.port, 80); + // artifacts.hash is the *payload* hash, not the overall request MAC. + do_check_eq(result.artifacts.hash, "bsvY3IfUllw6V5rvk4tStEvpBhE="); + do_check_eq(result.artifacts.ext, "Bazinga!"); + + let credentials_sha256 = { + id: "123456", + key: "2983d45yun89q", + algorithm: "sha256", + }; + + let uri_https = CommonUtils.makeURI("https://example.net/somewhere/over/the/rainbow"); + let sha256_opts = { credentials: credentials_sha256, + ext: "Bazinga!", + ts: ts, + nonce: nonce, + payload: "something to write about", + contentType: "text/plain", + }; + + result = compute(uri_https, method, sha256_opts); + do_check_eq(result.field, + 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ' + + 'hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", ' + + 'ext="Bazinga!", ' + + 'mac="q1CwFoSHzPZSkbIvl0oYlD+91rBUEvFk763nMjMndj8="' + ); + do_check_eq(result.artifacts.ts, ts); + do_check_eq(result.artifacts.nonce, nonce); + do_check_eq(result.artifacts.method, method); + do_check_eq(result.artifacts.resource, "/somewhere/over/the/rainbow"); + do_check_eq(result.artifacts.host, "example.net"); + do_check_eq(result.artifacts.port, 443); + do_check_eq(result.artifacts.hash, "2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY="); + do_check_eq(result.artifacts.ext, "Bazinga!"); + + let sha256_opts_noext = { credentials: credentials_sha256, + ts: ts, + nonce: nonce, + payload: "something to write about", + contentType: "text/plain", + }; + result = compute(uri_https, method, sha256_opts_noext); + do_check_eq(result.field, + 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ' + + 'hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", ' + + 'mac="HTgtd0jPI6E4izx8e4OHdO36q00xFCU0FolNq3RiCYs="' + ); + do_check_eq(result.artifacts.ts, ts); + do_check_eq(result.artifacts.nonce, nonce); + do_check_eq(result.artifacts.method, method); + do_check_eq(result.artifacts.resource, "/somewhere/over/the/rainbow"); + do_check_eq(result.artifacts.host, "example.net"); + do_check_eq(result.artifacts.port, 443); + do_check_eq(result.artifacts.hash, "2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY="); + + /* Leaving optional fields out should work, although of course then we can't + * assert much about the resulting hashes. The resulting header should look + * roughly like: + * Hawk id="123456", ts="1378764955", nonce="QkynqsrS44M=", mac="/C5NsoAs2fVn+d/I5wMfwe2Gr1MZyAJ6pFyDHG4Gf9U=" + */ + + result = compute(uri_https, method, { credentials: credentials_sha256 }); + let fields = result.field.split(" "); + do_check_eq(fields[0], "Hawk"); + do_check_eq(fields[1], 'id="123456",'); // from creds.id + do_check_true(fields[2].startsWith('ts="')); + /* The HAWK spec calls for seconds-since-epoch, not ms-since-epoch. + * Warning: this test will fail in the year 33658, and for time travellers + * who journey earlier than 2001. Please plan accordingly. */ + do_check_true(result.artifacts.ts > 1000*1000*1000); + do_check_true(result.artifacts.ts < 1000*1000*1000*1000); + do_check_true(fields[3].startsWith('nonce="')); + do_check_eq(fields[3].length, ('nonce="12345678901=",').length); + do_check_eq(result.artifacts.nonce.length, ("12345678901=").length); + + let result2 = compute(uri_https, method, { credentials: credentials_sha256 }); + do_check_neq(result.artifacts.nonce, result2.artifacts.nonce); + + /* Using an upper-case URI hostname shouldn't affect the hash. */ + + let uri_https_upper = CommonUtils.makeURI("https://EXAMPLE.NET/somewhere/over/the/rainbow"); + result = compute(uri_https_upper, method, sha256_opts); + do_check_eq(result.field, + 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ' + + 'hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", ' + + 'ext="Bazinga!", ' + + 'mac="q1CwFoSHzPZSkbIvl0oYlD+91rBUEvFk763nMjMndj8="' + ); + + /* Using a lower-case method name shouldn't affect the hash. */ + result = compute(uri_https_upper, method.toLowerCase(), sha256_opts); + do_check_eq(result.field, + 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ' + + 'hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", ' + + 'ext="Bazinga!", ' + + 'mac="q1CwFoSHzPZSkbIvl0oYlD+91rBUEvFk763nMjMndj8="' + ); + + /* The localtimeOffsetMsec field should be honored. HAWK uses this to + * compensate for clock skew between client and server: if the request is + * rejected with a timestamp out-of-range error, the error includes the + * server's time, and the client computes its clock offset and tries again. + * Clients can remember this offset for a while. + */ + + result = compute(uri_https, method, { credentials: credentials_sha256, + now: 1378848968650, + }); + do_check_eq(result.artifacts.ts, 1378848968); + + result = compute(uri_https, method, { credentials: credentials_sha256, + now: 1378848968650, + localtimeOffsetMsec: 1000*1000, + }); + do_check_eq(result.artifacts.ts, 1378848968 + 1000); + + /* Search/query-args in URIs should be included in the hash. */ + let makeURI = CommonUtils.makeURI; + result = compute(makeURI("http://example.net/path"), method, sha256_opts); + do_check_eq(result.artifacts.resource, "/path"); + do_check_eq(result.artifacts.mac, "WyKHJjWaeYt8aJD+H9UeCWc0Y9C+07ooTmrcrOW4MPI="); + + result = compute(makeURI("http://example.net/path/"), method, sha256_opts); + do_check_eq(result.artifacts.resource, "/path/"); + do_check_eq(result.artifacts.mac, "xAYp2MgZQFvTKJT9u8nsvMjshCRRkuaeYqQbYSFp9Qw="); + + result = compute(makeURI("http://example.net/path?query=search"), method, sha256_opts); + do_check_eq(result.artifacts.resource, "/path?query=search"); + do_check_eq(result.artifacts.mac, "C06a8pip2rA4QkBiosEmC32WcgFcW/R5SQC6kUWyqho="); + + /* Test handling of the payload, which is supposed to be a bytestring + (String with codepoints from U+0000 to U+00FF, pre-encoded). */ + + result = compute(makeURI("http://example.net/path"), method, + { credentials: credentials_sha256, + ts: 1353809207, + nonce: "Ygvqdz", + }); + do_check_eq(result.artifacts.hash, undefined); + do_check_eq(result.artifacts.mac, "S3f8E4hAURAqJxOlsYugkPZxLoRYrClgbSQ/3FmKMbY="); + + // Empty payload changes nothing. + result = compute(makeURI("http://example.net/path"), method, + { credentials: credentials_sha256, + ts: 1353809207, + nonce: "Ygvqdz", + payload: null, + }); + do_check_eq(result.artifacts.hash, undefined); + do_check_eq(result.artifacts.mac, "S3f8E4hAURAqJxOlsYugkPZxLoRYrClgbSQ/3FmKMbY="); + + result = compute(makeURI("http://example.net/path"), method, + { credentials: credentials_sha256, + ts: 1353809207, + nonce: "Ygvqdz", + payload: "hello", + }); + do_check_eq(result.artifacts.hash, "uZJnFj0XVBA6Rs1hEvdIDf8NraM0qRNXdFbR3NEQbVA="); + do_check_eq(result.artifacts.mac, "pLsHHzngIn5CTJhWBtBr+BezUFvdd/IadpTp/FYVIRM="); + + // update, utf-8 payload + result = compute(makeURI("http://example.net/path"), method, + { credentials: credentials_sha256, + ts: 1353809207, + nonce: "Ygvqdz", + payload: "andré@example.org", // non-ASCII + }); + do_check_eq(result.artifacts.hash, "66DiyapJ0oGgj09IXWdMv8VCg9xk0PL5RqX7bNnQW2k="); + do_check_eq(result.artifacts.mac, "2B++3x5xfHEZbPZGDiK3IwfPZctkV4DUr2ORg1vIHvk="); + + /* If "hash" is provided, "payload" is ignored. */ + result = compute(makeURI("http://example.net/path"), method, + { credentials: credentials_sha256, + ts: 1353809207, + nonce: "Ygvqdz", + hash: "66DiyapJ0oGgj09IXWdMv8VCg9xk0PL5RqX7bNnQW2k=", + payload: "something else", + }); + do_check_eq(result.artifacts.hash, "66DiyapJ0oGgj09IXWdMv8VCg9xk0PL5RqX7bNnQW2k="); + do_check_eq(result.artifacts.mac, "2B++3x5xfHEZbPZGDiK3IwfPZctkV4DUr2ORg1vIHvk="); + + // the payload "hash" is also non-urlsafe base64 (+/) + result = compute(makeURI("http://example.net/path"), method, + { credentials: credentials_sha256, + ts: 1353809207, + nonce: "Ygvqdz", + payload: "something else", + }); + do_check_eq(result.artifacts.hash, "lERFXr/IKOaAoYw+eBseDUSwmqZTX0uKZpcWLxsdzt8="); + do_check_eq(result.artifacts.mac, "jiZuhsac35oD7IdcblhFncBr8tJFHcwWLr8NIYWr9PQ="); + + /* Test non-ascii hostname. HAWK (via the node.js "url" module) punycodes + * "ëxample.net" into "xn--xample-ova.net" before hashing. I still think + * punycode was a bad joke that got out of the lab and into a spec. + */ + + result = compute(makeURI("http://ëxample.net/path"), method, + { credentials: credentials_sha256, + ts: 1353809207, + nonce: "Ygvqdz", + }); + do_check_eq(result.artifacts.mac, "pILiHl1q8bbNQIdaaLwAFyaFmDU70MGehFuCs3AA5M0="); + do_check_eq(result.artifacts.host, "xn--xample-ova.net"); + + result = compute(makeURI("http://example.net/path"), method, + { credentials: credentials_sha256, + ts: 1353809207, + nonce: "Ygvqdz", + ext: "backslash=\\ quote=\" EOF", + }); + do_check_eq(result.artifacts.mac, "BEMW76lwaJlPX4E/dajF970T6+GzWvaeyLzUt8eOTOc="); + do_check_eq(result.field, 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ext="backslash=\\\\ quote=\\\" EOF", mac="BEMW76lwaJlPX4E/dajF970T6+GzWvaeyLzUt8eOTOc="'); + + result = compute(makeURI("http://example.net:1234/path"), method, + { credentials: credentials_sha256, + ts: 1353809207, + nonce: "Ygvqdz", + }); + do_check_eq(result.artifacts.mac, "6D3JSFDtozuq8QvJTNUc1JzeCfy6h5oRvlhmSTPv6LE="); + do_check_eq(result.field, 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", mac="6D3JSFDtozuq8QvJTNUc1JzeCfy6h5oRvlhmSTPv6LE="'); + + /* HAWK (the node.js library) uses a URL parser which stores the "port" + * field as a string, but makeURI() gives us an integer. So we'll diverge + * on ports with a leading zero. This test vector would fail on the node.js + * library (HAWK-1.1.1), where they get a MAC of + * "T+GcAsDO8GRHIvZLeepSvXLwDlFJugcZroAy9+uAtcw=". I think HAWK should be + * updated to do what we do here, so port="01234" should get the same hash + * as port="1234". + */ + result = compute(makeURI("http://example.net:01234/path"), method, + { credentials: credentials_sha256, + ts: 1353809207, + nonce: "Ygvqdz", + }); + do_check_eq(result.artifacts.mac, "6D3JSFDtozuq8QvJTNUc1JzeCfy6h5oRvlhmSTPv6LE="); + do_check_eq(result.field, 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", mac="6D3JSFDtozuq8QvJTNUc1JzeCfy6h5oRvlhmSTPv6LE="'); + + run_next_test(); +}); + + +add_test(function test_strip_header_attributes() { + let strip = CryptoUtils.stripHeaderAttributes; + + do_check_eq(strip(undefined), ""); + do_check_eq(strip("text/plain"), "text/plain"); + do_check_eq(strip("TEXT/PLAIN"), "text/plain"); + do_check_eq(strip(" text/plain "), "text/plain"); + do_check_eq(strip("text/plain ; charset=utf-8 "), "text/plain"); + + run_next_test(); +}); diff --git a/services/crypto/tests/unit/test_utils_hkdfExpand.js b/services/crypto/tests/unit/test_utils_hkdfExpand.js new file mode 100644 index 0000000000..4b4b21900c --- /dev/null +++ b/services/crypto/tests/unit/test_utils_hkdfExpand.js @@ -0,0 +1,120 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +Cu.import("resource://services-common/utils.js"); +Cu.import("resource://services-crypto/utils.js"); + +// Test vectors from RFC 5869 + +// Test case 1 + +var tc1 = { + IKM: "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", + salt: "000102030405060708090a0b0c", + info: "f0f1f2f3f4f5f6f7f8f9", + L: 42, + PRK: "077709362c2e32df0ddc3f0dc47bba63" + + "90b6c73bb50f9c3122ec844ad7c2b3e5", + OKM: "3cb25f25faacd57a90434f64d0362f2a" + + "2d2d0a90cf1a5a4c5db02d56ecc4c5bf" + + "34007208d5b887185865" +}; + +// Test case 2 + +var tc2 = { + IKM: "000102030405060708090a0b0c0d0e0f" + + "101112131415161718191a1b1c1d1e1f" + + "202122232425262728292a2b2c2d2e2f" + + "303132333435363738393a3b3c3d3e3f" + + "404142434445464748494a4b4c4d4e4f", + salt: "606162636465666768696a6b6c6d6e6f" + + "707172737475767778797a7b7c7d7e7f" + + "808182838485868788898a8b8c8d8e8f" + + "909192939495969798999a9b9c9d9e9f" + + "a0a1a2a3a4a5a6a7a8a9aaabacadaeaf", + info: "b0b1b2b3b4b5b6b7b8b9babbbcbdbebf" + + "c0c1c2c3c4c5c6c7c8c9cacbcccdcecf" + + "d0d1d2d3d4d5d6d7d8d9dadbdcdddedf" + + "e0e1e2e3e4e5e6e7e8e9eaebecedeeef" + + "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff", + L: 82, + PRK: "06a6b88c5853361a06104c9ceb35b45c" + + "ef760014904671014a193f40c15fc244", + OKM: "b11e398dc80327a1c8e7f78c596a4934" + + "4f012eda2d4efad8a050cc4c19afa97c" + + "59045a99cac7827271cb41c65e590e09" + + "da3275600c2f09b8367793a9aca3db71" + + "cc30c58179ec3e87c14c01d5c1f3434f" + + "1d87" +}; + +// Test case 3 + +var tc3 = { + IKM: "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", + salt: "", + info: "", + L: 42, + PRK: "19ef24a32c717b167f33a91d6f648bdf" + + "96596776afdb6377ac434c1c293ccb04", + OKM: "8da4e775a563c18f715f802a063c5a31" + + "b8a11f5c5ee1879ec3454e5f3c738d2d" + + "9d201395faa4b61a96c8" +}; + +function sha256HMAC(message, key) { + let h = CryptoUtils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256, key); + return CryptoUtils.digestBytes(message, h); +} + +function _hexToString(hex) { + let ret = ""; + if (hex.length % 2 != 0) { + return false; + } + + for (let i = 0; i < hex.length; i += 2) { + let cur = hex[i] + hex[i + 1]; + ret += String.fromCharCode(parseInt(cur, 16)); + } + return ret; +} + +function extract_hex(salt, ikm) { + salt = _hexToString(salt); + ikm = _hexToString(ikm); + return CommonUtils.bytesAsHex(sha256HMAC(ikm, CryptoUtils.makeHMACKey(salt))); +} + +function expand_hex(prk, info, len) { + prk = _hexToString(prk); + info = _hexToString(info); + return CommonUtils.bytesAsHex(CryptoUtils.hkdfExpand(prk, info, len)); +} + +function hkdf_hex(ikm, salt, info, len) { + ikm = _hexToString(ikm); + if (salt) + salt = _hexToString(salt); + info = _hexToString(info); + return CommonUtils.bytesAsHex(CryptoUtils.hkdf(ikm, salt, info, len)); +} + +function run_test() { + _("Verifying Test Case 1"); + do_check_eq(extract_hex(tc1.salt, tc1.IKM), tc1.PRK); + do_check_eq(expand_hex(tc1.PRK, tc1.info, tc1.L), tc1.OKM); + do_check_eq(hkdf_hex(tc1.IKM, tc1.salt, tc1.info, tc1.L), tc1.OKM); + + _("Verifying Test Case 2"); + do_check_eq(extract_hex(tc2.salt, tc2.IKM), tc2.PRK); + do_check_eq(expand_hex(tc2.PRK, tc2.info, tc2.L), tc2.OKM); + do_check_eq(hkdf_hex(tc2.IKM, tc2.salt, tc2.info, tc2.L), tc2.OKM); + + _("Verifying Test Case 3"); + do_check_eq(extract_hex(tc3.salt, tc3.IKM), tc3.PRK); + do_check_eq(expand_hex(tc3.PRK, tc3.info, tc3.L), tc3.OKM); + do_check_eq(hkdf_hex(tc3.IKM, tc3.salt, tc3.info, tc3.L), tc3.OKM); + do_check_eq(hkdf_hex(tc3.IKM, undefined, tc3.info, tc3.L), tc3.OKM); +} diff --git a/services/crypto/tests/unit/test_utils_httpmac.js b/services/crypto/tests/unit/test_utils_httpmac.js new file mode 100644 index 0000000000..67b337373e --- /dev/null +++ b/services/crypto/tests/unit/test_utils_httpmac.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://services-common/utils.js"); +Cu.import("resource://services-crypto/utils.js"); + +function run_test() { + initTestLogging(); + + run_next_test(); +} + +add_test(function test_sha1() { + _("Ensure HTTP MAC SHA1 generation works as expected."); + + let id = "vmo1txkttblmn51u2p3zk2xiy16hgvm5ok8qiv1yyi86ffjzy9zj0ez9x6wnvbx7"; + let key = "b8u1cc5iiio5o319og7hh8faf2gi5ym4aq0zwf112cv1287an65fudu5zj7zo7dz"; + let ts = 1329181221; + let method = "GET"; + let nonce = "wGX71"; + let uri = CommonUtils.makeURI("http://10.250.2.176/alias/"); + + let result = CryptoUtils.computeHTTPMACSHA1(id, key, method, uri, + {ts: ts, nonce: nonce}); + + do_check_eq(btoa(result.mac), "jzh5chjQc2zFEvLbyHnPdX11Yck="); + + do_check_eq(result.getHeader(), + 'MAC id="vmo1txkttblmn51u2p3zk2xiy16hgvm5ok8qiv1yyi86ffjzy9zj0ez9x6wnvbx7", ' + + 'ts="1329181221", nonce="wGX71", mac="jzh5chjQc2zFEvLbyHnPdX11Yck="'); + + let ext = "EXTRA DATA; foo,bar=1"; + + result = CryptoUtils.computeHTTPMACSHA1(id, key, method, uri, + {ts: ts, nonce: nonce, ext: ext}); + do_check_eq(btoa(result.mac), "bNf4Fnt5k6DnhmyipLPkuZroH68="); + do_check_eq(result.getHeader(), + 'MAC id="vmo1txkttblmn51u2p3zk2xiy16hgvm5ok8qiv1yyi86ffjzy9zj0ez9x6wnvbx7", ' + + 'ts="1329181221", nonce="wGX71", mac="bNf4Fnt5k6DnhmyipLPkuZroH68=", ' + + 'ext="EXTRA DATA; foo,bar=1"'); + + run_next_test(); +}); + +add_test(function test_nonce_length() { + _("Ensure custom nonce lengths are honoured."); + + function get_mac(length) { + let uri = CommonUtils.makeURI("http://example.com/"); + return CryptoUtils.computeHTTPMACSHA1("foo", "bar", "GET", uri, { + nonce_bytes: length + }); + } + + let result = get_mac(12); + do_check_eq(12, atob(result.nonce).length); + + result = get_mac(2); + do_check_eq(2, atob(result.nonce).length); + + result = get_mac(0); + do_check_eq(8, atob(result.nonce).length); + + result = get_mac(-1); + do_check_eq(8, atob(result.nonce).length); + + run_next_test(); +}); diff --git a/services/crypto/tests/unit/test_utils_pbkdf2.js b/services/crypto/tests/unit/test_utils_pbkdf2.js new file mode 100644 index 0000000000..7313819ec4 --- /dev/null +++ b/services/crypto/tests/unit/test_utils_pbkdf2.js @@ -0,0 +1,162 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// XXX until bug 937114 is fixed +Cu.importGlobalProperties(['btoa']); +Cu.import("resource://services-crypto/utils.js"); +Cu.import("resource://services-common/utils.js"); + +var {bytesAsHex: b2h} = CommonUtils; + +function run_test() { + run_next_test(); +} + +add_task(function test_pbkdf2() { + let symmKey16 = CryptoUtils.pbkdf2Generate("secret phrase", "DNXPzPpiwn", 4096, 16); + do_check_eq(symmKey16.length, 16); + do_check_eq(btoa(symmKey16), "d2zG0d2cBfXnRwMUGyMwyg=="); + do_check_eq(CommonUtils.encodeBase32(symmKey16), "O5WMNUO5TQC7LZ2HAMKBWIZQZI======"); + let symmKey32 = CryptoUtils.pbkdf2Generate("passphrase", "salt", 4096, 32); + do_check_eq(symmKey32.length, 32); +}); + +// http://tools.ietf.org/html/rfc6070 +// PBKDF2 HMAC-SHA1 Test Vectors +add_task(function test_pbkdf2_hmac_sha1() { + let pbkdf2 = CryptoUtils.pbkdf2Generate; + let vectors = [ + {P: "password", // (8 octets) + S: "salt", // (4 octets) + c: 1, + dkLen: 20, + DK: h("0c 60 c8 0f 96 1f 0e 71"+ + "f3 a9 b5 24 af 60 12 06"+ + "2f e0 37 a6"), // (20 octets) + }, + + {P: "password", // (8 octets) + S: "salt", // (4 octets) + c: 2, + dkLen: 20, + DK: h("ea 6c 01 4d c7 2d 6f 8c"+ + "cd 1e d9 2a ce 1d 41 f0"+ + "d8 de 89 57"), // (20 octets) + }, + + {P: "password", // (8 octets) + S: "salt", // (4 octets) + c: 4096, + dkLen: 20, + DK: h("4b 00 79 01 b7 65 48 9a"+ + "be ad 49 d9 26 f7 21 d0"+ + "65 a4 29 c1"), // (20 octets) + }, + + // XXX Uncomment the following test after Bug 968567 lands + // + // XXX As it stands, I estimate that the CryptoUtils implementation will + // take approximately 16 hours in my 2.3GHz MacBook to perform this many + // rounds. + // + // {P: "password", // (8 octets) + // S: "salt" // (4 octets) + // c: 16777216, + // dkLen = 20, + // DK: h("ee fe 3d 61 cd 4d a4 e4"+ + // "e9 94 5b 3d 6b a2 15 8c"+ + // "26 34 e9 84"), // (20 octets) + // }, + + {P: "passwordPASSWORDpassword", // (24 octets) + S: "saltSALTsaltSALTsaltSALTsaltSALTsalt", // (36 octets) + c: 4096, + dkLen: 25, + DK: h("3d 2e ec 4f e4 1c 84 9b"+ + "80 c8 d8 36 62 c0 e4 4a"+ + "8b 29 1a 96 4c f2 f0 70"+ + "38"), // (25 octets) + + }, + + {P: "pass\0word", // (9 octets) + S: "sa\0lt", // (5 octets) + c: 4096, + dkLen: 16, + DK: h("56 fa 6a a7 55 48 09 9d"+ + "cc 37 d7 f0 34 25 e0 c3"), // (16 octets) + }, + ]; + + for (let v of vectors) { + do_check_eq(v.DK, b2h(pbkdf2(v.P, v.S, v.c, v.dkLen))); + } +}); + +// I can't find any normative ietf test vectors for pbkdf2 hmac-sha256. +// The following vectors are derived with the same inputs as above (the sha1 +// test). Results verified by users here: +// https://stackoverflow.com/questions/5130513/pbkdf2-hmac-sha2-test-vectors +add_task(function test_pbkdf2_hmac_sha256() { + let pbkdf2 = CryptoUtils.pbkdf2Generate; + let vectors = [ + {P: "password", // (8 octets) + S: "salt", // (4 octets) + c: 1, + dkLen: 32, + DK: h("12 0f b6 cf fc f8 b3 2c"+ + "43 e7 22 52 56 c4 f8 37"+ + "a8 65 48 c9 2c cc 35 48"+ + "08 05 98 7c b7 0b e1 7b"), // (32 octets) + }, + + {P: "password", // (8 octets) + S: "salt", // (4 octets) + c: 2, + dkLen: 32, + DK: h("ae 4d 0c 95 af 6b 46 d3"+ + "2d 0a df f9 28 f0 6d d0"+ + "2a 30 3f 8e f3 c2 51 df"+ + "d6 e2 d8 5a 95 47 4c 43"), // (32 octets) + }, + + {P: "password", // (8 octets) + S: "salt", // (4 octets) + c: 4096, + dkLen: 32, + DK: h("c5 e4 78 d5 92 88 c8 41"+ + "aa 53 0d b6 84 5c 4c 8d"+ + "96 28 93 a0 01 ce 4e 11"+ + "a4 96 38 73 aa 98 13 4a"), // (32 octets) + }, + + {P: "passwordPASSWORDpassword", // (24 octets) + S: "saltSALTsaltSALTsaltSALTsaltSALTsalt", // (36 octets) + c: 4096, + dkLen: 40, + DK: h("34 8c 89 db cb d3 2b 2f"+ + "32 d8 14 b8 11 6e 84 cf"+ + "2b 17 34 7e bc 18 00 18"+ + "1c 4e 2a 1f b8 dd 53 e1"+ + "c6 35 51 8c 7d ac 47 e9"), // (40 octets) + }, + + {P: "pass\0word", // (9 octets) + S: "sa\0lt", // (5 octets) + c: 4096, + dkLen: 16, + DK: h("89 b6 9d 05 16 f8 29 89"+ + "3c 69 62 26 65 0a 86 87"), // (16 octets) + }, + ]; + + for (let v of vectors) { + do_check_eq(v.DK, + b2h(pbkdf2(v.P, v.S, v.c, v.dkLen, Ci.nsICryptoHMAC.SHA256, 32))); + } +}); + +// turn formatted test vectors into normal hex strings +function h(hexStr) { + return hexStr.replace(/\s+/g, ""); +} diff --git a/services/crypto/tests/unit/test_utils_sha1.js b/services/crypto/tests/unit/test_utils_sha1.js new file mode 100644 index 0000000000..f99350754f --- /dev/null +++ b/services/crypto/tests/unit/test_utils_sha1.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +_("Make sure sha1 digests works with various messages"); + +Cu.import("resource://services-crypto/utils.js"); + +function run_test() { + let mes1 = "hello"; + let mes2 = "world"; + + let dig0 = CryptoUtils.UTF8AndSHA1(mes1); + do_check_eq(dig0, + "\xaa\xf4\xc6\x1d\xdc\xc5\xe8\xa2\xda\xbe\xde\x0f\x3b\x48\x2c\xd9\xae\xa9\x43\x4d"); + + _("Make sure right sha1 digests are generated"); + let dig1 = CryptoUtils.sha1(mes1); + do_check_eq(dig1, "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d"); + let dig2 = CryptoUtils.sha1(mes2); + do_check_eq(dig2, "7c211433f02071597741e6ff5a8ea34789abbf43"); + let dig12 = CryptoUtils.sha1(mes1 + mes2); + do_check_eq(dig12, "6adfb183a4a2c94a2f92dab5ade762a47889a5a1"); + let dig21 = CryptoUtils.sha1(mes2 + mes1); + do_check_eq(dig21, "5715790a892990382d98858c4aa38d0617151575"); + + _("Repeated sha1s shouldn't change the digest"); + do_check_eq(CryptoUtils.sha1(mes1), dig1); + do_check_eq(CryptoUtils.sha1(mes2), dig2); + do_check_eq(CryptoUtils.sha1(mes1 + mes2), dig12); + do_check_eq(CryptoUtils.sha1(mes2 + mes1), dig21); + + _("Nested sha1 should work just fine"); + let nest1 = CryptoUtils.sha1(CryptoUtils.sha1(CryptoUtils.sha1(CryptoUtils.sha1(CryptoUtils.sha1(mes1))))); + do_check_eq(nest1, "23f340d0cff31e299158b3181b6bcc7e8c7f985a"); + let nest2 = CryptoUtils.sha1(CryptoUtils.sha1(CryptoUtils.sha1(CryptoUtils.sha1(CryptoUtils.sha1(mes2))))); + do_check_eq(nest2, "1f6453867e3fb9876ae429918a64cdb8dc5ff2d0"); +} diff --git a/services/crypto/tests/unit/xpcshell.ini b/services/crypto/tests/unit/xpcshell.ini new file mode 100644 index 0000000000..0b3a9324c0 --- /dev/null +++ b/services/crypto/tests/unit/xpcshell.ini @@ -0,0 +1,20 @@ +[DEFAULT] +head = head_helpers.js ../../../common/tests/unit/head_helpers.js +tail = +firefox-appdir = browser +support-files = + !/services/common/tests/unit/head_helpers.js + +[test_load_modules.js] + +[test_crypto_crypt.js] +[test_crypto_deriveKey.js] +[test_crypto_random.js] +# Bug 676977: test hangs consistently on Android +skip-if = os == "android" + +[test_utils_hawk.js] +[test_utils_hkdfExpand.js] +[test_utils_httpmac.js] +[test_utils_pbkdf2.js] +[test_utils_sha1.js] |