diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /ipc/keystore | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'ipc/keystore')
-rw-r--r-- | ipc/keystore/KeyStore.cpp | 986 | ||||
-rw-r--r-- | ipc/keystore/KeyStore.h | 141 | ||||
-rw-r--r-- | ipc/keystore/KeyStoreConnector.cpp | 239 | ||||
-rw-r--r-- | ipc/keystore/KeyStoreConnector.h | 57 | ||||
-rw-r--r-- | ipc/keystore/moz.build | 18 |
5 files changed, 1441 insertions, 0 deletions
diff --git a/ipc/keystore/KeyStore.cpp b/ipc/keystore/KeyStore.cpp new file mode 100644 index 000000000..992bc9075 --- /dev/null +++ b/ipc/keystore/KeyStore.cpp @@ -0,0 +1,986 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=2 et ft=cpp: 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 <fcntl.h> +#include <limits.h> +#include <pwd.h> +#include <sys/stat.h> +#include <sys/types.h> + +#if defined(MOZ_WIDGET_GONK) +#include <android/log.h> +#define KEYSTORE_LOG(args...) __android_log_print(ANDROID_LOG_INFO, "Gonk", args) +#else +#define KEYSTORE_LOG(args...) printf(args); +#endif + +#include "KeyStore.h" +#include "jsfriendapi.h" +#include "KeyStoreConnector.h" +#include "MainThreadUtils.h" // For NS_IsMainThread. +#include "nsICryptoHash.h" + +#include "plbase64.h" +#include "certdb.h" +#include "ScopedNSSTypes.h" + +using namespace mozilla::ipc; +#if ANDROID_VERSION >= 18 +// After Android 4.3, it uses binder to access keystore instead of unix socket. +#include <android/log.h> +#include <binder/BinderService.h> +#include <binder/IPCThreadState.h> +#include <binder/IServiceManager.h> +#include <security/keystore/include/keystore/IKeystoreService.h> +#include <security/keystore/include/keystore/keystore.h> + +using namespace android; + +namespace android { +// This class is used to make compiler happy. +class BpKeystoreService : public BpInterface<IKeystoreService> +{ +public: + BpKeystoreService(const sp<IBinder>& impl) + : BpInterface<IKeystoreService>(impl) + { + } + + virtual int32_t get(const String16& name, uint8_t** item, size_t* itemLength) {return 0;} + virtual int32_t test() {return 0;} + virtual int32_t insert(const String16& name, const uint8_t* item, size_t itemLength, int uid, int32_t flags) {return 0;} + virtual int32_t del(const String16& name, int uid) {return 0;} + virtual int32_t exist(const String16& name, int uid) {return 0;} + virtual int32_t saw(const String16& name, int uid, Vector<String16>* matches) {return 0;} + virtual int32_t reset() {return 0;} + virtual int32_t password(const String16& password) {return 0;} + virtual int32_t lock() {return 0;} + virtual int32_t unlock(const String16& password) {return 0;} + virtual int32_t zero() {return 0;} + virtual int32_t import(const String16& name, const uint8_t* data, size_t length, int uid, int32_t flags) {return 0;} + virtual int32_t sign(const String16& name, const uint8_t* data, size_t length, uint8_t** out, size_t* outLength) {return 0;} + virtual int32_t verify(const String16& name, const uint8_t* data, size_t dataLength, const uint8_t* signature, size_t signatureLength) {return 0;} + virtual int32_t get_pubkey(const String16& name, uint8_t** pubkey, size_t* pubkeyLength) {return 0;} + virtual int32_t del_key(const String16& name, int uid) {return 0;} + virtual int32_t grant(const String16& name, int32_t granteeUid) {return 0;} + virtual int32_t ungrant(const String16& name, int32_t granteeUid) {return 0;} + virtual int64_t getmtime(const String16& name) {return 0;} + virtual int32_t duplicate(const String16& srcKey, int32_t srcUid, const String16& destKey, int32_t destUid) {return 0;} + virtual int32_t clear_uid(int64_t uid) {return 0;} +#if ANDROID_VERSION >= 21 + virtual int32_t generate(const String16& name, int32_t uid, int32_t keyType, int32_t keySize, int32_t flags, Vector<sp<KeystoreArg> >* args) {return 0;} + virtual int32_t is_hardware_backed(const String16& keyType) {return 0;} + virtual int32_t reset_uid(int32_t uid) {return 0;} + virtual int32_t sync_uid(int32_t sourceUid, int32_t targetUid) {return 0;} + virtual int32_t password_uid(const String16& password, int32_t uid) {return 0;} +#elif ANDROID_VERSION == 18 + virtual int32_t generate(const String16& name, int uid, int32_t flags) {return 0;} + virtual int32_t is_hardware_backed() {return 0;} +#else + virtual int32_t generate(const String16& name, int32_t uid, int32_t keyType, int32_t keySize, int32_t flags, Vector<sp<KeystoreArg> >* args) {return 0;} + virtual int32_t is_hardware_backed(const String16& keyType) {return 0;} +#endif +}; + +IMPLEMENT_META_INTERFACE(KeystoreService, "android.security.keystore"); + +// Here comes binder requests. +status_t BnKeystoreService::onTransact(uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags) +{ + switch(code) { + case TEST: { + CHECK_INTERFACE(IKeystoreService, data, reply); + reply->writeNoException(); + reply->writeInt32(test()); + return NO_ERROR; + } break; + case GET: { + CHECK_INTERFACE(IKeystoreService, data, reply); + String16 name = data.readString16(); + String8 tmp(name); + uint8_t* data = NULL; + size_t dataLength = 0; + int32_t ret = get(name, &data, &dataLength); + + reply->writeNoException(); + if (ret == 1) { + reply->writeInt32(dataLength); + void* buf = reply->writeInplace(dataLength); + memcpy(buf, data, dataLength); + free(data); + } else { + reply->writeInt32(-1); + } + return NO_ERROR; + } break; + case GET_PUBKEY: { + CHECK_INTERFACE(IKeystoreService, data, reply); + String16 name = data.readString16(); + uint8_t* data = nullptr; + size_t dataLength = 0; + int32_t ret = get_pubkey(name, &data, &dataLength); + + reply->writeNoException(); + if (dataLength > 0 && data != nullptr) { + reply->writeInt32(dataLength); + void* buf = reply->writeInplace(dataLength); + memcpy(buf, data, dataLength); + free(data); + } else { + reply->writeInt32(-1); + } + reply->writeInt32(ret); + return NO_ERROR; + } break; + case SIGN: { + CHECK_INTERFACE(IKeystoreService, data, reply); + String16 name = data.readString16(); + ssize_t signDataSize = data.readInt32(); + const uint8_t *signData = nullptr; + if (signDataSize >= 0 && (size_t)signDataSize <= data.dataAvail()) { + signData = (const uint8_t *)data.readInplace(signDataSize); + } + + uint8_t *signResult = nullptr; + size_t signResultSize; + int32_t ret = sign(name, signData, (size_t)signDataSize, &signResult, + &signResultSize); + + reply->writeNoException(); + if (signResultSize > 0 && signResult != nullptr) { + reply->writeInt32(signResultSize); + void* buf = reply->writeInplace(signResultSize); + memcpy(buf, signResult, signResultSize); + free(signResult); + } else { + reply->writeInt32(-1); + } + reply->writeInt32(ret); + return NO_ERROR; + } break; + default: + return NO_ERROR; + } +} + +// Provide service for binder. +class KeyStoreService : public BnKeystoreService + , public nsNSSShutDownObject +{ +public: + int32_t test() { + uid_t callingUid = IPCThreadState::self()->getCallingUid(); + if (!mozilla::ipc::checkPermission(callingUid)) { + return ::PERMISSION_DENIED; + } + + return ::NO_ERROR; + } + + int32_t get(const String16& name, uint8_t** item, size_t* itemLength) { + nsNSSShutDownPreventionLock locker; + if (isAlreadyShutDown()) { + return ::SYSTEM_ERROR; + } + + uid_t callingUid = IPCThreadState::self()->getCallingUid(); + if (!mozilla::ipc::checkPermission(callingUid)) { + return ::PERMISSION_DENIED; + } + + String8 certName(name); + if (!strncmp(certName.string(), "WIFI_USERKEY_", 13)) { + return getPrivateKey(certName.string(), (const uint8_t**)item, itemLength); + } + + return getCertificate(certName.string(), (const uint8_t**)item, itemLength); + } + + int32_t insert(const String16& name, const uint8_t* item, size_t itemLength, int uid, int32_t flags) {return ::UNDEFINED_ACTION;} + int32_t del(const String16& name, int uid) {return ::UNDEFINED_ACTION;} + int32_t exist(const String16& name, int uid) {return ::UNDEFINED_ACTION;} + int32_t saw(const String16& name, int uid, Vector<String16>* matches) {return ::UNDEFINED_ACTION;} + int32_t reset() {return ::UNDEFINED_ACTION;} + int32_t password(const String16& password) {return ::UNDEFINED_ACTION;} + int32_t lock() {return ::UNDEFINED_ACTION;} + int32_t unlock(const String16& password) {return ::UNDEFINED_ACTION;} + int32_t zero() {return ::UNDEFINED_ACTION;} + int32_t import(const String16& name, const uint8_t* data, size_t length, int uid, int32_t flags) {return ::UNDEFINED_ACTION;} + int32_t sign(const String16& name, const uint8_t* data, size_t length, uint8_t** out, size_t* outLength) + { + nsNSSShutDownPreventionLock locker; + if (isAlreadyShutDown()) { + return ::SYSTEM_ERROR; + } + + uid_t callingUid = IPCThreadState::self()->getCallingUid(); + if (!mozilla::ipc::checkPermission(callingUid)) { + return ::PERMISSION_DENIED; + } + + if (data == nullptr) { + return ::SYSTEM_ERROR; + } + + String8 keyName(name); + if (!strncmp(keyName.string(), "WIFI_USERKEY_", 13)) { + return signData(keyName.string(), data, length, out, outLength); + } + + return ::UNDEFINED_ACTION; + } + + int32_t verify(const String16& name, const uint8_t* data, size_t dataLength, const uint8_t* signature, size_t signatureLength) {return ::UNDEFINED_ACTION;} + int32_t get_pubkey(const String16& name, uint8_t** pubkey, size_t* pubkeyLength) { + nsNSSShutDownPreventionLock locker; + if (isAlreadyShutDown()) { + return ::SYSTEM_ERROR; + } + + uid_t callingUid = IPCThreadState::self()->getCallingUid(); + if (!mozilla::ipc::checkPermission(callingUid)) { + return ::PERMISSION_DENIED; + } + + String8 keyName(name); + if (!strncmp(keyName.string(), "WIFI_USERKEY_", 13)) { + return getPublicKey(keyName.string(), (const uint8_t**)pubkey, pubkeyLength); + } + + return ::UNDEFINED_ACTION; + } + + int32_t del_key(const String16& name, int uid) {return ::UNDEFINED_ACTION;} + int32_t grant(const String16& name, int32_t granteeUid) {return ::UNDEFINED_ACTION;} + int32_t ungrant(const String16& name, int32_t granteeUid) {return ::UNDEFINED_ACTION;} + int64_t getmtime(const String16& name) {return ::UNDEFINED_ACTION;} + int32_t duplicate(const String16& srcKey, int32_t srcUid, const String16& destKey, int32_t destUid) {return ::UNDEFINED_ACTION;} + int32_t clear_uid(int64_t uid) {return ::UNDEFINED_ACTION;} +#if ANDROID_VERSION >= 21 + virtual int32_t generate(const String16& name, int32_t uid, int32_t keyType, int32_t keySize, int32_t flags, Vector<sp<KeystoreArg> >* args) {return ::UNDEFINED_ACTION;} + virtual int32_t is_hardware_backed(const String16& keyType) {return ::UNDEFINED_ACTION;} + virtual int32_t reset_uid(int32_t uid) {return ::UNDEFINED_ACTION;;} + virtual int32_t sync_uid(int32_t sourceUid, int32_t targetUid) {return ::UNDEFINED_ACTION;} + virtual int32_t password_uid(const String16& password, int32_t uid) {return ::UNDEFINED_ACTION;} +#elif ANDROID_VERSION == 18 + virtual int32_t generate(const String16& name, int uid, int32_t flags) {return ::UNDEFINED_ACTION;} + virtual int32_t is_hardware_backed() {return ::UNDEFINED_ACTION;} +#else + virtual int32_t generate(const String16& name, int32_t uid, int32_t keyType, int32_t keySize, int32_t flags, Vector<sp<KeystoreArg> >* args) {return ::UNDEFINED_ACTION;} + virtual int32_t is_hardware_backed(const String16& keyType) {return ::UNDEFINED_ACTION;} +#endif + +protected: + virtual void virtualDestroyNSSReference() {} + +private: + ~KeyStoreService() { + nsNSSShutDownPreventionLock locker; + if (isAlreadyShutDown()) { + return; + } + shutdown(ShutdownCalledFrom::Object); + } +}; + +} // namespace android + +void startKeyStoreService() +{ + android::sp<android::IServiceManager> sm = android::defaultServiceManager(); + android::sp<android::KeyStoreService> keyStoreService = new android::KeyStoreService(); + sm->addService(String16("android.security.keystore"), keyStoreService); +} +#else +void startKeyStoreService() { return; } +#endif + +static const char *CA_BEGIN = "-----BEGIN ", + *CA_END = "-----END ", + *CA_TAILER = "-----\n"; + +namespace mozilla { +namespace ipc { + +static const char* KEYSTORE_ALLOWED_USERS[] = { + "root", + "wifi", + NULL +}; +static const char* KEYSTORE_ALLOWED_PREFIXES[] = { + "WIFI_SERVERCERT_", + "WIFI_USERCERT_", + "WIFI_USERKEY_", + NULL +}; + +// Transform base64 certification data into DER format +void +FormatCaData(const char *aCaData, int aCaDataLength, + const char *aName, const uint8_t **aFormatData, + size_t *aFormatDataLength) +{ + size_t bufSize = strlen(CA_BEGIN) + strlen(CA_END) + strlen(CA_TAILER) * 2 + + strlen(aName) * 2 + aCaDataLength + aCaDataLength/CA_LINE_SIZE + + 2; + char *buf = (char *)malloc(bufSize); + if (!buf) { + *aFormatData = nullptr; + return; + } + + *aFormatDataLength = bufSize; + *aFormatData = (const uint8_t *)buf; + + char *ptr = buf; + size_t len; + + // Create DER header. + len = snprintf(ptr, bufSize, "%s%s%s", CA_BEGIN, aName, CA_TAILER); + ptr += len; + bufSize -= len; + + // Split base64 data in lines. + int copySize; + while (aCaDataLength > 0) { + copySize = (aCaDataLength > CA_LINE_SIZE) ? CA_LINE_SIZE : aCaDataLength; + + memcpy(ptr, aCaData, copySize); + ptr += copySize; + aCaData += copySize; + aCaDataLength -= copySize; + bufSize -= copySize; + + *ptr = '\n'; + ptr++; + bufSize--; + } + + // Create DEA tailer. + snprintf(ptr, bufSize, "%s%s%s", CA_END, aName, CA_TAILER); +} + +ResponseCode +getCertificate(const char *aCertName, const uint8_t **aCertData, + size_t *aCertDataLength) +{ + // certificate name prefix check. + if (!aCertName) { + return KEY_NOT_FOUND; + } + + const char **prefix = KEYSTORE_ALLOWED_PREFIXES; + for (; *prefix; prefix++ ) { + if (!strncmp(*prefix, aCertName, strlen(*prefix))) { + break; + } + } + if (!(*prefix)) { + return KEY_NOT_FOUND; + } + + // Get cert from NSS by name + ScopedCERTCertificate cert(CERT_FindCertByNickname(CERT_GetDefaultCertDB(), + aCertName)); + + if (!cert) { + return KEY_NOT_FOUND; + } + + char *certDER = PL_Base64Encode((const char *)cert->derCert.data, + cert->derCert.len, nullptr); + if (!certDER) { + return SYSTEM_ERROR; + } + + FormatCaData(certDER, strlen(certDER), "CERTIFICATE", aCertData, + aCertDataLength); + PL_strfree(certDER); + + if (!(*aCertData)) { + return SYSTEM_ERROR; + } + + return SUCCESS; +} + +ResponseCode getPrivateKey(const char *aKeyName, const uint8_t **aKeyData, + size_t *aKeyDataLength) +{ + *aKeyData = nullptr; + // Get corresponding user certificate nickname + char userCertName[128] = {0}; + snprintf(userCertName, sizeof(userCertName) - 1, "WIFI_USERCERT_%s", aKeyName + 13); + + // Get private key from user certificate. + ScopedCERTCertificate userCert( + CERT_FindCertByNickname(CERT_GetDefaultCertDB(), userCertName)); + if (!userCert) { + return KEY_NOT_FOUND; + } + + ScopedSECKEYPrivateKey privateKey( + PK11_FindKeyByAnyCert(userCert.get(), nullptr)); + if (!privateKey) { + return KEY_NOT_FOUND; + } + + // Export private key in PKCS#12 encrypted format, no password. + unsigned char pwstr[] = {0, 0}; + SECItem password = {siBuffer, pwstr, sizeof(pwstr)}; + ScopedSECKEYEncryptedPrivateKeyInfo encryptedPrivateKey( + PK11_ExportEncryptedPrivKeyInfo(privateKey->pkcs11Slot, + SEC_OID_PKCS12_V2_PBE_WITH_SHA1_AND_40_BIT_RC4, &password, privateKey, 1, + privateKey->wincx)); + + if (!encryptedPrivateKey) { + return KEY_NOT_FOUND; + } + + // Decrypt into RSA private key. + // + // Generate key for PKCS#12 encryption, we use SHA1 with 1 iteration, as the + // parameters used in PK11_ExportEncryptedPrivKeyInfo() above. + // see: PKCS#12 v1.0, B.2. + // + uint8_t DSP[192] = {0}; + memset(DSP, 0x01, 64); // Diversifier part, ID = 1 for decryption. + memset(DSP + 128, 0x00, 64); // Password part, no password. + + uint8_t *S = &DSP[64]; // Salt part. + uint8_t *salt = encryptedPrivateKey->algorithm.parameters.data + 4; + int saltLength = (int)encryptedPrivateKey->algorithm.parameters.data[3]; + if (saltLength <= 0) { + return SYSTEM_ERROR; + } + for (int i = 0; i < 64; i++) { + S[i] = salt[i % saltLength]; + } + + // Generate key by SHA-1 + nsresult rv; + nsCOMPtr<nsICryptoHash> hash = + do_CreateInstance("@mozilla.org/security/hash;1", &rv); + if (NS_FAILED(rv)) { + return SYSTEM_ERROR; + } + + rv = hash->Init(nsICryptoHash::SHA1); + if (NS_FAILED(rv)) { + return SYSTEM_ERROR; + } + + rv = hash->Update(DSP, sizeof(DSP)); + if (NS_FAILED(rv)) { + return SYSTEM_ERROR; + } + + nsCString hashResult; + rv = hash->Finish(false, hashResult); + if (NS_FAILED(rv)) { + return SYSTEM_ERROR; + } + + // First 40-bit as key for RC4. + uint8_t key[5]; + memcpy(key, hashResult.get(), sizeof(key)); + + ScopedPK11SlotInfo slot(PK11_GetInternalSlot()); + if (!slot) { + return SYSTEM_ERROR; + } + + SECItem keyItem = {siBuffer, key, sizeof(key)}; + ScopedPK11SymKey symKey(PK11_ImportSymKey(slot, CKM_RC4, PK11_OriginUnwrap, + CKA_DECRYPT, &keyItem, nullptr)); + if (!symKey) { + return SYSTEM_ERROR; + } + + // Get expected decrypted data size then allocate memory. + uint8_t *encryptedData = (uint8_t *)encryptedPrivateKey->encryptedData.data; + unsigned int encryptedDataLen = encryptedPrivateKey->encryptedData.len; + unsigned int decryptedDataLen = encryptedDataLen; + SECStatus srv = PK11_Decrypt(symKey, CKM_RC4, &keyItem, nullptr, + &decryptedDataLen, encryptedDataLen, + encryptedData, encryptedDataLen); + if (srv != SECSuccess) { + return SYSTEM_ERROR; + } + + ScopedSECItem decryptedData(::SECITEM_AllocItem(nullptr, nullptr, + decryptedDataLen)); + if (!decryptedData) { + return SYSTEM_ERROR; + } + + // Decrypt by RC4. + srv = PK11_Decrypt(symKey, CKM_RC4, &keyItem, decryptedData->data, + &decryptedDataLen, decryptedData->len, encryptedData, + encryptedDataLen); + if (srv != SECSuccess) { + return SYSTEM_ERROR; + } + + // Export key in PEM format. + char *keyPEM = PL_Base64Encode((const char *)decryptedData->data, + decryptedDataLen, nullptr); + + if (!keyPEM) { + return SYSTEM_ERROR; + } + + FormatCaData(keyPEM, strlen(keyPEM), "PRIVATE KEY", aKeyData, aKeyDataLength); + PL_strfree(keyPEM); + + if (!(*aKeyData)) { + return SYSTEM_ERROR; + } + + return SUCCESS; +} + +ResponseCode getPublicKey(const char *aKeyName, const uint8_t **aKeyData, + size_t *aKeyDataLength) +{ + *aKeyData = nullptr; + + // Get corresponding user certificate nickname + char userCertName[128] = {0}; + snprintf(userCertName, sizeof(userCertName) - 1, "WIFI_USERCERT_%s", aKeyName + 13); + + // Get public key from user certificate. + ScopedCERTCertificate userCert( + CERT_FindCertByNickname(CERT_GetDefaultCertDB(), userCertName)); + if (!userCert) { + return KEY_NOT_FOUND; + } + + // Get public key. + ScopedSECKEYPublicKey publicKey(CERT_ExtractPublicKey(userCert)); + if (!publicKey) { + return KEY_NOT_FOUND; + } + + ScopedSECItem keyItem(PK11_DEREncodePublicKey(publicKey)); + if (!keyItem) { + return KEY_NOT_FOUND; + } + + size_t bufSize = keyItem->len; + char *buf = (char *)malloc(bufSize); + if (!buf) { + return SYSTEM_ERROR; + } + + memcpy(buf, keyItem->data, bufSize); + *aKeyData = (const uint8_t *)buf; + *aKeyDataLength = bufSize; + + return SUCCESS; +} + +ResponseCode signData(const char *aKeyName, const uint8_t *data, size_t length, + uint8_t **out, size_t *outLength) +{ + *out = nullptr; + // Get corresponding user certificate nickname + char userCertName[128] = {0}; + snprintf(userCertName, sizeof(userCertName) - 1, "WIFI_USERCERT_%s", aKeyName + 13); + + // Get private key from user certificate. + ScopedCERTCertificate userCert( + CERT_FindCertByNickname(CERT_GetDefaultCertDB(), userCertName)); + if (!userCert) { + return KEY_NOT_FOUND; + } + + ScopedSECKEYPrivateKey privateKey( + PK11_FindKeyByAnyCert(userCert.get(), nullptr)); + if (!privateKey) { + return KEY_NOT_FOUND; + } + + // + // Find hash data from incoming data. + // + // Incoming data might be padded by PKCS-1 format: + // 00 01 FF FF ... FF 00 || Hash of length 36 + // If the padding part exists, we have to ignore them. + // + uint8_t *hash = (uint8_t *)data; + const size_t HASH_LENGTH = 36; + if (length < HASH_LENGTH) { + return VALUE_CORRUPTED; + } + if (hash[0] == 0x00 && hash[1] == 0x01 && hash[2] == 0xFF && hash[3] == 0xFF) { + hash += 4; + while (*hash == 0xFF) { + if (hash + HASH_LENGTH > data + length) { + return VALUE_CORRUPTED; + } + hash++; + } + if (*hash != 0x00) { + return VALUE_CORRUPTED; + } + hash++; + } + if (hash + HASH_LENGTH != data + length) { + return VALUE_CORRUPTED; + } + SECItem hashItem = {siBuffer, hash, HASH_LENGTH}; + + // Sign hash. + ScopedSECItem signItem(::SECITEM_AllocItem(nullptr, nullptr, + PK11_SignatureLen(privateKey))); + if (!signItem) { + return SYSTEM_ERROR; + } + + SECStatus srv; + srv = PK11_Sign(privateKey, signItem.get(), &hashItem); + if (srv != SECSuccess) { + return SYSTEM_ERROR; + } + + uint8_t *buf = (uint8_t *)malloc(signItem->len); + if (!buf) { + return SYSTEM_ERROR; + } + + memcpy(buf, signItem->data, signItem->len); + *out = buf; + *outLength = signItem->len; + + return SUCCESS; +} + +bool +checkPermission(uid_t uid) +{ + struct passwd *userInfo = getpwuid(uid); + for (const char **user = KEYSTORE_ALLOWED_USERS; *user; user++ ) { + if (!strcmp(*user, userInfo->pw_name)) { + return true; + } + } + + return false; +} + +// +// KeyStore +// + +KeyStore::KeyStore() +: mShutdown(false) +{ + MOZ_COUNT_CTOR(KeyStore); + ::startKeyStoreService(); + Listen(); +} + +KeyStore::~KeyStore() +{ + nsNSSShutDownPreventionLock locker; + MOZ_COUNT_DTOR(KeyStore); + + if (isAlreadyShutDown()) { + return; + } + + shutdown(ShutdownCalledFrom::Object); + + MOZ_ASSERT(!mListenSocket); + MOZ_ASSERT(!mStreamSocket); +} + +void +KeyStore::Shutdown() +{ + // We set mShutdown first, so that |OnDisconnect| won't try to reconnect. + mShutdown = true; + + if (mStreamSocket) { + mStreamSocket->Close(); + mStreamSocket = nullptr; + } + if (mListenSocket) { + mListenSocket->Close(); + mListenSocket = nullptr; + } +} + +void +KeyStore::Listen() +{ + // We only allocate one |StreamSocket|, but re-use it for every connection. + if (mStreamSocket) { + mStreamSocket->Close(); + } else { + mStreamSocket = new StreamSocket(this, STREAM_SOCKET); + } + + if (!mListenSocket) { + // We only ever allocate one |ListenSocket|... + mListenSocket = new ListenSocket(this, LISTEN_SOCKET); + mListenSocket->Listen(new KeyStoreConnector(KEYSTORE_ALLOWED_USERS), + mStreamSocket); + } else { + // ... but keep it open. + mListenSocket->Listen(mStreamSocket); + } + + ResetHandlerInfo(); +} + +void +KeyStore::ResetHandlerInfo() +{ + mHandlerInfo.state = STATE_IDLE; + mHandlerInfo.command = 0; + mHandlerInfo.paramCount = 0; + mHandlerInfo.commandPattern = nullptr; + for (int i = 0; i < MAX_PARAM; i++) { + mHandlerInfo.param[i].length = 0; + memset(mHandlerInfo.param[i].data, 0, VALUE_SIZE); + } +} + +bool +KeyStore::CheckSize(UnixSocketBuffer *aMessage, size_t aExpectSize) +{ + return (aMessage->GetSize() >= aExpectSize); +} + +ResponseCode +KeyStore::ReadCommand(UnixSocketBuffer *aMessage) +{ + if (mHandlerInfo.state != STATE_IDLE) { + NS_WARNING("Wrong state in ReadCommand()!"); + return SYSTEM_ERROR; + } + + if (!CheckSize(aMessage, 1)) { + NS_WARNING("Data size error in ReadCommand()!"); + return PROTOCOL_ERROR; + } + + mHandlerInfo.command = *aMessage->GetData(); + aMessage->Consume(1); + + // Find corrsponding command pattern + const struct ProtocolCommand *command = commands; + while (command->command && command->command != mHandlerInfo.command) { + command++; + } + + if (!command->command) { + NS_WARNING("Unsupported command!"); + return PROTOCOL_ERROR; + } + + // Get command pattern. + mHandlerInfo.commandPattern = command; + if (command->paramNum) { + // Read command parameter if needed. + mHandlerInfo.state = STATE_READ_PARAM_LEN; + } else { + mHandlerInfo.state = STATE_PROCESSING; + } + + return SUCCESS; +} + +ResponseCode +KeyStore::ReadLength(UnixSocketBuffer *aMessage) +{ + if (mHandlerInfo.state != STATE_READ_PARAM_LEN) { + NS_WARNING("Wrong state in ReadLength()!"); + return SYSTEM_ERROR; + } + + if (!CheckSize(aMessage, 2)) { + NS_WARNING("Data size error in ReadLength()!"); + return PROTOCOL_ERROR; + } + + // Read length of command parameter. + // FIXME: Depends on endianess and (sizeof(unsigned short) == 2) + unsigned short dataLength; + memcpy(&dataLength, aMessage->GetData(), 2); + aMessage->Consume(2); + mHandlerInfo.param[mHandlerInfo.paramCount].length = ntohs(dataLength); + + mHandlerInfo.state = STATE_READ_PARAM_DATA; + + return SUCCESS; +} + +ResponseCode +KeyStore::ReadData(UnixSocketBuffer *aMessage) +{ + if (mHandlerInfo.state != STATE_READ_PARAM_DATA) { + NS_WARNING("Wrong state in ReadData()!"); + return SYSTEM_ERROR; + } + + if (!CheckSize(aMessage, mHandlerInfo.param[mHandlerInfo.paramCount].length)) { + NS_WARNING("Data size error in ReadData()!"); + return PROTOCOL_ERROR; + } + + // Read command parameter. + memcpy(mHandlerInfo.param[mHandlerInfo.paramCount].data, + aMessage->GetData(), + mHandlerInfo.param[mHandlerInfo.paramCount].length); + aMessage->Consume(mHandlerInfo.param[mHandlerInfo.paramCount].length); + mHandlerInfo.paramCount++; + + if (mHandlerInfo.paramCount == mHandlerInfo.commandPattern->paramNum) { + mHandlerInfo.state = STATE_PROCESSING; + } else { + mHandlerInfo.state = STATE_READ_PARAM_LEN; + } + + return SUCCESS; +} + +// Status response +void +KeyStore::SendResponse(ResponseCode aResponse) +{ + MOZ_ASSERT(mStreamSocket); + + if (aResponse == NO_RESPONSE) + return; + + uint8_t response = (uint8_t)aResponse; + UnixSocketRawData* data = new UnixSocketRawData((const void *)&response, 1); + mStreamSocket->SendSocketData(data); +} + +// Data response +void +KeyStore::SendData(const uint8_t *aData, int aLength) +{ + MOZ_ASSERT(mStreamSocket); + + unsigned short dataLength = htons(aLength); + + UnixSocketRawData* length = new UnixSocketRawData((const void *)&dataLength, 2); + mStreamSocket->SendSocketData(length); + + UnixSocketRawData* data = new UnixSocketRawData((const void *)aData, aLength); + mStreamSocket->SendSocketData(data); +} + +// |StreamSocketConsumer|, |ListenSocketConsumer| + +void +KeyStore::ReceiveSocketData(int aIndex, UniquePtr<UnixSocketBuffer>& aMessage) +{ + MOZ_ASSERT(NS_IsMainThread()); + + // Handle request. + ResponseCode result = SUCCESS; + while (aMessage->GetSize() || + mHandlerInfo.state == STATE_PROCESSING) { + switch (mHandlerInfo.state) { + case STATE_IDLE: + result = ReadCommand(aMessage.get()); + break; + case STATE_READ_PARAM_LEN: + result = ReadLength(aMessage.get()); + break; + case STATE_READ_PARAM_DATA: + result = ReadData(aMessage.get()); + break; + case STATE_PROCESSING: + if (mHandlerInfo.command == 'g') { + result = SYSTEM_ERROR; + + nsNSSShutDownPreventionLock locker; + if (isAlreadyShutDown()) { + break; + } + + // Get CA + const uint8_t *data; + size_t dataLength; + const char *name = (const char *)mHandlerInfo.param[0].data; + + if (!strncmp(name, "WIFI_USERKEY_", 13)) { + result = getPrivateKey(name, &data, &dataLength); + } else { + result = getCertificate(name, &data, &dataLength); + } + if (result != SUCCESS) { + break; + } + + SendResponse(SUCCESS); + SendData(data, (int)dataLength); + + free((void *)data); + } + + ResetHandlerInfo(); + break; + } + + if (result != SUCCESS) { + SendResponse(result); + ResetHandlerInfo(); + return; + } + } +} + +void +KeyStore::OnConnectSuccess(int aIndex) +{ + if (aIndex == STREAM_SOCKET) { + mShutdown = false; + } +} + +void +KeyStore::OnConnectError(int aIndex) +{ + if (mShutdown) { + return; + } + + if (aIndex == STREAM_SOCKET) { + // Stream socket error; start listening again + Listen(); + } +} + +void +KeyStore::OnDisconnect(int aIndex) +{ + if (mShutdown) { + return; + } + + switch (aIndex) { + case LISTEN_SOCKET: + // Listen socket disconnected; start anew. + mListenSocket = nullptr; + Listen(); + break; + case STREAM_SOCKET: + // Stream socket disconnected; start listening again. + Listen(); + break; + } +} + +} // namespace ipc +} // namespace mozilla diff --git a/ipc/keystore/KeyStore.h b/ipc/keystore/KeyStore.h new file mode 100644 index 000000000..c6bb09023 --- /dev/null +++ b/ipc/keystore/KeyStore.h @@ -0,0 +1,141 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=2 et ft=cpp: 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/. */ + +#ifndef mozilla_ipc_KeyStore_h +#define mozilla_ipc_KeyStore_h 1 + +#include <sys/socket.h> +#include <sys/un.h> +#include "cert.h" +#include "mozilla/ipc/ListenSocket.h" +#include "mozilla/ipc/ListenSocketConsumer.h" +#include "mozilla/ipc/StreamSocket.h" +#include "mozilla/ipc/StreamSocketConsumer.h" +#include "nsNSSShutDown.h" + +namespace mozilla { +namespace ipc { + +enum ResponseCode { + SUCCESS = 1, + LOCKED = 2, + UNINITIALIZED = 3, + SYSTEM_ERROR = 4, + PROTOCOL_ERROR = 5, + PERMISSION_DENIED = 6, + KEY_NOT_FOUND = 7, + VALUE_CORRUPTED = 8, + UNDEFINED_ACTION = 9, + WRONG_PASSWORD_0 = 10, + WRONG_PASSWORD_1 = 11, + WRONG_PASSWORD_2 = 12, + WRONG_PASSWORD_3 = 13, // MAX_RETRY = 4 + NO_RESPONSE +}; + +void FormatCaData(const uint8_t *aCaData, int aCaDataLength, + const char *aName, const uint8_t **aFormatData, + size_t *aFormatDataLength); + +ResponseCode getCertificate(const char *aCertName, const uint8_t **aCertData, + size_t *aCertDataLength); +ResponseCode getPrivateKey(const char *aKeyName, const uint8_t **aKeyData, + size_t *aKeyDataLength); +ResponseCode getPublicKey(const char *aKeyName, const uint8_t **aKeyData, + size_t *aKeyDataLength); +ResponseCode signData(const char *aKeyName, const uint8_t *data, size_t length, + uint8_t **out, size_t *outLength); + +bool checkPermission(uid_t uid); + +static const int MAX_PARAM = 2; +static const int KEY_SIZE = ((NAME_MAX - 15) / 2); +static const int VALUE_SIZE = 32768; +static const int PASSWORD_SIZE = VALUE_SIZE; + +static const int CA_LINE_SIZE = 64; + +struct ProtocolCommand { + int8_t command; + int paramNum; +}; + +static const struct ProtocolCommand commands[] = { + {'g', 1}, // Get CA, command "g CERT_NAME" + { 0, 0} +}; + +struct ProtocolParam{ + uint length; + int8_t data[VALUE_SIZE]; +}; + +typedef enum { + STATE_IDLE, + STATE_READ_PARAM_LEN, + STATE_READ_PARAM_DATA, + STATE_PROCESSING +} ProtocolHandlerState; + +class KeyStore final + : public StreamSocketConsumer + , public ListenSocketConsumer + , public nsNSSShutDownObject +{ +public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(KeyStore) + + KeyStore(); + + void Shutdown(); + +protected: + virtual void virtualDestroyNSSReference() {} + +private: + enum SocketType { + LISTEN_SOCKET, + STREAM_SOCKET + }; + + ~KeyStore(); + + struct { + ProtocolHandlerState state; + uint8_t command; + struct ProtocolParam param[MAX_PARAM]; + int paramCount; + const struct ProtocolCommand *commandPattern; + } mHandlerInfo; + void ResetHandlerInfo(); + void Listen(); + + bool CheckSize(UnixSocketBuffer *aMessage, size_t aExpectSize); + ResponseCode ReadCommand(UnixSocketBuffer *aMessage); + ResponseCode ReadLength(UnixSocketBuffer *aMessage); + ResponseCode ReadData(UnixSocketBuffer *aMessage); + void SendResponse(ResponseCode response); + void SendData(const uint8_t *data, int length); + + // Methods for |StreamSocketConsumer| + // + + void ReceiveSocketData(int aIndex, + UniquePtr<UnixSocketBuffer>& aMessage) override; + void OnConnectSuccess(int aIndex) override; + void OnConnectError(int aIndex) override; + void OnDisconnect(int aIndex) override; + + bool mShutdown; + + RefPtr<ListenSocket> mListenSocket; + RefPtr<StreamSocket> mStreamSocket; +}; + +} // namespace ipc +} // namespace mozilla + +#endif // mozilla_ipc_KeyStore_h diff --git a/ipc/keystore/KeyStoreConnector.cpp b/ipc/keystore/KeyStoreConnector.cpp new file mode 100644 index 000000000..0e11fcec6 --- /dev/null +++ b/ipc/keystore/KeyStoreConnector.cpp @@ -0,0 +1,239 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=2 et ft=cpp: 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 "KeyStoreConnector.h" +#include <fcntl.h> +#include <pwd.h> +#include <sys/stat.h> +#include <sys/un.h> +#include "nsISupportsImpl.h" // for MOZ_COUNT_CTOR, MOZ_COUNT_DTOR +#include "nsThreadUtils.h" // For NS_IsMainThread. + +#ifdef MOZ_WIDGET_GONK +#include <android/log.h> +#define KEYSTORE_LOG(args...) __android_log_print(ANDROID_LOG_INFO, "Gonk", args) +#else +#define KEYSTORE_LOG(args...) printf(args); +#endif + +namespace mozilla { +namespace ipc { + +static const char KEYSTORE_SOCKET_PATH[] = "/dev/socket/keystore"; + +KeyStoreConnector::KeyStoreConnector(const char** const aAllowedUsers) + : mAllowedUsers(aAllowedUsers) +{ + MOZ_COUNT_CTOR_INHERITED(KeyStoreConnector, UnixSocketConnector); +} + +KeyStoreConnector::~KeyStoreConnector() +{ + MOZ_COUNT_DTOR_INHERITED(KeyStoreConnector, UnixSocketConnector); +} + +nsresult +KeyStoreConnector::CreateSocket(int& aFd) const +{ + unlink(KEYSTORE_SOCKET_PATH); + + aFd = socket(AF_LOCAL, SOCK_STREAM, 0); + if (aFd < 0) { + KEYSTORE_LOG("Could not open KeyStore socket!"); + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +nsresult +KeyStoreConnector::SetSocketFlags(int aFd) const +{ + static const int sReuseAddress = 1; + + // Set close-on-exec bit. + int flags = TEMP_FAILURE_RETRY(fcntl(aFd, F_GETFD)); + if (flags < 0) { + return NS_ERROR_FAILURE; + } + flags |= FD_CLOEXEC; + int res = TEMP_FAILURE_RETRY(fcntl(aFd, F_SETFD, flags)); + if (res < 0) { + return NS_ERROR_FAILURE; + } + + // Set non-blocking status flag. + flags = TEMP_FAILURE_RETRY(fcntl(aFd, F_GETFL)); + if (flags < 0) { + return NS_ERROR_FAILURE; + } + flags |= O_NONBLOCK; + res = TEMP_FAILURE_RETRY(fcntl(aFd, F_SETFL, flags)); + if (res < 0) { + return NS_ERROR_FAILURE; + } + + // Set socket addr to be reused even if kernel is still waiting to close. + res = setsockopt(aFd, SOL_SOCKET, SO_REUSEADDR, &sReuseAddress, + sizeof(sReuseAddress)); + if (res < 0) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +nsresult +KeyStoreConnector::CheckPermission(int aFd) const +{ + struct ucred userCred; + socklen_t len = sizeof(userCred); + + if (getsockopt(aFd, SOL_SOCKET, SO_PEERCRED, &userCred, &len)) { + return NS_ERROR_FAILURE; + } + + const struct passwd* userInfo = getpwuid(userCred.uid); + if (!userInfo) { + return NS_ERROR_FAILURE; + } + + if (!mAllowedUsers) { + return NS_ERROR_FAILURE; + } + + for (const char** user = mAllowedUsers; *user; ++user) { + if (!strcmp(*user, userInfo->pw_name)) { + return NS_OK; + } + } + + return NS_ERROR_FAILURE; +} + +nsresult +KeyStoreConnector::CreateAddress(struct sockaddr& aAddress, + socklen_t& aAddressLength) const +{ + struct sockaddr_un* address = + reinterpret_cast<struct sockaddr_un*>(&aAddress); + + size_t namesiz = strlen(KEYSTORE_SOCKET_PATH) + 1; // include trailing '\0' + + if (namesiz > sizeof(address->sun_path)) { + KEYSTORE_LOG("Address too long for socket struct!"); + return NS_ERROR_FAILURE; + } + + address->sun_family = AF_UNIX; + memcpy(address->sun_path, KEYSTORE_SOCKET_PATH, namesiz); + + aAddressLength = offsetof(struct sockaddr_un, sun_path) + namesiz; + + return NS_OK; +} + +// |UnixSocketConnector| + +nsresult +KeyStoreConnector::ConvertAddressToString(const struct sockaddr& aAddress, + socklen_t aAddressLength, + nsACString& aAddressString) +{ + MOZ_ASSERT(aAddress.sa_family == AF_UNIX); + + const struct sockaddr_un* un = + reinterpret_cast<const struct sockaddr_un*>(&aAddress); + + size_t len = aAddressLength - offsetof(struct sockaddr_un, sun_path); + + aAddressString.Assign(un->sun_path, len); + + return NS_OK; +} + +nsresult +KeyStoreConnector::CreateListenSocket(struct sockaddr* aAddress, + socklen_t* aAddressLength, + int& aListenFd) +{ + ScopedClose fd; + + nsresult rv = CreateSocket(fd.rwget()); + if (NS_FAILED(rv)) { + return rv; + } + rv = SetSocketFlags(fd); + if (NS_FAILED(rv)) { + return rv; + } + if (aAddress && aAddressLength) { + rv = CreateAddress(*aAddress, *aAddressLength); + if (NS_FAILED(rv)) { + return rv; + } + } + + // Allow access for wpa_supplicant (different user, different group) + // + // TODO: Improve this by setting specific user/group for + // wpa_supplicant by calling |fchmod| and |fchown|. + // + chmod(KEYSTORE_SOCKET_PATH, S_IRUSR|S_IWUSR| + S_IRGRP|S_IWGRP| + S_IROTH|S_IWOTH); + + aListenFd = fd.forget(); + + return NS_OK; +} + +nsresult +KeyStoreConnector::AcceptStreamSocket(int aListenFd, + struct sockaddr* aAddress, + socklen_t* aAddressLength, + int& aStreamFd) +{ + ScopedClose fd( + TEMP_FAILURE_RETRY(accept(aListenFd, aAddress, aAddressLength))); + if (fd < 0) { + NS_WARNING("Cannot accept file descriptor!"); + return NS_ERROR_FAILURE; + } + nsresult rv = SetSocketFlags(fd); + if (NS_FAILED(rv)) { + return rv; + } + rv = CheckPermission(fd); + if (NS_FAILED(rv)) { + return rv; + } + + aStreamFd = fd.forget(); + + return NS_OK; +} + +nsresult +KeyStoreConnector::CreateStreamSocket(struct sockaddr* aAddress, + socklen_t* aAddressLength, + int& aStreamFd) +{ + MOZ_CRASH("|KeyStoreConnector| does not support creating stream sockets."); + return NS_ERROR_FAILURE; +} + +nsresult +KeyStoreConnector::Duplicate(UnixSocketConnector*& aConnector) +{ + aConnector = new KeyStoreConnector(*this); + + return NS_OK; +} + +} +} diff --git a/ipc/keystore/KeyStoreConnector.h b/ipc/keystore/KeyStoreConnector.h new file mode 100644 index 000000000..349b030de --- /dev/null +++ b/ipc/keystore/KeyStoreConnector.h @@ -0,0 +1,57 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=2 et ft=cpp: 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/. */ + +#ifndef mozilla_ipc_KeyStoreConnector_h +#define mozilla_ipc_KeyStoreConnector_h + +#include "mozilla/ipc/UnixSocketConnector.h" + +namespace mozilla { +namespace ipc { + +class KeyStoreConnector final : public UnixSocketConnector +{ +public: + KeyStoreConnector(const char** const aAllowedUsers); + ~KeyStoreConnector(); + + // Methods for |UnixSocketConnector| + // + + nsresult ConvertAddressToString(const struct sockaddr& aAddress, + socklen_t aAddressLength, + nsACString& aAddressString) override; + + nsresult CreateListenSocket(struct sockaddr* aAddress, + socklen_t* aAddressLength, + int& aListenFd) override; + + nsresult AcceptStreamSocket(int aListenFd, + struct sockaddr* aAddress, + socklen_t* aAddressLength, + int& aStreamFd) override; + + nsresult CreateStreamSocket(struct sockaddr* aAddress, + socklen_t* aAddressLength, + int& aStreamFd) override; + + nsresult Duplicate(UnixSocketConnector*& aConnector) override; + +private: + nsresult CreateSocket(int& aFd) const; + nsresult SetSocketFlags(int aFd) const; + nsresult CheckPermission(int aFd) const; + nsresult CreateAddress(struct sockaddr& aAddress, + socklen_t& aAddressLength) const; + + const char** const mAllowedUsers; +}; + +} +} + +#endif diff --git a/ipc/keystore/moz.build b/ipc/keystore/moz.build new file mode 100644 index 000000000..e5df10a30 --- /dev/null +++ b/ipc/keystore/moz.build @@ -0,0 +1,18 @@ +# -*- 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/. + +EXPORTS.mozilla.ipc += [ + 'KeyStore.h' +] + +SOURCES += [ + 'KeyStore.cpp', + 'KeyStoreConnector.cpp' +] + +include('/ipc/chromium/chromium-config.mozbuild') + +FINAL_LIBRARY = 'xul' |