/* * BlobCipher.cpp * * This source file is part of the FoundationDB open source project * * Copyright 2013-2024 Apple Inc. and the FoundationDB project authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "fdbclient/BlobCipher.h" #include "fdbclient/FDBTypes.h" #include "fdbclient/Knobs.h" #include "flow/Arena.h" #include "flow/EncryptUtils.h" #include "flow/FileIdentifier.h" #include "flow/FastRef.h" #include "flow/IndexedSet.h" #include "flow/flow.h" #include "flow/Error.h" #include "flow/Knobs.h" #include "flow/IRandom.h" #include "flow/ITrace.h" #include "flow/ObjectSerializer.h" #include "flow/Platform.h" #include "flow/ProtocolVersion.h" #include "flow/network.h" #include "flow/serialize.h" #include "flow/Trace.h" #include "flow/UnitTest.h" #include #include #include #include #include #include #include #include #include #ifndef _WIN32 #include #else #include #endif #define BLOB_CIPHER_DEBUG DEBUG_ENCRYPT_KEY_CIPHER namespace { void validateEncryptHeaderFlagVersion(const int flagsVersion) { if (flagsVersion > CLIENT_KNOBS->ENCRYPT_HEADER_FLAGS_VERSION) { TraceEvent("EncryptHeaderUnsupportedFlagVersion") .detail("MaxSupportedVersion", CLIENT_KNOBS->ENCRYPT_HEADER_FLAGS_VERSION) .detail("Version", flagsVersion); throw not_implemented(); } } void validateEncryptHeaderAlgoHeaderVersion(const EncryptCipherMode cipherMode, const EncryptAuthTokenMode authMode, const EncryptAuthTokenAlgo authAlgo, const int version) { if (cipherMode != ENCRYPT_CIPHER_MODE_AES_256_CTR) { TraceEvent("EncryptHeaderUnsupportedEncryptCipherMode") .detail("MaxSupportedVersion", CLIENT_KNOBS->ENCRYPT_HEADER_FLAGS_VERSION) .detail("CipherMode", cipherMode); throw not_implemented(); } int maxSupportedVersion = -1; if (authMode == ENCRYPT_HEADER_AUTH_TOKEN_MODE_NONE) { maxSupportedVersion = CLIENT_KNOBS->ENCRYPT_HEADER_AES_CTR_NO_AUTH_VERSION; } else { ASSERT_EQ(authMode, ENCRYPT_HEADER_AUTH_TOKEN_MODE_SINGLE); if (authAlgo == ENCRYPT_HEADER_AUTH_TOKEN_ALGO_HMAC_SHA) { maxSupportedVersion = CLIENT_KNOBS->ENCRYPT_HEADER_AES_CTR_HMAC_SHA_AUTH_VERSION; } else if (authAlgo == ENCRYPT_HEADER_AUTH_TOKEN_ALGO_AES_CMAC) { maxSupportedVersion = CLIENT_KNOBS->ENCRYPT_HEADER_AES_CTR_AES_CMAC_AUTH_VERSION; } else { // Unknown encryption authentication algo } } if (version > maxSupportedVersion || maxSupportedVersion == -1) { TraceEvent("EncryptHeaderUnsupportedEncryptAuthToken") .detail("CipherMode", cipherMode) .detail("AuthMode", authMode) .detail("AuthAlgo", authAlgo) .detail("AlgoHeaderVersion", version) .detail("MaxSsupportedVersion", maxSupportedVersion); throw not_implemented(); } } } // namespace // BlobCipherEncryptHeaderRef uint32_t BlobCipherEncryptHeaderRef::getHeaderSize(const int flagVersion, const int algoVersion, const EncryptCipherMode cipherMode, const EncryptAuthTokenMode authMode, const EncryptAuthTokenAlgo authAlgo) { if (flagVersion != 1) { TraceEvent("BlobCipherGetHeaderSizeInvalidFlagVersion").detail("FlagVersion", flagVersion); throw not_implemented(); } if (algoVersion != 1) { TraceEvent("BlobCipherGetHeaderSizeInvalidAlgoVersion").detail("AlgoVersion", algoVersion); throw not_implemented(); } uint32_t total = sizeof(BlobCipherEncryptHeaderFlagsV1) + 2; // 2 bytes of std::variant index if (cipherMode != ENCRYPT_CIPHER_MODE_AES_256_CTR) { throw not_implemented(); } if (authMode == ENCRYPT_HEADER_AUTH_TOKEN_MODE_NONE) { total += AesCtrNoAuth::getSize(); } else { if (authAlgo == ENCRYPT_HEADER_AUTH_TOKEN_ALGO_HMAC_SHA) { total += AesCtrWithHmac::getSize(); } else { ASSERT_EQ(authAlgo, ENCRYPT_HEADER_AUTH_TOKEN_ALGO_AES_CMAC); total += AesCtrWithCmac::getSize(); } } return total; } const uint8_t* BlobCipherEncryptHeaderRef::getIV() const { validateEncryptHeaderFlagVersion(flagsVersion()); ASSERT_EQ(flagsVersion(), 1); BlobCipherEncryptHeaderFlagsV1 flags = std::get(this->flags); validateEncryptHeaderAlgoHeaderVersion((EncryptCipherMode)flags.encryptMode, (EncryptAuthTokenMode)flags.authTokenMode, (EncryptAuthTokenAlgo)flags.authTokenAlgo, algoHeaderVersion()); ASSERT_EQ(algoHeaderVersion(), 1); return std::visit([](auto& h) { return h.v1.iv; }, algoHeader); } template inline constexpr bool always_false_v = false; const EncryptHeaderCipherDetails BlobCipherEncryptHeaderRef::getCipherDetails() const { validateEncryptHeaderFlagVersion(flagsVersion()); ASSERT_EQ(flagsVersion(), 1); BlobCipherEncryptHeaderFlagsV1 flags = std::get(this->flags); validateEncryptHeaderAlgoHeaderVersion((EncryptCipherMode)flags.encryptMode, (EncryptAuthTokenMode)flags.authTokenMode, (EncryptAuthTokenAlgo)flags.authTokenAlgo, algoHeaderVersion()); ASSERT_EQ(algoHeaderVersion(), 1); // TODO: Replace with "Overload visitor pattern" someday. return std::visit( [](auto&& h) { using T = std::decay_t; if constexpr (std::is_same_v) { return EncryptHeaderCipherDetails(h.v1.cipherTextDetails); } else if constexpr (std::is_same_v || std::is_same_v) { return EncryptHeaderCipherDetails(h.v1.cipherTextDetails, h.v1.cipherHeaderDetails); } else { static_assert(always_false_v, "Unknown encryption authentication"); } }, algoHeader); } EncryptAuthTokenMode BlobCipherEncryptHeaderRef::getAuthTokenMode() const { // TODO: Replace with "Overload visitor pattern" someday. return std::visit( [](auto&& f) { using T = std::decay_t; if constexpr (std::is_same_v) { return (EncryptAuthTokenMode)f.authTokenMode; } else { static_assert(always_false_v, "Unknown encryption flag header"); } }, flags); } EncryptCipherDomainId BlobCipherEncryptHeaderRef::getDomainId() const { return std::visit([](auto& h) { return h.v1.cipherTextDetails.encryptDomainId; }, algoHeader); } EncryptHeaderCipherKCVs BlobCipherEncryptHeaderRef::getKCVs() const { validateEncryptHeaderFlagVersion(flagsVersion()); ASSERT_EQ(flagsVersion(), 1); BlobCipherEncryptHeaderFlagsV1 flags = std::get(this->flags); validateEncryptHeaderAlgoHeaderVersion((EncryptCipherMode)flags.encryptMode, (EncryptAuthTokenMode)flags.authTokenMode, (EncryptAuthTokenAlgo)flags.authTokenAlgo, algoHeaderVersion()); ASSERT_EQ(algoHeaderVersion(), 1); // TODO: Replace with "Overload visitor pattern" someday. return std::visit( [](auto&& h) { using T = std::decay_t; if constexpr (std::is_same_v) { return EncryptHeaderCipherKCVs(h.v1.textKCV); } else if constexpr (std::is_same_v || std::is_same_v) { return EncryptHeaderCipherKCVs(h.v1.textKCV, h.v1.headerKCV); } else { static_assert(always_false_v, "Unknown encryption authentication"); } }, algoHeader); } void BlobCipherEncryptHeaderRef::validateEncryptionHeaderDetails(const BlobCipherDetails& textCipherDetails, const BlobCipherDetails& headerCipherDetails, const EncryptHeaderCipherKCVs& kcvs, const StringRef& ivRef) const { validateEncryptHeaderFlagVersion(flagsVersion()); ASSERT_EQ(flagsVersion(), 1); BlobCipherEncryptHeaderFlagsV1 flags = std::get(this->flags); validateEncryptHeaderAlgoHeaderVersion((EncryptCipherMode)flags.encryptMode, (EncryptAuthTokenMode)flags.authTokenMode, (EncryptAuthTokenAlgo)flags.authTokenAlgo, algoHeaderVersion()); ASSERT_EQ(algoHeaderVersion(), 1); BlobCipherDetails persistedTextCipherDetails; BlobCipherDetails persistedHeaderCipherDetails; uint8_t* persistedIV = nullptr; EncryptCipherKeyCheckValue persistedTextKCV; Optional persistedHeaderKCV; // TODO: Replace with "Overload visitor pattern" someday. return std::visit( [&persistedTextCipherDetails, &persistedHeaderCipherDetails, &persistedIV, &persistedTextKCV, &persistedHeaderKCV](auto&& h) { using T = std::decay_t; if constexpr (std::is_same_v) { persistedTextCipherDetails = h.v1.cipherTextDetails; persistedIV = (uint8_t*)&h.v1.iv[0]; persistedTextKCV = h.v1.textKCV; } else if constexpr (std::is_same_v || std::is_same_v) { persistedTextCipherDetails = h.v1.cipherTextDetails; persistedHeaderCipherDetails = h.v1.cipherHeaderDetails; persistedIV = (uint8_t*)&h.v1.iv[0]; persistedTextKCV = h.v1.textKCV; persistedHeaderKCV = h.v1.headerKCV; } else { static_assert(always_false_v, "Unknown encryption authentication"); } }, algoHeader); // Validate encryption header 'cipherHeader' details sanity if (flags.authTokenMode != ENCRYPT_HEADER_AUTH_TOKEN_MODE_NONE && headerCipherDetails != persistedHeaderCipherDetails) { TraceEvent(SevError, "ValidateEncryptHeaderMismatch") .detail("HeaderDomainId", headerCipherDetails.encryptDomainId) .detail("PersistedHeaderDomainId", persistedHeaderCipherDetails.encryptDomainId) .detail("HeaderBaseCipherId", headerCipherDetails.baseCipherId) .detail("ExpectedHeaderBaseCipherId", persistedHeaderCipherDetails.baseCipherId) .detail("HeaderSalt", headerCipherDetails.salt) .detail("ExpectedHeaderSalt", persistedHeaderCipherDetails.salt); throw encrypt_header_metadata_mismatch(); } // Validate encryption header 'cipherText' details sanity if (textCipherDetails != persistedTextCipherDetails) { TraceEvent(SevError, "ValidateEncryptHeaderMismatch") .detail("TextDomainId", textCipherDetails.encryptDomainId) .detail("PersistedTextDomainId", persistedTextCipherDetails.encryptDomainId) .detail("TextBaseCipherId", textCipherDetails.baseCipherId) .detail("PersistedTextBaseCipherId", persistedTextCipherDetails.encryptDomainId) .detail("TextSalt", textCipherDetails.salt) .detail("PersistedTextSalt", persistedTextCipherDetails.salt); throw encrypt_header_metadata_mismatch(); } // Validate 'Initialization Vector' sanity if (memcmp(ivRef.begin(), persistedIV, AES_256_IV_LENGTH) != 0) { TraceEvent(SevError, "EncryptionHeaderIVMismatch") .detail("IVChecksum", XXH3_64bits(ivRef.begin(), ivRef.size())) .detail("ExpectedIVChecksum", XXH3_64bits(persistedIV, AES_256_IV_LENGTH)); throw encrypt_header_metadata_mismatch(); } // Validate baseCipher KCVs if (persistedTextKCV != kcvs.textKCV) { TraceEvent(SevError, "EncryptionHeadeTextKCVMismatch") .detail("Persisted", persistedTextKCV) .detail("Expected", kcvs.textKCV); throw encrypt_key_check_value_mismatch(); } if (persistedHeaderKCV.present()) { if (!kcvs.headerKCV.present()) { TraceEvent(SevError, "EncryptionHeadeMissingHeaderKCV"); throw encrypt_key_check_value_mismatch(); } if (persistedHeaderKCV.get() != kcvs.headerKCV.get()) { TraceEvent(SevError, "EncryptionHeadeTextKCVMismatch") .detail("Persisted", persistedTextKCV) .detail("Expected", kcvs.textKCV); throw encrypt_key_check_value_mismatch(); } } } // BlobCipherMetrics methods const std::unordered_map BlobCipherMetrics::usageTypeNames = { { BlobCipherMetrics::UsageType::ALL, "" }, { BlobCipherMetrics::UsageType::TLOG, "TLog" }, { BlobCipherMetrics::UsageType::TLOG_POST_RESOLUTION, "TLogPostResolution" }, { BlobCipherMetrics::UsageType::KV_MEMORY, "KVMemory" }, { BlobCipherMetrics::UsageType::KV_REDWOOD, "KVRedwood" }, { BlobCipherMetrics::UsageType::BLOB_GRANULE, "BlobGranule" }, { BlobCipherMetrics::UsageType::BACKUP, "Backup" }, { BlobCipherMetrics::UsageType::RESTORE, "Restore" }, { BlobCipherMetrics::UsageType::TEST, "Test" }, }; BlobCipherMetrics::BlobCipherMetrics() : cc("BlobCipher"), cipherKeyCacheHit("CipherKeyCacheHit", cc), cipherKeyCacheMiss("CipherKeyCacheMiss", cc), cipherKeyCacheExpired("CipherKeyCacheExpired", cc), latestCipherKeyCacheHit("LatestCipherKeyCacheHit", cc), latestCipherKeyCacheMiss("LatestCipherKeyCacheMiss", cc), latestCipherKeyCacheNeedsRefresh("LatestCipherKeyCacheNeedsRefresh", cc), getBlobMetadataLatency("GetBlobMetadataLatency", UID(), FLOW_KNOBS->ENCRYPT_KEY_CACHE_LOGGING_INTERVAL, FLOW_KNOBS->ENCRYPT_KEY_CACHE_LOGGING_SKETCH_ACCURACY), getCipherKeysLatency("GetCipherKeysLatency", UID(), FLOW_KNOBS->ENCRYPT_KEY_CACHE_LOGGING_INTERVAL, FLOW_KNOBS->ENCRYPT_KEY_CACHE_LOGGING_SKETCH_ACCURACY, true), getLatestCipherKeysLatency("GetLatestCipherKeysLatency", UID(), FLOW_KNOBS->ENCRYPT_KEY_CACHE_LOGGING_INTERVAL, FLOW_KNOBS->ENCRYPT_KEY_CACHE_LOGGING_SKETCH_ACCURACY, true) { specialCounter(cc, "CacheSize", []() { return BlobCipherKeyCache::getInstance()->getSize(); }); traceFuture = cc.traceCounters("BlobCipherMetrics", UID(), FLOW_KNOBS->ENCRYPT_KEY_CACHE_LOGGING_INTERVAL); } std::string toString(BlobCipherMetrics::UsageType type) { switch (type) { case BlobCipherMetrics::UsageType::TLOG: return "TLog"; case BlobCipherMetrics::UsageType::TLOG_POST_RESOLUTION: return "TLogPostResolution"; case BlobCipherMetrics::UsageType::KV_MEMORY: return "KVMemory"; case BlobCipherMetrics::UsageType::KV_REDWOOD: return "KVRedwood"; case BlobCipherMetrics::UsageType::BLOB_GRANULE: return "BlobGranule"; case BlobCipherMetrics::UsageType::BACKUP: return "Backup"; case BlobCipherMetrics::UsageType::RESTORE: return "Restore"; case BlobCipherMetrics::UsageType::TEST: return "Test"; default: ASSERT(false); return ""; } } // BlobCipherKey class methods BlobCipherKey::BlobCipherKey(const EncryptCipherDomainId& domainId, const EncryptCipherBaseKeyId& baseCiphId, const uint8_t* baseCiph, const int baseCiphLen, const EncryptCipherKeyCheckValue baseCiphKCV, const int64_t refreshAt, const int64_t expireAt) { // Salt generated is used while applying HMAC Key derivation, hence, not using crypto-secure hash algorithm is // ok. Further, 'deterministic' salt generation is used to preserve simulation determinism properties. EncryptCipherRandomSalt salt; if (g_network->isSimulated()) { salt = deterministicRandom()->randomUInt64(); } else { salt = nondeterministicRandom()->randomUInt64(); } // Support two type of CipherKeys: 'revocable' & 'non-revocable' ciphers. // In all cases, either cipherKey never expires i.e. refreshAt == infinite, or, refreshAt needs <= expireAt // timestamp. ASSERT(refreshAt == std::numeric_limits::max() || (refreshAt <= expireAt)); initKey(domainId, baseCiphId, baseCiph, baseCiphLen, baseCiphKCV, salt, refreshAt, expireAt); } BlobCipherKey::BlobCipherKey(const EncryptCipherDomainId& domainId, const EncryptCipherBaseKeyId& baseCiphId, const uint8_t* baseCiph, const int baseCiphLen, const EncryptCipherKeyCheckValue baseCiphKCV, const EncryptCipherRandomSalt& salt, const int64_t refreshAt, const int64_t expireAt) { initKey(domainId, baseCiphId, baseCiph, baseCiphLen, baseCiphKCV, salt, refreshAt, expireAt); } void BlobCipherKey::initKey(const EncryptCipherDomainId& domainId, const EncryptCipherBaseKeyId& baseCiphId, const uint8_t* baseCiph, const int baseCiphLen, const EncryptCipherKeyCheckValue baseCiphKCV, const EncryptCipherRandomSalt& salt, const int64_t refreshAt, const int64_t expireAt) { if (baseCiphLen > MAX_BASE_CIPHER_LEN) { // HMAC_SHA digest generation accepts upto MAX_BASE_CIPHER_LEN key-buffer, longer keys are truncated and weakens // the security guarantees. TraceEvent(SevWarnAlways, "MaxBaseCipherKeyLimit") .detail("MaxAllowed", MAX_BASE_CIPHER_LEN) .detail("BaseCipherLen", baseCiphLen); CODE_PROBE(true, "Encryption max base cipher len violation", probe::decoration::rare); throw encrypt_max_base_cipher_len(); } const EncryptCipherKeyCheckValue computedKCV = Sha256KCV().computeKCV(baseCiph, baseCiphLen); if (computedKCV != baseCiphKCV) { TraceEvent(SevWarnAlways, "BlobCipherKeyInitBaseCipherKCVMismatch") .detail("DomId", domainId) .detail("BaseCipherId", baseCiphId) .detail("Computed", computedKCV) .detail("BaseCipherKCV", baseCipherKCV); throw encrypt_ops_error(); } // Set the base encryption key properties baseCipher = std::make_unique(baseCiphLen); memcpy(baseCipher.get(), baseCiph, baseCiphLen); baseCipherLen = baseCiphLen; baseCipherKCV = baseCiphKCV; baseCipherId = baseCiphId; // Set the encryption domain for the base encryption key encryptDomainId = domainId; randomSalt = salt; // derive the encryption key cipher = std::make_unique(AES_256_KEY_LENGTH); memset(cipher.get(), 0, AES_256_KEY_LENGTH); applyHmacSha256Derivation(); // update cipher 'refresh' and 'expire' TS refreshAtTS = refreshAt; expireAtTS = expireAt; #if BLOB_CIPHER_DEBUG TraceEvent(SevDebug, "BlobCipherKeyInit") .detail("DomainId", domainId) .detail("BaseCipherId", baseCipherId) .detail("BaseCipherLen", baseCipherLen) .detail("RandomSalt", randomSalt) .detail("RefreshAt", refreshAtTS) .detail("ExpireAtTS", expireAtTS) .detail("BaseCipherKCV", baseCipherKCV); #endif } void BlobCipherKey::applyHmacSha256Derivation() { Arena arena; uint8_t buf[baseCipherLen + sizeof(EncryptCipherRandomSalt)]; memcpy(&buf[0], baseCipher.get(), baseCipherLen); memcpy(&buf[0] + baseCipherLen, &randomSalt, sizeof(EncryptCipherRandomSalt)); HmacSha256DigestGen hmacGen(baseCipher.get(), baseCipherLen); unsigned int digestLen = hmacGen.digest( { { &buf[0], baseCipherLen + sizeof(EncryptCipherRandomSalt) } }, cipher.get(), AUTH_TOKEN_HMAC_SHA_SIZE); if (digestLen < AES_256_KEY_LENGTH) { memcpy(cipher.get() + digestLen, buf, AES_256_KEY_LENGTH - digestLen); } } void BlobCipherKey::reset() { memset(baseCipher.get(), 0, baseCipherLen); memset(cipher.get(), 0, AES_256_KEY_LENGTH); } // BlobKeyIdCache class methods BlobCipherKeyIdCache::BlobCipherKeyIdCache(EncryptCipherDomainId dId, size_t* sizeStat) : domainId(dId), latestBaseCipherKeyId(), latestRandomSalt(), sizeStat(sizeStat) { ASSERT(sizeStat != nullptr); TraceEvent(SevInfo, "BlobCipherKeyIdCacheInit").detail("DomainId", domainId); } BlobCipherKeyIdCacheKey BlobCipherKeyIdCache::getCacheKey(const EncryptCipherBaseKeyId& baseCipherKeyId, const EncryptCipherRandomSalt& salt) { if (baseCipherKeyId == INVALID_ENCRYPT_CIPHER_KEY_ID || salt == INVALID_ENCRYPT_RANDOM_SALT) { throw encrypt_invalid_id(); } return std::make_pair(baseCipherKeyId, salt); } Reference BlobCipherKeyIdCache::getLatestCipherKey() { if (!latestBaseCipherKeyId.present()) { return Reference(); } ASSERT_NE(latestBaseCipherKeyId.get(), INVALID_ENCRYPT_CIPHER_KEY_ID); ASSERT(latestRandomSalt.present()); ASSERT_NE(latestRandomSalt.get(), INVALID_ENCRYPT_RANDOM_SALT); Reference latest = getCipherByBaseCipherId(latestBaseCipherKeyId.get(), latestRandomSalt.get()); if (!latest.isValid()) { // Cipher already 'expired' return Reference(); } ASSERT(!latest->isExpired()); ASSERT_EQ(latest->getBaseCipherId(), latestBaseCipherKeyId.get()); ASSERT_EQ(latest->getSalt(), latestRandomSalt.get()); if (latest->needsRefresh()) { #if BLOB_CIPHER_DEBUG TraceEvent(SevDebug, "BlobCipherGetLatestNeedsRefresh") .detail("DomainId", domainId) .detail("Now", now()) .detail("RefreshAt", latest->getRefreshAtTS()) .detail("ExpireAt", latest->getExpireAtTS()); #endif ++BlobCipherMetrics::getInstance()->latestCipherKeyCacheNeedsRefresh; latestBaseCipherKeyId.reset(); latestRandomSalt.reset(); return Reference(); } return latest; } Reference BlobCipherKeyIdCache::getCipherByBaseCipherId(const EncryptCipherBaseKeyId& baseCipherKeyId, const EncryptCipherRandomSalt& salt) { BlobCipherKeyIdCacheMapCItr itr = keyIdCache.find(getCacheKey(baseCipherKeyId, salt)); if (itr == keyIdCache.end()) { return Reference(); } if (itr->second->isExpired()) { #if BLOB_CIPHER_DEBUG TraceEvent(SevDebug, "BlobCipherGetCipherExpired") .detail("DomainId", domainId) .detail("BaseCipherId", itr->second->getBaseCipherId()) .detail("Now", now()) .detail("ExpireAt", itr->second->getExpireAtTS()); #endif ++BlobCipherMetrics::getInstance()->cipherKeyCacheExpired; // remove the expired key from the cache keyIdCache.erase(itr); return Reference(); } return itr->second; } Reference BlobCipherKeyIdCache::insertBaseCipherKey(const EncryptCipherBaseKeyId& baseCipherId, const uint8_t* baseCipher, const int baseCipherLen, const EncryptCipherKeyCheckValue baseCipherKCV, const int64_t refreshAt, const int64_t expireAt) { ASSERT_GT(baseCipherId, INVALID_ENCRYPT_CIPHER_KEY_ID); ASSERT_GT(baseCipherLen, 0); // BaseCipherKeys are immutable, given the routine invocation updates 'latestCipher', // ensure no key-tampering is done Reference latestCipherKey = getLatestCipherKey(); if (latestCipherKey.isValid() && latestCipherKey->getBaseCipherId() == baseCipherId) { if (memcmp(latestCipherKey->rawBaseCipher(), baseCipher, baseCipherLen) == 0) { #if BLOB_CIPHER_DEBUG TraceEvent(SevDebug, "InsertBaseCipherKeyAlreadyPresent") .detail("BaseCipherKeyId", baseCipherId) .detail("DomainId", domainId) .detail("BaseCipherKCV", baseCipherKCV); #endif // Key is already present; nothing more to do. return latestCipherKey; } else { TraceEvent(SevInfo, "BlobCipherUpdatetBaseCipherKey") .detail("BaseCipherKeyId", baseCipherId) .detail("DomainId", domainId); throw encrypt_update_cipher(); } } // Logging only tracks newly inserted cipher in the cache // Approach limits the logging to two instances when new cipher gets added to the cache, two // possible scenarios could be: // 1. Cold start - cache getting warmed up // 2. New cipher - new Tenant and/or KMS driven key-rotation // Frequency of the log is governed by KMS driven `refreshAt` interval which is usually a long duration (days if // not months) TraceEvent(SevInfo, "BlobCipherKeyInsertBaseCipherKeyLatest") .detail("DomainId", domainId) .detail("BaseCipherId", baseCipherId) .detail("BaseCipherLen", baseCipherLen) .detail("BaseCipherKCV", baseCipherKCV) .detail("RefreshAt", refreshAt) .detail("ExpireAt", expireAt); Reference cipherKey = makeReference( domainId, baseCipherId, baseCipher, baseCipherLen, baseCipherKCV, refreshAt, expireAt); BlobCipherKeyIdCacheKey cacheKey = getCacheKey(cipherKey->getBaseCipherId(), cipherKey->getSalt()); auto result = keyIdCache.emplace(cacheKey, cipherKey); ASSERT(result.second); // Update the latest BaseCipherKeyId for the given encryption domain latestBaseCipherKeyId = baseCipherId; latestRandomSalt = cipherKey->getSalt(); (*sizeStat)++; return cipherKey; } Reference BlobCipherKeyIdCache::insertBaseCipherKey(const EncryptCipherBaseKeyId& baseCipherId, const uint8_t* baseCipher, const int baseCipherLen, const EncryptCipherKeyCheckValue baseCipherKCV, const EncryptCipherRandomSalt& salt, const int64_t refreshAt, const int64_t expireAt) { ASSERT_NE(baseCipherId, INVALID_ENCRYPT_CIPHER_KEY_ID); ASSERT_NE(salt, INVALID_ENCRYPT_RANDOM_SALT); ASSERT_GT(baseCipherLen, 0); BlobCipherKeyIdCacheKey cacheKey = getCacheKey(baseCipherId, salt); // BaseCipherKeys are immutable, ensure that cached value doesn't get updated. BlobCipherKeyIdCacheMapCItr itr = keyIdCache.find(cacheKey); if (itr != keyIdCache.end()) { if (memcmp(itr->second->rawBaseCipher(), baseCipher, baseCipherLen) == 0) { #if BLOB_CIPHER_DEBUG TraceEvent(SevDebug, "InsertBaseCipherKeyAlreadyPresent") .detail("BaseCipherKeyId", baseCipherId) .detail("DomainId", domainId) .detail("BaseCipherKCV", baseCipherKCV); #endif // Key is already present; nothing more to do. return itr->second; } else { TraceEvent(SevInfo, "BlobCipherUpdateBaseCipherKey") .detail("BaseCipherKeyId", baseCipherId) .detail("DomainId", domainId); throw encrypt_update_cipher(); } } // Logging only tracks newly inserted cipher in the cache // possible scenarios could be: // 1. Cold start - cache getting warmed up // 2. New cipher - new Tenant and/or KMS driven key-rotation // Frequency of the log is governed by KMS driven `refreshAt` interval which is usually a long duration (days if // not months) TraceEvent(SevInfo, "BlobCipherKeyInsertBaseCipherKey") .detail("DomainId", domainId) .detail("BaseCipherId", baseCipherId) .detail("BaseCipherLen", baseCipherLen) .detail("BaseCipherKCV", baseCipherKCV) .detail("Salt", salt) .detail("RefreshAt", refreshAt) .detail("ExpireAt", expireAt); Reference cipherKey = makeReference( domainId, baseCipherId, baseCipher, baseCipherLen, baseCipherKCV, salt, refreshAt, expireAt); auto result = keyIdCache.emplace(cacheKey, cipherKey); ASSERT(result.second); (*sizeStat)++; return cipherKey; } void BlobCipherKeyIdCache::cleanup() { for (auto& keyItr : keyIdCache) { keyItr.second->reset(); } keyIdCache.clear(); } std::vector> BlobCipherKeyIdCache::getAllCipherKeys() { std::vector> cipherKeys; for (auto& keyItr : keyIdCache) { cipherKeys.push_back(keyItr.second); } return cipherKeys; } // BlobCipherKeyCache class methods Reference BlobCipherKeyCache::insertCipherKey(const EncryptCipherDomainId& domainId, const EncryptCipherBaseKeyId& baseCipherId, const uint8_t* baseCipher, const int baseCipherLen, const EncryptCipherKeyCheckValue baseCipherKCV, const int64_t refreshAt, const int64_t expireAt) { if (domainId == INVALID_ENCRYPT_DOMAIN_ID || baseCipherId == INVALID_ENCRYPT_CIPHER_KEY_ID) { throw encrypt_invalid_id(); } Reference cipherKey; try { auto domainItr = domainCacheMap.find(domainId); if (domainItr == domainCacheMap.end()) { // Add mapping to track new encryption domain Reference keyIdCache = makeReference(domainId, &size); cipherKey = keyIdCache->insertBaseCipherKey( baseCipherId, baseCipher, baseCipherLen, baseCipherKCV, refreshAt, expireAt); domainCacheMap.emplace(domainId, keyIdCache); } else { // Track new baseCipher keys Reference keyIdCache = domainItr->second; cipherKey = keyIdCache->insertBaseCipherKey( baseCipherId, baseCipher, baseCipherLen, baseCipherKCV, refreshAt, expireAt); } } catch (Error& e) { TraceEvent(SevWarn, "BlobCipherInsertCipherKeyFailed") .detail("BaseCipherKeyId", baseCipherId) .detail("DomainId", domainId); throw; } return cipherKey; } Reference BlobCipherKeyCache::insertCipherKey(const EncryptCipherDomainId& domainId, const EncryptCipherBaseKeyId& baseCipherId, const uint8_t* baseCipher, const int baseCipherLen, const EncryptCipherKeyCheckValue baseCipherKCV, const EncryptCipherRandomSalt& salt, const int64_t refreshAt, const int64_t expireAt) { if (domainId == INVALID_ENCRYPT_DOMAIN_ID || baseCipherId == INVALID_ENCRYPT_CIPHER_KEY_ID || salt == INVALID_ENCRYPT_RANDOM_SALT) { throw encrypt_invalid_id(); } Reference cipherKey; try { auto domainItr = domainCacheMap.find(domainId); if (domainItr == domainCacheMap.end()) { // Add mapping to track new encryption domain Reference keyIdCache = makeReference(domainId, &size); cipherKey = keyIdCache->insertBaseCipherKey( baseCipherId, baseCipher, baseCipherLen, baseCipherKCV, salt, refreshAt, expireAt); domainCacheMap.emplace(domainId, keyIdCache); } else { // Track new baseCipher keys Reference keyIdCache = domainItr->second; cipherKey = keyIdCache->insertBaseCipherKey( baseCipherId, baseCipher, baseCipherLen, baseCipherKCV, salt, refreshAt, expireAt); } } catch (Error& e) { TraceEvent(SevWarn, "BlobCipherInsertCipherKeyFailed") .detail("BaseCipherKeyId", baseCipherId) .detail("DomainId", domainId) .detail("Salt", salt); throw; } return cipherKey; } Reference BlobCipherKeyCache::getLatestCipherKey(const EncryptCipherDomainId& domainId) { if (domainId == INVALID_ENCRYPT_DOMAIN_ID) { TraceEvent(SevWarn, "BlobCipherGetLatestCipherKeyInvalidID").detail("DomainId", domainId); throw encrypt_invalid_id(); } auto domainItr = domainCacheMap.find(domainId); if (domainItr == domainCacheMap.end()) { TraceEvent(SevInfo, "BlobCipherGetLatestCipherKeyDomainNotFound").detail("DomainId", domainId); return Reference(); } Reference keyIdCache = domainItr->second; Reference cipherKey = keyIdCache->getLatestCipherKey(); cipherKey.isValid() ? ++BlobCipherMetrics::getInstance()->latestCipherKeyCacheHit : ++BlobCipherMetrics::getInstance()->latestCipherKeyCacheMiss; return cipherKey; } Reference BlobCipherKeyCache::getCipherKey(const EncryptCipherDomainId& domainId, const EncryptCipherBaseKeyId& baseCipherId, const EncryptCipherRandomSalt& salt) { auto domainItr = domainCacheMap.find(domainId); if (domainItr == domainCacheMap.end()) { return Reference(); } Reference keyIdCache = domainItr->second; Reference cipherKey = keyIdCache->getCipherByBaseCipherId(baseCipherId, salt); cipherKey.isValid() ? ++BlobCipherMetrics::getInstance()->cipherKeyCacheHit : ++BlobCipherMetrics::getInstance()->cipherKeyCacheMiss; return cipherKey; } void BlobCipherKeyCache::resetEncryptDomainId(const EncryptCipherDomainId domainId) { auto domainItr = domainCacheMap.find(domainId); if (domainItr == domainCacheMap.end()) { return; } Reference keyIdCache = domainItr->second; ASSERT(keyIdCache->getSize() <= size); size -= keyIdCache->getSize(); keyIdCache->cleanup(); TraceEvent(SevInfo, "BlobCipherResetEncryptDomainId").detail("DomainId", domainId); } void BlobCipherKeyCache::cleanup() noexcept { Reference instance = BlobCipherKeyCache::getInstance(); TraceEvent(SevInfo, "BlobCipherKeyCacheCleanup").log(); for (auto& domainItr : instance->domainCacheMap) { Reference keyIdCache = domainItr.second; keyIdCache->cleanup(); TraceEvent(SevInfo, "BlobCipherKeyCacheCleanup").detail("DomainId", domainItr.first); } instance->domainCacheMap.clear(); instance->size = 0; } std::vector> BlobCipherKeyCache::getAllCiphers(const EncryptCipherDomainId& domainId) { auto domainItr = domainCacheMap.find(domainId); if (domainItr == domainCacheMap.end()) { return {}; } Reference keyIdCache = domainItr->second; return keyIdCache->getAllCipherKeys(); } int getEncryptCurrentAlgoHeaderVersion(const EncryptAuthTokenMode mode, const EncryptAuthTokenAlgo algo) { if (mode == EncryptAuthTokenMode::ENCRYPT_HEADER_AUTH_TOKEN_MODE_NONE) { return CLIENT_KNOBS->ENCRYPT_HEADER_AES_CTR_NO_AUTH_VERSION; } else { ASSERT_EQ(mode, EncryptAuthTokenMode::ENCRYPT_HEADER_AUTH_TOKEN_MODE_SINGLE); if (algo == ENCRYPT_HEADER_AUTH_TOKEN_ALGO_AES_CMAC) { return CLIENT_KNOBS->ENCRYPT_HEADER_AES_CTR_AES_CMAC_AUTH_VERSION; } else { ASSERT_EQ(algo, ENCRYPT_HEADER_AUTH_TOKEN_ALGO_HMAC_SHA); return CLIENT_KNOBS->ENCRYPT_HEADER_AES_CTR_HMAC_SHA_AUTH_VERSION; } } } void BlobCipherDetails::validateCipherDetailsWithCipherKey(Reference cipherKey) { if (!(baseCipherId == cipherKey->getBaseCipherId() && encryptDomainId == cipherKey->getDomainId() && salt == cipherKey->getSalt())) { TraceEvent(SevWarn, "EncryptionHeaderCipherMismatch") .detail("TextDomainId", cipherKey->getDomainId()) .detail("ExpectedTextDomainId", encryptDomainId) .detail("TextBaseCipherId", cipherKey->getBaseCipherId()) .detail("ExpectedTextBaseCipherId", baseCipherId) .detail("TextSalt", cipherKey->getSalt()) .detail("ExpectedTextSalt", salt); throw encrypt_header_metadata_mismatch(); } } // EncryptBlobCipherAes265Ctr class methods EncryptBlobCipherAes265Ctr::EncryptBlobCipherAes265Ctr(Reference tCipherKey, Optional> hCipherKeyOpt, const uint8_t* cipherIV, const int ivLen, const EncryptAuthTokenMode mode, BlobCipherMetrics::UsageType usageType) : ctx(EVP_CIPHER_CTX_new()), textCipherKey(tCipherKey), headerCipherKeyOpt(hCipherKeyOpt), authTokenMode(mode) { ASSERT_EQ(ivLen, AES_256_IV_LENGTH); authTokenAlgo = getAuthTokenAlgoFromMode(authTokenMode); memcpy(&iv[0], cipherIV, ivLen); init(); } EncryptBlobCipherAes265Ctr::EncryptBlobCipherAes265Ctr(Reference tCipherKey, Optional> hCipherKeyOpt, const uint8_t* cipherIV, const int ivLen, const EncryptAuthTokenMode mode, const EncryptAuthTokenAlgo algo, BlobCipherMetrics::UsageType usageType) : ctx(EVP_CIPHER_CTX_new()), textCipherKey(tCipherKey), headerCipherKeyOpt(hCipherKeyOpt), authTokenMode(mode), authTokenAlgo(algo) { ASSERT_EQ(ivLen, AES_256_IV_LENGTH); memcpy(&iv[0], cipherIV, ivLen); init(); } EncryptBlobCipherAes265Ctr::EncryptBlobCipherAes265Ctr(Reference tCipherKey, Optional> hCipherKeyOpt, const EncryptAuthTokenMode mode, BlobCipherMetrics::UsageType usageType) : ctx(EVP_CIPHER_CTX_new()), textCipherKey(tCipherKey), headerCipherKeyOpt(hCipherKeyOpt), authTokenMode(mode) { authTokenAlgo = getAuthTokenAlgoFromMode(authTokenMode); deterministicRandom()->randomBytes(iv, AES_256_IV_LENGTH); init(); } EncryptBlobCipherAes265Ctr::EncryptBlobCipherAes265Ctr(Reference tCipherKey, Optional> hCipherKeyOpt, const EncryptAuthTokenMode mode, const EncryptAuthTokenAlgo algo, BlobCipherMetrics::UsageType usageType) : ctx(EVP_CIPHER_CTX_new()), textCipherKey(tCipherKey), headerCipherKeyOpt(hCipherKeyOpt), authTokenMode(mode), authTokenAlgo(algo) { deterministicRandom()->randomBytes(iv, AES_256_IV_LENGTH); init(); } void EncryptBlobCipherAes265Ctr::init() { ASSERT(textCipherKey.isValid()); if (FLOW_KNOBS->ENCRYPT_HEADER_AUTH_TOKEN_ENABLED) { ASSERT(headerCipherKeyOpt.present() && headerCipherKeyOpt.get().isValid()); } if (!isEncryptHeaderAuthTokenDetailsValid(authTokenMode, authTokenAlgo)) { TraceEvent(SevWarn, "InvalidAuthTokenDetails") .detail("TokenMode", authTokenMode) .detail("TokenAlgo", authTokenAlgo); throw internal_error(); } if (ctx == nullptr) { throw encrypt_ops_error(); } if (EVP_EncryptInit_ex(ctx, EVP_aes_256_ctr(), nullptr, nullptr, nullptr) != 1) { throw encrypt_ops_error(); } if (EVP_EncryptInit_ex(ctx, nullptr, nullptr, textCipherKey.getPtr()->data(), iv) != 1) { throw encrypt_ops_error(); } } template void EncryptBlobCipherAes265Ctr::setCipherAlgoHeaderWithAuthV1(const uint8_t* ciphertext, const int ciphertextLen, const BlobCipherEncryptHeaderFlagsV1& flags, BlobCipherEncryptHeaderRef* headerRef) { ASSERT(headerCipherKeyOpt.present() && headerCipherKeyOpt.get().isValid()); // Construct algorithm specific details except 'authToken', serialize the details into 'headerRef' to allow // authToken generation AesCtrWithAuthV1 algoHeader( BlobCipherDetails(textCipherKey->getDomainId(), textCipherKey->getBaseCipherId(), textCipherKey->getSalt()), textCipherKey->getBaseCipherKCV(), BlobCipherDetails(headerCipherKeyOpt.get()->getDomainId(), headerCipherKeyOpt.get()->getBaseCipherId(), headerCipherKeyOpt.get()->getSalt()), headerCipherKeyOpt.get()->getBaseCipherKCV(), iv, AES_256_IV_LENGTH); headerRef->algoHeader = AesCtrWithAuth(algoHeader); // compute the authentication token Standalone serialized = BlobCipherEncryptHeaderRef::toStringRef(*headerRef); uint8_t computed[Params::authTokenSize]{ 0, }; computeAuthToken({ { ciphertext, ciphertextLen }, { serialized.begin(), serialized.size() } }, headerCipherKeyOpt.get()->rawCipher(), AES_256_KEY_LENGTH, &computed[0], (EncryptAuthTokenAlgo)flags.authTokenAlgo, AUTH_TOKEN_MAX_SIZE); memcpy(&algoHeader.authToken[0], &computed[0], Params::authTokenSize); // Populate headerRef algorithm specific header details headerRef->algoHeader = algoHeader; } void EncryptBlobCipherAes265Ctr::setCipherAlgoHeaderNoAuthV1(const BlobCipherEncryptHeaderFlagsV1& flags, BlobCipherEncryptHeaderRef* headerRef) { ASSERT_EQ(flags.authTokenMode, EncryptAuthTokenMode::ENCRYPT_HEADER_AUTH_TOKEN_MODE_NONE); AesCtrNoAuthV1 aesCtrNoAuth( BlobCipherDetails(textCipherKey->getDomainId(), textCipherKey->getBaseCipherId(), textCipherKey->getSalt()), textCipherKey->getBaseCipherKCV(), iv, AES_256_IV_LENGTH); headerRef->algoHeader = AesCtrNoAuth(aesCtrNoAuth); } void EncryptBlobCipherAes265Ctr::setCipherAlgoHeaderV1(const uint8_t* ciphertext, const int ciphertextLen, const BlobCipherEncryptHeaderFlagsV1& flags, BlobCipherEncryptHeaderRef* headerRef) { ASSERT_EQ(1, getEncryptCurrentAlgoHeaderVersion((EncryptAuthTokenMode)flags.authTokenMode, (EncryptAuthTokenAlgo)flags.authTokenAlgo)); if (flags.authTokenMode == EncryptAuthTokenMode::ENCRYPT_HEADER_AUTH_TOKEN_MODE_NONE) { setCipherAlgoHeaderNoAuthV1(flags, headerRef); } else if (flags.authTokenAlgo == ENCRYPT_HEADER_AUTH_TOKEN_ALGO_AES_CMAC) { setCipherAlgoHeaderWithAuthV1(ciphertext, ciphertextLen, flags, headerRef); } else { ASSERT_EQ(flags.authTokenAlgo, ENCRYPT_HEADER_AUTH_TOKEN_ALGO_HMAC_SHA); setCipherAlgoHeaderWithAuthV1(ciphertext, ciphertextLen, flags, headerRef); } } void EncryptBlobCipherAes265Ctr::updateEncryptHeaderFlagsV1(BlobCipherEncryptHeaderRef* headerRef, BlobCipherEncryptHeaderFlagsV1* flags) { // Populate encryption header flags details flags->encryptMode = ENCRYPT_CIPHER_MODE_AES_256_CTR; flags->authTokenMode = authTokenMode; flags->authTokenAlgo = authTokenAlgo; headerRef->flags = *flags; } void EncryptBlobCipherAes265Ctr::updateEncryptHeader(const uint8_t* ciphertext, const int ciphertextLen, BlobCipherEncryptHeaderRef* headerRef) { ASSERT_LE(CLIENT_KNOBS->ENCRYPT_HEADER_FLAGS_VERSION, std::numeric_limits::max()); ASSERT_EQ(1, CLIENT_KNOBS->ENCRYPT_HEADER_FLAGS_VERSION); // update header flags BlobCipherEncryptHeaderFlagsV1 flags; updateEncryptHeaderFlagsV1(headerRef, &flags); // update cipher algo header int algoHeaderVersion = getEncryptCurrentAlgoHeaderVersion(authTokenMode, authTokenAlgo); ASSERT_EQ(algoHeaderVersion, 1); setCipherAlgoHeaderV1(ciphertext, ciphertextLen, flags, headerRef); } void EncryptBlobCipherAes265Ctr::updateEncryptHeader(const uint8_t* ciphertext, const int ciphertextLen, BlobCipherEncryptHeader* header) { // Populate encryption header flags details header->flags.size = sizeof(BlobCipherEncryptHeader); header->flags.headerVersion = EncryptBlobCipherAes265Ctr::ENCRYPT_HEADER_VERSION; header->flags.encryptMode = ENCRYPT_CIPHER_MODE_AES_256_CTR; header->flags.authTokenMode = authTokenMode; header->flags.authTokenAlgo = authTokenAlgo; // Ensure encryption header authToken details sanity ASSERT(isEncryptHeaderAuthTokenDetailsValid(authTokenMode, authTokenAlgo)); // Populate cipherText encryption-key details header->cipherTextDetails = textCipherKey->details(); header->textKCV = textCipherKey->getBaseCipherKCV(); // Populate header encryption-key details if (authTokenMode != ENCRYPT_HEADER_AUTH_TOKEN_MODE_NONE) { header->cipherHeaderDetails = headerCipherKeyOpt.get()->details(); header->headerKCV = headerCipherKeyOpt.get()->getBaseCipherKCV(); } else { header->cipherHeaderDetails = BlobCipherDetails(); header->headerKCV = 0; ASSERT_EQ(INVALID_ENCRYPT_DOMAIN_ID, header->cipherHeaderDetails.encryptDomainId); ASSERT_EQ(INVALID_ENCRYPT_CIPHER_KEY_ID, header->cipherHeaderDetails.baseCipherId); ASSERT_EQ(INVALID_ENCRYPT_RANDOM_SALT, header->cipherHeaderDetails.salt); } memcpy(&header->iv[0], &iv[0], AES_256_IV_LENGTH); if (authTokenMode == EncryptAuthTokenMode::ENCRYPT_HEADER_AUTH_TOKEN_MODE_NONE) { // No header 'authToken' generation needed. } else { // Populate header authToken details ASSERT_EQ(header->flags.authTokenMode, EncryptAuthTokenMode::ENCRYPT_HEADER_AUTH_TOKEN_MODE_SINGLE); computeAuthToken({ { ciphertext, ciphertextLen }, { reinterpret_cast(header), sizeof(BlobCipherEncryptHeader) } }, headerCipherKeyOpt.get()->rawCipher(), AES_256_KEY_LENGTH, &header->singleAuthToken.authToken[0], (EncryptAuthTokenAlgo)header->flags.authTokenAlgo, AUTH_TOKEN_MAX_SIZE); } } StringRef EncryptBlobCipherAes265Ctr::encrypt(const uint8_t* plaintext, const int plaintextLen, BlobCipherEncryptHeaderRef* headerRef, Arena& arena, double* encryptTime) { double startTime = 0.0; if (CLIENT_KNOBS->ENABLE_ENCRYPTION_CPU_TIME_LOGGING && encryptTime) { startTime = timer_monotonic(); } StringRef encryptBuf = makeString(plaintextLen, arena); uint8_t* ciphertext = mutateString(encryptBuf); if (!g_network->isSimulated() || !ENABLE_MUTATION_TRACKING_WITH_BLOB_CIPHER) { int bytes{ 0 }; if (EVP_EncryptUpdate(ctx, ciphertext, &bytes, plaintext, plaintextLen) != 1) { TraceEvent(SevWarn, "BlobCipherEncryptUpdateFailed") .detail("BaseCipherId", textCipherKey->getBaseCipherId()) .detail("EncryptDomainId", textCipherKey->getDomainId()); throw encrypt_ops_error(); } // Padding is not needed for AES CTR mode, so EncryptUpdate() should encrypt all the data at once. if (bytes != plaintextLen) { TraceEvent(SevWarn, "BlobCipherEncryptUnexpectedCipherLen") .detail("PlaintextLen", plaintextLen) .detail("EncryptedBufLen", bytes); throw encrypt_ops_error(); } // EVP_CIPHER_CTX_reset(ctx) is called after EncryptUpdate() to make sure the same encryptor // `EncryptBlobCipherAes265Ctr` could be reused to encrypt multiple text. Otherwise, // ctx = EVP_CIPHER_CTX_new() is required before calling encrypt(). } else { memcpy(ciphertext, plaintext, plaintextLen); } // Ensure encryption header authToken details sanity ASSERT(isEncryptHeaderAuthTokenDetailsValid(authTokenMode, authTokenAlgo)); updateEncryptHeader(ciphertext, plaintextLen, headerRef); if (CLIENT_KNOBS->ENABLE_ENCRYPTION_CPU_TIME_LOGGING && encryptTime) { *encryptTime = timer_monotonic() - startTime; } CODE_PROBE(authTokenMode == EncryptAuthTokenMode::ENCRYPT_HEADER_AUTH_TOKEN_MODE_NONE, "ConfigurableEncryption: Encryption with Auth token generation disabled"); CODE_PROBE(authTokenAlgo == EncryptAuthTokenAlgo::ENCRYPT_HEADER_AUTH_TOKEN_ALGO_HMAC_SHA, "ConfigurableEncryption: Encryption with HMAC_SHA Auth token generation"); CODE_PROBE(authTokenAlgo == EncryptAuthTokenAlgo::ENCRYPT_HEADER_AUTH_TOKEN_ALGO_AES_CMAC, "ConfigurableEncryption: Encryption with AES_CMAC Auth token generation"); return encryptBuf.substr(0, plaintextLen); } void EncryptBlobCipherAes265Ctr::encryptInplace(uint8_t* plaintext, const int plaintextLen, BlobCipherEncryptHeaderRef* headerRef, double* encryptTime) { double startTime = 0.0; if (CLIENT_KNOBS->ENABLE_ENCRYPTION_CPU_TIME_LOGGING && encryptTime) { startTime = timer_monotonic(); } if (!g_network->isSimulated() || !ENABLE_MUTATION_TRACKING_WITH_BLOB_CIPHER) { int bytes{ 0 }; if (EVP_EncryptUpdate(ctx, plaintext, &bytes, plaintext, plaintextLen) != 1) { TraceEvent(SevWarn, "BlobCipherInplaceEncryptUpdateFailed") .detail("BaseCipherId", textCipherKey->getBaseCipherId()) .detail("EncryptDomainId", textCipherKey->getDomainId()); throw encrypt_ops_error(); } // Padding should be 0 for AES CTR mode, so encryptUpdate() should encrypt all the data if (bytes != plaintextLen) { TraceEvent(SevWarn, "BlobCipherInplaceEncryptUnexpectedCipherLen") .detail("PlaintextLen", plaintextLen) .detail("EncryptedBufLen", bytes); throw encrypt_ops_error(); } } // Ensure encryption header authToken details sanity ASSERT(isEncryptHeaderAuthTokenDetailsValid(authTokenMode, authTokenAlgo)); updateEncryptHeader(plaintext, plaintextLen, headerRef); if (CLIENT_KNOBS->ENABLE_ENCRYPTION_CPU_TIME_LOGGING && encryptTime) { *encryptTime = timer_monotonic() - startTime; } CODE_PROBE(authTokenMode == EncryptAuthTokenMode::ENCRYPT_HEADER_AUTH_TOKEN_MODE_NONE, "encryptInplace: ConfigurableEncryption: Encryption with Auth token generation disabled"); CODE_PROBE(authTokenAlgo == EncryptAuthTokenAlgo::ENCRYPT_HEADER_AUTH_TOKEN_ALGO_HMAC_SHA, "encryptInplace: ConfigurableEncryption: Encryption with HMAC_SHA Auth token generation"); CODE_PROBE(authTokenAlgo == EncryptAuthTokenAlgo::ENCRYPT_HEADER_AUTH_TOKEN_ALGO_AES_CMAC, "encryptInplace: ConfigurableEncryption: Encryption with AES_CMAC Auth token generation"); } Reference EncryptBlobCipherAes265Ctr::encrypt(const uint8_t* plaintext, const int plaintextLen, BlobCipherEncryptHeader* header, Arena& arena, double* encryptTime) { double startTime = 0.0; if (CLIENT_KNOBS->ENABLE_ENCRYPTION_CPU_TIME_LOGGING && encryptTime) { startTime = timer_monotonic(); } memset(reinterpret_cast(header), 0, sizeof(BlobCipherEncryptHeader)); // Alloc buffer computation accounts for 'header authentication' generation scheme. If single-auth-token needs // to be generated, allocate buffer sufficient to append header to the cipherText optimizing memcpy cost. Reference encryptBuf = makeReference(plaintextLen, arena); uint8_t* ciphertext = encryptBuf->begin(); if (!g_network->isSimulated() || !ENABLE_MUTATION_TRACKING_WITH_BLOB_CIPHER) { int bytes{ 0 }; if (EVP_EncryptUpdate(ctx, ciphertext, &bytes, plaintext, plaintextLen) != 1) { TraceEvent(SevWarn, "BlobCipherEncryptUpdateFailed") .detail("BaseCipherId", textCipherKey->getBaseCipherId()) .detail("EncryptDomainId", textCipherKey->getDomainId()); throw encrypt_ops_error(); } if (bytes != plaintextLen) { TraceEvent(SevWarn, "BlobCipherEncryptUnexpectedCipherLen") .detail("PlaintextLen", plaintextLen) .detail("EncryptedBufLen", bytes); throw encrypt_ops_error(); } } else { memcpy(ciphertext, plaintext, plaintextLen); } updateEncryptHeader(ciphertext, plaintextLen, header); encryptBuf->setLogicalSize(plaintextLen); if (CLIENT_KNOBS->ENABLE_ENCRYPTION_CPU_TIME_LOGGING && encryptTime) { *encryptTime = timer_monotonic() - startTime; } CODE_PROBE(true, "BlobCipher data encryption", probe::decoration::rare); CODE_PROBE(header->flags.authTokenAlgo == EncryptAuthTokenMode::ENCRYPT_HEADER_AUTH_TOKEN_MODE_NONE, "Encryption authentication disabled"); CODE_PROBE(header->flags.authTokenAlgo == EncryptAuthTokenAlgo::ENCRYPT_HEADER_AUTH_TOKEN_ALGO_HMAC_SHA, "HMAC_SHA Auth token generation"); CODE_PROBE(header->flags.authTokenAlgo == EncryptAuthTokenAlgo::ENCRYPT_HEADER_AUTH_TOKEN_ALGO_AES_CMAC, "AES_CMAC Auth token generation"); return encryptBuf; } void EncryptBlobCipherAes265Ctr::encryptInplace(uint8_t* plaintext, const int plaintextLen, BlobCipherEncryptHeader* header, double* encryptTime) { double startTime = 0.0; if (CLIENT_KNOBS->ENABLE_ENCRYPTION_CPU_TIME_LOGGING && encryptTime) { startTime = timer_monotonic(); } memset(reinterpret_cast(header), 0, sizeof(BlobCipherEncryptHeader)); if (!g_network->isSimulated() || !ENABLE_MUTATION_TRACKING_WITH_BLOB_CIPHER) { int bytes{ 0 }; if (EVP_EncryptUpdate(ctx, plaintext, &bytes, plaintext, plaintextLen) != 1) { TraceEvent(SevWarn, "BlobCipherInplaceEncryptUpdateFailed") .detail("BaseCipherId", textCipherKey->getBaseCipherId()) .detail("EncryptDomainId", textCipherKey->getDomainId()); throw encrypt_ops_error(); } // Padding should be 0 for AES CTR mode, so encryptUpdate() should encrypt all the data if (bytes != plaintextLen) { TraceEvent(SevWarn, "BlobCipherInplaceEncryptUnexpectedCipherLen") .detail("PlaintextLen", plaintextLen) .detail("EncryptedBufLen", bytes); throw encrypt_ops_error(); } } updateEncryptHeader(plaintext, plaintextLen, header); if (CLIENT_KNOBS->ENABLE_ENCRYPTION_CPU_TIME_LOGGING && encryptTime) { *encryptTime = timer_monotonic() - startTime; } CODE_PROBE(true, "encryptInplace: BlobCipher data encryption", probe::decoration::rare); CODE_PROBE(header->flags.authTokenAlgo == EncryptAuthTokenMode::ENCRYPT_HEADER_AUTH_TOKEN_MODE_NONE, "encryptInplace: Encryption authentication disabled"); CODE_PROBE(header->flags.authTokenAlgo == EncryptAuthTokenAlgo::ENCRYPT_HEADER_AUTH_TOKEN_ALGO_HMAC_SHA, "encryptInplace: HMAC_SHA Auth token generation"); CODE_PROBE(header->flags.authTokenAlgo == EncryptAuthTokenAlgo::ENCRYPT_HEADER_AUTH_TOKEN_ALGO_AES_CMAC, "encryptInplace: AES_CMAC Auth token generation"); } EncryptBlobCipherAes265Ctr::~EncryptBlobCipherAes265Ctr() { if (ctx != nullptr) { EVP_CIPHER_CTX_free(ctx); } } // DecryptBlobCipherAes256Ctr class methods DecryptBlobCipherAes256Ctr::DecryptBlobCipherAes256Ctr(Reference tCipherKey, Optional> hCipherKeyOpt, const uint8_t* iv, BlobCipherMetrics::UsageType usageType) : ctx(EVP_CIPHER_CTX_new()), textCipherKey(tCipherKey), headerCipherKeyOpt(hCipherKeyOpt), authTokensValidationDone(false) { if (ctx == nullptr) { throw encrypt_ops_error(); } if (!EVP_DecryptInit_ex(ctx, EVP_aes_256_ctr(), nullptr, nullptr, nullptr)) { throw encrypt_ops_error(); } if (!EVP_DecryptInit_ex(ctx, nullptr, nullptr, tCipherKey.getPtr()->data(), iv)) { throw encrypt_ops_error(); } } template void DecryptBlobCipherAes256Ctr::validateAuthTokenV1(const uint8_t* ciphertext, const int ciphertextLen, const BlobCipherEncryptHeaderFlagsV1& flags, const BlobCipherEncryptHeaderRef& headerRef) { ASSERT_EQ(flags.encryptMode, ENCRYPT_HEADER_AUTH_TOKEN_MODE_SINGLE); ASSERT_LE(Params::authTokenSize, AUTH_TOKEN_MAX_SIZE); ASSERT(headerCipherKeyOpt.present() && headerCipherKeyOpt.get().isValid()); Arena tmpArena; uint8_t persisted[Params::authTokenSize]; uint8_t computed[Params::authTokenSize]{ 0, }; // prepare the payload {cipherText + encryptionHeader} // ensure the 'authToken' is reset before computing the 'authentication token' BlobCipherEncryptHeaderRef headerRefCopy = BlobCipherEncryptHeaderRef(headerRef); AesCtrWithAuth algoHeaderCopy = std::get>(headerRefCopy.algoHeader); // preserve the 'persisted' token for future validation before resetting the field memcpy(&persisted[0], &algoHeaderCopy.v1.authToken[0], Params::authTokenSize); memset(&algoHeaderCopy.v1.authToken[0], 0, Params::authTokenSize); headerRefCopy.algoHeader = algoHeaderCopy; Standalone serializedHeader = BlobCipherEncryptHeaderRef::toStringRef(headerRefCopy); computeAuthToken({ { ciphertext, ciphertextLen }, { serializedHeader.begin(), serializedHeader.size() } }, headerCipherKeyOpt.get()->rawCipher(), AES_256_KEY_LENGTH, &computed[0], (EncryptAuthTokenAlgo)flags.authTokenAlgo, AUTH_TOKEN_MAX_SIZE); if (memcmp(&persisted[0], &computed[0], Params::authTokenSize) != 0) { TraceEvent(SevWarn, "BlobCipherVerifyEncryptBlobHeaderAuthTokenMismatch") .detail("HeaderFlagsVersion", headerRef.flagsVersion()) .detail("HeaderMode", flags.encryptMode) .detail("SingleAuthToken", StringRef(tmpArena, persisted, Params::authTokenSize)) .detail("ComputedSingleAuthToken", StringRef(tmpArena, computed, Params::authTokenSize)); CODE_PROBE(flags.authTokenAlgo == ENCRYPT_HEADER_AUTH_TOKEN_ALGO_HMAC_SHA, "ConfigurableEncryption: AuthToken value mismatch - HMAC_SHA auth token generation"); CODE_PROBE(flags.authTokenAlgo == ENCRYPT_HEADER_AUTH_TOKEN_ALGO_AES_CMAC, "ConfigurableEncryption: AuthToken value mismatch - AES_CMAC auth token generation"); throw encrypt_header_authtoken_mismatch(); } } void DecryptBlobCipherAes256Ctr::validateHeaderSingleAuthTokenV1(const uint8_t* ciphertext, const int ciphertextLen, const BlobCipherEncryptHeaderFlagsV1& flags, const BlobCipherEncryptHeaderRef& headerRef) { // prepare the payload {cipherText + encryptionHeader} // ensure the 'authToken' is reset before computing the 'authentication token' if (flags.authTokenAlgo == EncryptAuthTokenAlgo::ENCRYPT_HEADER_AUTH_TOKEN_ALGO_AES_CMAC) { validateAuthTokenV1(ciphertext, ciphertextLen, flags, headerRef); } else { ASSERT_EQ(flags.authTokenAlgo, EncryptAuthTokenAlgo::ENCRYPT_HEADER_AUTH_TOKEN_ALGO_HMAC_SHA); validateAuthTokenV1(ciphertext, ciphertextLen, flags, headerRef); } } void DecryptBlobCipherAes256Ctr::validateAuthTokensV1(const uint8_t* ciphertext, const int ciphertextLen, const BlobCipherEncryptHeaderFlagsV1& flags, const BlobCipherEncryptHeaderRef& headerRef) { if (flags.authTokenMode == EncryptAuthTokenMode::ENCRYPT_HEADER_AUTH_TOKEN_MODE_NONE) { // No embedded 'authToken'; do nothing return; } ASSERT_EQ(flags.authTokenMode, EncryptAuthTokenMode::ENCRYPT_HEADER_AUTH_TOKEN_MODE_SINGLE); validateHeaderSingleAuthTokenV1(ciphertext, ciphertextLen, flags, headerRef); authTokensValidationDone = true; } void DecryptBlobCipherAes256Ctr::validateEncryptHeaderFlagsV1(const uint32_t headerVersion, const BlobCipherEncryptHeaderFlagsV1& flags) { // validate header flag sanity if (flags.encryptMode != EncryptCipherMode::ENCRYPT_CIPHER_MODE_AES_256_CTR || !isEncryptHeaderAuthTokenModeValid((EncryptAuthTokenMode)flags.authTokenMode)) { TraceEvent(SevWarn, "BlobCipherVerifyEncryptBlobHeader") .detail("HeaderVersion", headerVersion) .detail("ExpectedVersion", CLIENT_KNOBS->ENCRYPT_HEADER_FLAGS_VERSION) .detail("EncryptCipherMode", flags.encryptMode) .detail("ExpectedCipherMode", EncryptCipherMode::ENCRYPT_CIPHER_MODE_AES_256_CTR) .detail("EncryptHeaderAuthTokenMode", flags.authTokenMode); CODE_PROBE(true, "ConfigurableEncryption: Encryption header metadata mismatch", probe::decoration::rare); throw encrypt_header_metadata_mismatch(); } } void DecryptBlobCipherAes256Ctr::vaidateEncryptHeaderCipherKCVs(const BlobCipherEncryptHeaderRef& headerRef, const BlobCipherEncryptHeaderFlagsV1& flags) { const EncryptHeaderCipherKCVs kcvs = headerRef.getKCVs(); Sha256KCV::checkEqual(textCipherKey, kcvs.textKCV); if (flags.authTokenMode != ENCRYPT_HEADER_AUTH_TOKEN_MODE_NONE) { if (!kcvs.headerKCV.present()) { TraceEvent(SevWarnAlways, "MissingHeaderKCV"); throw encrypt_key_check_value_mismatch(); } Sha256KCV::checkEqual(headerCipherKeyOpt.get(), kcvs.headerKCV.get()); } } void DecryptBlobCipherAes256Ctr::validateEncryptHeader(const uint8_t* ciphertext, const int ciphertextLen, const BlobCipherEncryptHeaderRef& headerRef, EncryptAuthTokenMode* authTokenMode, EncryptAuthTokenAlgo* authTokenAlgo) { // FlagsVersion is computed based on std::variant available index ASSERT_EQ(headerRef.flagsVersion(), 1); BlobCipherEncryptHeaderFlagsV1 flags = std::get(headerRef.flags); validateEncryptHeaderFlagsV1(headerRef.flagsVersion(), flags); vaidateEncryptHeaderCipherKCVs(headerRef, flags); validateAuthTokensV1(ciphertext, ciphertextLen, flags, headerRef); *authTokenMode = (EncryptAuthTokenMode)flags.authTokenMode; *authTokenAlgo = (EncryptAuthTokenAlgo)flags.authTokenAlgo; } StringRef DecryptBlobCipherAes256Ctr::decrypt(const uint8_t* ciphertext, const int ciphertextLen, const BlobCipherEncryptHeaderRef& headerRef, Arena& arena, double* decryptTime) { double startTime = 0.0; if (CLIENT_KNOBS->ENABLE_ENCRYPTION_CPU_TIME_LOGGING && decryptTime) { startTime = timer_monotonic(); } EncryptAuthTokenMode authTokenMode; EncryptAuthTokenAlgo authTokenAlgo; validateEncryptHeader(ciphertext, ciphertextLen, headerRef, &authTokenMode, &authTokenAlgo); StringRef decrypted = makeString(ciphertextLen, arena); uint8_t* plaintext = mutateString(decrypted); if (!g_network->isSimulated() || !ENABLE_MUTATION_TRACKING_WITH_BLOB_CIPHER) { int bytesDecrypted{ 0 }; if (!EVP_DecryptUpdate(ctx, plaintext, &bytesDecrypted, ciphertext, ciphertextLen)) { TraceEvent(SevWarn, "BlobCipherDecryptUpdateFailed") .detail("BaseCipherId", textCipherKey->getBaseCipherId()) .detail("EncryptDomainId", textCipherKey->getDomainId()); throw encrypt_ops_error(); } if (bytesDecrypted != ciphertextLen) { TraceEvent(SevWarn, "BlobCipherDecryptUnexpectedPlaintextLen") .detail("CiphertextLen", ciphertextLen) .detail("DecryptedBufLen", bytesDecrypted); throw encrypt_ops_error(); } } else { memcpy(plaintext, ciphertext, ciphertextLen); } if (CLIENT_KNOBS->ENABLE_ENCRYPTION_CPU_TIME_LOGGING && decryptTime) { *decryptTime = timer_monotonic() - startTime; } CODE_PROBE(authTokenMode == EncryptAuthTokenMode::ENCRYPT_HEADER_AUTH_TOKEN_MODE_NONE, "ConfigurableEncryption: Decryption with Auth token generation disabled"); CODE_PROBE(authTokenAlgo == EncryptAuthTokenAlgo::ENCRYPT_HEADER_AUTH_TOKEN_ALGO_HMAC_SHA, "ConfigurableEncryption: Decryption with HMAC_SHA Auth token generation"); CODE_PROBE(authTokenAlgo == EncryptAuthTokenAlgo::ENCRYPT_HEADER_AUTH_TOKEN_ALGO_AES_CMAC, "ConfigurableEncryption: Decryption with AES_CMAC Auth token generation"); return decrypted.substr(0, ciphertextLen); } void DecryptBlobCipherAes256Ctr::verifyHeaderSingleAuthToken(const uint8_t* ciphertext, const int ciphertextLen, const BlobCipherEncryptHeader& header) { ASSERT(headerCipherKeyOpt.present() && headerCipherKeyOpt.get().isValid()); // prepare the payload {cipherText + encryptionHeader} // ensure the 'authToken' is reset before computing the 'authentication token' BlobCipherEncryptHeader headerCopy; memcpy(reinterpret_cast(&headerCopy), reinterpret_cast(&header), sizeof(BlobCipherEncryptHeader)); memset(reinterpret_cast(&headerCopy.singleAuthToken), 0, AUTH_TOKEN_MAX_SIZE); uint8_t computed[AUTH_TOKEN_MAX_SIZE]{ 0, }; computeAuthToken({ { ciphertext, ciphertextLen }, { reinterpret_cast(&headerCopy), sizeof(BlobCipherEncryptHeader) } }, headerCipherKeyOpt.get()->rawCipher(), AES_256_KEY_LENGTH, &computed[0], (EncryptAuthTokenAlgo)header.flags.authTokenAlgo, AUTH_TOKEN_MAX_SIZE); int authTokenSize = getEncryptHeaderAuthTokenSize(header.flags.authTokenAlgo); ASSERT_LE(authTokenSize, AUTH_TOKEN_MAX_SIZE); if (memcmp(&header.singleAuthToken.authToken[0], &computed[0], authTokenSize) != 0) { TraceEvent(SevWarn, "BlobCipherVerifyEncryptBlobHeaderAuthTokenMismatch") .detail("HeaderVersion", header.flags.headerVersion) .detail("HeaderMode", header.flags.encryptMode) .detail("SingleAuthToken", StringRef(&header.singleAuthToken.authToken[0], AUTH_TOKEN_MAX_SIZE).toString()) .detail("ComputedSingleAuthToken", StringRef(computed, AUTH_TOKEN_MAX_SIZE)); CODE_PROBE(header.flags.authTokenAlgo == ENCRYPT_HEADER_AUTH_TOKEN_ALGO_HMAC_SHA, "AuthToken size mismatch - HMAC_SHA auth token generation"); CODE_PROBE(header.flags.authTokenAlgo == ENCRYPT_HEADER_AUTH_TOKEN_ALGO_AES_CMAC, "AuthToken size mismatch - AES_CMAC auth token generation"); throw encrypt_header_authtoken_mismatch(); } } void DecryptBlobCipherAes256Ctr::verifyAuthTokens(const uint8_t* ciphertext, const int ciphertextLen, const BlobCipherEncryptHeader& header) { ASSERT_EQ(header.flags.authTokenMode, EncryptAuthTokenMode::ENCRYPT_HEADER_AUTH_TOKEN_MODE_SINGLE); verifyHeaderSingleAuthToken(ciphertext, ciphertextLen, header); authTokensValidationDone = true; } void DecryptBlobCipherAes256Ctr::verifyEncryptHeaderMetadata(const BlobCipherEncryptHeader& header) { // validate header flag sanity if (header.flags.headerVersion != EncryptBlobCipherAes265Ctr::ENCRYPT_HEADER_VERSION || header.flags.encryptMode != EncryptCipherMode::ENCRYPT_CIPHER_MODE_AES_256_CTR || !isEncryptHeaderAuthTokenModeValid((EncryptAuthTokenMode)header.flags.authTokenMode)) { TraceEvent(SevWarn, "BlobCipherVerifyEncryptBlobHeader") .detail("HeaderVersion", header.flags.headerVersion) .detail("ExpectedVersion", EncryptBlobCipherAes265Ctr::ENCRYPT_HEADER_VERSION) .detail("EncryptCipherMode", header.flags.encryptMode) .detail("ExpectedCipherMode", EncryptCipherMode::ENCRYPT_CIPHER_MODE_AES_256_CTR) .detail("EncryptHeaderAuthTokenMode", header.flags.authTokenMode); CODE_PROBE(true, "Encryption header metadata mismatch", probe::decoration::rare); throw encrypt_header_metadata_mismatch(); } Sha256KCV::checkEqual(textCipherKey, header.textKCV); if (header.flags.authTokenMode != ENCRYPT_HEADER_AUTH_TOKEN_MODE_NONE) { Sha256KCV::checkEqual(headerCipherKeyOpt.get(), header.headerKCV); } } Reference DecryptBlobCipherAes256Ctr::decrypt(const uint8_t* ciphertext, const int ciphertextLen, const BlobCipherEncryptHeader& header, Arena& arena, double* decryptTime) { double startTime = 0.0; if (CLIENT_KNOBS->ENABLE_ENCRYPTION_CPU_TIME_LOGGING && decryptTime) { startTime = timer_monotonic(); } verifyEncryptHeaderMetadata(header); if (header.flags.authTokenMode != EncryptAuthTokenMode::ENCRYPT_HEADER_AUTH_TOKEN_MODE_NONE && (!headerCipherKeyOpt.present() || !headerCipherKeyOpt.get().isValid())) { TraceEvent(SevWarn, "BlobCipherDecryptInvalidHeaderCipherKey") .detail("AuthTokenMode", header.flags.authTokenMode); throw encrypt_ops_error(); } Reference decrypted = makeReference(ciphertextLen, arena); if (header.flags.authTokenMode != EncryptAuthTokenMode::ENCRYPT_HEADER_AUTH_TOKEN_MODE_NONE) { verifyAuthTokens(ciphertext, ciphertextLen, header); ASSERT(authTokensValidationDone); } uint8_t* plaintext = decrypted->begin(); if (!g_network->isSimulated() || !ENABLE_MUTATION_TRACKING_WITH_BLOB_CIPHER) { int bytesDecrypted{ 0 }; if (!EVP_DecryptUpdate(ctx, plaintext, &bytesDecrypted, ciphertext, ciphertextLen)) { TraceEvent(SevWarn, "BlobCipherDecryptUpdateFailed") .detail("BaseCipherId", header.cipherTextDetails.baseCipherId) .detail("EncryptDomainId", header.cipherTextDetails.encryptDomainId); throw encrypt_ops_error(); } if (bytesDecrypted != ciphertextLen) { TraceEvent(SevWarn, "BlobCipherDecryptUnexpectedPlaintextLen") .detail("CiphertextLen", ciphertextLen) .detail("DecryptedBufLen", bytesDecrypted); throw encrypt_ops_error(); } } else { memcpy(plaintext, ciphertext, ciphertextLen); } decrypted->setLogicalSize(ciphertextLen); if (CLIENT_KNOBS->ENABLE_ENCRYPTION_CPU_TIME_LOGGING && decryptTime) { *decryptTime = timer_monotonic() - startTime; } CODE_PROBE(true, "BlobCipher data decryption"); CODE_PROBE(header.flags.authTokenAlgo == EncryptAuthTokenMode::ENCRYPT_HEADER_AUTH_TOKEN_MODE_NONE, "Decryption authentication disabled"); CODE_PROBE(header.flags.authTokenAlgo == EncryptAuthTokenAlgo::ENCRYPT_HEADER_AUTH_TOKEN_ALGO_HMAC_SHA, "Decryption HMAC_SHA Auth token verification"); CODE_PROBE(header.flags.authTokenAlgo == EncryptAuthTokenAlgo::ENCRYPT_HEADER_AUTH_TOKEN_ALGO_AES_CMAC, "Decryption AES_CMAC Auth token verification"); return decrypted; } void DecryptBlobCipherAes256Ctr::decryptInplace(uint8_t* ciphertext, const int ciphertextLen, const BlobCipherEncryptHeader& header, double* decryptTime) { double startTime = 0.0; if (CLIENT_KNOBS->ENABLE_ENCRYPTION_CPU_TIME_LOGGING && decryptTime) { startTime = timer_monotonic(); } verifyEncryptHeaderMetadata(header); if (header.flags.authTokenMode != EncryptAuthTokenMode::ENCRYPT_HEADER_AUTH_TOKEN_MODE_NONE && (!headerCipherKeyOpt.present() || !headerCipherKeyOpt.get().isValid())) { TraceEvent(SevWarn, "BlobCipherDecryptInvalidHeaderCipherKey") .detail("AuthTokenMode", header.flags.authTokenMode); throw encrypt_ops_error(); } if (header.flags.authTokenMode != EncryptAuthTokenMode::ENCRYPT_HEADER_AUTH_TOKEN_MODE_NONE) { verifyAuthTokens(ciphertext, ciphertextLen, header); ASSERT(authTokensValidationDone); } if (!g_network->isSimulated() || !ENABLE_MUTATION_TRACKING_WITH_BLOB_CIPHER) { int bytesDecrypted{ 0 }; if (!EVP_DecryptUpdate(ctx, ciphertext, &bytesDecrypted, ciphertext, ciphertextLen)) { TraceEvent(SevWarn, "BlobCipherDecryptUpdateFailed") .detail("BaseCipherId", header.cipherTextDetails.baseCipherId) .detail("EncryptDomainId", header.cipherTextDetails.encryptDomainId); throw encrypt_ops_error(); } // Padding should be 0 for AES CTR mode, so DecryptUpdate() should decrypt all the data if (bytesDecrypted != ciphertextLen) { TraceEvent(SevWarn, "BlobCipherDecryptUnexpectedPlaintextLen") .detail("CiphertextLen", ciphertextLen) .detail("DecryptedBufLen", bytesDecrypted); throw encrypt_ops_error(); } } if (CLIENT_KNOBS->ENABLE_ENCRYPTION_CPU_TIME_LOGGING && decryptTime) { *decryptTime = timer_monotonic() - startTime; } CODE_PROBE(true, "decryptInplace: BlobCipher data decryption"); CODE_PROBE(header.flags.authTokenAlgo == EncryptAuthTokenMode::ENCRYPT_HEADER_AUTH_TOKEN_MODE_NONE, "decryptInplace: Decryption authentication disabled"); CODE_PROBE(header.flags.authTokenAlgo == EncryptAuthTokenAlgo::ENCRYPT_HEADER_AUTH_TOKEN_ALGO_HMAC_SHA, "decryptInplace: Decryption HMAC_SHA Auth token verification"); CODE_PROBE(header.flags.authTokenAlgo == EncryptAuthTokenAlgo::ENCRYPT_HEADER_AUTH_TOKEN_ALGO_AES_CMAC, "decryptInplace: Decryption AES_CMAC Auth token verification"); } void DecryptBlobCipherAes256Ctr::decryptInplace(uint8_t* ciphertext, const int ciphertextLen, const BlobCipherEncryptHeaderRef& headerRef, double* decryptTime) { double startTime = 0.0; if (CLIENT_KNOBS->ENABLE_ENCRYPTION_CPU_TIME_LOGGING && decryptTime) { startTime = timer_monotonic(); } EncryptAuthTokenMode authTokenMode; EncryptAuthTokenAlgo authTokenAlgo; validateEncryptHeader(ciphertext, ciphertextLen, headerRef, &authTokenMode, &authTokenAlgo); if (!g_network->isSimulated() || !ENABLE_MUTATION_TRACKING_WITH_BLOB_CIPHER) { int bytesDecrypted{ 0 }; if (!EVP_DecryptUpdate(ctx, ciphertext, &bytesDecrypted, ciphertext, ciphertextLen)) { TraceEvent(SevWarn, "BlobCipherDecryptUpdateFailed") .detail("BaseCipherId", textCipherKey->getBaseCipherId()) .detail("EncryptDomainId", textCipherKey->getDomainId()); throw encrypt_ops_error(); } // Padding should be 0 for AES CTR mode, so DecryptUpdate() should decrypt all the data if (bytesDecrypted != ciphertextLen) { TraceEvent(SevWarn, "BlobCipherDecryptUnexpectedPlaintextLen") .detail("CiphertextLen", ciphertextLen) .detail("DecryptedBufLen", bytesDecrypted); throw encrypt_ops_error(); } } if (CLIENT_KNOBS->ENABLE_ENCRYPTION_CPU_TIME_LOGGING && decryptTime) { *decryptTime = timer_monotonic() - startTime; } CODE_PROBE(authTokenMode == EncryptAuthTokenMode::ENCRYPT_HEADER_AUTH_TOKEN_MODE_NONE, "decryptInplace: ConfigurableEncryption: Decryption with Auth token generation disabled"); CODE_PROBE(authTokenAlgo == EncryptAuthTokenAlgo::ENCRYPT_HEADER_AUTH_TOKEN_ALGO_HMAC_SHA, "decryptInplace: ConfigurableEncryption: Decryption with HMAC_SHA Auth token generation"); CODE_PROBE(authTokenAlgo == EncryptAuthTokenAlgo::ENCRYPT_HEADER_AUTH_TOKEN_ALGO_AES_CMAC, "decryptInplace: ConfigurableEncryption: Decryption with AES_CMAC Auth token generation"); } DecryptBlobCipherAes256Ctr::~DecryptBlobCipherAes256Ctr() { if (ctx != nullptr) { EVP_CIPHER_CTX_free(ctx); } } // HmacSha256DigestGen class methods HmacSha256DigestGen::HmacSha256DigestGen(const unsigned char* key, size_t len) : ctx(HMAC_CTX_new()) { if (!HMAC_Init_ex(ctx, key, len, EVP_sha256(), nullptr)) { throw encrypt_ops_error(); } } HmacSha256DigestGen::~HmacSha256DigestGen() { if (ctx != nullptr) { HMAC_CTX_free(ctx); } } unsigned int HmacSha256DigestGen::digest(const std::vector>& payload, unsigned char* buf, unsigned int bufLen) { ASSERT_EQ(bufLen, HMAC_size(ctx)); for (const auto& p : payload) { if (HMAC_Update(ctx, p.first, p.second) != 1) { throw encrypt_ops_error(); } } unsigned int digestLen = 0; if (HMAC_Final(ctx, buf, &digestLen) != 1) { throw encrypt_ops_error(); } CODE_PROBE(true, "HMAC_SHA Digest generation"); return digestLen; } // Aes256CtrCmacDigestGen methods Aes256CmacDigestGen::Aes256CmacDigestGen(const unsigned char* key, size_t keylen) : ctx(CMAC_CTX_new()) { ASSERT_EQ(keylen, AES_256_KEY_LENGTH); if (ctx == nullptr) { throw encrypt_ops_error(); } if (!CMAC_Init(ctx, key, keylen, EVP_aes_256_cbc(), NULL)) { throw encrypt_ops_error(); } } size_t Aes256CmacDigestGen::digest(const std::vector>& payload, uint8_t* digest, int digestlen) { ASSERT(ctx != nullptr); ASSERT_GE(digestlen, AUTH_TOKEN_AES_CMAC_SIZE); for (const auto& p : payload) { if (!CMAC_Update(ctx, p.first, p.second)) { throw encrypt_ops_error(); } } size_t ret; if (!CMAC_Final(ctx, digest, &ret)) { throw encrypt_ops_error(); } return ret; } Aes256CmacDigestGen::~Aes256CmacDigestGen() { if (ctx != nullptr) { CMAC_CTX_free(ctx); } } void computeAuthToken(const std::vector>& payload, const uint8_t* key, const int keyLen, unsigned char* digestBuf, const EncryptAuthTokenAlgo algo, unsigned int digestBufMaxSz) { ASSERT_EQ(digestBufMaxSz, AUTH_TOKEN_MAX_SIZE); ASSERT_EQ(keyLen, AES_256_KEY_LENGTH); ASSERT(isEncryptHeaderAuthTokenAlgoValid(algo)); int authTokenSz = getEncryptHeaderAuthTokenSize(algo); ASSERT_LE(authTokenSz, AUTH_TOKEN_MAX_SIZE); if (algo == EncryptAuthTokenAlgo::ENCRYPT_HEADER_AUTH_TOKEN_ALGO_HMAC_SHA) { ASSERT_EQ(authTokenSz, AUTH_TOKEN_HMAC_SHA_SIZE); HmacSha256DigestGen hmacGenerator(key, keyLen); unsigned int digestLen = hmacGenerator.digest(payload, digestBuf, authTokenSz); ASSERT_EQ(digestLen, authTokenSz); } else if (algo == EncryptAuthTokenAlgo::ENCRYPT_HEADER_AUTH_TOKEN_ALGO_AES_CMAC) { ASSERT_EQ(authTokenSz, AUTH_TOKEN_AES_CMAC_SIZE); Aes256CmacDigestGen cmacGenerator(key, keyLen); size_t digestLen = cmacGenerator.digest(payload, digestBuf, authTokenSz); ASSERT_EQ(digestLen, authTokenSz); } else { throw not_implemented(); } } EncryptAuthTokenMode getEncryptAuthTokenMode(const EncryptAuthTokenMode mode) { // Override mode if authToken isn't enabled return FLOW_KNOBS->ENCRYPT_HEADER_AUTH_TOKEN_ENABLED ? mode : EncryptAuthTokenMode::ENCRYPT_HEADER_AUTH_TOKEN_MODE_NONE; } Sha256KCV::Sha256KCV() : ctx(EVP_MD_CTX_new()) { if (ctx == nullptr) { TraceEvent(SevError, "ComputeSha256AllocFailed"); throw encrypt_ops_error(); } } Sha256KCV::~Sha256KCV() { if (ctx != nullptr) { EVP_MD_CTX_free(ctx); } } EncryptCipherKeyCheckValue Sha256KCV::computeKCV(const uint8_t* cipher, const int len) { if (!EVP_DigestInit_ex(ctx, EVP_sha256(), NULL)) { TraceEvent(SevWarnAlways, "ComputeSha256DigestInitFailed"); throw encrypt_ops_error(); } if (!EVP_DigestUpdate(ctx, cipher, len)) { TraceEvent(SevWarnAlways, "ComputeSha256DigestUpdateFailed"); throw encrypt_ops_error(); } unsigned char sha256[EVP_MAX_MD_SIZE]; unsigned int sha256Len; if (!EVP_DigestFinal_ex(ctx, sha256, &sha256Len)) { TraceEvent(SevWarnAlways, "ComputeSha256DigestFinalFailed"); throw encrypt_ops_error(); } // KeyValueCheck token allows FDB code to sanitize 'baseCipher' buffer, an external input to FDB. Given the token is // NOT meant to protect against any tampering attack, it is OK to truncate generated SHA256 KCV. ASSERT_LE(sizeof(EncryptCipherKeyCheckValue), EVP_MAX_MD_SIZE); EncryptCipherKeyCheckValue kcv; std::memcpy((uint8_t*)&kcv, sha256, sizeof(EncryptCipherKeyCheckValue)); #if BLOB_CIPHER_DEBUG TraceEvent("ComputeSha256KCV").detail("KCV", kcv); #endif CODE_PROBE(true, "Sha256 KCV generation done"); return kcv; } void Sha256KCV::checkEqual(const Reference& cipher, const EncryptCipherKeyCheckValue persisted) { ASSERT(cipher.isValid()); #if BLOB_CIPHER_DEBUG TraceEvent(SevDebug, "Sha256KCVCheckEqual") .detail("CipherKCV", cipher->getBaseCipherKCV()) .detail("Persisted", persisted); #endif if (cipher->getBaseCipherKCV() != persisted) { CODE_PROBE(true, "Sha256 Key Check Value mismatch"); TraceEvent(SevWarnAlways, "Sha256KCVMismatch") .detail("Computed", cipher->getBaseCipherKCV()) .detail("Persisted", persisted) .detail("DomainId", cipher->getDomainId()) .detail("BaseCipherId", cipher->getBaseCipherId()); throw encrypt_key_check_value_mismatch(); } CODE_PROBE(true, "Sha256 KCV validation done"); } // Only used to link unit tests void forceLinkBlobCipherTests() {} // Tests cases includes: // 1. Populate cache by inserting 'baseCipher' details for new encryptionDomainIds // 2. Random lookup for cipherKeys and content validation // 3. Inserting of 'identical' cipherKey (already cached) more than once works as desired. // 4. Inserting of 'non-identical' cipherKey (already cached) more than once works as desired. // 5. Validation encryption ops (correctness): // 5.1. Encrypt a buffer followed by decryption of the buffer, validate the contents. // 5.2. Simulate anomalies such as: EncryptionHeader corruption, authToken mismatch / encryptionMode mismatch etc. // 6. Cache cleanup // 6.1 cleanup cipherKeys by given encryptDomainId // 6.2. Cleanup all cached cipherKeys namespace { // Construct a dummy External Key Manager representation and populate with some keys class BaseCipher : public ReferenceCounted, NonCopyable { public: EncryptCipherDomainId domainId; int len; EncryptCipherBaseKeyId keyId; std::unique_ptr key; EncryptCipherKeyCheckValue kcv; int64_t refreshAt; int64_t expireAt; EncryptCipherRandomSalt generatedSalt; BaseCipher(const EncryptCipherDomainId& dId, const EncryptCipherBaseKeyId& kId, const int64_t rAt, const int64_t eAt) : domainId(dId), len(deterministicRandom()->randomInt(4, MAX_BASE_CIPHER_LEN + 1)), keyId(kId), key(std::make_unique(len)), refreshAt(rAt), expireAt(eAt) { deterministicRandom()->randomBytes(key.get(), len); kcv = Sha256KCV().computeKCV(key.get(), len); } }; Reference corruptCipherKey(const Reference& cipherKey) { std::unique_ptr corruptedBaseCipher = std::make_unique(cipherKey->getBaseCipherLen()); memcpy(corruptedBaseCipher.get(), cipherKey->rawBaseCipher(), cipherKey->getBaseCipherLen()); const int idx = deterministicRandom()->randomInt(0, cipherKey->getBaseCipherLen()); corruptedBaseCipher.get()[idx]++; const EncryptCipherKeyCheckValue baseCipherKCV = Sha256KCV().computeKCV(corruptedBaseCipher.get(), cipherKey->getBaseCipherLen()); return makeReference(cipherKey->getDomainId(), cipherKey->getBaseCipherId(), corruptedBaseCipher.get(), cipherKey->getBaseCipherLen(), baseCipherKCV, cipherKey->getRefreshAtTS(), cipherKey->getExpireAtTS()); } using BaseKeyMap = std::unordered_map>; using DomainKeyMap = std::unordered_map; } // namespace void testMaxBaseCipherLen() { TraceEvent("TestMaxBaseCipherLenStart"); try { const int baseCipherLen = deterministicRandom()->randomInt(MAX_BASE_CIPHER_LEN + 1, MAX_BASE_CIPHER_LEN + 10); uint8_t baseCipher[baseCipherLen]; deterministicRandom()->randomBytes(&baseCipher[0], baseCipherLen); const EncryptCipherKeyCheckValue baseCipherKCV = Sha256KCV().computeKCV(&baseCipher[0], baseCipherLen); Reference cipher = makeReference(1, 1, &baseCipher[0], baseCipherLen, baseCipherKCV, std::numeric_limits::max(), std::numeric_limits::max()); ASSERT(false); // error expected } catch (Error& e) { ASSERT_EQ(e.code(), error_code_encrypt_max_base_cipher_len); } TraceEvent("TestMaxBaseCipherLenDone"); } void testKeyCacheEssentials(DomainKeyMap& domainKeyMap, const int minDomainId, const int maxDomainId, const int minBaseCipherKeyId) { TraceEvent("TestCacheEssentialsStart"); Reference cipherKeyCache = BlobCipherKeyCache::getInstance(); // validate getLatestCipherKey return empty when there's no cipher key TraceEvent("BlobCipherTestLatestKeyNotExists").log(); try { cipherKeyCache->getLatestCipherKey(INVALID_ENCRYPT_DOMAIN_ID); ASSERT(false); // shouldn't get here } catch (Error& e) { ASSERT_EQ(e.code(), error_code_encrypt_invalid_id); } // insert BlobCipher keys into BlobCipherKeyCache map and validate TraceEvent("TestInsertKeys").log(); for (auto& domainItr : domainKeyMap) { for (auto& baseKeyItr : domainItr.second) { Reference baseCipher = baseKeyItr.second; cipherKeyCache->insertCipherKey(baseCipher->domainId, baseCipher->keyId, baseCipher->key.get(), baseCipher->len, baseCipher->kcv, baseCipher->refreshAt, baseCipher->expireAt); Reference fetchedKey = cipherKeyCache->getLatestCipherKey(baseCipher->domainId); baseCipher->generatedSalt = fetchedKey->getSalt(); } } // insert EncryptHeader BlobCipher key Reference headerBaseCipher = makeReference( ENCRYPT_HEADER_DOMAIN_ID, 1, std::numeric_limits::max(), std::numeric_limits::max()); cipherKeyCache->insertCipherKey(headerBaseCipher->domainId, headerBaseCipher->keyId, headerBaseCipher->key.get(), headerBaseCipher->len, headerBaseCipher->kcv, headerBaseCipher->refreshAt, headerBaseCipher->expireAt); TraceEvent("TestInsertKeysDone").log(); // validate the cipherKey lookups work as desired for (auto& domainItr : domainKeyMap) { for (auto& baseKeyItr : domainItr.second) { Reference baseCipher = baseKeyItr.second; Reference cipherKey = cipherKeyCache->getCipherKey(baseCipher->domainId, baseCipher->keyId, baseCipher->generatedSalt); ASSERT(cipherKey.isValid()); // validate common cipher properties - domainId, baseCipherId, baseCipherLen, rawBaseCipher ASSERT_EQ(cipherKey->getBaseCipherId(), baseCipher->keyId); ASSERT_EQ(cipherKey->getDomainId(), baseCipher->domainId); ASSERT_EQ(cipherKey->getBaseCipherLen(), baseCipher->len); // ensure that baseCipher matches with the cached information ASSERT_EQ(std::memcmp(cipherKey->rawBaseCipher(), baseCipher->key.get(), cipherKey->getBaseCipherLen()), 0); // validate the encryption derivation const int len = std::min(AES_256_KEY_LENGTH, cipherKey->getBaseCipherLen()); ASSERT_NE(std::memcmp(cipherKey->rawCipher(), baseCipher->key.get(), len), 0); } } TraceEvent("TestLooksupDone").log(); // Ensure attempting to insert existing cipherKey (identical) more than once is treated as a NOP try { Reference baseCipher = domainKeyMap[minDomainId][minBaseCipherKeyId]; cipherKeyCache->insertCipherKey(baseCipher->domainId, baseCipher->keyId, baseCipher->key.get(), baseCipher->len, baseCipher->kcv, std::numeric_limits::max(), std::numeric_limits::max()); } catch (Error& e) { throw; } TraceEvent("TestReinsertIdempotentKeyDone").log(); // Ensure attempting to insert an existing cipherKey (modified) fails with appropriate error try { Reference baseCipher = domainKeyMap[minDomainId][minBaseCipherKeyId]; uint8_t rawCipher[baseCipher->len]; memcpy(rawCipher, baseCipher->key.get(), baseCipher->len); // modify cipherKey by flipping a bit const int idx = deterministicRandom()->randomInt(0, baseCipher->len); rawCipher[idx]++; cipherKeyCache->insertCipherKey(baseCipher->domainId, baseCipher->keyId, &rawCipher[0], baseCipher->len, baseCipher->kcv, std::numeric_limits::max(), std::numeric_limits::max()); } catch (Error& e) { if (e.code() != error_code_encrypt_update_cipher) { throw; } TraceEvent("TestReinsertNonIdempotentKeyDone"); } TraceEvent("TestCacheEssentialsEnd"); } void testKeyCacheRefreshExpireCipherKey(DomainKeyMap& domainKeyMap, const int maxDomainId) { TraceEvent("BlobCipherCacheRefreshCipherKey"); Reference cipherKeyCache = BlobCipherKeyCache::getInstance(); EncryptCipherDomainId domId = maxDomainId + 1; Reference cipherKey = cipherKeyCache->getLatestCipherKey(domId); ASSERT(!cipherKey.isValid()); Standalone baseCipher = makeString(4); deterministicRandom()->randomBytes(mutateString(baseCipher), 4); EncryptCipherKeyCheckValue baseCipherKCV = Sha256KCV().computeKCV(baseCipher.begin(), baseCipher.size()); Counter::Value expectedNeedRefreshCount = BlobCipherMetrics::getInstance()->latestCipherKeyCacheNeedsRefresh.getValue(); Counter::Value expectedLatestHitCount = BlobCipherMetrics::getInstance()->latestCipherKeyCacheHit.getValue(); Counter::Value expectedLatestMissCount = BlobCipherMetrics::getInstance()->cipherKeyCacheMiss.getValue(); Counter::Value expectedMissCount = BlobCipherMetrics::getInstance()->cipherKeyCacheMiss.getValue(); Counter::Value expectedHitCount = BlobCipherMetrics::getInstance()->cipherKeyCacheHit.getValue(); Counter::Value expectedExpiredKeys = BlobCipherMetrics::getInstance()->cipherKeyCacheExpired.getValue(); // Insert key that needs refresh int64_t refreshAt = now() - 1; int64_t expireAt = std::numeric_limits::max(); Reference inserted = cipherKeyCache->insertCipherKey( domId, 1, baseCipher.begin(), baseCipher.size(), baseCipherKCV, refreshAt, expireAt); EncryptCipherRandomSalt salt = inserted->getSalt(); Reference cipher = cipherKeyCache->getLatestCipherKey(domId); expectedLatestMissCount++; expectedNeedRefreshCount++; // Ensure cache return an invalid cipher ASSERT(!cipher.isValid()); ASSERT_EQ(BlobCipherMetrics::getInstance()->latestCipherKeyCacheNeedsRefresh.getValue(), expectedNeedRefreshCount); ASSERT_EQ(BlobCipherMetrics::getInstance()->latestCipherKeyCacheMiss.getValue(), expectedLatestMissCount); ASSERT_EQ(BlobCipherMetrics::getInstance()->latestCipherKeyCacheHit.getValue(), expectedLatestHitCount); ASSERT_EQ(BlobCipherMetrics::getInstance()->cipherKeyCacheMiss.getValue(), expectedMissCount); ASSERT_EQ(BlobCipherMetrics::getInstance()->cipherKeyCacheHit.getValue(), expectedHitCount); ASSERT_EQ(BlobCipherMetrics::getInstance()->cipherKeyCacheExpired.getValue(), expectedExpiredKeys); // Ensure point-lookup still returns valid key cipher = cipherKeyCache->getCipherKey(domId, 1, salt); expectedHitCount++; ASSERT(cipher.isValid()); ASSERT_EQ(cipher->getDomainId(), domId); ASSERT_EQ(cipher->getBaseCipherId(), 1); ASSERT_EQ(cipher->getBaseCipherLen(), 4); ASSERT_EQ(memcmp(cipher->rawBaseCipher(), baseCipher.begin(), 4), 0); ASSERT_EQ(cipher->getRefreshAtTS(), refreshAt); ASSERT_EQ(cipher->getExpireAtTS(), expireAt); ASSERT_EQ(BlobCipherMetrics::getInstance()->latestCipherKeyCacheNeedsRefresh.getValue(), expectedNeedRefreshCount); ASSERT_EQ(BlobCipherMetrics::getInstance()->latestCipherKeyCacheMiss.getValue(), expectedLatestMissCount); ASSERT_EQ(BlobCipherMetrics::getInstance()->latestCipherKeyCacheHit.getValue(), expectedLatestHitCount); ASSERT_EQ(BlobCipherMetrics::getInstance()->cipherKeyCacheMiss.getValue(), expectedMissCount); ASSERT_EQ(BlobCipherMetrics::getInstance()->cipherKeyCacheHit.getValue(), expectedHitCount); ASSERT_EQ(BlobCipherMetrics::getInstance()->cipherKeyCacheExpired.getValue(), expectedExpiredKeys); // Re-insert same key with same 'baseCipherId' and cache should accept it refreshAt = now() + 5; expireAt = now() + 10; // limit the expiry of the cipher Reference insertAgain = cipherKeyCache->insertCipherKey( domId, 1, baseCipher.begin(), baseCipher.size(), baseCipherKCV, refreshAt, expireAt); salt = insertAgain->getSalt(); cipher = cipherKeyCache->getLatestCipherKey(domId); expectedLatestHitCount++; ASSERT(cipher.isValid()); ASSERT_EQ(cipher->getDomainId(), domId); ASSERT_EQ(cipher->getBaseCipherId(), 1); ASSERT_EQ(cipher->getBaseCipherLen(), 4); ASSERT_EQ(memcmp(cipher->rawBaseCipher(), baseCipher.begin(), 4), 0); ASSERT_EQ(cipher->getRefreshAtTS(), refreshAt); ASSERT_EQ(cipher->getExpireAtTS(), expireAt); ASSERT_EQ(BlobCipherMetrics::getInstance()->latestCipherKeyCacheNeedsRefresh.getValue(), expectedNeedRefreshCount); ASSERT_EQ(BlobCipherMetrics::getInstance()->latestCipherKeyCacheMiss.getValue(), expectedLatestMissCount); ASSERT_EQ(BlobCipherMetrics::getInstance()->latestCipherKeyCacheHit.getValue(), expectedLatestHitCount); ASSERT_EQ(BlobCipherMetrics::getInstance()->cipherKeyCacheMiss.getValue(), expectedMissCount); ASSERT_EQ(BlobCipherMetrics::getInstance()->cipherKeyCacheHit.getValue(), expectedHitCount); ASSERT_EQ(BlobCipherMetrics::getInstance()->cipherKeyCacheExpired.getValue(), expectedExpiredKeys); // Insert an expired cipherkey domId++; expireAt = now() - 100; refreshAt = expireAt - 10; inserted = cipherKeyCache->insertCipherKey( domId, 1, baseCipher.begin(), baseCipher.size(), baseCipherKCV, refreshAt, expireAt); salt = inserted->getSalt(); // Ensure getLatestCipher desired behavior cipher = cipherKeyCache->getLatestCipherKey(domId); ASSERT(!cipher.isValid()); // Already expired key, hence, getLookupByBaseCipher would fail, hence, NOT increment 'needsRefresh' counter expectedLatestMissCount++; expectedExpiredKeys++; ASSERT_EQ(BlobCipherMetrics::getInstance()->latestCipherKeyCacheNeedsRefresh.getValue(), expectedNeedRefreshCount); ASSERT_EQ(BlobCipherMetrics::getInstance()->latestCipherKeyCacheMiss.getValue(), expectedLatestMissCount); ASSERT_EQ(BlobCipherMetrics::getInstance()->latestCipherKeyCacheHit.getValue(), expectedLatestHitCount); ASSERT_EQ(BlobCipherMetrics::getInstance()->cipherKeyCacheMiss.getValue(), expectedMissCount); ASSERT_EQ(BlobCipherMetrics::getInstance()->cipherKeyCacheHit.getValue(), expectedHitCount); ASSERT_EQ(BlobCipherMetrics::getInstance()->cipherKeyCacheExpired.getValue(), expectedExpiredKeys); // Ensure getCipher desired behavior inserted = cipherKeyCache->insertCipherKey( domId, 1, baseCipher.begin(), baseCipher.size(), baseCipherKCV, refreshAt, expireAt); salt = inserted->getSalt(); cipher = cipherKeyCache->getCipherKey(domId, 1, salt); ASSERT(!cipher.isValid()); expectedMissCount++; expectedExpiredKeys++; ASSERT_EQ(BlobCipherMetrics::getInstance()->latestCipherKeyCacheNeedsRefresh.getValue(), expectedNeedRefreshCount); ASSERT_EQ(BlobCipherMetrics::getInstance()->latestCipherKeyCacheMiss.getValue(), expectedLatestMissCount); ASSERT_EQ(BlobCipherMetrics::getInstance()->latestCipherKeyCacheHit.getValue(), expectedLatestHitCount); ASSERT_EQ(BlobCipherMetrics::getInstance()->cipherKeyCacheMiss.getValue(), expectedMissCount); ASSERT_EQ(BlobCipherMetrics::getInstance()->cipherKeyCacheHit.getValue(), expectedHitCount); ASSERT_EQ(BlobCipherMetrics::getInstance()->cipherKeyCacheExpired.getValue(), expectedExpiredKeys); } void testNoAuthMode(const int minDomainId) { TraceEvent("TestNoAuthModeStart"); Reference cipherKeyCache = BlobCipherKeyCache::getInstance(); // Validate Encryption ops Reference cipherKey = cipherKeyCache->getLatestCipherKey(minDomainId); Reference headerCipherKey = cipherKeyCache->getLatestCipherKey(ENCRYPT_HEADER_DOMAIN_ID); const int bufLen = deterministicRandom()->randomInt(786, 2127) + 512; uint8_t orgData[bufLen]; deterministicRandom()->randomBytes(&orgData[0], bufLen); Arena arena; uint8_t iv[AES_256_IV_LENGTH]; deterministicRandom()->randomBytes(&iv[0], AES_256_IV_LENGTH); EncryptBlobCipherAes265Ctr encryptor(cipherKey, headerCipherKey, iv, AES_256_IV_LENGTH, EncryptAuthTokenMode::ENCRYPT_HEADER_AUTH_TOKEN_MODE_NONE, BlobCipherMetrics::TEST); BlobCipherEncryptHeader header; Reference encrypted = encryptor.encrypt(&orgData[0], bufLen, &header, arena); ASSERT_EQ(encrypted->getLogicalSize(), bufLen); if (g_network->isSimulated() && ENABLE_MUTATION_TRACKING_WITH_BLOB_CIPHER) ASSERT_EQ(memcmp(&orgData[0], encrypted->begin(), bufLen), 0); else ASSERT_NE(memcmp(&orgData[0], encrypted->begin(), bufLen), 0); ASSERT_EQ(header.flags.headerVersion, EncryptBlobCipherAes265Ctr::ENCRYPT_HEADER_VERSION); ASSERT_EQ(header.flags.encryptMode, EncryptCipherMode::ENCRYPT_CIPHER_MODE_AES_256_CTR); ASSERT_EQ(header.flags.authTokenMode, EncryptAuthTokenMode::ENCRYPT_HEADER_AUTH_TOKEN_MODE_NONE); TraceEvent("TestNoAuthEncryptDone") .detail("HeaderVersion", header.flags.headerVersion) .detail("HeaderEncryptMode", header.flags.encryptMode) .detail("HeaderEncryptAuthTokenMode", header.flags.authTokenMode) .detail("HeaderEncryptAuthTokenAlgo", header.flags.authTokenAlgo) .detail("DomainId", header.cipherTextDetails.encryptDomainId) .detail("BaseCipherId", header.cipherTextDetails.baseCipherId); Reference tCipherKey = cipherKeyCache->getCipherKey( header.cipherTextDetails.encryptDomainId, header.cipherTextDetails.baseCipherId, header.cipherTextDetails.salt); ASSERT(tCipherKey->isEqual(cipherKey)); DecryptBlobCipherAes256Ctr decryptor( tCipherKey, Reference(), &header.iv[0], BlobCipherMetrics::TEST); Reference decrypted = decryptor.decrypt(encrypted->begin(), bufLen, header, arena); ASSERT_EQ(decrypted->getLogicalSize(), bufLen); ASSERT_EQ(memcmp(decrypted->begin(), &orgData[0], bufLen), 0); TraceEvent("TestNoAuthDecryptDone"); // induce encryption header corruption - headerVersion corrupted BlobCipherEncryptHeader headerCopy; memcpy(reinterpret_cast(&headerCopy), reinterpret_cast(&header), sizeof(BlobCipherEncryptHeader)); headerCopy.flags.headerVersion += 1; try { encrypted = encryptor.encrypt(&orgData[0], bufLen, &header, arena); DecryptBlobCipherAes256Ctr decryptor( tCipherKey, Reference(), header.iv, BlobCipherMetrics::TEST); decrypted = decryptor.decrypt(encrypted->begin(), bufLen, headerCopy, arena); ASSERT(false); // error expected } catch (Error& e) { if (e.code() != error_code_encrypt_header_metadata_mismatch) { throw; } TraceEvent("TestNoAuthHeaderVersionCorruptionDone"); } // induce encryption header corruption - encryptionMode corrupted memcpy(reinterpret_cast(&headerCopy), reinterpret_cast(&header), sizeof(BlobCipherEncryptHeader)); headerCopy.flags.encryptMode += 1; try { encrypted = encryptor.encrypt(&orgData[0], bufLen, &header, arena); DecryptBlobCipherAes256Ctr decryptor( tCipherKey, Reference(), header.iv, BlobCipherMetrics::TEST); decrypted = decryptor.decrypt(encrypted->begin(), bufLen, headerCopy, arena); ASSERT(false); // error expected } catch (Error& e) { if (e.code() != error_code_encrypt_header_metadata_mismatch) { throw; } TraceEvent("TestNoAuthEncryptModeCorruptionDone"); } // induce encrypted buffer payload corruption try { encrypted = encryptor.encrypt(&orgData[0], bufLen, &header, arena); uint8_t temp[bufLen]; deterministicRandom()->randomBytes(&temp[0], bufLen); memcpy(encrypted->begin(), &temp[0], bufLen); int tIdx = deterministicRandom()->randomInt(0, bufLen - 1); temp[tIdx] += 1; DecryptBlobCipherAes256Ctr decryptor( tCipherKey, Reference(), header.iv, BlobCipherMetrics::TEST); decrypted = decryptor.decrypt(&temp[0], bufLen, header, arena); TraceEvent("TestNoAuthEncryptPayloadCorruptionDone"); } catch (Error& e) { // No authToken, hence, no corruption detection supported ASSERT(false); } // induce baseCipher corruption try { encrypted = encryptor.encrypt(&orgData[0], bufLen, &header, arena); Reference corruptedCipher = corruptCipherKey(tCipherKey); DecryptBlobCipherAes256Ctr decryptor( corruptedCipher, Reference(), header.iv, BlobCipherMetrics::TEST); decrypted = decryptor.decrypt(encrypted->begin(), bufLen, header, arena); ASSERT(false); // error expected } catch (Error& e) { ASSERT_EQ(e.code(), error_code_encrypt_key_check_value_mismatch); TraceEvent("TestNoAuthEncryptBaseCipherCorruptionDone"); } TraceEvent("BlobCipherTestNoAuthModeDone"); } void testConfigurableEncryptionBlobCipherHeaderFlagsV1Ser() { Arena arena; // Version-1 BlobCipherEncryptHeaderFlagsV1 flags( ENCRYPT_CIPHER_MODE_AES_256_CTR, getRandomAuthTokenMode(), getRandomAuthTokenAlgo()); Standalone ser = BlobCipherEncryptHeaderFlagsV1::toStringRef(flags, arena); ASSERT_EQ(ser.size(), sizeof(flags)); } void testConfigurableEncryptionAesCtrNoAuthV1Ser(const int minDomainId) { Arena arena; BlobCipherEncryptHeaderRef headerRef; uint32_t size = 0; BlobCipherEncryptHeaderFlagsV1 flags = BlobCipherEncryptHeaderFlagsV1( ENCRYPT_CIPHER_MODE_AES_256_CTR, ENCRYPT_HEADER_AUTH_TOKEN_MODE_NONE, ENCRYPT_HEADER_AUTH_TOKEN_ALGO_NONE); size += sizeof(BlobCipherEncryptHeaderFlagsV1) + 2; headerRef.flags = flags; AesCtrNoAuth noAuth; noAuth.v1.cipherTextDetails = BlobCipherDetails(1, 2, 23); deterministicRandom()->randomBytes(&noAuth.v1.iv[0], AES_256_IV_LENGTH); Standalone serAlgo = AesCtrNoAuth::toStringRef(noAuth); ASSERT_EQ(serAlgo.size(), sizeof(noAuth)); size += AesCtrNoAuth::getSize(); headerRef.algoHeader = noAuth; Standalone serHeader = BlobCipherEncryptHeaderRef::toStringRef(headerRef); ASSERT_EQ(serHeader.size(), size); ASSERT_EQ(size, BlobCipherEncryptHeaderRef::getHeaderSize(headerRef.flagsVersion(), headerRef.algoHeaderVersion(), (EncryptCipherMode)flags.encryptMode, (EncryptAuthTokenMode)flags.authTokenMode, (EncryptAuthTokenAlgo)flags.authTokenAlgo)); } template void testConfigurableEncryptionAesCtrWithAuthSer(const int minDomainId) { constexpr bool isHmac = std::is_same_v; Arena arena; BlobCipherEncryptHeaderRef headerRef; uint32_t size = 0; BlobCipherEncryptHeaderFlagsV1 flags = BlobCipherEncryptHeaderFlagsV1( ENCRYPT_CIPHER_MODE_AES_256_CTR, ENCRYPT_HEADER_AUTH_TOKEN_MODE_SINGLE, isHmac ? ENCRYPT_HEADER_AUTH_TOKEN_ALGO_HMAC_SHA : ENCRYPT_HEADER_AUTH_TOKEN_ALGO_AES_CMAC); size += sizeof(BlobCipherEncryptHeaderFlagsV1) + 2; headerRef.flags = flags; AesCtrWithAuth withAuth; withAuth.v1.cipherTextDetails = BlobCipherDetails(1, 2, 23); withAuth.v1.cipherHeaderDetails = BlobCipherDetails(ENCRYPT_HEADER_DOMAIN_ID, 2, 23); deterministicRandom()->randomBytes(&withAuth.v1.iv[0], AES_256_IV_LENGTH); deterministicRandom()->randomBytes(&withAuth.v1.authToken[0], Params::authTokenSize); Standalone serAlgo = AesCtrWithAuth::toStringRef(withAuth); ASSERT_EQ(serAlgo.size(), sizeof(withAuth)); size += AesCtrWithAuth::getSize(); headerRef.algoHeader = withAuth; Standalone serHeader = BlobCipherEncryptHeaderRef::toStringRef(headerRef); ASSERT_EQ(serHeader.size(), size); ASSERT_EQ(size, BlobCipherEncryptHeaderRef::getHeaderSize(headerRef.flagsVersion(), headerRef.algoHeaderVersion(), (EncryptCipherMode)flags.encryptMode, (EncryptAuthTokenMode)flags.authTokenMode, (EncryptAuthTokenAlgo)flags.authTokenAlgo)); } void testConfigurableEncryptionHeaderNoAuthMode(const int minDomainId) { TraceEvent("TestConfigurableEncryptionHeader").detail("Mode", "No-Auth"); Reference cipherKeyCache = BlobCipherKeyCache::getInstance(); // Validate Encryption ops Reference cipherKey = cipherKeyCache->getLatestCipherKey(minDomainId); Reference headerCipherKey = cipherKeyCache->getLatestCipherKey(ENCRYPT_HEADER_DOMAIN_ID); const int bufLen = deterministicRandom()->randomInt(786, 2127) + 512; uint8_t orgData[bufLen]; deterministicRandom()->randomBytes(&orgData[0], bufLen); Arena arena; uint8_t iv[AES_256_IV_LENGTH]; deterministicRandom()->randomBytes(&iv[0], AES_256_IV_LENGTH); EncryptBlobCipherAes265Ctr encryptor(cipherKey, headerCipherKey, iv, AES_256_IV_LENGTH, EncryptAuthTokenMode::ENCRYPT_HEADER_AUTH_TOKEN_MODE_NONE, BlobCipherMetrics::TEST); BlobCipherEncryptHeaderRef headerRef; encryptor.encrypt(&orgData[0], bufLen, &headerRef, arena); ASSERT_EQ(headerRef.flagsVersion(), 1); BlobCipherEncryptHeaderFlagsV1 flags = std::get(headerRef.flags); ASSERT_EQ(flags.authTokenMode, headerRef.getAuthTokenMode()); AesCtrNoAuth noAuth = std::get(headerRef.algoHeader); const uint8_t* headerIV = headerRef.getIV(); ASSERT_EQ(memcmp(&headerIV[0], &iv[0], AES_256_IV_LENGTH), 0); EncryptHeaderCipherDetails validateDetails = headerRef.getCipherDetails(); ASSERT(validateDetails.textCipherDetails.isValid() && validateDetails.textCipherDetails == BlobCipherDetails(cipherKey->getDomainId(), cipherKey->getBaseCipherId(), cipherKey->getSalt())); ASSERT(!validateDetails.headerCipherDetails.present()); Standalone serHeaderRef = BlobCipherEncryptHeaderRef::toStringRef(headerRef); BlobCipherEncryptHeaderRef validateHeader = BlobCipherEncryptHeaderRef::fromStringRef(serHeaderRef); BlobCipherEncryptHeaderFlagsV1 validateFlags = std::get(validateHeader.flags); ASSERT(validateFlags == flags); AesCtrNoAuth validateAlgo = std::get(validateHeader.algoHeader); ASSERT(validateAlgo.v1.cipherTextDetails == noAuth.v1.cipherTextDetails); ASSERT_EQ(memcmp(&validateAlgo.v1.iv[0], &noAuth.v1.iv[0], AES_256_IV_LENGTH), 0); TraceEvent("NoAuthHeaderSize") .detail("Flags", sizeof(flags)) .detail("AlgoHeader", noAuth.getSize()) .detail("TotalHeader", serHeaderRef.size()); TraceEvent("TestConfigurableEncryptionHeader").detail("Mode", "No-Auth"); } void testConfigurableEncryptionNoAuthMode(const int minDomainId) { TraceEvent("TestConfigurableEncryptionNoAuthModeStart"); Reference cipherKeyCache = BlobCipherKeyCache::getInstance(); // Validate Encryption ops Reference cipherKey = cipherKeyCache->getLatestCipherKey(minDomainId); Reference headerCipherKey = cipherKeyCache->getLatestCipherKey(ENCRYPT_HEADER_DOMAIN_ID); const int bufLen = deterministicRandom()->randomInt(786, 2127) + 512; uint8_t orgData[bufLen]; deterministicRandom()->randomBytes(&orgData[0], bufLen); Arena arena; uint8_t iv[AES_256_IV_LENGTH]; deterministicRandom()->randomBytes(&iv[0], AES_256_IV_LENGTH); EncryptBlobCipherAes265Ctr encryptor(cipherKey, headerCipherKey, iv, AES_256_IV_LENGTH, EncryptAuthTokenMode::ENCRYPT_HEADER_AUTH_TOKEN_MODE_NONE, BlobCipherMetrics::TEST); BlobCipherEncryptHeaderRef headerRef; StringRef encryptedBuf = encryptor.encrypt(&orgData[0], bufLen, &headerRef, arena); // validate header version details AesCtrNoAuth noAuth = std::get(headerRef.algoHeader); Reference tCipherKey = cipherKeyCache->getCipherKey(noAuth.v1.cipherTextDetails.encryptDomainId, noAuth.v1.cipherTextDetails.baseCipherId, noAuth.v1.cipherTextDetails.salt); ASSERT(tCipherKey->isEqual(cipherKey)); DecryptBlobCipherAes256Ctr decryptor( tCipherKey, Reference(), &noAuth.v1.iv[0], BlobCipherMetrics::TEST); StringRef decryptedBuf = decryptor.decrypt(encryptedBuf.begin(), encryptedBuf.size(), headerRef, arena); ASSERT_EQ(decryptedBuf.size(), bufLen); ASSERT_EQ(memcmp(decryptedBuf.begin(), &orgData[0], bufLen), 0); TraceEvent("TestConfigurableEncryptionNoAuthDecryptDone") .detail("HeaderFlagsVersion", headerRef.flagsVersion()) .detail("AlgoHeaderVersion", headerRef.algoHeaderVersion()) .detail("HeaderEncryptMode", ENCRYPT_CIPHER_MODE_AES_256_CTR) .detail("HeaderEncryptAuthTokenMode", ENCRYPT_HEADER_AUTH_TOKEN_MODE_NONE) .detail("HeaderEncryptAuthTokenAlgo", ENCRYPT_HEADER_AUTH_TOKEN_MODE_NONE) .detail("DomainId", noAuth.v1.cipherTextDetails.encryptDomainId) .detail("BaseCipherId", noAuth.v1.cipherTextDetails.baseCipherId) .detail("Salt", noAuth.v1.cipherTextDetails.salt); // induce encryption header corruption - encryptionMode corrupted BlobCipherEncryptHeaderRef corruptedHeaderRef = BlobCipherEncryptHeaderRef(headerRef); BlobCipherEncryptHeaderFlagsV1 corruptedFlags = std::get(headerRef.flags); corruptedFlags.encryptMode += 1; corruptedHeaderRef.flags = corruptedFlags; try { encryptedBuf = encryptor.encrypt(&orgData[0], bufLen, &headerRef, arena); DecryptBlobCipherAes256Ctr decryptor(tCipherKey, Reference(), &iv[0], BlobCipherMetrics::TEST); decryptedBuf = decryptor.decrypt(encryptedBuf.begin(), bufLen, corruptedHeaderRef, arena); ASSERT(false); // error expected } catch (Error& e) { if (e.code() != error_code_encrypt_header_metadata_mismatch) { throw; } TraceEvent("TestConfigurableEncryptionNoAuthHeaderCorruptionDone"); } // induce encrypted buffer payload corruption try { encryptedBuf = encryptor.encrypt(&orgData[0], bufLen, &headerRef, arena); uint8_t temp[bufLen]; deterministicRandom()->randomBytes(&temp[0], bufLen); memcpy((void*)encryptedBuf.begin(), &temp[0], bufLen); int tIdx = deterministicRandom()->randomInt(0, bufLen - 1); temp[tIdx] += 1; DecryptBlobCipherAes256Ctr decryptor(tCipherKey, Reference(), &iv[0], BlobCipherMetrics::TEST); decryptedBuf = decryptor.decrypt(&temp[0], bufLen, headerRef, arena); ASSERT_NE(memcmp(decryptedBuf.begin(), &orgData[0], bufLen), 0); TraceEvent("TestConfigurableEncryptionNoAuthPayloadCorruptionDone"); } catch (Error& e) { // No authToken, hence, no corruption detection supported ASSERT(false); } // induce baseCipher corruption try { Reference corruptedTextCipher = corruptCipherKey(tCipherKey); encryptedBuf = encryptor.encrypt(&orgData[0], bufLen, &headerRef, arena); DecryptBlobCipherAes256Ctr decryptor( corruptedTextCipher, Reference(), &iv[0], BlobCipherMetrics::TEST); decryptedBuf = decryptor.decrypt(encryptedBuf.begin(), bufLen, headerRef, arena); ASSERT(false); // error expected } catch (Error& e) { ASSERT_EQ(e.code(), error_code_encrypt_key_check_value_mismatch); TraceEvent("TestConfigurableEncryptionNoAuthBaseCipherCorruptionDone"); } TraceEvent("ConfigurableEncryptionNoAuthDone"); } // validate basic encrypt followed by decrypt operation for AUTH_TOKEN_MODE_SINGLE // HMAC_SHA authToken algorithm template void testSingleAuthMode(const int minDomainId) { constexpr bool isHmac = std::is_same_v; const std::string authAlgoStr = isHmac ? "HMAC-SHA" : "AES-CMAC"; const EncryptAuthTokenAlgo authAlgo = isHmac ? EncryptAuthTokenAlgo::ENCRYPT_HEADER_AUTH_TOKEN_ALGO_HMAC_SHA : EncryptAuthTokenAlgo::ENCRYPT_HEADER_AUTH_TOKEN_ALGO_AES_CMAC; TraceEvent("TestSingleAuthTokenStart").detail("Mode", authAlgoStr); Reference cipherKeyCache = BlobCipherKeyCache::getInstance(); // Validate Encryption ops Reference cipherKey = cipherKeyCache->getLatestCipherKey(minDomainId); Reference headerCipherKey = cipherKeyCache->getLatestCipherKey(ENCRYPT_HEADER_DOMAIN_ID); const int bufLen = deterministicRandom()->randomInt(786, 2127) + 512; Arena arena; uint8_t iv[AES_256_IV_LENGTH]; deterministicRandom()->randomBytes(&iv[0], AES_256_IV_LENGTH); uint8_t orgData[bufLen]; deterministicRandom()->randomBytes(&orgData[0], bufLen); EncryptBlobCipherAes265Ctr encryptor(cipherKey, headerCipherKey, iv, AES_256_IV_LENGTH, EncryptAuthTokenMode::ENCRYPT_HEADER_AUTH_TOKEN_MODE_SINGLE, authAlgo, BlobCipherMetrics::TEST); BlobCipherEncryptHeader header; Reference encrypted = encryptor.encrypt(&orgData[0], bufLen, &header, arena); ASSERT_EQ(encrypted->getLogicalSize(), bufLen); if (g_network->isSimulated() && ENABLE_MUTATION_TRACKING_WITH_BLOB_CIPHER) ASSERT_EQ(memcmp(&orgData[0], encrypted->begin(), bufLen), 0); else ASSERT_NE(memcmp(&orgData[0], encrypted->begin(), bufLen), 0); ASSERT_EQ(header.flags.headerVersion, EncryptBlobCipherAes265Ctr::ENCRYPT_HEADER_VERSION); ASSERT_EQ(header.flags.encryptMode, ENCRYPT_CIPHER_MODE_AES_256_CTR); ASSERT_EQ(header.flags.authTokenMode, EncryptAuthTokenMode::ENCRYPT_HEADER_AUTH_TOKEN_MODE_SINGLE); ASSERT_EQ(header.flags.authTokenAlgo, authAlgo); TraceEvent("TestSingleAuthTokenEncryptDone") .detail("HeaderVersion", header.flags.headerVersion) .detail("HeaderEncryptMode", header.flags.encryptMode) .detail("HeaderEncryptAuthTokenMode", header.flags.authTokenMode) .detail("HeaderEncryptAuthTokenAlgo", header.flags.authTokenAlgo) .detail("DomainId", header.cipherTextDetails.encryptDomainId) .detail("BaseCipherId", header.cipherTextDetails.baseCipherId) .detail("HeaderAuthToken", StringRef(arena, &header.singleAuthToken.authToken[0], Params::authTokenSize).toString()); Reference tCipherKey = cipherKeyCache->getCipherKey( header.cipherTextDetails.encryptDomainId, header.cipherTextDetails.baseCipherId, header.cipherTextDetails.salt); Reference hCipherKey = cipherKeyCache->getCipherKey(header.cipherHeaderDetails.encryptDomainId, header.cipherHeaderDetails.baseCipherId, header.cipherHeaderDetails.salt); ASSERT(tCipherKey->isEqual(cipherKey)); DecryptBlobCipherAes256Ctr decryptor(tCipherKey, hCipherKey, header.iv, BlobCipherMetrics::TEST); Reference decrypted = decryptor.decrypt(encrypted->begin(), bufLen, header, arena); ASSERT_EQ(decrypted->getLogicalSize(), bufLen); ASSERT_EQ(memcmp(decrypted->begin(), &orgData[0], bufLen), 0); TraceEvent("TestSingleAuthTokenDecryptDone").detail("Mode", authAlgoStr); // induce encryption header corruption - headerVersion corrupted BlobCipherEncryptHeader headerCopy; encrypted = encryptor.encrypt(&orgData[0], bufLen, &header, arena); memcpy(reinterpret_cast(&headerCopy), reinterpret_cast(&header), sizeof(BlobCipherEncryptHeader)); headerCopy.flags.headerVersion += 1; try { DecryptBlobCipherAes256Ctr decryptor(tCipherKey, hCipherKey, header.iv, BlobCipherMetrics::TEST); decrypted = decryptor.decrypt(encrypted->begin(), bufLen, headerCopy, arena); ASSERT(false); // error expected } catch (Error& e) { if (e.code() != error_code_encrypt_header_metadata_mismatch) { throw; } TraceEvent("TestSingleAuthTokenHeaderVersionCorruptionDone").detail("Mode", authAlgoStr); } // induce encryption header corruption - encryptionMode corrupted encrypted = encryptor.encrypt(&orgData[0], bufLen, &header, arena); memcpy(reinterpret_cast(&headerCopy), reinterpret_cast(&header), sizeof(BlobCipherEncryptHeader)); headerCopy.flags.encryptMode += 1; try { DecryptBlobCipherAes256Ctr decryptor(tCipherKey, hCipherKey, header.iv, BlobCipherMetrics::TEST); decrypted = decryptor.decrypt(encrypted->begin(), bufLen, headerCopy, arena); ASSERT(false); // error expected } catch (Error& e) { if (e.code() != error_code_encrypt_header_metadata_mismatch) { throw; } TraceEvent("TestSingleAuthTokenEncryptModeCorruptionDone").detail("Mode", authAlgoStr); } // induce encryption header corruption - authToken mismatch encrypted = encryptor.encrypt(&orgData[0], bufLen, &header, arena); memcpy(reinterpret_cast(&headerCopy), reinterpret_cast(&header), sizeof(BlobCipherEncryptHeader)); int hIdx = deterministicRandom()->randomInt(0, Params::authTokenSize - 1); headerCopy.singleAuthToken.authToken[hIdx] += 1; try { DecryptBlobCipherAes256Ctr decryptor(tCipherKey, hCipherKey, header.iv, BlobCipherMetrics::TEST); decrypted = decryptor.decrypt(encrypted->begin(), bufLen, headerCopy, arena); ASSERT(false); // error expected } catch (Error& e) { if (e.code() != error_code_encrypt_header_authtoken_mismatch) { throw; } TraceEvent("TestSingleAuthTokenAuthTokenMismatchDone").detail("Mode", authAlgoStr); } // induce encrypted buffer payload corruption try { encrypted = encryptor.encrypt(&orgData[0], bufLen, &header, arena); uint8_t temp[bufLen]; deterministicRandom()->randomBytes(temp, bufLen); memcpy(encrypted->begin(), &temp[0], bufLen); int tIdx = deterministicRandom()->randomInt(0, bufLen - 1); temp[tIdx] += 1; DecryptBlobCipherAes256Ctr decryptor(tCipherKey, hCipherKey, header.iv, BlobCipherMetrics::TEST); decrypted = decryptor.decrypt(&temp[0], bufLen, header, arena); } catch (Error& e) { if (e.code() != error_code_encrypt_header_authtoken_mismatch) { throw; } TraceEvent("TestSingleAuthTokenPayloadCorruptionDone").detail("Mode", authAlgoStr); } // induce baseCipher corruption try { const bool corruptTextCipher = deterministicRandom()->coinflip(); encrypted = encryptor.encrypt(&orgData[0], bufLen, &header, arena); if (corruptTextCipher) { Reference corruptedCipher = corruptCipherKey(tCipherKey); DecryptBlobCipherAes256Ctr decryptor(corruptedCipher, hCipherKey, header.iv, BlobCipherMetrics::TEST); decrypted = decryptor.decrypt(encrypted->begin(), bufLen, header, arena); } else { Reference corruptedCipher = corruptCipherKey(hCipherKey); DecryptBlobCipherAes256Ctr decryptor(tCipherKey, corruptedCipher, header.iv, BlobCipherMetrics::TEST); decrypted = decryptor.decrypt(encrypted->begin(), bufLen, header, arena); } ASSERT(false); // error expected } catch (Error& e) { ASSERT_EQ(e.code(), error_code_encrypt_key_check_value_mismatch); TraceEvent("TestSingleAuthTokenBaseCipherCorruptionDone").detail("Mode", authAlgoStr); } TraceEvent("BlobCipherTestSingleAuthTokenEnd").detail("Mode", authAlgoStr); } template void testConfigurableEncryptionHeaderSingleAuthMode(int minDomainId) { constexpr bool isHmac = std::is_same_v; TraceEvent("TestEncryptionHeaderStart").detail("Mode", isHmac ? "HMAC_SHA" : "AES-CMAC"); Reference cipherKeyCache = BlobCipherKeyCache::getInstance(); // Validate Encryption ops Reference cipherKey = cipherKeyCache->getLatestCipherKey(minDomainId); Reference headerCipherKey = cipherKeyCache->getLatestCipherKey(ENCRYPT_HEADER_DOMAIN_ID); const int bufLen = deterministicRandom()->randomInt(786, 2127) + 512; Arena arena; uint8_t iv[AES_256_IV_LENGTH]; deterministicRandom()->randomBytes(&iv[0], AES_256_IV_LENGTH); uint8_t orgData[bufLen]; deterministicRandom()->randomBytes(&orgData[0], bufLen); EncryptBlobCipherAes265Ctr encryptor(cipherKey, headerCipherKey, iv, AES_256_IV_LENGTH, EncryptAuthTokenMode::ENCRYPT_HEADER_AUTH_TOKEN_MODE_SINGLE, std::is_same_v ? EncryptAuthTokenAlgo::ENCRYPT_HEADER_AUTH_TOKEN_ALGO_HMAC_SHA : EncryptAuthTokenAlgo::ENCRYPT_HEADER_AUTH_TOKEN_ALGO_AES_CMAC, BlobCipherMetrics::TEST); BlobCipherEncryptHeaderRef headerRef; encryptor.encrypt(&orgData[0], bufLen, &headerRef, arena); ASSERT_EQ(headerRef.flagsVersion(), 1); BlobCipherEncryptHeaderFlagsV1 flags = std::get(headerRef.flags); ASSERT_EQ(flags.authTokenMode, headerRef.getAuthTokenMode()); AesCtrWithAuth algoHeader = std::get>(headerRef.algoHeader); const uint8_t* headerIV = headerRef.getIV(); ASSERT_EQ(memcmp(&headerIV[0], &iv[0], AES_256_IV_LENGTH), 0); EncryptHeaderCipherDetails validateDetails = headerRef.getCipherDetails(); ASSERT(validateDetails.textCipherDetails.isValid() && validateDetails.textCipherDetails == BlobCipherDetails(cipherKey->getDomainId(), cipherKey->getBaseCipherId(), cipherKey->getSalt())); ASSERT(validateDetails.headerCipherDetails.present() && validateDetails.headerCipherDetails.get().isValid() && validateDetails.headerCipherDetails.get() == BlobCipherDetails(headerCipherKey->getDomainId(), headerCipherKey->getBaseCipherId(), headerCipherKey->getSalt())); Standalone serHeaderRef = BlobCipherEncryptHeaderRef::toStringRef(headerRef); BlobCipherEncryptHeaderRef validateHeader = BlobCipherEncryptHeaderRef::fromStringRef(serHeaderRef); BlobCipherEncryptHeaderFlagsV1 validateFlags = std::get(validateHeader.flags); ASSERT(validateFlags == flags); AesCtrWithAuth validateAlgo = std::get>(validateHeader.algoHeader); ASSERT(validateAlgo.v1.cipherTextDetails == algoHeader.v1.cipherTextDetails); ASSERT(validateAlgo.v1.cipherHeaderDetails == algoHeader.v1.cipherHeaderDetails); ASSERT_EQ(memcmp(&iv[0], &validateAlgo.v1.iv[0], AES_256_IV_LENGTH), 0); ASSERT_EQ(memcmp(&algoHeader.v1.authToken[0], &validateAlgo.v1.authToken[0], Params::authTokenSize), 0); TraceEvent("HeaderSize") .detail("Flags", sizeof(flags)) .detail("AlgoHeader", algoHeader.getSize()) .detail("TotalHeader", serHeaderRef.size()); TraceEvent("TestEncryptionHeaderEnd").detail("Mode", isHmac ? "HMAC_SHA" : "AES-CMAC"); } // validate basic encrypt followed by decrypt operation for AUTH_TOKEN_MODE_SINGLE template void testConfigurableEncryptionSingleAuthMode(const int minDomainId) { constexpr bool isHmac = std::is_same_v; const std::string authAlgoStr = isHmac ? "HMAC-SHA" : "AES-CMAC"; const EncryptAuthTokenAlgo authAlgo = isHmac ? EncryptAuthTokenAlgo::ENCRYPT_HEADER_AUTH_TOKEN_ALGO_HMAC_SHA : EncryptAuthTokenAlgo::ENCRYPT_HEADER_AUTH_TOKEN_ALGO_AES_CMAC; const int algoHeaderVersion = isHmac ? CLIENT_KNOBS->ENCRYPT_HEADER_AES_CTR_HMAC_SHA_AUTH_VERSION : CLIENT_KNOBS->ENCRYPT_HEADER_AES_CTR_AES_CMAC_AUTH_VERSION; TraceEvent("TestConfigurableEncryptionSingleAuthStart").detail("Mode", authAlgoStr); Reference cipherKeyCache = BlobCipherKeyCache::getInstance(); // Validate Encryption ops Reference cipherKey = cipherKeyCache->getLatestCipherKey(minDomainId); Reference headerCipherKey = cipherKeyCache->getLatestCipherKey(ENCRYPT_HEADER_DOMAIN_ID); const int bufLen = deterministicRandom()->randomInt(786, 2127) + 512; Arena arena; uint8_t iv[AES_256_IV_LENGTH]; deterministicRandom()->randomBytes(&iv[0], AES_256_IV_LENGTH); uint8_t orgData[bufLen]; deterministicRandom()->randomBytes(&orgData[0], bufLen); EncryptBlobCipherAes265Ctr encryptor(cipherKey, headerCipherKey, iv, AES_256_IV_LENGTH, EncryptAuthTokenMode::ENCRYPT_HEADER_AUTH_TOKEN_MODE_SINGLE, authAlgo, BlobCipherMetrics::TEST); BlobCipherEncryptHeaderRef headerRef; StringRef encryptedBuf = encryptor.encrypt(&orgData[0], bufLen, &headerRef, arena); ASSERT_EQ(encryptedBuf.size(), bufLen); if (g_network->isSimulated() && ENABLE_MUTATION_TRACKING_WITH_BLOB_CIPHER) ASSERT_EQ(memcmp(&orgData[0], encryptedBuf.begin(), bufLen), 0); else ASSERT_NE(memcmp(&orgData[0], encryptedBuf.begin(), bufLen), 0); ASSERT_EQ(headerRef.flagsVersion(), CLIENT_KNOBS->ENCRYPT_HEADER_FLAGS_VERSION); ASSERT_EQ(headerRef.algoHeaderVersion(), algoHeaderVersion); // validate flags BlobCipherEncryptHeaderFlagsV1 flags = std::get(headerRef.flags); ASSERT_EQ(flags.encryptMode, EncryptCipherMode::ENCRYPT_CIPHER_MODE_AES_256_CTR); ASSERT_EQ(flags.authTokenMode, EncryptAuthTokenMode::ENCRYPT_HEADER_AUTH_TOKEN_MODE_SINGLE); ASSERT_EQ(flags.authTokenAlgo, authAlgo); // validate IV AesCtrWithAuth withAuth = std::get>(headerRef.algoHeader); ASSERT_EQ(memcmp(&iv[0], &withAuth.v1.iv[0], AES_256_IV_LENGTH), 0); if (g_network->isSimulated() && ENABLE_MUTATION_TRACKING_WITH_BLOB_CIPHER) ASSERT_EQ(memcmp(&orgData[0], encryptedBuf.begin(), bufLen), 0); else ASSERT_NE(memcmp(&orgData[0], encryptedBuf.begin(), bufLen), 0); // validate cipherKey details ASSERT_EQ(withAuth.v1.cipherTextDetails.encryptDomainId, cipherKey->getDomainId()); ASSERT_EQ(withAuth.v1.cipherTextDetails.baseCipherId, cipherKey->getBaseCipherId()); ASSERT_EQ(withAuth.v1.cipherTextDetails.salt, cipherKey->getSalt()); ASSERT_EQ(withAuth.v1.cipherHeaderDetails.encryptDomainId, headerCipherKey->getDomainId()); ASSERT_EQ(withAuth.v1.cipherHeaderDetails.baseCipherId, headerCipherKey->getBaseCipherId()); ASSERT_EQ(withAuth.v1.cipherHeaderDetails.salt, headerCipherKey->getSalt()); Reference tCipherKey = cipherKeyCache->getCipherKey(withAuth.v1.cipherTextDetails.encryptDomainId, withAuth.v1.cipherTextDetails.baseCipherId, withAuth.v1.cipherTextDetails.salt); Reference hCipherKey = cipherKeyCache->getCipherKey(withAuth.v1.cipherHeaderDetails.encryptDomainId, withAuth.v1.cipherHeaderDetails.baseCipherId, withAuth.v1.cipherHeaderDetails.salt); ASSERT(tCipherKey->isEqual(cipherKey)); ASSERT(hCipherKey->isEqual(headerCipherKey)); DecryptBlobCipherAes256Ctr decryptor(tCipherKey, hCipherKey, &withAuth.v1.iv[0], BlobCipherMetrics::TEST); StringRef decryptedBuf = decryptor.decrypt(encryptedBuf.begin(), bufLen, headerRef, arena); ASSERT_EQ(decryptedBuf.size(), bufLen); ASSERT_EQ(memcmp(decryptedBuf.begin(), &orgData[0], bufLen), 0); TraceEvent("TestConfigurableEncryptSingleAuthDecryptDone") .detail("HeaderFlagsVersion", headerRef.flagsVersion()) .detail("AlgoHeaderVersion", headerRef.algoHeaderVersion()) .detail("HeaderEncryptMode", flags.encryptMode) .detail("HeaderEncryptAuthTokenMode", flags.authTokenMode) .detail("HeaderEncryptAuthTokenAlgo", flags.authTokenAlgo) .detail("TextDomainId", withAuth.v1.cipherTextDetails.encryptDomainId) .detail("TextBaseCipherId", withAuth.v1.cipherTextDetails.baseCipherId) .detail("TextSalt", withAuth.v1.cipherTextDetails.salt) .detail("HeaderDomainId", withAuth.v1.cipherHeaderDetails.encryptDomainId) .detail("HeaderBaseCipherId", withAuth.v1.cipherHeaderDetails.baseCipherId) .detail("HeaderSalt", withAuth.v1.cipherHeaderDetails.salt); // induce encryption header corruption - encryptionMode corrupted BlobCipherEncryptHeaderRef corruptedHeaderRef = BlobCipherEncryptHeaderRef(headerRef); BlobCipherEncryptHeaderFlagsV1 corruptedFlags = std::get(headerRef.flags); corruptedFlags.encryptMode += 1; corruptedHeaderRef.flags = corruptedFlags; try { encryptedBuf = encryptor.encrypt(&orgData[0], bufLen, &headerRef, arena); DecryptBlobCipherAes256Ctr decryptor(tCipherKey, hCipherKey, &iv[0], BlobCipherMetrics::TEST); decryptedBuf = decryptor.decrypt(encryptedBuf.begin(), bufLen, corruptedHeaderRef, arena); ASSERT(false); // error expected } catch (Error& e) { if (e.code() != error_code_encrypt_header_metadata_mismatch) { throw; } TraceEvent("TestConfigurableEncryptionCorruptEncryptModeDone").detail("Mode", authAlgoStr); } // induce encrypted buffer payload corruption try { encryptedBuf = encryptor.encrypt(&orgData[0], bufLen, &headerRef, arena); uint8_t temp[bufLen]; deterministicRandom()->randomBytes(temp, bufLen); memcpy((void*)encryptedBuf.begin(), &temp[0], bufLen); int tIdx = deterministicRandom()->randomInt(0, bufLen - 1); temp[tIdx] += 1; DecryptBlobCipherAes256Ctr decryptor(tCipherKey, hCipherKey, &iv[0], BlobCipherMetrics::TEST); decryptedBuf = decryptor.decrypt(&temp[0], bufLen, headerRef, arena); ASSERT_NE(memcmp(decryptedBuf.begin(), &orgData[0], bufLen), 0); } catch (Error& e) { if (e.code() != error_code_encrypt_header_authtoken_mismatch) { throw; } TraceEvent("TestConfigurableEncryptionCorruptPayloadDone").detail("Mode", authAlgoStr); } // induce baseCipher payload corruption try { const bool corruptTextCipher = deterministicRandom()->coinflip(); encryptedBuf = encryptor.encrypt(&orgData[0], bufLen, &headerRef, arena); if (corruptTextCipher) { Reference corruptedCipher = corruptCipherKey(tCipherKey); DecryptBlobCipherAes256Ctr decryptor(corruptedCipher, hCipherKey, &iv[0], BlobCipherMetrics::TEST); decryptedBuf = decryptor.decrypt(encryptedBuf.begin(), bufLen, headerRef, arena); } else { Reference corruptedCipher = corruptCipherKey(hCipherKey); DecryptBlobCipherAes256Ctr decryptor(tCipherKey, corruptedCipher, &iv[0], BlobCipherMetrics::TEST); decryptedBuf = decryptor.decrypt(encryptedBuf.begin(), bufLen, headerRef, arena); } ASSERT(false); // error expected } catch (Error& e) { ASSERT_EQ(e.code(), error_code_encrypt_key_check_value_mismatch); TraceEvent("TestConfigurableEncryptionBaseCipherCorruptionDone").detail("Mode", authAlgoStr); } TraceEvent("TestSingleAuthTokenConfigurableEncryptionEnd").detail("Mode", authAlgoStr); } void testKeyCacheCleanup(const int minDomainId, const int maxDomainId) { TraceEvent("BlobCipherTestKeyCacheCleanupStart"); Reference cipherKeyCache = BlobCipherKeyCache::getInstance(); // Validate dropping encryptDomainId cached keys const EncryptCipherDomainId candidate = deterministicRandom()->randomInt(minDomainId, maxDomainId); cipherKeyCache->resetEncryptDomainId(candidate); std::vector> cachedKeys = cipherKeyCache->getAllCiphers(candidate); ASSERT(cachedKeys.empty()); // Validate dropping all cached cipherKeys cipherKeyCache->cleanup(); for (int dId = minDomainId; dId < maxDomainId; dId++) { std::vector> cachedKeys = cipherKeyCache->getAllCiphers(dId); ASSERT(cachedKeys.empty()); } TraceEvent("BlobCipherTestKeyCacheCleanupDone"); } void testEncryptInplaceNoAuthMode(const int minDomainId) { TraceEvent("EncryptInplaceStart"); Reference cipherKeyCache = BlobCipherKeyCache::getInstance(); // Validate Encryption ops Reference cipherKey = cipherKeyCache->getLatestCipherKey(minDomainId); Reference headerCipherKey = cipherKeyCache->getLatestCipherKey(ENCRYPT_HEADER_DOMAIN_ID); const int bufLen = deterministicRandom()->randomInt(786, 2127) + 512; // allocate the data align with AES_BLOCK_SIZE, encryption starts from orgData[1] so it's not aligned. alignas(AES_BLOCK_SIZE) uint8_t orgData[bufLen + 1]; uint8_t* plaintext = &orgData[1]; deterministicRandom()->randomBytes(plaintext, bufLen); uint8_t dataClone[bufLen]; memcpy(dataClone, plaintext, bufLen); Arena arena; uint8_t iv[AES_256_IV_LENGTH]; deterministicRandom()->randomBytes(&iv[0], AES_256_IV_LENGTH); EncryptBlobCipherAes265Ctr encryptor(cipherKey, headerCipherKey, iv, AES_256_IV_LENGTH, EncryptAuthTokenMode::ENCRYPT_HEADER_AUTH_TOKEN_MODE_NONE, BlobCipherMetrics::TEST); BlobCipherEncryptHeaderRef headerRef; encryptor.encryptInplace(plaintext, bufLen, &headerRef); // validate header version details AesCtrNoAuth noAuth = std::get(headerRef.algoHeader); Reference tCipherKey = cipherKeyCache->getCipherKey(noAuth.v1.cipherTextDetails.encryptDomainId, noAuth.v1.cipherTextDetails.baseCipherId, noAuth.v1.cipherTextDetails.salt); ASSERT(tCipherKey->isEqual(cipherKey)); DecryptBlobCipherAes256Ctr decryptor( tCipherKey, Reference(), &noAuth.v1.iv[0], BlobCipherMetrics::TEST); decryptor.decryptInplace(plaintext, bufLen, headerRef); ASSERT_EQ(memcmp(dataClone, plaintext, bufLen), 0); TraceEvent("EncryptInplaceDone"); } template void testEncryptInplaceSingleAuthMode(const int minDomainId) { constexpr bool isHmac = std::is_same_v; const std::string authAlgoStr = isHmac ? "HMAC-SHA" : "AES-CMAC"; const EncryptAuthTokenAlgo authAlgo = isHmac ? EncryptAuthTokenAlgo::ENCRYPT_HEADER_AUTH_TOKEN_ALGO_HMAC_SHA : EncryptAuthTokenAlgo::ENCRYPT_HEADER_AUTH_TOKEN_ALGO_AES_CMAC; TraceEvent("BlobCipherTestEncryptInplaceSingleAuthStart").detail("Mode", authAlgoStr); Reference cipherKeyCache = BlobCipherKeyCache::getInstance(); // Validate Encryption ops Reference cipherKey = cipherKeyCache->getLatestCipherKey(minDomainId); Reference headerCipherKey = cipherKeyCache->getLatestCipherKey(ENCRYPT_HEADER_DOMAIN_ID); const int bufLen = deterministicRandom()->randomInt(786, 2127) + 512; Arena arena; uint8_t iv[AES_256_IV_LENGTH]; deterministicRandom()->randomBytes(&iv[0], AES_256_IV_LENGTH); uint8_t orgData[bufLen + 100]; memset(orgData + bufLen, 0, 100); deterministicRandom()->randomBytes(&orgData[0], bufLen); uint8_t dataClone[bufLen]; memcpy(dataClone, orgData, bufLen); EncryptBlobCipherAes265Ctr encryptor(cipherKey, headerCipherKey, iv, AES_256_IV_LENGTH, EncryptAuthTokenMode::ENCRYPT_HEADER_AUTH_TOKEN_MODE_SINGLE, authAlgo, BlobCipherMetrics::TEST); BlobCipherEncryptHeader header; encryptor.encryptInplace(&orgData[0], bufLen, &header); uint8_t empty_buff[100]; memset(empty_buff, 0, 100); Reference tCipherKey = cipherKeyCache->getCipherKey( header.cipherTextDetails.encryptDomainId, header.cipherTextDetails.baseCipherId, header.cipherTextDetails.salt); Reference hCipherKey = cipherKeyCache->getCipherKey(header.cipherHeaderDetails.encryptDomainId, header.cipherHeaderDetails.baseCipherId, header.cipherHeaderDetails.salt); DecryptBlobCipherAes256Ctr decryptor(tCipherKey, hCipherKey, header.iv, BlobCipherMetrics::TEST); decryptor.decryptInplace(&orgData[0], bufLen, header); ASSERT_EQ(memcmp(dataClone, &orgData[0], bufLen), 0); TraceEvent("BlobCipherTestEncryptInplaceSingleAuthEnd").detail("Mode", authAlgoStr); } void testConfigurableEncryptionInvalidEncryptionKeyNoAuth(const int minDomainId) { TraceEvent("TestConfigurableEncryptionInvalidEncryptKeyNoAuthStart"); Reference cipherKeyCache = BlobCipherKeyCache::getInstance(); // Validate Encryption ops Reference cipherKey = cipherKeyCache->getLatestCipherKey(minDomainId); Reference headerCipherKey = cipherKeyCache->getLatestCipherKey(ENCRYPT_HEADER_DOMAIN_ID); const int bufLen = deterministicRandom()->randomInt(786, 2127) + 512; uint8_t orgData[bufLen]; deterministicRandom()->randomBytes(&orgData[0], bufLen); Arena arena; uint8_t iv[AES_256_IV_LENGTH]; deterministicRandom()->randomBytes(&iv[0], AES_256_IV_LENGTH); EncryptBlobCipherAes265Ctr encryptor(cipherKey, headerCipherKey, iv, AES_256_IV_LENGTH, EncryptAuthTokenMode::ENCRYPT_HEADER_AUTH_TOKEN_MODE_NONE, BlobCipherMetrics::TEST); BlobCipherEncryptHeaderRef headerRef; StringRef encryptedBuf = encryptor.encrypt(&orgData[0], bufLen, &headerRef, arena); // Test scenario where 'encryption key' with which the data was encrypted is 'different' from the one decryption // gets attempted AesCtrNoAuth noAuth = std::get(headerRef.algoHeader); Reference tCipherKey = makeReference(cipherKey->getDomainId(), cipherKey->getBaseCipherId(), cipherKey->rawBaseCipher(), cipherKey->getBaseCipherLen(), cipherKey->getBaseCipherKCV(), cipherKey->getRefreshAtTS(), cipherKey->getExpireAtTS()); // BlobCipherKey uses unique random salt to ensure generated encryption-keys are different ASSERT(!tCipherKey->isEqual(cipherKey)); DecryptBlobCipherAes256Ctr decryptor( tCipherKey, Reference(), &noAuth.v1.iv[0], BlobCipherMetrics::TEST); try { StringRef decryptedBuf = decryptor.decrypt(encryptedBuf.begin(), encryptedBuf.size(), headerRef, arena); ASSERT_EQ(decryptedBuf.size(), bufLen); if (g_network->isSimulated() && ENABLE_MUTATION_TRACKING_WITH_BLOB_CIPHER) ASSERT_EQ(memcmp(decryptedBuf.begin(), &orgData[0], bufLen), 0); else ASSERT_NE(memcmp(decryptedBuf.begin(), &orgData[0], bufLen), 0); } catch (Error& e) { // underlying layer 'may' throw exception TraceEvent("InvalidEncryptKeyError").error(e); } TraceEvent("TestConfigurableEncryptionInvalidEncryptKeyNoAuthEnd"); } template void testConfigurableEncryptionInvalidEncryptKeySingleAuthMode(const int minDomainId) { constexpr bool isHmac = std::is_same_v; const std::string authAlgoStr = isHmac ? "HMAC-SHA" : "AES-CMAC"; const EncryptAuthTokenAlgo authAlgo = isHmac ? EncryptAuthTokenAlgo::ENCRYPT_HEADER_AUTH_TOKEN_ALGO_HMAC_SHA : EncryptAuthTokenAlgo::ENCRYPT_HEADER_AUTH_TOKEN_ALGO_AES_CMAC; const int algoHeaderVersion = isHmac ? CLIENT_KNOBS->ENCRYPT_HEADER_AES_CTR_HMAC_SHA_AUTH_VERSION : CLIENT_KNOBS->ENCRYPT_HEADER_AES_CTR_AES_CMAC_AUTH_VERSION; TraceEvent("TestConfigurableEncryptionSingleAuthStart").detail("Mode", authAlgoStr); Reference cipherKeyCache = BlobCipherKeyCache::getInstance(); // Validate Encryption ops Reference cipherKey = cipherKeyCache->getLatestCipherKey(minDomainId); Reference headerCipherKey = cipherKeyCache->getLatestCipherKey(ENCRYPT_HEADER_DOMAIN_ID); const int bufLen = deterministicRandom()->randomInt(786, 2127) + 512; Arena arena; uint8_t iv[AES_256_IV_LENGTH]; deterministicRandom()->randomBytes(&iv[0], AES_256_IV_LENGTH); uint8_t orgData[bufLen]; deterministicRandom()->randomBytes(&orgData[0], bufLen); EncryptBlobCipherAes265Ctr encryptor(cipherKey, headerCipherKey, iv, AES_256_IV_LENGTH, EncryptAuthTokenMode::ENCRYPT_HEADER_AUTH_TOKEN_MODE_SINGLE, authAlgo, BlobCipherMetrics::TEST); BlobCipherEncryptHeaderRef headerRef; StringRef encryptedBuf = encryptor.encrypt(&orgData[0], bufLen, &headerRef, arena); ASSERT_EQ(encryptedBuf.size(), bufLen); if (g_network->isSimulated() && ENABLE_MUTATION_TRACKING_WITH_BLOB_CIPHER) ASSERT_EQ(memcmp(&orgData[0], encryptedBuf.begin(), bufLen), 0); else ASSERT_NE(memcmp(&orgData[0], encryptedBuf.begin(), bufLen), 0); ASSERT_EQ(headerRef.flagsVersion(), CLIENT_KNOBS->ENCRYPT_HEADER_FLAGS_VERSION); ASSERT_EQ(headerRef.algoHeaderVersion(), algoHeaderVersion); // validate flags BlobCipherEncryptHeaderFlagsV1 flags = std::get(headerRef.flags); ASSERT_EQ(flags.encryptMode, EncryptCipherMode::ENCRYPT_CIPHER_MODE_AES_256_CTR); ASSERT_EQ(flags.authTokenMode, EncryptAuthTokenMode::ENCRYPT_HEADER_AUTH_TOKEN_MODE_SINGLE); ASSERT_EQ(flags.authTokenAlgo, authAlgo); // validate IV AesCtrWithAuth withAuth = std::get>(headerRef.algoHeader); ASSERT_EQ(memcmp(&iv[0], &withAuth.v1.iv[0], AES_256_IV_LENGTH), 0); if (g_network->isSimulated() && ENABLE_MUTATION_TRACKING_WITH_BLOB_CIPHER) ASSERT_EQ(memcmp(&orgData[0], encryptedBuf.begin(), bufLen), 0); else ASSERT_NE(memcmp(&orgData[0], encryptedBuf.begin(), bufLen), 0); // validate cipherKey details ASSERT_EQ(withAuth.v1.cipherTextDetails.encryptDomainId, cipherKey->getDomainId()); ASSERT_EQ(withAuth.v1.cipherTextDetails.baseCipherId, cipherKey->getBaseCipherId()); ASSERT_EQ(withAuth.v1.cipherTextDetails.salt, cipherKey->getSalt()); ASSERT_EQ(withAuth.v1.cipherHeaderDetails.encryptDomainId, headerCipherKey->getDomainId()); ASSERT_EQ(withAuth.v1.cipherHeaderDetails.baseCipherId, headerCipherKey->getBaseCipherId()); ASSERT_EQ(withAuth.v1.cipherHeaderDetails.salt, headerCipherKey->getSalt()); Reference tCipherKey = cipherKeyCache->getCipherKey(withAuth.v1.cipherTextDetails.encryptDomainId, withAuth.v1.cipherTextDetails.baseCipherId, withAuth.v1.cipherTextDetails.salt); Reference hCipherKey = cipherKeyCache->getCipherKey(withAuth.v1.cipherHeaderDetails.encryptDomainId, withAuth.v1.cipherHeaderDetails.baseCipherId, withAuth.v1.cipherHeaderDetails.salt); ASSERT(tCipherKey->isEqual(cipherKey)); ASSERT(hCipherKey->isEqual(headerCipherKey)); try { // Switch text & header cipher keys to simulate decryption using invalid encryption keys DecryptBlobCipherAes256Ctr decryptor(hCipherKey, tCipherKey, &withAuth.v1.iv[0], BlobCipherMetrics::TEST); StringRef decryptedBuf = decryptor.decrypt(encryptedBuf.begin(), bufLen, headerRef, arena); ASSERT_EQ(decryptedBuf.size(), bufLen); ASSERT_NE(memcmp(decryptedBuf.begin(), &orgData[0], bufLen), 0); } catch (Error& e) { // underlying layer 'may' throw exception TraceEvent("InvalidEncryptKeyError").error(e); } TraceEvent("TestConfigurableEncryptionInvalidEncryptKeySingleAuthTokenEnd").detail("Mode", authAlgoStr); } TEST_CASE("/blobCipher") { DomainKeyMap domainKeyMap; const EncryptCipherDomainId minDomainId = 1; const EncryptCipherDomainId maxDomainId = deterministicRandom()->randomInt(minDomainId, minDomainId + 10) + 5; const EncryptCipherBaseKeyId minBaseCipherKeyId = 100; const EncryptCipherBaseKeyId maxBaseCipherKeyId = deterministicRandom()->randomInt(minBaseCipherKeyId, minBaseCipherKeyId + 50) + 15; for (int dId = minDomainId; dId <= maxDomainId; dId++) { for (int kId = minBaseCipherKeyId; kId <= maxBaseCipherKeyId; kId++) { domainKeyMap[dId].emplace( kId, makeReference( dId, kId, std::numeric_limits::max(), std::numeric_limits::max())); } } ASSERT_EQ(domainKeyMap.size(), maxDomainId); testMaxBaseCipherLen(); testKeyCacheEssentials(domainKeyMap, minDomainId, maxDomainId, minBaseCipherKeyId); testKeyCacheRefreshExpireCipherKey(domainKeyMap, maxDomainId); testConfigurableEncryptionBlobCipherHeaderFlagsV1Ser(); testConfigurableEncryptionAesCtrNoAuthV1Ser(minDomainId); testConfigurableEncryptionAesCtrWithAuthSer(minDomainId); testConfigurableEncryptionAesCtrWithAuthSer(minDomainId); testConfigurableEncryptionHeaderNoAuthMode(minDomainId); testConfigurableEncryptionHeaderSingleAuthMode(minDomainId); testConfigurableEncryptionHeaderSingleAuthMode(minDomainId); testNoAuthMode(minDomainId); testSingleAuthMode(minDomainId); testSingleAuthMode(minDomainId); testConfigurableEncryptionNoAuthMode(minDomainId); testConfigurableEncryptionSingleAuthMode(minDomainId); testConfigurableEncryptionSingleAuthMode(minDomainId); testConfigurableEncryptionInvalidEncryptionKeyNoAuth(minDomainId); testConfigurableEncryptionInvalidEncryptKeySingleAuthMode(minDomainId); testConfigurableEncryptionInvalidEncryptKeySingleAuthMode(minDomainId); testEncryptInplaceNoAuthMode(minDomainId); testEncryptInplaceSingleAuthMode(minDomainId); testEncryptInplaceSingleAuthMode(minDomainId); testKeyCacheCleanup(minDomainId, maxDomainId); return Void(); }