util/EncryptionUtils.cpp (436 lines of code) (raw):

/** * Copyright (c) 2014-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. */ #include <wdt/util/EncryptionUtils.h> #include <folly/Conv.h> #include <folly/SpinLock.h> #include <folly/String.h> // for humanify #include <wdt/WdtTransferRequest.h> // for to/fromHex utils #include <openssl/crypto.h> #include <openssl/rand.h> #include <string.h> // for memset namespace facebook { namespace wdt { using std::string; // When we get more than 15 types we need to start encoding with more // than 1 hex character, the decoding already support more than 1 static_assert(NUM_ENC_TYPES <= 16, "need to change encoding for types"); const char* const kEncryptionTypeDescriptions[] = {"none", "aes128ctr", "aes128gcm"}; static_assert(NUM_ENC_TYPES == sizeof(kEncryptionTypeDescriptions) / sizeof(kEncryptionTypeDescriptions[0]), "must provide description for all encryption types"); std::string encryptionTypeToStr(EncryptionType encryptionType) { if (encryptionType >= NUM_ENC_TYPES) { WLOG(ERROR) << "Unknown encryption type " << encryptionType; return folly::to<std::string>(encryptionType); } return kEncryptionTypeDescriptions[encryptionType]; } size_t encryptionTypeToTagLen(EncryptionType type) { return (type == ENC_AES128_GCM) ? kAESBlockSize : 0; } static int s_numOpensslLocks = 0; static folly::SpinLock* s_opensslLocks{nullptr}; static void opensslLock(int mode, int type, const char* file, int line) { WDT_CHECK_LT(type, s_numOpensslLocks); if (mode & CRYPTO_LOCK) { s_opensslLocks[type].lock(); WVLOG(3) << "Lock requested for " << type << " " << file << " " << line; return; } WVLOG(3) << "unlock requested for " << type << " " << file << " " << line; s_opensslLocks[type].unlock(); } static void opensslThreadId(CRYPTO_THREADID* id) { CRYPTO_THREADID_set_numeric(id, (unsigned long)pthread_self()); } WdtCryptoIntializer::WdtCryptoIntializer() { #if OPENSSL_VERSION_NUMBER < 0x10100000L if (CRYPTO_get_locking_callback()) { WLOG(WARNING) << "Openssl crypto library already initialized"; return; } s_numOpensslLocks = CRYPTO_num_locks(); s_opensslLocks = new folly::SpinLock[s_numOpensslLocks]; if (!s_opensslLocks) { WLOG(ERROR) << "Unable to allocate openssl locks " << s_numOpensslLocks; return; } CRYPTO_set_locking_callback(opensslLock); if (!CRYPTO_THREADID_get_callback()) { CRYPTO_THREADID_set_callback(opensslThreadId); } else { WLOG(INFO) << "Openssl id callback already set"; } WLOG(INFO) << "Openssl library initialized"; #endif } WdtCryptoIntializer::~WdtCryptoIntializer() { #if OPENSSL_VERSION_NUMBER < 0x10100000L WVLOG(1) << "Cleaning up openssl"; if (CRYPTO_get_locking_callback() != opensslLock) { WLOG(WARNING) << "Openssl not initialized by wdt"; return; } CRYPTO_set_locking_callback(nullptr); if (CRYPTO_THREADID_get_callback() == opensslThreadId) { CRYPTO_THREADID_set_callback(nullptr); } delete[] s_opensslLocks; #endif } EncryptionType parseEncryptionType(const std::string& str) { if (str == kEncryptionTypeDescriptions[ENC_AES128_GCM]) { return ENC_AES128_GCM; } if (str == kEncryptionTypeDescriptions[ENC_AES128_CTR]) { return ENC_AES128_CTR; } if (str == kEncryptionTypeDescriptions[ENC_NONE]) { return ENC_NONE; } WLOG(WARNING) << "Unknown encryption type" << str << ", defaulting to none"; return ENC_NONE; } EncryptionParams::EncryptionParams(EncryptionType type, const string& data) : type_(type), data_(data) { WLOG(INFO) << "New encryption params " << this << " " << getLogSafeString(); if (type_ >= NUM_ENC_TYPES) { WLOG(ERROR) << "Unsupported type " << type; erase(); } } bool EncryptionParams::operator==(const EncryptionParams& that) const { return (type_ == that.type_) && (data_ == that.data_); } void EncryptionParams::erase() { WVLOG(1) << " Erasing EncryptionParams " << this << " " << type_; // Erase the key (once for now...) if (!data_.empty()) { // Can't use .data() here (copy on write fbstring) memset(&data_.front(), 0, data_.size()); } data_.clear(); type_ = ENC_NONE; } EncryptionParams::~EncryptionParams() { erase(); } string EncryptionParams::getUrlSafeString() const { string res; res.reserve(/* 1 byte type, 1 byte colon */ 2 + /* hex is 2x length */ (2 * data_.length())); res.push_back(WdtUri::toHex(type_)); res.push_back(':'); for (unsigned char c : data_) { res.push_back(WdtUri::toHex(c >> 4)); res.push_back(WdtUri::toHex(c & 0xf)); } return res; } string EncryptionParams::getLogSafeString() const { string res; res.push_back(WdtUri::toHex(type_)); res.push_back(':'); res.append("..."); res.append(std::to_string(std::hash<string>()(data_))); res.append("..."); return res; } /* static */ ErrorCode EncryptionParams::unserialize(const string& input, EncryptionParams& out) { out.erase(); enum { IN_TYPE, FIRST_HEX, LEFT_HEX, RIGHT_HEX, } state = IN_TYPE; int type = 0; int byte = 0; for (char c : input) { if (state == IN_TYPE) { // In type section (before ':') if (c == ':') { if (type == 0) { WLOG(ERROR) << "Enc type still none when ':' reached " << input; return ERROR; } state = FIRST_HEX; continue; } } int v = WdtUri::fromHex(c); if (v < 0) { WLOG(ERROR) << "Not hex found " << (int)c << " in " << input; return ERROR; } if (state == IN_TYPE) { // Pre : hex digits type = (type << 4) | v; continue; } if (state != RIGHT_HEX) { // First or Left (even) hex digit: byte = v << 4; state = RIGHT_HEX; continue; } // Right (odd) hex digit: out.data_.push_back((char)(byte | v)); state = LEFT_HEX; byte = 0; // not needed but safer } if (state == IN_TYPE) { WLOG(ERROR) << "Missing ':' in encryption data " << input; return ERROR; } if (state != LEFT_HEX) { WLOG(ERROR) << "Odd number of hex in encryption data " << input << " decoded up to: " << out.data_; return ERROR; } if (type <= ENC_NONE || type >= NUM_ENC_TYPES) { WLOG(ERROR) << "Encryption type out of range " << type; return ERROR; } out.type_ = static_cast<EncryptionType>(type); WVLOG(1) << "Deserialized Encryption Params " << out.getLogSafeString(); return OK; } /* static */ EncryptionParams EncryptionParams::generateEncryptionParams( EncryptionType type) { if (type == ENC_NONE) { return EncryptionParams(); } WDT_CHECK(type > ENC_NONE && type < NUM_ENC_TYPES); uint8_t key[kAESBlockSize]; if (RAND_bytes(key, kAESBlockSize) != 1) { WLOG(ERROR) << "RAND_bytes failed, unable to generate symmetric key"; return EncryptionParams(); } return EncryptionParams(type, std::string(key, key + kAESBlockSize)); } bool AESBase::cloneCtx(EVP_CIPHER_CTX* ctxOut) const { WDT_CHECK(encryptionTypeToTagLen(type_)); int status = EVP_CIPHER_CTX_copy(ctxOut, evpCtx_.get()); if (status != 1) { WLOG(ERROR) << "Cipher ctx copy failed " << status; return false; } return true; } const EVP_CIPHER* AESBase::getCipher(const EncryptionType encryptionType) { if (encryptionType == ENC_AES128_CTR) { return EVP_aes_128_ctr(); } if (encryptionType == ENC_AES128_GCM) { return EVP_aes_128_gcm(); } WLOG(ERROR) << "Unknown encryption type " << encryptionType; return nullptr; } EVP_CIPHER_CTX* createAndInitCtx() { auto ctx = EVP_CIPHER_CTX_new(); EVP_CIPHER_CTX_init(ctx); return ctx; } void cleanupAndDestroyCtx(EVP_CIPHER_CTX* ctx) { EVP_CIPHER_CTX_cleanup(ctx); EVP_CIPHER_CTX_free(ctx); } bool AESEncryptor::start(const EncryptionParams& encryptionData, std::string& ivOut) { WDT_CHECK(!started_); // reset the enc ctx // To reuse the same ctx, we have to have different reset code for different // openssl version. So, we will just create another ctx for simplification evpCtx_.reset(createAndInitCtx()); type_ = encryptionData.getType(); const std::string& key = encryptionData.getSecret(); if (key.length() != kAESBlockSize) { WLOG(ERROR) << "Encryption key size must be " << kAESBlockSize << ", but input size length " << key.length(); return false; } ivOut.resize(kAESBlockSize); uint8_t* ivPtr = (uint8_t*)(&ivOut.front()); uint8_t* keyPtr = (uint8_t*)(&key.front()); if (RAND_bytes(ivPtr, kAESBlockSize) != 1) { WLOG(ERROR) << "RAND_bytes failed, unable to generate initialization vector"; return false; } const EVP_CIPHER* cipher = getCipher(type_); if (cipher == nullptr) { return false; } int cipherBlockSize = EVP_CIPHER_block_size(cipher); WDT_CHECK_EQ(1, cipherBlockSize); // Not super clear this is actually needed - but probably if not set // gcm only uses 96 out of the 128 bits of IV. Let's use all of it to // reduce chances of attacks on large data transfers. if (type_ == ENC_AES128_GCM) { if (EVP_EncryptInit_ex(evpCtx_.get(), cipher, nullptr, nullptr, nullptr) != 1) { WLOG(ERROR) << "GCM First init error"; } if (EVP_CIPHER_CTX_ctrl(evpCtx_.get(), EVP_CTRL_GCM_SET_IVLEN, ivOut.size(), nullptr) != 1) { WLOG(ERROR) << "Encrypt Init ivlen set failed"; } } if (EVP_EncryptInit_ex(evpCtx_.get(), cipher, nullptr, keyPtr, ivPtr) != 1) { WLOG(ERROR) << "Encrypt Init failed"; return false; } started_ = true; return true; } bool AESEncryptor::encrypt(const char* in, const int inLength, char* out) { WDT_CHECK(started_); int outLength; if (EVP_EncryptUpdate(evpCtx_.get(), (uint8_t*)out, &outLength, (uint8_t*)in, inLength) != 1) { WLOG(ERROR) << "EncryptUpdate failed"; return false; } WDT_CHECK_EQ(inLength, outLength); numProcessed_ += inLength; return true; } /* static */ bool AESEncryptor::finishInternal(EVP_CIPHER_CTX* ctx, const EncryptionType type, std::string& tagOut) { int outLength; int status = EVP_EncryptFinal(ctx, nullptr, &outLength); if (status != 1) { WLOG(ERROR) << "EncryptFinal failed"; return false; } WDT_CHECK_EQ(0, outLength); size_t tagSize = encryptionTypeToTagLen(type); if (tagSize) { tagOut.resize(tagSize); status = EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, tagOut.size(), &(tagOut.front())); if (status != 1) { WLOG(ERROR) << "EncryptFinal Tag extraction error " << folly::humanify(tagOut); tagOut.clear(); } } return true; } bool AESEncryptor::finish(std::string& tagOut) { tagOut.clear(); if (!started_) { return true; } started_ = false; bool status = finishInternal(evpCtx_.get(), type_, tagOut); WLOG_IF(INFO, status) << "Encryption finish tag = " << folly::humanify(tagOut); return status; } std::string AESEncryptor::computeCurrentTag() { std::unique_ptr<EVP_CIPHER_CTX, CipherCtxDeleter> ctx{createAndInitCtx()}; std::string tag; if (!cloneCtx(ctx.get())) { return tag; } finishInternal(ctx.get(), type_, tag); return tag; } AESEncryptor::~AESEncryptor() { std::string tag; finish(tag); } bool AESDecryptor::start(const EncryptionParams& encryptionData, const std::string& iv) { WDT_CHECK(!started_); // reset the enc ctx evpCtx_.reset(createAndInitCtx()); type_ = encryptionData.getType(); const std::string& key = encryptionData.getSecret(); if (key.length() != kAESBlockSize) { WLOG(ERROR) << "Encryption key size must be " << kAESBlockSize << ", but input size length " << key.length(); return false; } if (iv.length() != kAESBlockSize) { WLOG(ERROR) << "Initialization size must be " << kAESBlockSize << ", but input size length " << iv.length(); return false; } uint8_t* ivPtr = (uint8_t*)(&iv.front()); uint8_t* keyPtr = (uint8_t*)(&key.front()); const EVP_CIPHER* cipher = getCipher(type_); if (cipher == nullptr) { return false; } int cipherBlockSize = EVP_CIPHER_block_size(cipher); // block size for ctr mode should be 1 WDT_CHECK_EQ(1, cipherBlockSize); if (type_ == ENC_AES128_GCM) { if (EVP_EncryptInit_ex(evpCtx_.get(), cipher, nullptr, nullptr, nullptr) != 1) { WLOG(ERROR) << "GCM Decryptor First init error"; } if (EVP_CIPHER_CTX_ctrl(evpCtx_.get(), EVP_CTRL_GCM_SET_IVLEN, iv.size(), nullptr) != 1) { WLOG(ERROR) << "Encrypt Init ivlen set failed"; } } if (EVP_DecryptInit_ex(evpCtx_.get(), cipher, nullptr, keyPtr, ivPtr) != 1) { WLOG(ERROR) << "Decrypt Init failed"; return false; } started_ = true; return true; } bool AESDecryptor::decrypt(const char* in, const int inLength, char* out) { WDT_CHECK(started_); int outLength; if (EVP_DecryptUpdate(evpCtx_.get(), (uint8_t*)out, &outLength, (uint8_t*)in, inLength) != 1) { WLOG(ERROR) << "DecryptUpdate failed"; return false; } WDT_CHECK_EQ(inLength, outLength); numProcessed_ += inLength; return true; } bool AESDecryptor::verifyTag(const std::string& tag) { WDT_CHECK_EQ(ENC_AES128_GCM, type_); std::unique_ptr<EVP_CIPHER_CTX, CipherCtxDeleter> clonedCtx{ createAndInitCtx()}; if (!cloneCtx(clonedCtx.get())) { return false; } return finishInternal(clonedCtx.get(), type_, tag); } /* static */ bool AESDecryptor::finishInternal(EVP_CIPHER_CTX* ctx, const EncryptionType type, const std::string& tag) { int status; size_t tagSize = encryptionTypeToTagLen(type); if (tagSize) { if (tag.size() != tagSize) { WLOG(ERROR) << "Need tag for gcm mode " << folly::humanify(tag); return false; } // EVP_CIPHER_CTX_ctrl takes a non const buffer. But, for set tag the buffer // will not be modified. So, it is safe to use const_cast here. char* tagBuf = const_cast<char*>(tag.data()); status = EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, tag.size(), tagBuf); if (status != 1) { WLOG(ERROR) << "Decrypt final tag set error " << folly::humanify(tag); } } int outLength = 0; status = EVP_DecryptFinal(ctx, nullptr, &outLength); if (status != 1) { WLOG(ERROR) << "DecryptFinal failed " << outLength; return false; } WDT_CHECK_EQ(0, outLength); return true; } bool AESDecryptor::finish(const std::string& tag) { if (!started_) { return true; } started_ = false; bool status = finishInternal(evpCtx_.get(), type_, tag); WLOG_IF(INFO, status) << "Successful end of decryption with tag = " << folly::humanify(tag); return status; } AESDecryptor::~AESDecryptor() { std::string tag; finish(tag); } } } // end of namespaces