summaryrefslogtreecommitdiffstats
path: root/services/crypto
diff options
context:
space:
mode:
Diffstat (limited to 'services/crypto')
-rw-r--r--services/crypto/component/IdentityCryptoService.cpp461
-rw-r--r--services/crypto/component/components.conf13
-rw-r--r--services/crypto/component/moz.build21
-rw-r--r--services/crypto/component/nsIIdentityCryptoService.idl106
-rw-r--r--services/crypto/cryptoComponents.manifest1
-rw-r--r--services/crypto/modules/WeaveCrypto.js245
-rw-r--r--services/crypto/modules/jwcrypto.jsm377
-rw-r--r--services/crypto/modules/utils.js587
-rw-r--r--services/crypto/moz.build22
-rw-r--r--services/crypto/tests/unit/head_helpers.js79
-rw-r--r--services/crypto/tests/unit/test_crypto_crypt.js227
-rw-r--r--services/crypto/tests/unit/test_crypto_random.js50
-rw-r--r--services/crypto/tests/unit/test_crypto_service.js133
-rw-r--r--services/crypto/tests/unit/test_jwcrypto.js240
-rw-r--r--services/crypto/tests/unit/test_load_modules.js12
-rw-r--r--services/crypto/tests/unit/test_utils_hawk.js346
-rw-r--r--services/crypto/tests/unit/test_utils_httpmac.js73
-rw-r--r--services/crypto/tests/unit/xpcshell.ini17
18 files changed, 3010 insertions, 0 deletions
diff --git a/services/crypto/component/IdentityCryptoService.cpp b/services/crypto/component/IdentityCryptoService.cpp
new file mode 100644
index 0000000000..f2c59eb9d5
--- /dev/null
+++ b/services/crypto/component/IdentityCryptoService.cpp
@@ -0,0 +1,461 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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 "nsIIdentityCryptoService.h"
+#include "nsServiceManagerUtils.h"
+#include "nsIThread.h"
+#include "nsThreadUtils.h"
+#include "nsCOMPtr.h"
+#include "nsProxyRelease.h"
+#include "nsString.h"
+#include "mozilla/ArrayUtils.h" // ArrayLength
+#include "mozilla/Base64.h"
+#include "mozilla/Components.h"
+#include "ScopedNSSTypes.h"
+#include "NSSErrorsService.h"
+
+#include "nss.h"
+#include "pk11pub.h"
+#include "secmod.h"
+#include "secerr.h"
+#include "keyhi.h"
+#include "cryptohi.h"
+
+#include <limits.h>
+
+using namespace mozilla;
+
+namespace {
+
+void HexEncode(const SECItem* it, nsACString& result) {
+ static const char digits[] = "0123456789ABCDEF";
+ result.SetLength(it->len * 2);
+ char* p = result.BeginWriting();
+ for (unsigned int i = 0; i < it->len; ++i) {
+ *p++ = digits[it->data[i] >> 4];
+ *p++ = digits[it->data[i] & 0x0f];
+ }
+}
+
+#define DSA_KEY_TYPE_STRING ("DS160"_ns)
+#define RSA_KEY_TYPE_STRING ("RS256"_ns)
+
+class KeyPair : public nsIIdentityKeyPair {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIIDENTITYKEYPAIR
+
+ KeyPair(SECKEYPrivateKey* aPrivateKey, SECKEYPublicKey* aPublicKey,
+ nsIEventTarget* aOperationThread);
+
+ private:
+ virtual ~KeyPair() {
+ if (mPrivateKey) {
+ SECKEY_DestroyPrivateKey(mPrivateKey);
+ }
+ if (mPublicKey) {
+ SECKEY_DestroyPublicKey(mPublicKey);
+ }
+ }
+
+ SECKEYPrivateKey* mPrivateKey;
+ SECKEYPublicKey* mPublicKey;
+ nsCOMPtr<nsIEventTarget> mThread;
+
+ KeyPair(const KeyPair&) = delete;
+ void operator=(const KeyPair&) = delete;
+};
+
+NS_IMPL_ISUPPORTS(KeyPair, nsIIdentityKeyPair)
+
+class KeyGenRunnable : public Runnable {
+ public:
+ NS_DECL_NSIRUNNABLE
+
+ KeyGenRunnable(KeyType keyType, nsIIdentityKeyGenCallback* aCallback,
+ nsIEventTarget* aOperationThread);
+
+ private:
+ const KeyType mKeyType; // in
+ nsMainThreadPtrHandle<nsIIdentityKeyGenCallback> mCallback; // in
+ nsresult mRv; // out
+ nsCOMPtr<nsIIdentityKeyPair> mKeyPair; // out
+ nsCOMPtr<nsIEventTarget> mThread;
+
+ KeyGenRunnable(const KeyGenRunnable&) = delete;
+ void operator=(const KeyGenRunnable&) = delete;
+};
+
+class SignRunnable : public Runnable {
+ public:
+ NS_DECL_NSIRUNNABLE
+
+ SignRunnable(const nsACString& textToSign, SECKEYPrivateKey* privateKey,
+ nsIIdentitySignCallback* aCallback);
+
+ private:
+ ~SignRunnable() override {
+ if (mPrivateKey) {
+ SECKEY_DestroyPrivateKey(mPrivateKey);
+ }
+ }
+
+ const nsCString mTextToSign; // in
+ SECKEYPrivateKey* mPrivateKey; // in
+ nsMainThreadPtrHandle<nsIIdentitySignCallback> mCallback; // in
+ nsresult mRv; // out
+ nsCString mSignature; // out
+
+ SignRunnable(const SignRunnable&) = delete;
+ void operator=(const SignRunnable&) = delete;
+};
+
+class IdentityCryptoService final : public nsIIdentityCryptoService {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIIDENTITYCRYPTOSERVICE
+
+ IdentityCryptoService() = default;
+ nsresult Init() {
+ nsresult rv;
+ nsCOMPtr<nsISupports> dummyUsedToEnsureNSSIsInitialized =
+ do_GetService("@mozilla.org/psm;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIThread> thread;
+ rv = NS_NewNamedThread("IdentityCrypto", getter_AddRefs(thread));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mThread = std::move(thread);
+
+ return NS_OK;
+ }
+
+ private:
+ ~IdentityCryptoService() = default;
+ IdentityCryptoService(const KeyPair&) = delete;
+ void operator=(const IdentityCryptoService&) = delete;
+
+ nsCOMPtr<nsIEventTarget> mThread;
+};
+
+NS_IMPL_ISUPPORTS(IdentityCryptoService, nsIIdentityCryptoService)
+
+NS_IMETHODIMP
+IdentityCryptoService::GenerateKeyPair(const nsACString& keyTypeString,
+ nsIIdentityKeyGenCallback* callback) {
+ KeyType keyType;
+ if (keyTypeString.Equals(RSA_KEY_TYPE_STRING)) {
+ keyType = rsaKey;
+ } else if (keyTypeString.Equals(DSA_KEY_TYPE_STRING)) {
+ keyType = dsaKey;
+ } else {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ nsCOMPtr<nsIRunnable> r = new KeyGenRunnable(keyType, callback, mThread);
+ nsresult rv = mThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+IdentityCryptoService::Base64UrlEncode(const nsACString& utf8Input,
+ nsACString& result) {
+ return Base64URLEncode(
+ utf8Input.Length(),
+ reinterpret_cast<const uint8_t*>(utf8Input.BeginReading()),
+ Base64URLEncodePaddingPolicy::Include, result);
+}
+
+KeyPair::KeyPair(SECKEYPrivateKey* privateKey, SECKEYPublicKey* publicKey,
+ nsIEventTarget* operationThread)
+ : mPrivateKey(privateKey), mPublicKey(publicKey), mThread(operationThread) {
+ MOZ_ASSERT(!NS_IsMainThread());
+}
+
+NS_IMETHODIMP
+KeyPair::GetHexRSAPublicKeyExponent(nsACString& result) {
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_ENSURE_TRUE(mPublicKey, NS_ERROR_NOT_AVAILABLE);
+ NS_ENSURE_TRUE(mPublicKey->keyType == rsaKey, NS_ERROR_NOT_AVAILABLE);
+ HexEncode(&mPublicKey->u.rsa.publicExponent, result);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+KeyPair::GetHexRSAPublicKeyModulus(nsACString& result) {
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_ENSURE_TRUE(mPublicKey, NS_ERROR_NOT_AVAILABLE);
+ NS_ENSURE_TRUE(mPublicKey->keyType == rsaKey, NS_ERROR_NOT_AVAILABLE);
+ HexEncode(&mPublicKey->u.rsa.modulus, result);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+KeyPair::GetHexDSAPrime(nsACString& result) {
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_ENSURE_TRUE(mPublicKey, NS_ERROR_NOT_AVAILABLE);
+ NS_ENSURE_TRUE(mPublicKey->keyType == dsaKey, NS_ERROR_NOT_AVAILABLE);
+ HexEncode(&mPublicKey->u.dsa.params.prime, result);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+KeyPair::GetHexDSASubPrime(nsACString& result) {
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_ENSURE_TRUE(mPublicKey, NS_ERROR_NOT_AVAILABLE);
+ NS_ENSURE_TRUE(mPublicKey->keyType == dsaKey, NS_ERROR_NOT_AVAILABLE);
+ HexEncode(&mPublicKey->u.dsa.params.subPrime, result);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+KeyPair::GetHexDSAGenerator(nsACString& result) {
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_ENSURE_TRUE(mPublicKey, NS_ERROR_NOT_AVAILABLE);
+ NS_ENSURE_TRUE(mPublicKey->keyType == dsaKey, NS_ERROR_NOT_AVAILABLE);
+ HexEncode(&mPublicKey->u.dsa.params.base, result);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+KeyPair::GetHexDSAPublicValue(nsACString& result) {
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_ENSURE_TRUE(mPublicKey, NS_ERROR_NOT_AVAILABLE);
+ NS_ENSURE_TRUE(mPublicKey->keyType == dsaKey, NS_ERROR_NOT_AVAILABLE);
+ HexEncode(&mPublicKey->u.dsa.publicValue, result);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+KeyPair::GetKeyType(nsACString& result) {
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_ENSURE_TRUE(mPublicKey, NS_ERROR_NOT_AVAILABLE);
+
+ switch (mPublicKey->keyType) {
+ case rsaKey:
+ result = RSA_KEY_TYPE_STRING;
+ return NS_OK;
+ case dsaKey:
+ result = DSA_KEY_TYPE_STRING;
+ return NS_OK;
+ default:
+ return NS_ERROR_UNEXPECTED;
+ }
+}
+
+NS_IMETHODIMP
+KeyPair::Sign(const nsACString& textToSign, nsIIdentitySignCallback* callback) {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsCOMPtr<nsIRunnable> r = new SignRunnable(textToSign, mPrivateKey, callback);
+
+ return mThread->Dispatch(r, NS_DISPATCH_NORMAL);
+}
+
+KeyGenRunnable::KeyGenRunnable(KeyType keyType,
+ nsIIdentityKeyGenCallback* callback,
+ nsIEventTarget* operationThread)
+ : mozilla::Runnable("KeyGenRunnable"),
+ mKeyType(keyType),
+ mCallback(new nsMainThreadPtrHolder<nsIIdentityKeyGenCallback>(
+ "KeyGenRunnable::mCallback", callback)),
+ mRv(NS_ERROR_NOT_INITIALIZED),
+ mThread(operationThread) {}
+
+[[nodiscard]] nsresult GenerateKeyPair(PK11SlotInfo* slot,
+ SECKEYPrivateKey** privateKey,
+ SECKEYPublicKey** publicKey,
+ CK_MECHANISM_TYPE mechanism,
+ void* params) {
+ *publicKey = nullptr;
+ *privateKey = PK11_GenerateKeyPair(
+ slot, mechanism, params, publicKey, PR_FALSE /*isPerm*/,
+ PR_TRUE /*isSensitive*/, nullptr /*&pwdata*/);
+ if (!*privateKey) {
+ MOZ_ASSERT(!*publicKey);
+ return mozilla::psm::GetXPCOMFromNSSError(PR_GetError());
+ }
+ if (!*publicKey) {
+ SECKEY_DestroyPrivateKey(*privateKey);
+ *privateKey = nullptr;
+ MOZ_CRASH("PK11_GnerateKeyPair returned private key without public key");
+ }
+
+ return NS_OK;
+}
+
+[[nodiscard]] nsresult GenerateRSAKeyPair(PK11SlotInfo* slot,
+ SECKEYPrivateKey** privateKey,
+ SECKEYPublicKey** publicKey) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ PK11RSAGenParams rsaParams;
+ rsaParams.keySizeInBits = 2048;
+ rsaParams.pe = 0x10001;
+ return GenerateKeyPair(slot, privateKey, publicKey, CKM_RSA_PKCS_KEY_PAIR_GEN,
+ &rsaParams);
+}
+
+[[nodiscard]] nsresult GenerateDSAKeyPair(PK11SlotInfo* slot,
+ SECKEYPrivateKey** privateKey,
+ SECKEYPublicKey** publicKey) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ // XXX: These could probably be static const arrays, but this way we avoid
+ // compiler warnings and also we avoid having to worry much about whether the
+ // functions that take these inputs will (unexpectedly) modify them.
+
+ // Using NIST parameters. Some other BrowserID components require that these
+ // exact parameters are used.
+ uint8_t P[] = {
+ 0xFF, 0x60, 0x04, 0x83, 0xDB, 0x6A, 0xBF, 0xC5, 0xB4, 0x5E, 0xAB, 0x78,
+ 0x59, 0x4B, 0x35, 0x33, 0xD5, 0x50, 0xD9, 0xF1, 0xBF, 0x2A, 0x99, 0x2A,
+ 0x7A, 0x8D, 0xAA, 0x6D, 0xC3, 0x4F, 0x80, 0x45, 0xAD, 0x4E, 0x6E, 0x0C,
+ 0x42, 0x9D, 0x33, 0x4E, 0xEE, 0xAA, 0xEF, 0xD7, 0xE2, 0x3D, 0x48, 0x10,
+ 0xBE, 0x00, 0xE4, 0xCC, 0x14, 0x92, 0xCB, 0xA3, 0x25, 0xBA, 0x81, 0xFF,
+ 0x2D, 0x5A, 0x5B, 0x30, 0x5A, 0x8D, 0x17, 0xEB, 0x3B, 0xF4, 0xA0, 0x6A,
+ 0x34, 0x9D, 0x39, 0x2E, 0x00, 0xD3, 0x29, 0x74, 0x4A, 0x51, 0x79, 0x38,
+ 0x03, 0x44, 0xE8, 0x2A, 0x18, 0xC4, 0x79, 0x33, 0x43, 0x8F, 0x89, 0x1E,
+ 0x22, 0xAE, 0xEF, 0x81, 0x2D, 0x69, 0xC8, 0xF7, 0x5E, 0x32, 0x6C, 0xB7,
+ 0x0E, 0xA0, 0x00, 0xC3, 0xF7, 0x76, 0xDF, 0xDB, 0xD6, 0x04, 0x63, 0x8C,
+ 0x2E, 0xF7, 0x17, 0xFC, 0x26, 0xD0, 0x2E, 0x17};
+
+ uint8_t Q[] = {0xE2, 0x1E, 0x04, 0xF9, 0x11, 0xD1, 0xED, 0x79, 0x91, 0x00,
+ 0x8E, 0xCA, 0xAB, 0x3B, 0xF7, 0x75, 0x98, 0x43, 0x09, 0xC3};
+
+ uint8_t G[] = {
+ 0xC5, 0x2A, 0x4A, 0x0F, 0xF3, 0xB7, 0xE6, 0x1F, 0xDF, 0x18, 0x67, 0xCE,
+ 0x84, 0x13, 0x83, 0x69, 0xA6, 0x15, 0x4F, 0x4A, 0xFA, 0x92, 0x96, 0x6E,
+ 0x3C, 0x82, 0x7E, 0x25, 0xCF, 0xA6, 0xCF, 0x50, 0x8B, 0x90, 0xE5, 0xDE,
+ 0x41, 0x9E, 0x13, 0x37, 0xE0, 0x7A, 0x2E, 0x9E, 0x2A, 0x3C, 0xD5, 0xDE,
+ 0xA7, 0x04, 0xD1, 0x75, 0xF8, 0xEB, 0xF6, 0xAF, 0x39, 0x7D, 0x69, 0xE1,
+ 0x10, 0xB9, 0x6A, 0xFB, 0x17, 0xC7, 0xA0, 0x32, 0x59, 0x32, 0x9E, 0x48,
+ 0x29, 0xB0, 0xD0, 0x3B, 0xBC, 0x78, 0x96, 0xB1, 0x5B, 0x4A, 0xDE, 0x53,
+ 0xE1, 0x30, 0x85, 0x8C, 0xC3, 0x4D, 0x96, 0x26, 0x9A, 0xA8, 0x90, 0x41,
+ 0xF4, 0x09, 0x13, 0x6C, 0x72, 0x42, 0xA3, 0x88, 0x95, 0xC9, 0xD5, 0xBC,
+ 0xCA, 0xD4, 0xF3, 0x89, 0xAF, 0x1D, 0x7A, 0x4B, 0xD1, 0x39, 0x8B, 0xD0,
+ 0x72, 0xDF, 0xFA, 0x89, 0x62, 0x33, 0x39, 0x7A};
+
+ static_assert(MOZ_ARRAY_LENGTH(P) == 1024 / CHAR_BIT, "bad DSA P");
+ static_assert(MOZ_ARRAY_LENGTH(Q) == 160 / CHAR_BIT, "bad DSA Q");
+ static_assert(MOZ_ARRAY_LENGTH(G) == 1024 / CHAR_BIT, "bad DSA G");
+
+ PQGParams pqgParams = {
+ nullptr /*arena*/,
+ {siBuffer, P, static_cast<unsigned int>(mozilla::ArrayLength(P))},
+ {siBuffer, Q, static_cast<unsigned int>(mozilla::ArrayLength(Q))},
+ {siBuffer, G, static_cast<unsigned int>(mozilla::ArrayLength(G))}};
+
+ return GenerateKeyPair(slot, privateKey, publicKey, CKM_DSA_KEY_PAIR_GEN,
+ &pqgParams);
+}
+
+NS_IMETHODIMP
+KeyGenRunnable::Run() {
+ if (!NS_IsMainThread()) {
+ // We always want to use the internal slot for BrowserID; in particular,
+ // we want to avoid smartcard slots.
+ PK11SlotInfo* slot = PK11_GetInternalSlot();
+ if (!slot) {
+ mRv = NS_ERROR_UNEXPECTED;
+ } else {
+ SECKEYPrivateKey* privk = nullptr;
+ SECKEYPublicKey* pubk = nullptr;
+
+ switch (mKeyType) {
+ case rsaKey:
+ mRv = GenerateRSAKeyPair(slot, &privk, &pubk);
+ break;
+ case dsaKey:
+ mRv = GenerateDSAKeyPair(slot, &privk, &pubk);
+ break;
+ default:
+ MOZ_CRASH("unknown key type");
+ }
+
+ PK11_FreeSlot(slot);
+
+ if (NS_SUCCEEDED(mRv)) {
+ MOZ_ASSERT(privk);
+ MOZ_ASSERT(pubk);
+ // mKeyPair will take over ownership of privk and pubk
+ mKeyPair = new KeyPair(privk, pubk, mThread);
+ }
+ }
+
+ NS_DispatchToMainThread(this);
+ } else {
+ // Back on Main Thread
+ (void)mCallback->GenerateKeyPairFinished(mRv, mKeyPair);
+ }
+ return NS_OK;
+}
+
+SignRunnable::SignRunnable(const nsACString& aText,
+ SECKEYPrivateKey* privateKey,
+ nsIIdentitySignCallback* aCallback)
+ : mozilla::Runnable("SignRunnable"),
+ mTextToSign(aText),
+ mPrivateKey(SECKEY_CopyPrivateKey(privateKey)),
+ mCallback(new nsMainThreadPtrHolder<nsIIdentitySignCallback>(
+ "SignRunnable::mCallback", aCallback)),
+ mRv(NS_ERROR_NOT_INITIALIZED) {}
+
+NS_IMETHODIMP
+SignRunnable::Run() {
+ if (!NS_IsMainThread()) {
+ // We need the output in PKCS#11 format, not DER encoding, so we must use
+ // PK11_HashBuf and PK11_Sign instead of SEC_SignData.
+
+ SECItem sig = {siBuffer, nullptr, 0};
+ int sigLength = PK11_SignatureLen(mPrivateKey);
+ if (sigLength <= 0) {
+ mRv = mozilla::psm::GetXPCOMFromNSSError(PR_GetError());
+ } else if (!SECITEM_AllocItem(nullptr, &sig, sigLength)) {
+ mRv = mozilla::psm::GetXPCOMFromNSSError(PR_GetError());
+ } else {
+ uint8_t hash[32]; // big enough for SHA-1 or SHA-256
+ SECOidTag hashAlg =
+ mPrivateKey->keyType == dsaKey ? SEC_OID_SHA1 : SEC_OID_SHA256;
+ SECItem hashItem = {siBuffer, hash, hashAlg == SEC_OID_SHA1 ? 20u : 32u};
+
+ mRv = MapSECStatus(
+ PK11_HashBuf(hashAlg, hash,
+ const_cast<uint8_t*>(
+ reinterpret_cast<const uint8_t*>(mTextToSign.get())),
+ mTextToSign.Length()));
+ if (NS_SUCCEEDED(mRv)) {
+ mRv = MapSECStatus(PK11_Sign(mPrivateKey, &sig, &hashItem));
+ }
+ if (NS_SUCCEEDED(mRv)) {
+ mRv =
+ Base64URLEncode(sig.len, sig.data,
+ Base64URLEncodePaddingPolicy::Include, mSignature);
+ }
+ SECITEM_FreeItem(&sig, false);
+ }
+
+ NS_DispatchToMainThread(this);
+ } else {
+ // Back on Main Thread
+ (void)mCallback->SignFinished(mRv, mSignature);
+ }
+
+ return NS_OK;
+}
+} // unnamed namespace
+
+// XPCOM module registration
+
+NS_IMPL_COMPONENT_FACTORY(nsIIdentityCryptoService) {
+ auto inst = MakeRefPtr<IdentityCryptoService>();
+ if (NS_SUCCEEDED(inst->Init())) {
+ return inst.forget().downcast<nsIIdentityCryptoService>();
+ }
+ return nullptr;
+}
diff --git a/services/crypto/component/components.conf b/services/crypto/component/components.conf
new file mode 100644
index 0000000000..49a4b74d7a
--- /dev/null
+++ b/services/crypto/component/components.conf
@@ -0,0 +1,13 @@
+# -*- 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/.
+
+Classes = [
+ {
+ 'cid': '{bea13a3a-44e8-4d7f-a0a2-2c67f84e3a97}',
+ 'contract_ids': ['@mozilla.org/identity/crypto-service;1'],
+ 'type': 'nsIIdentityCryptoService',
+ },
+]
diff --git a/services/crypto/component/moz.build b/services/crypto/component/moz.build
new file mode 100644
index 0000000000..41b47f25c6
--- /dev/null
+++ b/services/crypto/component/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/.
+
+XPIDL_SOURCES += [
+ "nsIIdentityCryptoService.idl",
+]
+
+XPIDL_MODULE = "services-crypto-component"
+
+SOURCES += [
+ "IdentityCryptoService.cpp",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+FINAL_LIBRARY = "xul"
diff --git a/services/crypto/component/nsIIdentityCryptoService.idl b/services/crypto/component/nsIIdentityCryptoService.idl
new file mode 100644
index 0000000000..90149e2e82
--- /dev/null
+++ b/services/crypto/component/nsIIdentityCryptoService.idl
@@ -0,0 +1,106 @@
+/* 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"
+
+interface nsIURI;
+interface nsIIdentityKeyGenCallback;
+interface nsIIdentitySignCallback;
+
+/* Naming and calling conventions:
+ *
+ * A"hex" prefix means "hex-encoded string representation of a byte sequence"
+ * e.g. "ae34bcdf123"
+ *
+ * A "base64url" prefix means "base-64-URL-encoded string repressentation of a
+ * byte sequence.
+ * e.g. "eyJhbGciOiJSUzI1NiJ9"
+ * http://en.wikipedia.org/wiki/Base64#Variants_summary_table
+ * we use the padded approach to base64-url-encoding
+ *
+ * Callbacks take an "in nsresult rv" argument that indicates whether the async
+ * operation succeeded. On success, rv will be a success code
+ * (NS_SUCCEEDED(rv) / Components.isSuccessCode(rv)) and the remaining
+ * arguments are as defined in the documentation for the callback. When the
+ * operation fails, rv will be a failure code (NS_FAILED(rv) /
+ * !Components.isSuccessCode(rv)) and the values of the remaining arguments will
+ * be unspecified.
+ *
+ * Key Types:
+ *
+ * "RS256": RSA + SHA-256.
+ *
+ * "DS160": DSA with SHA-1. A 1024-bit prime and a 160-bit subprime with SHA-1.
+ *
+ * we use these abbreviated algorithm names as per the JWA spec
+ * http://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-02
+ */
+
+// "@mozilla.org/identity/crypto-service;1"
+[scriptable, builtinclass, uuid(f087e6bc-dd33-4f6c-a106-dd786e052ee9)]
+interface nsIIdentityCryptoService : nsISupports
+{
+ void generateKeyPair(in AUTF8String algorithm,
+ in nsIIdentityKeyGenCallback callback);
+
+ ACString base64UrlEncode(in AUTF8String toEncode);
+};
+
+/**
+ * This interface provides a keypair and signing interface for Identity functionality
+ */
+[scriptable, uuid(73962dc7-8ee7-4346-a12b-b039e1d9b54d)]
+interface nsIIdentityKeyPair : nsISupports
+{
+ readonly attribute AUTF8String keyType;
+
+ // RSA properties, only accessible when keyType == "RS256"
+
+ readonly attribute AUTF8String hexRSAPublicKeyExponent;
+ readonly attribute AUTF8String hexRSAPublicKeyModulus;
+
+ // DSA properties, only accessible when keyType == "DS128"
+ readonly attribute AUTF8String hexDSAPrime; // p
+ readonly attribute AUTF8String hexDSASubPrime; // q
+ readonly attribute AUTF8String hexDSAGenerator; // g
+ readonly attribute AUTF8String hexDSAPublicValue; // y
+
+ void sign(in AUTF8String aText,
+ in nsIIdentitySignCallback callback);
+
+ // XXX implement verification bug 769856
+ // AUTF8String verify(in AUTF8String aSignature, in AUTF8String encodedPublicKey);
+
+};
+
+/**
+ * This interface provides a JavaScript callback object used to collect the
+ * nsIIdentityServeKeyPair when the keygen operation is complete
+ *
+ * though there is discussion as to whether we need the nsresult,
+ * we keep it so we can track deeper crypto errors.
+ */
+[scriptable, function, uuid(90f24ca2-2b05-4ca9-8aec-89d38e2f905a)]
+interface nsIIdentityKeyGenCallback : nsISupports
+{
+ void generateKeyPairFinished(in nsresult rv,
+ in nsIIdentityKeyPair keyPair);
+};
+
+/**
+ * This interface provides a JavaScript callback object used to collect the
+ * AUTF8String signature
+ */
+[scriptable, function, uuid(2d3e5036-374b-4b47-a430-1196b67b890f)]
+interface nsIIdentitySignCallback : nsISupports
+{
+ /** On success, base64urlSignature is the base-64-URL-encoded signature
+ *
+ * For RS256 signatures, XXX bug 769858
+ *
+ * For DSA128 signatures, the signature is the r value concatenated with the
+ * s value, each component padded with leading zeroes as necessary.
+ */
+ void signFinished(in nsresult rv, in ACString base64urlSignature);
+};
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..fb9bfd5553
--- /dev/null
+++ b/services/crypto/modules/WeaveCrypto.js
@@ -0,0 +1,245 @@
+/* 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 EXPORTED_SYMBOLS = ["WeaveCrypto"];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyGlobalGetters(this, ["crypto"]);
+
+const CRYPT_ALGO = "AES-CBC";
+const CRYPT_ALGO_LENGTH = 256;
+const CRYPT_ALGO_USAGES = ["encrypt", "decrypt"];
+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;
+
+function WeaveCrypto() {
+ this.init();
+}
+
+WeaveCrypto.prototype = {
+ prefBranch: null,
+ debug: true, // services.sync.log.cryptoDebug
+
+ observer: {
+ _self: null,
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIObserver",
+ "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);
+ this.observer._self = this;
+ this.debug = this.prefBranch.getBoolPref("cryptoDebug", 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;
+ },
+
+ async encrypt(clearTextUCS2, symmetricKey, iv) {
+ this.log("encrypt() called");
+ let clearTextBuffer = this.encoder.encode(clearTextUCS2).buffer;
+ let encrypted = await this._commonCrypt(
+ clearTextBuffer,
+ symmetricKey,
+ iv,
+ OPERATIONS.ENCRYPT
+ );
+ return this.encodeBase64(encrypted);
+ },
+
+ async decrypt(cipherText, symmetricKey, iv) {
+ this.log("decrypt() called");
+ if (cipherText.length) {
+ cipherText = atob(cipherText);
+ }
+ let cipherTextBuffer = this.byteCompressInts(cipherText);
+ let decrypted = await 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)
+ */
+ async _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 new Error(`Invalid IV size; must be ${AES_CBC_IV_SIZE} bytes.`);
+ }
+
+ let iv = this.byteCompressInts(ivStr);
+ let symKey = await this.importSymKey(symKeyStr, operation);
+ let cryptMethod = (operation === OPERATIONS.ENCRYPT
+ ? crypto.subtle.encrypt
+ : crypto.subtle.decrypt
+ ).bind(crypto.subtle);
+ let algo = { name: CRYPT_ALGO, iv };
+
+ let keyBytes = await cryptMethod.call(crypto.subtle, algo, symKey, data);
+ return new Uint8Array(keyBytes);
+ },
+
+ async generateRandomKey() {
+ this.log("generateRandomKey() called");
+ let algo = {
+ name: CRYPT_ALGO,
+ length: CRYPT_ALGO_LENGTH,
+ };
+ let key = await crypto.subtle.generateKey(algo, true, CRYPT_ALGO_USAGES);
+ let keyBytes = await crypto.subtle.exportKey("raw", key);
+ return this.encodeBase64(new Uint8Array(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: {},
+ async 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 new Error("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"];
+ let symKey = await crypto.subtle.importKey(
+ "raw",
+ symmetricKeyBuffer,
+ algo,
+ false,
+ usages
+ );
+ 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);
+ },
+};
diff --git a/services/crypto/modules/jwcrypto.jsm b/services/crypto/modules/jwcrypto.jsm
new file mode 100644
index 0000000000..61f7d136e3
--- /dev/null
+++ b/services/crypto/modules/jwcrypto.jsm
@@ -0,0 +1,377 @@
+/* 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/. */
+
+"use strict";
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { Log } = ChromeUtils.import("resource://gre/modules/Log.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "IdentityCryptoService",
+ "@mozilla.org/identity/crypto-service;1",
+ "nsIIdentityCryptoService"
+);
+XPCOMUtils.defineLazyGlobalGetters(this, ["crypto"]);
+
+const EXPORTED_SYMBOLS = ["jwcrypto"];
+
+const PREF_LOG_LEVEL = "services.crypto.jwcrypto.log.level";
+XPCOMUtils.defineLazyGetter(this, "log", function() {
+ const log = Log.repository.getLogger("Services.Crypto.jwcrypto");
+ // Default log level is "Error", but consumers can change this with the pref
+ // "services.crypto.jwcrypto.log.level".
+ log.level = Log.Level.Error;
+ const appender = new Log.DumpAppender();
+ log.addAppender(appender);
+ try {
+ const level =
+ Services.prefs.getPrefType(PREF_LOG_LEVEL) ==
+ Ci.nsIPrefBranch.PREF_STRING &&
+ Services.prefs.getCharPref(PREF_LOG_LEVEL);
+ log.level = Log.Level[level] || Log.Level.Error;
+ } catch (e) {
+ log.error(e);
+ }
+
+ return log;
+});
+
+const ASSERTION_DEFAULT_DURATION_MS = 1000 * 60 * 2; // 2 minutes default assertion lifetime
+const ECDH_PARAMS = {
+ name: "ECDH",
+ namedCurve: "P-256",
+};
+const AES_PARAMS = {
+ name: "AES-GCM",
+ length: 256,
+};
+const AES_TAG_LEN = 128;
+const AES_GCM_IV_SIZE = 12;
+const UTF8_ENCODER = new TextEncoder();
+const UTF8_DECODER = new TextDecoder();
+
+class JWCrypto {
+ /**
+ * Encrypts the given data into a JWE using AES-256-GCM content encryption.
+ *
+ * This function implements a very small subset of the JWE encryption standard
+ * from https://tools.ietf.org/html/rfc7516. The only supported content encryption
+ * algorithm is enc="A256GCM" [1] and the only supported key encryption algorithm
+ * is alg="ECDH-ES" [2].
+ *
+ * @param {Object} key Peer Public JWK.
+ * @param {ArrayBuffer} data
+ *
+ * [1] https://tools.ietf.org/html/rfc7518#section-5.3
+ * [2] https://tools.ietf.org/html/rfc7518#section-4.6
+ *
+ * @returns {Promise<String>}
+ */
+ async generateJWE(key, data) {
+ // Generate an ephemeral key to use just for this encryption.
+ // The public component gets embedded in the JWE header.
+ const epk = await crypto.subtle.generateKey(ECDH_PARAMS, true, [
+ "deriveKey",
+ ]);
+ const ownPublicJWK = await crypto.subtle.exportKey("jwk", epk.publicKey);
+ // Remove properties added by our WebCrypto implementation but that aren't typically
+ // used with JWE in the wild. This saves space in the resulting JWE, and makes it easier
+ // to re-import the resulting JWK.
+ delete ownPublicJWK.key_ops;
+ delete ownPublicJWK.ext;
+ let header = { alg: "ECDH-ES", enc: "A256GCM", epk: ownPublicJWK };
+ // Import the peer's public key.
+ const peerPublicKey = await crypto.subtle.importKey(
+ "jwk",
+ key,
+ ECDH_PARAMS,
+ false,
+ ["deriveKey"]
+ );
+ if (key.hasOwnProperty("kid")) {
+ header.kid = key.kid;
+ }
+ // Do ECDH agreement to get the content encryption key.
+ const contentKey = await deriveECDHSharedAESKey(
+ epk.privateKey,
+ peerPublicKey,
+ ["encrypt"]
+ );
+ // Encrypt with AES-GCM using the generated key.
+ // Note that the IV is generated randomly, which *in general* is not safe to do with AES-GCM because
+ // it's too short to guarantee uniqueness. But we know that the AES-GCM key itself is unique and will
+ // only be used for this single encryption, making a random IV safe to use for this particular use-case.
+ let iv = crypto.getRandomValues(new Uint8Array(AES_GCM_IV_SIZE));
+ // Yes, additionalData is the byte representation of the base64 representation of the stringified header.
+ const additionalData = UTF8_ENCODER.encode(
+ ChromeUtils.base64URLEncode(UTF8_ENCODER.encode(JSON.stringify(header)), {
+ pad: false,
+ })
+ );
+ const encrypted = await crypto.subtle.encrypt(
+ {
+ name: "AES-GCM",
+ iv,
+ additionalData,
+ tagLength: AES_TAG_LEN,
+ },
+ contentKey,
+ data
+ );
+ // JWE needs the authentication tag as a separate string.
+ const tagIdx = encrypted.byteLength - ((AES_TAG_LEN + 7) >> 3);
+ let ciphertext = encrypted.slice(0, tagIdx);
+ let tag = encrypted.slice(tagIdx);
+ // JWE serialization in compact format.
+ header = UTF8_ENCODER.encode(JSON.stringify(header));
+ header = ChromeUtils.base64URLEncode(header, { pad: false });
+ tag = ChromeUtils.base64URLEncode(tag, { pad: false });
+ ciphertext = ChromeUtils.base64URLEncode(ciphertext, { pad: false });
+ iv = ChromeUtils.base64URLEncode(iv, { pad: false });
+ return `${header}..${iv}.${ciphertext}.${tag}`; // No CEK
+ }
+
+ /**
+ * Decrypts the given JWE using AES-256-GCM content encryption into a byte array.
+ * This function does the opposite of `JWCrypto.generateJWE`.
+ * The only supported content encryption algorithm is enc="A256GCM" [1]
+ * and the only supported key encryption algorithm is alg="ECDH-ES" [2].
+ *
+ * @param {"ECDH-ES"} algorithm
+ * @param {CryptoKey} key Local private key
+ *
+ * [1] https://tools.ietf.org/html/rfc7518#section-5.3
+ * [2] https://tools.ietf.org/html/rfc7518#section-4.6
+ *
+ * @returns {Promise<Uint8Array>}
+ */
+ async decryptJWE(jwe, key) {
+ let [header, cek, iv, ciphertext, authTag] = jwe.split(".");
+ const additionalData = UTF8_ENCODER.encode(header);
+ header = JSON.parse(
+ UTF8_DECODER.decode(
+ ChromeUtils.base64URLDecode(header, { padding: "reject" })
+ )
+ );
+ if (
+ cek.length > 0 ||
+ header.enc !== "A256GCM" ||
+ header.alg !== "ECDH-ES"
+ ) {
+ throw new Error("Unknown algorithm.");
+ }
+ if ("apu" in header || "apv" in header) {
+ throw new Error("apu and apv header values are not supported.");
+ }
+ const peerPublicKey = await crypto.subtle.importKey(
+ "jwk",
+ header.epk,
+ ECDH_PARAMS,
+ false,
+ ["deriveKey"]
+ );
+ // Do ECDH agreement to get the content encryption key.
+ const contentKey = await deriveECDHSharedAESKey(key, peerPublicKey, [
+ "decrypt",
+ ]);
+ iv = ChromeUtils.base64URLDecode(iv, { padding: "reject" });
+ ciphertext = new Uint8Array(
+ ChromeUtils.base64URLDecode(ciphertext, { padding: "reject" })
+ );
+ authTag = new Uint8Array(
+ ChromeUtils.base64URLDecode(authTag, { padding: "reject" })
+ );
+ const bundle = new Uint8Array([...ciphertext, ...authTag]);
+
+ const decrypted = await crypto.subtle.decrypt(
+ {
+ name: "AES-GCM",
+ iv,
+ tagLength: AES_TAG_LEN,
+ additionalData,
+ },
+ contentKey,
+ bundle
+ );
+ return new Uint8Array(decrypted);
+ }
+
+ generateKeyPair(aAlgorithmName, aCallback) {
+ log.debug("generating");
+ log.debug("Generate key pair; alg = " + aAlgorithmName);
+
+ IdentityCryptoService.generateKeyPair(aAlgorithmName, (rv, aKeyPair) => {
+ if (!Components.isSuccessCode(rv)) {
+ return aCallback("key generation failed");
+ }
+
+ let publicKey;
+
+ switch (aKeyPair.keyType) {
+ case "RS256":
+ publicKey = {
+ algorithm: "RS",
+ exponent: aKeyPair.hexRSAPublicKeyExponent,
+ modulus: aKeyPair.hexRSAPublicKeyModulus,
+ };
+ break;
+
+ case "DS160":
+ publicKey = {
+ algorithm: "DS",
+ y: aKeyPair.hexDSAPublicValue,
+ p: aKeyPair.hexDSAPrime,
+ q: aKeyPair.hexDSASubPrime,
+ g: aKeyPair.hexDSAGenerator,
+ };
+ break;
+
+ default:
+ return aCallback("unknown key type");
+ }
+
+ const keyWrapper = {
+ serializedPublicKey: JSON.stringify(publicKey),
+ _kp: aKeyPair,
+ };
+
+ return aCallback(null, keyWrapper);
+ });
+ }
+
+ /**
+ * Generate an assertion and return it through the provided callback.
+ *
+ * @param aCert
+ * Identity certificate
+ *
+ * @param aKeyPair
+ * KeyPair object
+ *
+ * @param aAudience
+ * Audience of the assertion
+ *
+ * @param aOptions (optional)
+ * Can include:
+ * {
+ * localtimeOffsetMsec: <clock offset in milliseconds>,
+ * now: <current date in milliseconds>
+ * duration: <validity duration for this assertion in milliseconds>
+ * }
+ *
+ * localtimeOffsetMsec is the number of milliseconds that need to be
+ * added to the local clock time to make it concur with the server.
+ * For example, if the local clock is two minutes fast, the offset in
+ * milliseconds would be -120000.
+ *
+ * @param aCallback
+ * Function to invoke with resulting assertion. Assertion
+ * will be string or null on failure.
+ */
+ generateAssertion(aCert, aKeyPair, aAudience, aOptions, aCallback) {
+ if (typeof aOptions == "function") {
+ aCallback = aOptions;
+ aOptions = {};
+ }
+
+ // for now, we hack the algorithm name
+ // XXX bug 769851
+ const header = { alg: "DS128" };
+ const headerBytes = IdentityCryptoService.base64UrlEncode(
+ JSON.stringify(header)
+ );
+
+ function getExpiration(
+ duration = ASSERTION_DEFAULT_DURATION_MS,
+ localtimeOffsetMsec = 0,
+ now = Date.now()
+ ) {
+ return now + localtimeOffsetMsec + duration;
+ }
+
+ const payload = {
+ exp: getExpiration(
+ aOptions.duration,
+ aOptions.localtimeOffsetMsec,
+ aOptions.now
+ ),
+ aud: aAudience,
+ };
+ const payloadBytes = IdentityCryptoService.base64UrlEncode(
+ JSON.stringify(payload)
+ );
+
+ log.debug("payload", { payload, payloadBytes });
+ const message = headerBytes + "." + payloadBytes;
+ aKeyPair._kp.sign(message, (rv, signature) => {
+ if (!Components.isSuccessCode(rv)) {
+ log.error("signer.sign failed");
+ aCallback("Sign failed");
+ return;
+ }
+ log.debug("signer.sign: success");
+ const signedAssertion = message + "." + signature;
+ aCallback(null, aCert + "~" + signedAssertion);
+ });
+ }
+}
+
+/**
+ * Do an ECDH agreement between a public and private key,
+ * returning the derived encryption key as specced by
+ * JWA RFC.
+ * The raw ECDH secret is derived into a key using
+ * Concat KDF, as defined in Section 5.8.1 of [NIST.800-56A].
+ * @param {CryptoKey} privateKey
+ * @param {CryptoKey} publicKey
+ * @param {String[]} keyUsages See `SubtleCrypto.deriveKey` 5th paramater documentation.
+ * @returns {Promise<CryptoKey>}
+ */
+async function deriveECDHSharedAESKey(privateKey, publicKey, keyUsages) {
+ const params = { ...ECDH_PARAMS, ...{ public: publicKey } };
+ const sharedKey = await crypto.subtle.deriveKey(
+ params,
+ privateKey,
+ AES_PARAMS,
+ true,
+ keyUsages
+ );
+ // This is the NIST Concat KDF specialized to a specific set of parameters,
+ // which basically turn it into a single application of SHA256.
+ // The details are from the JWA RFC.
+ let sharedKeyBytes = await crypto.subtle.exportKey("raw", sharedKey);
+ sharedKeyBytes = new Uint8Array(sharedKeyBytes);
+ const info = [
+ "\x00\x00\x00\x07A256GCM", // 7-byte algorithm identifier
+ "\x00\x00\x00\x00", // empty PartyUInfo
+ "\x00\x00\x00\x00", // empty PartyVInfo
+ "\x00\x00\x01\x00", // keylen == 256
+ ].join("");
+ const pkcs = `\x00\x00\x00\x01${String.fromCharCode.apply(
+ null,
+ sharedKeyBytes
+ )}${info}`;
+ const pkcsBuf = Uint8Array.from(
+ Array.prototype.map.call(pkcs, c => c.charCodeAt(0))
+ );
+ const derivedKeyBytes = await crypto.subtle.digest(
+ {
+ name: "SHA-256",
+ },
+ pkcsBuf
+ );
+ return crypto.subtle.importKey(
+ "raw",
+ derivedKeyBytes,
+ AES_PARAMS,
+ false,
+ keyUsages
+ );
+}
+
+const jwcrypto = new JWCrypto();
diff --git a/services/crypto/modules/utils.js b/services/crypto/modules/utils.js
new file mode 100644
index 0000000000..3f37260f13
--- /dev/null
+++ b/services/crypto/modules/utils.js
@@ -0,0 +1,587 @@
+/* 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 EXPORTED_SYMBOLS = ["CryptoUtils"];
+
+const { Observers } = ChromeUtils.import(
+ "resource://services-common/observers.js"
+);
+const { CommonUtils } = ChromeUtils.import(
+ "resource://services-common/utils.js"
+);
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+XPCOMUtils.defineLazyGlobalGetters(this, ["crypto"]);
+
+XPCOMUtils.defineLazyGetter(this, "textEncoder", function() {
+ return new TextEncoder();
+});
+
+/**
+ * A number of `Legacy` suffixed functions are exposed by CryptoUtils.
+ * They work with octet strings, which were used before Javascript
+ * got ArrayBuffer and friends.
+ */
+var CryptoUtils = {
+ 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.
+ * @returns {String} Octet string
+ */
+ generateRandomBytesLegacy(length) {
+ let bytes = CryptoUtils.generateRandomBytes(length);
+ return CommonUtils.arrayBufferToByteString(bytes);
+ },
+
+ generateRandomBytes(length) {
+ return crypto.getRandomValues(new Uint8Array(length));
+ },
+
+ /**
+ * 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(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 (if necessary) and hash it with
+ * the given hasher. Returns a string containing bytes.
+ * The hasher is reset if it's an HMAC hasher.
+ */
+ digestBytes(bytes, hasher) {
+ if (typeof bytes == "string" || bytes instanceof String) {
+ bytes = CommonUtils.byteStringToArrayBuffer(bytes);
+ }
+ return CryptoUtils.digestBytesArray(bytes, hasher);
+ },
+
+ digestBytesArray(bytes, hasher) {
+ 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(message, hasher) {
+ let bytes = this._utf8Converter.convertToByteArray(message, {});
+ hasher.update(bytes, bytes.length);
+ },
+
+ sha256(message) {
+ let hasher = Cc["@mozilla.org/security/hash;1"].createInstance(
+ Ci.nsICryptoHash
+ );
+ hasher.init(hasher.SHA256);
+ return CommonUtils.bytesAsHex(CryptoUtils.digestUTF8(message, hasher));
+ },
+
+ sha256Base64(message) {
+ let data = this._utf8Converter.convertToByteArray(message, {});
+ let hasher = Cc["@mozilla.org/security/hash;1"].createInstance(
+ Ci.nsICryptoHash
+ );
+ hasher.init(hasher.SHA256);
+ hasher.update(data, data.length);
+ return hasher.finish(true);
+ },
+
+ /**
+ * 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;
+ },
+
+ /**
+ * @param {string} alg Hash algorithm (common values are SHA-1 or SHA-256)
+ * @param {string} key Key as an octet string.
+ * @param {string} data Data as an octet string.
+ */
+ async hmacLegacy(alg, key, data) {
+ if (!key || !key.length) {
+ key = "\0";
+ }
+ data = CommonUtils.byteStringToArrayBuffer(data);
+ key = CommonUtils.byteStringToArrayBuffer(key);
+ const result = await CryptoUtils.hmac(alg, key, data);
+ return CommonUtils.arrayBufferToByteString(result);
+ },
+
+ /**
+ * @param {string} ikm IKM as an octet string.
+ * @param {string} salt Salt as an Hex string.
+ * @param {string} info Info as a regular string.
+ * @param {Number} len Desired output length in bytes.
+ */
+ async hkdfLegacy(ikm, xts, info, len) {
+ ikm = CommonUtils.byteStringToArrayBuffer(ikm);
+ xts = CommonUtils.byteStringToArrayBuffer(xts);
+ info = textEncoder.encode(info);
+ const okm = await CryptoUtils.hkdf(ikm, xts, info, len);
+ return CommonUtils.arrayBufferToByteString(okm);
+ },
+
+ /**
+ * @param {String} alg Hash algorithm (common values are SHA-1 or SHA-256)
+ * @param {ArrayBuffer} key
+ * @param {ArrayBuffer} data
+ * @param {Number} len Desired output length in bytes.
+ * @returns {Uint8Array}
+ */
+ async hmac(alg, key, data) {
+ const hmacKey = await crypto.subtle.importKey(
+ "raw",
+ key,
+ { name: "HMAC", hash: alg },
+ false,
+ ["sign"]
+ );
+ const result = await crypto.subtle.sign("HMAC", hmacKey, data);
+ return new Uint8Array(result);
+ },
+
+ /**
+ * @param {ArrayBuffer} ikm
+ * @param {ArrayBuffer} salt
+ * @param {ArrayBuffer} info
+ * @param {Number} len Desired output length in bytes.
+ * @returns {Uint8Array}
+ */
+ async hkdf(ikm, salt, info, len) {
+ const key = await crypto.subtle.importKey(
+ "raw",
+ ikm,
+ { name: "HKDF" },
+ false,
+ ["deriveBits"]
+ );
+ const okm = await crypto.subtle.deriveBits(
+ {
+ name: "HKDF",
+ hash: "SHA-256",
+ salt,
+ info,
+ },
+ key,
+ len * 8
+ );
+ return new Uint8Array(okm);
+ },
+
+ /**
+ * PBKDF2 password stretching with SHA-256 hmac.
+ *
+ * @param {string} passphrase Passphrase as an octet string.
+ * @param {string} salt Salt as an octet string.
+ * @param {string} iterations Number of iterations, a positive integer.
+ * @param {string} len Desired output length in bytes.
+ */
+ async pbkdf2Generate(passphrase, salt, iterations, len) {
+ passphrase = CommonUtils.byteStringToArrayBuffer(passphrase);
+ salt = CommonUtils.byteStringToArrayBuffer(salt);
+ const key = await crypto.subtle.importKey(
+ "raw",
+ passphrase,
+ { name: "PBKDF2" },
+ false,
+ ["deriveBits"]
+ );
+ const output = await crypto.subtle.deriveBits(
+ {
+ name: "PBKDF2",
+ hash: "SHA-256",
+ salt,
+ iterations,
+ },
+ key,
+ len * 8
+ );
+ return CommonUtils.arrayBufferToByteString(new Uint8Array(output));
+ },
+
+ /**
+ * 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.
+ */
+ async 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.generateRandomBytesLegacy(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.pathQueryRef +
+ "\n" +
+ host +
+ "\n" +
+ port +
+ "\n" +
+ ext +
+ "\n";
+
+ const mac = await CryptoUtils.hmacLegacy("SHA-1", key, requestString);
+
+ function getHeader() {
+ return CryptoUtils.getHTTPMACSHA1Header(
+ this.identifier,
+ this.ts,
+ this.nonce,
+ this.mac,
+ this.ext
+ );
+ }
+
+ return {
+ identifier,
+ key,
+ method: usedMethod,
+ hostname: host,
+ port,
+ mac,
+ nonce,
+ ts,
+ ext,
+ 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(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
+ * 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
+ * Promise<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)
+ */
+ async computeHAWK(uri, method, options) {
+ let credentials = options.credentials;
+ let ts =
+ options.ts ||
+ Math.floor(
+ ((options.now || Date.now()) + (options.localtimeOffsetMsec || 0)) /
+ 1000
+ );
+ 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,
+ nonce: options.nonce || btoa(CryptoUtils.generateRandomBytesLegacy(8)),
+ method: method.toUpperCase(),
+ resource: uri.pathQueryRef, // 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
+ ) {
+ const buffer = textEncoder.encode(
+ `hawk.1.payload\n${contentType}\n${options.payload}\n`
+ );
+ const hash = await crypto.subtle.digest("SHA-256", buffer);
+ // HAWK specifies this .hash to use +/ (not _-) and include the
+ // trailing "==" padding.
+ artifacts.hash = ChromeUtils.base64URLEncode(hash, { pad: true })
+ .replace(/-/g, "+")
+ .replace(/_/g, "/");
+ }
+
+ 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";
+
+ const hash = await CryptoUtils.hmacLegacy(
+ "SHA-256",
+ credentials.key,
+ requestString
+ );
+ artifacts.mac = btoa(hash);
+ // 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,
+ 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"
+);
+
+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..e03be96a81
--- /dev/null
+++ b/services/crypto/moz.build
@@ -0,0 +1,22 @@
+# -*- 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 = ("Firefox", "Sync")
+
+DIRS += ["component"]
+
+XPCSHELL_TESTS_MANIFESTS += ["tests/unit/xpcshell.ini"]
+
+EXTRA_JS_MODULES["services-crypto"] += [
+ "modules/jwcrypto.jsm",
+ "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..8849eff262
--- /dev/null
+++ b/services/crypto/tests/unit/head_helpers.js
@@ -0,0 +1,79 @@
+/* import-globals-from ../../../common/tests/unit/head_helpers.js */
+
+var { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+try {
+ // In the context of xpcshell tests, there won't be a default AppInfo
+ // eslint-disable-next-line mozilla/use-services
+ 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";
+ }
+
+ ChromeUtils.import("resource://testing-common/AppInfo.jsm", this);
+ updateAppInfo({
+ name: "XPCShell",
+ ID: "{3e3ba16c-1675-4e88-b9c8-afef81b3d2ef}",
+ version: "1",
+ platformVersion: "",
+ OS,
+ });
+}
+
+function base64UrlDecode(s) {
+ s = s.replace(/-/g, "+");
+ s = s.replace(/_/g, "/");
+
+ // Replace padding if it was stripped by the sender.
+ // See http://tools.ietf.org/html/rfc4648#section-4
+ switch (s.length % 4) {
+ case 0:
+ break; // No pad chars in this case
+ case 2:
+ s += "==";
+ break; // Two pad chars
+ case 3:
+ s += "=";
+ break; // One pad char
+ default:
+ throw new Error("Illegal base64url string!");
+ }
+
+ // With correct padding restored, apply the standard base64 decoder
+ return atob(s);
+}
+
+// Register resource alias. Normally done in SyncComponents.manifest.
+function addResourceAlias() {
+ const { Services } = ChromeUtils.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/");
+ 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.from(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..b79f2d8daa
--- /dev/null
+++ b/services/crypto/tests/unit/test_crypto_crypt.js
@@ -0,0 +1,227 @@
+const { WeaveCrypto } = ChromeUtils.import(
+ "resource://services-crypto/WeaveCrypto.js"
+);
+Cu.importGlobalProperties(["crypto"]);
+
+var cryptoSvc = new WeaveCrypto();
+
+add_task(async 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 = await 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.
+ Assert.equal(c, 0);
+ let cipherText = await cryptoSvc.encrypt("Hello, world.", key, iv);
+ Assert.equal(c, 1);
+ cipherText = await cryptoSvc.encrypt("Hello, world.", key, iv);
+ Assert.equal(c, 1);
+
+ // ... as should decryption.
+ await cryptoSvc.decrypt(cipherText, key, iv);
+ await cryptoSvc.decrypt(cipherText, key, iv);
+ await cryptoSvc.decrypt(cipherText, key, iv);
+ Assert.equal(c, 2);
+
+ // Un-swizzle.
+ cryptoGlobal.subtle.importKey = oldImport;
+});
+
+// Just verify that it gets populated with the correct bytes.
+add_task(async function test_makeUint8Array() {
+ ChromeUtils.import("resource://gre/modules/ctypes.jsm");
+
+ let item1 = cryptoSvc.makeUint8Array("abcdefghi", false);
+ Assert.ok(item1);
+ for (let i = 0; i < 8; ++i) {
+ Assert.equal(item1[i], "abcdefghi".charCodeAt(i));
+ }
+});
+
+add_task(async 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();
+ Assert.equal(iv.length, 24);
+
+ var key = await cryptoSvc.generateRandomKey();
+ Assert.equal(key.length, 44);
+
+ var mySecret = "bacon is a vegetable";
+ var cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
+ Assert.equal(cipherText.length, 44);
+
+ var clearText = await cryptoSvc.decrypt(cipherText, key, iv);
+ Assert.equal(clearText.length, 20);
+
+ // Did the text survive the encryption round-trip?
+ Assert.equal(clearText, mySecret);
+ Assert.notEqual(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 {
+ await cryptoSvc.encrypt(mySecret, key, shortiv);
+ } catch (ex) {
+ err = ex;
+ }
+ Assert.ok(!!err);
+
+ _("Testing long IV.");
+ let longiv = "gsgLRDaxWvIfKt75RjuvFWERt83FFsY2A0TW+0b2iVk=";
+ try {
+ await cryptoSvc.encrypt(mySecret, key, longiv);
+ } catch (ex) {
+ err = ex;
+ }
+ Assert.ok(!!err);
+
+ // Test small input sizes
+ mySecret = "";
+ cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
+ clearText = await cryptoSvc.decrypt(cipherText, key, iv);
+ Assert.equal(cipherText, "OGQjp6mK1a3fs9k9Ml4L3w==");
+ Assert.equal(clearText, mySecret);
+
+ mySecret = "x";
+ cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
+ clearText = await cryptoSvc.decrypt(cipherText, key, iv);
+ Assert.equal(cipherText, "96iMl4vhOxFUW/lVHHzVqg==");
+ Assert.equal(clearText, mySecret);
+
+ mySecret = "xx";
+ cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
+ clearText = await cryptoSvc.decrypt(cipherText, key, iv);
+ Assert.equal(cipherText, "olpPbETRYROCSqFWcH2SWg==");
+ Assert.equal(clearText, mySecret);
+
+ mySecret = "xxx";
+ cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
+ clearText = await cryptoSvc.decrypt(cipherText, key, iv);
+ Assert.equal(cipherText, "rRbpHGyVSZizLX/x43Wm+Q==");
+ Assert.equal(clearText, mySecret);
+
+ mySecret = "xxxx";
+ cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
+ clearText = await cryptoSvc.decrypt(cipherText, key, iv);
+ Assert.equal(cipherText, "HeC7miVGDcpxae9RmiIKAw==");
+ Assert.equal(clearText, mySecret);
+
+ // Test non-ascii input
+ // ("testuser1" using similar-looking glyphs)
+ mySecret = String.fromCharCode(355, 277, 349, 357, 533, 537, 101, 345, 185);
+ cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
+ clearText = await cryptoSvc.decrypt(cipherText, key, iv);
+ Assert.equal(cipherText, "Pj4ixByXoH3SU3JkOXaEKPgwRAWplAWFLQZkpJd5Kr4=");
+ Assert.equal(clearText, mySecret);
+
+ // Tests input spanning a block boundary (AES block size is 16 bytes)
+ mySecret = "123456789012345";
+ cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
+ clearText = await cryptoSvc.decrypt(cipherText, key, iv);
+ Assert.equal(cipherText, "e6c5hwphe45/3VN/M0bMUA==");
+ Assert.equal(clearText, mySecret);
+
+ mySecret = "1234567890123456";
+ cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
+ clearText = await cryptoSvc.decrypt(cipherText, key, iv);
+ Assert.equal(cipherText, "V6aaOZw8pWlYkoIHNkhsP1JOIQF87E2vTUvBUQnyV04=");
+ Assert.equal(clearText, mySecret);
+
+ mySecret = "12345678901234567";
+ cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
+ clearText = await cryptoSvc.decrypt(cipherText, key, iv);
+ Assert.equal(cipherText, "V6aaOZw8pWlYkoIHNkhsP5GvxWJ9+GIAS6lXw+5fHTI=");
+ Assert.equal(clearText, mySecret);
+
+ key = "iz35tuIMq4/H+IYw2KTgow==";
+ iv = "TJYrvva2KxvkM8hvOIvWp3==";
+ mySecret = "i like pie";
+
+ cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
+ clearText = await cryptoSvc.decrypt(cipherText, key, iv);
+ Assert.equal(cipherText, "DLGx8BWqSCLGG7i/xwvvxg==");
+ Assert.equal(clearText, mySecret);
+
+ key = "c5hG3YG+NC61FFy8NOHQak1ZhMEWO79bwiAfar2euzI=";
+ iv = "gsgLRDaxWvIfKt75RjuvFW==";
+ mySecret = "i like pie";
+
+ cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
+ clearText = await cryptoSvc.decrypt(cipherText, key, iv);
+ Assert.equal(cipherText, "o+ADtdMd8ubzNWurS6jt0Q==");
+ Assert.equal(clearText, mySecret);
+
+ key = "St1tFCor7vQEJNug/465dQ==";
+ iv = "oLjkfrLIOnK2bDRvW4kXYA==";
+ mySecret = "does thunder read testcases?";
+ cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
+ Assert.equal(cipherText, "T6fik9Ros+DB2ablH9zZ8FWZ0xm/szSwJjIHZu7sjPs=");
+
+ var badkey = "badkeybadkeybadkeybadk==";
+ var badiv = "badivbadivbadivbadivbad=";
+ var badcipher = "crapinputcrapinputcrapinputcrapinputcrapinp=";
+ var failure;
+
+ try {
+ failure = false;
+ clearText = await cryptoSvc.decrypt(cipherText, badkey, iv);
+ } catch (e) {
+ failure = true;
+ }
+ Assert.ok(failure);
+
+ try {
+ failure = false;
+ clearText = await cryptoSvc.decrypt(cipherText, key, badiv);
+ } catch (e) {
+ failure = true;
+ }
+ Assert.ok(failure);
+
+ try {
+ failure = false;
+ clearText = await cryptoSvc.decrypt(cipherText, badkey, badiv);
+ } catch (e) {
+ failure = true;
+ }
+ Assert.ok(failure);
+
+ try {
+ failure = false;
+ clearText = await cryptoSvc.decrypt(badcipher, key, iv);
+ } catch (e) {
+ failure = true;
+ }
+ Assert.ok(failure);
+});
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..5162b5f3fd
--- /dev/null
+++ b/services/crypto/tests/unit/test_crypto_random.js
@@ -0,0 +1,50 @@
+ChromeUtils.import("resource://services-crypto/WeaveCrypto.js", this);
+
+var cryptoSvc = new WeaveCrypto();
+
+add_task(async function test_crypto_random() {
+ if (this.gczeal) {
+ _("Running crypto random tests with gczeal(2).");
+ gczeal(2);
+ }
+
+ // Test salt generation.
+ var salt;
+
+ salt = cryptoSvc.generateRandomBytes(0);
+ Assert.equal(salt.length, 0);
+ salt = cryptoSvc.generateRandomBytes(1);
+ Assert.equal(salt.length, 4);
+ salt = cryptoSvc.generateRandomBytes(2);
+ Assert.equal(salt.length, 4);
+ salt = cryptoSvc.generateRandomBytes(3);
+ Assert.equal(salt.length, 4);
+ salt = cryptoSvc.generateRandomBytes(4);
+ Assert.equal(salt.length, 8);
+ salt = cryptoSvc.generateRandomBytes(8);
+ Assert.equal(salt.length, 12);
+
+ // sanity check to make sure salts seem random
+ var salt2 = cryptoSvc.generateRandomBytes(8);
+ Assert.equal(salt2.length, 12);
+ Assert.notEqual(salt, salt2);
+
+ salt = cryptoSvc.generateRandomBytes(1024);
+ Assert.equal(salt.length, 1368);
+ salt = cryptoSvc.generateRandomBytes(16);
+ Assert.equal(salt.length, 24);
+
+ // Test random key generation
+ var keydata, keydata2, iv;
+
+ keydata = await cryptoSvc.generateRandomKey();
+ Assert.equal(keydata.length, 44);
+ keydata2 = await cryptoSvc.generateRandomKey();
+ Assert.notEqual(keydata, keydata2); // sanity check for randomness
+ iv = cryptoSvc.generateRandomIV();
+ Assert.equal(iv.length, 24);
+
+ if (this.gczeal) {
+ gczeal(0);
+ }
+});
diff --git a/services/crypto/tests/unit/test_crypto_service.js b/services/crypto/tests/unit/test_crypto_service.js
new file mode 100644
index 0000000000..8488924113
--- /dev/null
+++ b/services/crypto/tests/unit/test_crypto_service.js
@@ -0,0 +1,133 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const idService = Cc["@mozilla.org/identity/crypto-service;1"].getService(
+ Ci.nsIIdentityCryptoService
+);
+
+const ALG_DSA = "DS160";
+const ALG_RSA = "RS256";
+
+const BASE64_URL_ENCODINGS = [
+ // The vectors from RFC 4648 are very silly, but we may as well include them.
+ ["", ""],
+ ["f", "Zg=="],
+ ["fo", "Zm8="],
+ ["foo", "Zm9v"],
+ ["foob", "Zm9vYg=="],
+ ["fooba", "Zm9vYmE="],
+ ["foobar", "Zm9vYmFy"],
+
+ // It's quite likely you could get a string like this in an assertion audience
+ ["i-like-pie.com", "aS1saWtlLXBpZS5jb20="],
+
+ // A few extra to be really sure
+ ["andré@example.com", "YW5kcsOpQGV4YW1wbGUuY29t"],
+ [
+ "πόλλ' οἶδ' ἀλώπηξ, ἀλλ' ἐχῖνος ἓν μέγα",
+ "z4DPjM67zrsnIM6_4by2zrQnIOG8gM67z47PgM63zr4sIOG8gM67zrsnIOG8kM-H4b-Wzr3Ov8-CIOG8k869IM68zq3Os86x",
+ ],
+];
+
+let log = Log.repository.getLogger("crypto.service.test");
+(function() {
+ let appender = new Log.DumpAppender();
+ log.level = Log.Level.Debug;
+ log.addAppender(appender);
+})();
+
+// When the output of an operation is a
+function do_check_eq_or_slightly_less(x, y) {
+ Assert.ok(x >= y - 3 * 8);
+}
+
+function test_base64_roundtrip() {
+ let message = "Attack at dawn!";
+ let encoded = idService.base64UrlEncode(message);
+ let decoded = base64UrlDecode(encoded);
+ Assert.notEqual(message, encoded);
+ Assert.equal(decoded, message);
+ run_next_test();
+}
+
+function test_dsa() {
+ idService.generateKeyPair(ALG_DSA, function(rv, keyPair) {
+ log.debug("DSA generateKeyPair finished " + rv);
+ Assert.ok(Components.isSuccessCode(rv));
+ Assert.equal(typeof keyPair.sign, "function");
+ Assert.equal(keyPair.keyType, ALG_DSA);
+ do_check_eq_or_slightly_less(
+ keyPair.hexDSAGenerator.length,
+ (1024 / 8) * 2
+ );
+ do_check_eq_or_slightly_less(keyPair.hexDSAPrime.length, (1024 / 8) * 2);
+ do_check_eq_or_slightly_less(keyPair.hexDSASubPrime.length, (160 / 8) * 2);
+ do_check_eq_or_slightly_less(
+ keyPair.hexDSAPublicValue.length,
+ (1024 / 8) * 2
+ );
+ // XXX: test that RSA parameters throw the correct error
+
+ log.debug("about to sign with DSA key");
+ keyPair.sign("foo", function(rv2, signature) {
+ log.debug("DSA sign finished " + rv2 + " " + signature);
+ Assert.ok(Components.isSuccessCode(rv2));
+ Assert.ok(signature.length > 1);
+ // TODO: verify the signature with the public key
+ run_next_test();
+ });
+ });
+}
+
+function test_rsa() {
+ idService.generateKeyPair(ALG_RSA, function(rv, keyPair) {
+ log.debug("RSA generateKeyPair finished " + rv);
+ Assert.ok(Components.isSuccessCode(rv));
+ Assert.equal(typeof keyPair.sign, "function");
+ Assert.equal(keyPair.keyType, ALG_RSA);
+ do_check_eq_or_slightly_less(
+ keyPair.hexRSAPublicKeyModulus.length,
+ 2048 / 8
+ );
+ Assert.ok(keyPair.hexRSAPublicKeyExponent.length > 1);
+
+ log.debug("about to sign with RSA key");
+ keyPair.sign("foo", function(rv2, signature) {
+ log.debug("RSA sign finished " + rv2 + " " + signature);
+ Assert.ok(Components.isSuccessCode(rv2));
+ Assert.ok(signature.length > 1);
+ run_next_test();
+ });
+ });
+}
+
+function test_base64UrlEncode() {
+ for (let [source, target] of BASE64_URL_ENCODINGS) {
+ Assert.equal(target, idService.base64UrlEncode(source));
+ }
+ run_next_test();
+}
+
+function test_base64UrlDecode() {
+ let utf8Converter = Cc[
+ "@mozilla.org/intl/scriptableunicodeconverter"
+ ].createInstance(Ci.nsIScriptableUnicodeConverter);
+ utf8Converter.charset = "UTF-8";
+
+ // We know the encoding of our inputs - on conversion back out again, make
+ // sure they're the same.
+ for (let [source, target] of BASE64_URL_ENCODINGS) {
+ let result = utf8Converter.ConvertToUnicode(base64UrlDecode(target));
+ result += utf8Converter.Finish();
+ Assert.equal(source, result);
+ }
+ run_next_test();
+}
+
+add_test(test_base64_roundtrip);
+add_test(test_dsa);
+add_test(test_rsa);
+add_test(test_base64UrlEncode);
+add_test(test_base64UrlDecode);
diff --git a/services/crypto/tests/unit/test_jwcrypto.js b/services/crypto/tests/unit/test_jwcrypto.js
new file mode 100644
index 0000000000..2c51395bb3
--- /dev/null
+++ b/services/crypto/tests/unit/test_jwcrypto.js
@@ -0,0 +1,240 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "jwcrypto",
+ "resource://services-crypto/jwcrypto.jsm"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "CryptoService",
+ "@mozilla.org/identity/crypto-service;1",
+ "nsIIdentityCryptoService"
+);
+
+Cu.importGlobalProperties(["crypto"]);
+
+const RP_ORIGIN = "http://123done.org";
+const INTERNAL_ORIGIN = "browserid://";
+
+const SECOND_MS = 1000;
+const MINUTE_MS = SECOND_MS * 60;
+const HOUR_MS = MINUTE_MS * 60;
+
+// Enable logging from jwcrypto.jsm.
+Services.prefs.setCharPref("services.crypto.jwcrypto.log.level", "Debug");
+
+function promisify(fn) {
+ return (...args) => {
+ return new Promise((res, rej) => {
+ fn(...args, (err, result) => {
+ err ? rej(err) : res(result);
+ });
+ });
+ };
+}
+const generateKeyPair = promisify(jwcrypto.generateKeyPair);
+const generateAssertion = promisify(jwcrypto.generateAssertion);
+
+add_task(async function test_jwe_roundtrip_ecdh_es_encryption() {
+ const plaintext = crypto.getRandomValues(new Uint8Array(123));
+ const remoteKey = await crypto.subtle.generateKey(
+ {
+ name: "ECDH",
+ namedCurve: "P-256",
+ },
+ true,
+ ["deriveKey"]
+ );
+ const remoteJWK = await crypto.subtle.exportKey("jwk", remoteKey.publicKey);
+ delete remoteJWK.key_ops;
+ const jwe = await jwcrypto.generateJWE(remoteJWK, plaintext);
+ const decrypted = await jwcrypto.decryptJWE(jwe, remoteKey.privateKey);
+ Assert.deepEqual(plaintext, decrypted);
+});
+
+add_task(async function test_jwe_header_includes_key_id() {
+ const plaintext = crypto.getRandomValues(new Uint8Array(123));
+ const remoteKey = await crypto.subtle.generateKey(
+ {
+ name: "ECDH",
+ namedCurve: "P-256",
+ },
+ true,
+ ["deriveKey"]
+ );
+ const remoteJWK = await crypto.subtle.exportKey("jwk", remoteKey.publicKey);
+ delete remoteJWK.key_ops;
+ remoteJWK.kid = "key identifier";
+ const jwe = await jwcrypto.generateJWE(remoteJWK, plaintext);
+ let [header /* other items deliberately ignored */] = jwe.split(".");
+ header = JSON.parse(
+ new TextDecoder().decode(
+ ChromeUtils.base64URLDecode(header, { padding: "reject" })
+ )
+ );
+ Assert.equal(header.kid, "key identifier");
+});
+
+add_task(async function test_sanity() {
+ // Shouldn't reject.
+ await generateKeyPair("DS160");
+});
+
+add_task(async function test_generate() {
+ let kp = await generateKeyPair("DS160");
+ Assert.notEqual(kp, null);
+});
+
+add_task(async function test_get_assertion() {
+ let kp = await generateKeyPair("DS160");
+ let backedAssertion = await generateAssertion("fake-cert", kp, RP_ORIGIN);
+ Assert.equal(backedAssertion.split("~").length, 2);
+ Assert.equal(backedAssertion.split(".").length, 3);
+});
+
+add_task(async function test_rsa() {
+ let kpo = await generateKeyPair("RS256");
+ Assert.notEqual(kpo, undefined);
+ info(kpo.serializedPublicKey);
+ let pk = JSON.parse(kpo.serializedPublicKey);
+ Assert.equal(pk.algorithm, "RS");
+ /* TODO
+ do_check_neq(kpo.sign, null);
+ do_check_eq(typeof kpo.sign, "function");
+ do_check_neq(kpo.userID, null);
+ do_check_neq(kpo.url, null);
+ do_check_eq(kpo.url, INTERNAL_ORIGIN);
+ do_check_neq(kpo.exponent, null);
+ do_check_neq(kpo.modulus, null);
+
+ // TODO: should sign be async?
+ let sig = kpo.sign("This is a message to sign");
+
+ do_check_neq(sig, null);
+ do_check_eq(typeof sig, "string");
+ do_check_true(sig.length > 1);
+ */
+});
+
+add_task(async function test_dsa() {
+ let kpo = await generateKeyPair("DS160");
+ info(kpo.serializedPublicKey);
+ let pk = JSON.parse(kpo.serializedPublicKey);
+ Assert.equal(pk.algorithm, "DS");
+ /* TODO
+ do_check_neq(kpo.sign, null);
+ do_check_eq(typeof kpo.sign, "function");
+ do_check_neq(kpo.userID, null);
+ do_check_neq(kpo.url, null);
+ do_check_eq(kpo.url, INTERNAL_ORIGIN);
+ do_check_neq(kpo.generator, null);
+ do_check_neq(kpo.prime, null);
+ do_check_neq(kpo.subPrime, null);
+ do_check_neq(kpo.publicValue, null);
+
+ let sig = kpo.sign("This is a message to sign");
+
+ do_check_neq(sig, null);
+ do_check_eq(typeof sig, "string");
+ do_check_true(sig.length > 1);
+ */
+});
+
+add_task(async function test_get_assertion_with_offset() {
+ // Use an arbitrary date in the past to ensure we don't accidentally pass
+ // this test with current dates, missing offsets, etc.
+ let serverMsec = Date.parse("Tue Oct 31 2000 00:00:00 GMT-0800");
+
+ // local clock skew
+ // clock is 12 hours fast; -12 hours offset must be applied
+ let localtimeOffsetMsec = -1 * 12 * HOUR_MS;
+ let localMsec = serverMsec - localtimeOffsetMsec;
+
+ let kp = await generateKeyPair("DS160");
+ let backedAssertion = await generateAssertion("fake-cert", kp, RP_ORIGIN, {
+ duration: MINUTE_MS,
+ localtimeOffsetMsec,
+ now: localMsec,
+ });
+ // properly formed
+ let cert;
+ let assertion;
+ [cert, assertion] = backedAssertion.split("~");
+
+ Assert.equal(cert, "fake-cert");
+ Assert.equal(assertion.split(".").length, 3);
+
+ let components = extractComponents(assertion);
+
+ // Expiry is within two minutes, corrected for skew
+ let exp = parseInt(components.payload.exp, 10);
+ Assert.ok(exp - serverMsec === MINUTE_MS);
+});
+
+add_task(async function test_assertion_lifetime() {
+ let kp = await generateKeyPair("DS160");
+ let backedAssertion = await generateAssertion("fake-cert", kp, RP_ORIGIN, {
+ duration: MINUTE_MS,
+ });
+ // properly formed
+ let cert;
+ let assertion;
+ [cert, assertion] = backedAssertion.split("~");
+
+ Assert.equal(cert, "fake-cert");
+ Assert.equal(assertion.split(".").length, 3);
+
+ let components = extractComponents(assertion);
+
+ // Expiry is within one minute, as we specified above
+ let exp = parseInt(components.payload.exp, 10);
+ Assert.ok(Math.abs(Date.now() - exp) > 50 * SECOND_MS);
+ Assert.ok(Math.abs(Date.now() - exp) <= MINUTE_MS);
+});
+
+add_task(async function test_audience_encoding_bug972582() {
+ let audience = "i-like-pie.com";
+ let kp = await generateKeyPair("DS160");
+ let backedAssertion = await generateAssertion("fake-cert", kp, audience);
+ let [, /* cert */ assertion] = backedAssertion.split("~");
+ let components = extractComponents(assertion);
+ Assert.equal(components.payload.aud, audience);
+});
+
+function extractComponents(signedObject) {
+ if (typeof signedObject != "string") {
+ throw new Error("malformed signature " + typeof signedObject);
+ }
+
+ let parts = signedObject.split(".");
+ if (parts.length != 3) {
+ throw new Error(
+ "signed object must have three parts, this one has " + parts.length
+ );
+ }
+
+ let headerSegment = parts[0];
+ let payloadSegment = parts[1];
+ let cryptoSegment = parts[2];
+
+ let header = JSON.parse(base64UrlDecode(headerSegment));
+ let payload = JSON.parse(base64UrlDecode(payloadSegment));
+
+ // Ensure well-formed header
+ Assert.equal(Object.keys(header).length, 1);
+ Assert.ok(!!header.alg);
+
+ // Ensure well-formed payload
+ for (let field of ["exp", "aud"]) {
+ Assert.ok(!!payload[field]);
+ }
+
+ return { header, payload, headerSegment, payloadSegment, cryptoSegment };
+}
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..1ab3a888d9
--- /dev/null
+++ b/services/crypto/tests/unit/test_load_modules.js
@@ -0,0 +1,12 @@
+/* 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);
+ ChromeUtils.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..bfeb5e859f
--- /dev/null
+++ b/services/crypto/tests/unit/test_utils_hawk.js
@@ -0,0 +1,346 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { CryptoUtils } = ChromeUtils.import(
+ "resource://services-crypto/utils.js"
+);
+
+function run_test() {
+ initTestLogging();
+
+ run_next_test();
+}
+
+add_task(async function test_hawk() {
+ let compute = CryptoUtils.computeHAWK;
+
+ let method = "POST";
+ let ts = 1353809207;
+ let nonce = "Ygvqdz";
+
+ let credentials = {
+ id: "123456",
+ key: "2983d45yun89q",
+ };
+
+ let uri_https = CommonUtils.makeURI(
+ "https://example.net/somewhere/over/the/rainbow"
+ );
+ let opts = {
+ credentials,
+ ext: "Bazinga!",
+ ts,
+ nonce,
+ payload: "something to write about",
+ contentType: "text/plain",
+ };
+
+ let result = await compute(uri_https, method, opts);
+ Assert.equal(
+ result.field,
+ 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ' +
+ 'hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", ' +
+ 'ext="Bazinga!", ' +
+ 'mac="q1CwFoSHzPZSkbIvl0oYlD+91rBUEvFk763nMjMndj8="'
+ );
+ Assert.equal(result.artifacts.ts, ts);
+ Assert.equal(result.artifacts.nonce, nonce);
+ Assert.equal(result.artifacts.method, method);
+ Assert.equal(result.artifacts.resource, "/somewhere/over/the/rainbow");
+ Assert.equal(result.artifacts.host, "example.net");
+ Assert.equal(result.artifacts.port, 443);
+ Assert.equal(
+ result.artifacts.hash,
+ "2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY="
+ );
+ Assert.equal(result.artifacts.ext, "Bazinga!");
+
+ let opts_noext = {
+ credentials,
+ ts,
+ nonce,
+ payload: "something to write about",
+ contentType: "text/plain",
+ };
+ result = await compute(uri_https, method, opts_noext);
+ Assert.equal(
+ result.field,
+ 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ' +
+ 'hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", ' +
+ 'mac="HTgtd0jPI6E4izx8e4OHdO36q00xFCU0FolNq3RiCYs="'
+ );
+ Assert.equal(result.artifacts.ts, ts);
+ Assert.equal(result.artifacts.nonce, nonce);
+ Assert.equal(result.artifacts.method, method);
+ Assert.equal(result.artifacts.resource, "/somewhere/over/the/rainbow");
+ Assert.equal(result.artifacts.host, "example.net");
+ Assert.equal(result.artifacts.port, 443);
+ Assert.equal(
+ 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 = await compute(uri_https, method, { credentials });
+ let fields = result.field.split(" ");
+ Assert.equal(fields[0], "Hawk");
+ Assert.equal(fields[1], 'id="123456",'); // from creds.id
+ Assert.ok(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. */
+ Assert.ok(result.artifacts.ts > 1000 * 1000 * 1000);
+ Assert.ok(result.artifacts.ts < 1000 * 1000 * 1000 * 1000);
+ Assert.ok(fields[3].startsWith('nonce="'));
+ Assert.equal(fields[3].length, 'nonce="12345678901=",'.length);
+ Assert.equal(result.artifacts.nonce.length, "12345678901=".length);
+
+ let result2 = await compute(uri_https, method, { credentials });
+ Assert.notEqual(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 = await compute(uri_https_upper, method, opts);
+ Assert.equal(
+ 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 = await compute(uri_https_upper, method.toLowerCase(), opts);
+ Assert.equal(
+ 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 = await compute(uri_https, method, {
+ credentials,
+ now: 1378848968650,
+ });
+ Assert.equal(result.artifacts.ts, 1378848968);
+
+ result = await compute(uri_https, method, {
+ credentials,
+ now: 1378848968650,
+ localtimeOffsetMsec: 1000 * 1000,
+ });
+ Assert.equal(result.artifacts.ts, 1378848968 + 1000);
+
+ /* Search/query-args in URIs should be included in the hash. */
+ let makeURI = CommonUtils.makeURI;
+ result = await compute(makeURI("http://example.net/path"), method, opts);
+ Assert.equal(result.artifacts.resource, "/path");
+ Assert.equal(
+ result.artifacts.mac,
+ "WyKHJjWaeYt8aJD+H9UeCWc0Y9C+07ooTmrcrOW4MPI="
+ );
+
+ result = await compute(makeURI("http://example.net/path/"), method, opts);
+ Assert.equal(result.artifacts.resource, "/path/");
+ Assert.equal(
+ result.artifacts.mac,
+ "xAYp2MgZQFvTKJT9u8nsvMjshCRRkuaeYqQbYSFp9Qw="
+ );
+
+ result = await compute(
+ makeURI("http://example.net/path?query=search"),
+ method,
+ opts
+ );
+ Assert.equal(result.artifacts.resource, "/path?query=search");
+ Assert.equal(
+ 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 = await compute(makeURI("http://example.net/path"), method, {
+ credentials,
+ ts: 1353809207,
+ nonce: "Ygvqdz",
+ });
+ Assert.equal(result.artifacts.hash, undefined);
+ Assert.equal(
+ result.artifacts.mac,
+ "S3f8E4hAURAqJxOlsYugkPZxLoRYrClgbSQ/3FmKMbY="
+ );
+
+ // Empty payload changes nothing.
+ result = await compute(makeURI("http://example.net/path"), method, {
+ credentials,
+ ts: 1353809207,
+ nonce: "Ygvqdz",
+ payload: null,
+ });
+ Assert.equal(result.artifacts.hash, undefined);
+ Assert.equal(
+ result.artifacts.mac,
+ "S3f8E4hAURAqJxOlsYugkPZxLoRYrClgbSQ/3FmKMbY="
+ );
+
+ result = await compute(makeURI("http://example.net/path"), method, {
+ credentials,
+ ts: 1353809207,
+ nonce: "Ygvqdz",
+ payload: "hello",
+ });
+ Assert.equal(
+ result.artifacts.hash,
+ "uZJnFj0XVBA6Rs1hEvdIDf8NraM0qRNXdFbR3NEQbVA="
+ );
+ Assert.equal(
+ result.artifacts.mac,
+ "pLsHHzngIn5CTJhWBtBr+BezUFvdd/IadpTp/FYVIRM="
+ );
+
+ // update, utf-8 payload
+ result = await compute(makeURI("http://example.net/path"), method, {
+ credentials,
+ ts: 1353809207,
+ nonce: "Ygvqdz",
+ payload: "andré@example.org", // non-ASCII
+ });
+ Assert.equal(
+ result.artifacts.hash,
+ "66DiyapJ0oGgj09IXWdMv8VCg9xk0PL5RqX7bNnQW2k="
+ );
+ Assert.equal(
+ result.artifacts.mac,
+ "2B++3x5xfHEZbPZGDiK3IwfPZctkV4DUr2ORg1vIHvk="
+ );
+
+ /* If "hash" is provided, "payload" is ignored. */
+ result = await compute(makeURI("http://example.net/path"), method, {
+ credentials,
+ ts: 1353809207,
+ nonce: "Ygvqdz",
+ hash: "66DiyapJ0oGgj09IXWdMv8VCg9xk0PL5RqX7bNnQW2k=",
+ payload: "something else",
+ });
+ Assert.equal(
+ result.artifacts.hash,
+ "66DiyapJ0oGgj09IXWdMv8VCg9xk0PL5RqX7bNnQW2k="
+ );
+ Assert.equal(
+ result.artifacts.mac,
+ "2B++3x5xfHEZbPZGDiK3IwfPZctkV4DUr2ORg1vIHvk="
+ );
+
+ // the payload "hash" is also non-urlsafe base64 (+/)
+ result = await compute(makeURI("http://example.net/path"), method, {
+ credentials,
+ ts: 1353809207,
+ nonce: "Ygvqdz",
+ payload: "something else",
+ });
+ Assert.equal(
+ result.artifacts.hash,
+ "lERFXr/IKOaAoYw+eBseDUSwmqZTX0uKZpcWLxsdzt8="
+ );
+ Assert.equal(
+ 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 = await compute(makeURI("http://ëxample.net/path"), method, {
+ credentials,
+ ts: 1353809207,
+ nonce: "Ygvqdz",
+ });
+ Assert.equal(
+ result.artifacts.mac,
+ "pILiHl1q8bbNQIdaaLwAFyaFmDU70MGehFuCs3AA5M0="
+ );
+ Assert.equal(result.artifacts.host, "xn--xample-ova.net");
+
+ result = await compute(makeURI("http://example.net/path"), method, {
+ credentials,
+ ts: 1353809207,
+ nonce: "Ygvqdz",
+ ext: 'backslash=\\ quote=" EOF',
+ });
+ Assert.equal(
+ result.artifacts.mac,
+ "BEMW76lwaJlPX4E/dajF970T6+GzWvaeyLzUt8eOTOc="
+ );
+ Assert.equal(
+ result.field,
+ 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ext="backslash=\\\\ quote=\\" EOF", mac="BEMW76lwaJlPX4E/dajF970T6+GzWvaeyLzUt8eOTOc="'
+ );
+
+ result = await compute(makeURI("http://example.net:1234/path"), method, {
+ credentials,
+ ts: 1353809207,
+ nonce: "Ygvqdz",
+ });
+ Assert.equal(
+ result.artifacts.mac,
+ "6D3JSFDtozuq8QvJTNUc1JzeCfy6h5oRvlhmSTPv6LE="
+ );
+ Assert.equal(
+ 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 = await compute(makeURI("http://example.net:01234/path"), method, {
+ credentials,
+ ts: 1353809207,
+ nonce: "Ygvqdz",
+ });
+ Assert.equal(
+ result.artifacts.mac,
+ "6D3JSFDtozuq8QvJTNUc1JzeCfy6h5oRvlhmSTPv6LE="
+ );
+ Assert.equal(
+ result.field,
+ 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", mac="6D3JSFDtozuq8QvJTNUc1JzeCfy6h5oRvlhmSTPv6LE="'
+ );
+});
+
+add_test(function test_strip_header_attributes() {
+ let strip = CryptoUtils.stripHeaderAttributes;
+
+ Assert.equal(strip(undefined), "");
+ Assert.equal(strip("text/plain"), "text/plain");
+ Assert.equal(strip("TEXT/PLAIN"), "text/plain");
+ Assert.equal(strip(" text/plain "), "text/plain");
+ Assert.equal(strip("text/plain ; charset=utf-8 "), "text/plain");
+
+ run_next_test();
+});
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..e0936b9142
--- /dev/null
+++ b/services/crypto/tests/unit/test_utils_httpmac.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { CryptoUtils } = ChromeUtils.import(
+ "resource://services-crypto/utils.js"
+);
+
+add_test(function setup() {
+ initTestLogging();
+ run_next_test();
+});
+
+add_task(async 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 = await CryptoUtils.computeHTTPMACSHA1(id, key, method, uri, {
+ ts,
+ nonce,
+ });
+
+ Assert.equal(btoa(result.mac), "jzh5chjQc2zFEvLbyHnPdX11Yck=");
+
+ Assert.equal(
+ result.getHeader(),
+ 'MAC id="vmo1txkttblmn51u2p3zk2xiy16hgvm5ok8qiv1yyi86ffjzy9zj0ez9x6wnvbx7", ' +
+ 'ts="1329181221", nonce="wGX71", mac="jzh5chjQc2zFEvLbyHnPdX11Yck="'
+ );
+
+ let ext = "EXTRA DATA; foo,bar=1";
+
+ result = await CryptoUtils.computeHTTPMACSHA1(id, key, method, uri, {
+ ts,
+ nonce,
+ ext,
+ });
+ Assert.equal(btoa(result.mac), "bNf4Fnt5k6DnhmyipLPkuZroH68=");
+ Assert.equal(
+ result.getHeader(),
+ 'MAC id="vmo1txkttblmn51u2p3zk2xiy16hgvm5ok8qiv1yyi86ffjzy9zj0ez9x6wnvbx7", ' +
+ 'ts="1329181221", nonce="wGX71", mac="bNf4Fnt5k6DnhmyipLPkuZroH68=", ' +
+ 'ext="EXTRA DATA; foo,bar=1"'
+ );
+});
+
+add_task(async 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 = await get_mac(12);
+ Assert.equal(12, atob(result.nonce).length);
+
+ result = await get_mac(2);
+ Assert.equal(2, atob(result.nonce).length);
+
+ result = await get_mac(0);
+ Assert.equal(8, atob(result.nonce).length);
+
+ result = await get_mac(-1);
+ Assert.equal(8, atob(result.nonce).length);
+});
diff --git a/services/crypto/tests/unit/xpcshell.ini b/services/crypto/tests/unit/xpcshell.ini
new file mode 100644
index 0000000000..ec5f9a4211
--- /dev/null
+++ b/services/crypto/tests/unit/xpcshell.ini
@@ -0,0 +1,17 @@
+[DEFAULT]
+head = head_helpers.js ../../../common/tests/unit/head_helpers.js
+firefox-appdir = browser
+support-files =
+ !/services/common/tests/unit/head_helpers.js
+
+[test_load_modules.js]
+
+[test_crypto_crypt.js]
+[test_crypto_random.js]
+[test_crypto_service.js]
+skip-if = appname == 'thunderbird'
+[test_jwcrypto.js]
+skip-if = appname == 'thunderbird'
+
+[test_utils_hawk.js]
+[test_utils_httpmac.js]