mirror of
https://github.com/apple/foundationdb.git
synced 2025-05-14 01:42:37 +08:00
Description Major changes proposed: 1. This reverts commit f38b2e82098f432bc4437f823c5c1dafd52e1319. 2. Also add fix for Valgrind failure due to unintialized variables. 3. Improve checks to catch is cipherKey details cached in BlobCipherKeyCache isn't as expected Testing Overall correctness: 10K (20220421-193911-ahusain-foundationdb-a730e5cb38541e20) EncyrptionOps correctness: 100K (20220421-194315-ahusain-foundationdb-29c598a8b9420430) EncryptionOps Valgrind: 100 (20220421-194434-ahusain-foundationdb-7fc5f98eddc0921a)
1191 lines
50 KiB
C++
1191 lines
50 KiB
C++
/*
|
|
* BlobCipher.cpp
|
|
*
|
|
* This source file is part of the FoundationDB open source project
|
|
*
|
|
* Copyright 2013-2022 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 "flow/BlobCipher.h"
|
|
|
|
#include "flow/EncryptUtils.h"
|
|
#include "flow/Knobs.h"
|
|
#include "flow/Error.h"
|
|
#include "flow/FastRef.h"
|
|
#include "flow/IRandom.h"
|
|
#include "flow/ITrace.h"
|
|
#include "flow/network.h"
|
|
#include "flow/Trace.h"
|
|
#include "flow/UnitTest.h"
|
|
|
|
#include <cstring>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <utility>
|
|
|
|
#if ENCRYPTION_ENABLED
|
|
|
|
namespace {
|
|
bool isEncryptHeaderAuthTokenModeValid(const EncryptAuthTokenMode mode) {
|
|
return mode >= ENCRYPT_HEADER_AUTH_TOKEN_MODE_NONE && mode < ENCRYPT_HEADER_AUTH_TOKEN_LAST;
|
|
}
|
|
} // namespace
|
|
|
|
// BlobCipherKey class methods
|
|
|
|
BlobCipherKey::BlobCipherKey(const EncryptCipherDomainId& domainId,
|
|
const EncryptCipherBaseKeyId& baseCiphId,
|
|
const uint8_t* baseCiph,
|
|
int baseCiphLen) {
|
|
EncryptCipherRandomSalt salt;
|
|
if (g_network->isSimulated()) {
|
|
salt = deterministicRandom()->randomUInt64();
|
|
} else {
|
|
salt = nondeterministicRandom()->randomUInt64();
|
|
}
|
|
initKey(domainId, baseCiph, baseCiphLen, baseCiphId, salt);
|
|
}
|
|
|
|
BlobCipherKey::BlobCipherKey(const EncryptCipherDomainId& domainId,
|
|
const EncryptCipherBaseKeyId& baseCiphId,
|
|
const uint8_t* baseCiph,
|
|
int baseCiphLen,
|
|
const EncryptCipherRandomSalt& salt) {
|
|
initKey(domainId, baseCiph, baseCiphLen, baseCiphId, salt);
|
|
}
|
|
|
|
void BlobCipherKey::initKey(const EncryptCipherDomainId& domainId,
|
|
const uint8_t* baseCiph,
|
|
int baseCiphLen,
|
|
const EncryptCipherBaseKeyId& baseCiphId,
|
|
const EncryptCipherRandomSalt& salt) {
|
|
// Set the base encryption key properties
|
|
baseCipher = std::make_unique<uint8_t[]>(AES_256_KEY_LENGTH);
|
|
memset(baseCipher.get(), 0, AES_256_KEY_LENGTH);
|
|
memcpy(baseCipher.get(), baseCiph, std::min<int>(baseCiphLen, AES_256_KEY_LENGTH));
|
|
baseCipherLen = baseCiphLen;
|
|
baseCipherId = baseCiphId;
|
|
// Set the encryption domain for the base encryption key
|
|
encryptDomainId = domainId;
|
|
randomSalt = salt;
|
|
// derive the encryption key
|
|
cipher = std::make_unique<uint8_t[]>(AES_256_KEY_LENGTH);
|
|
memset(cipher.get(), 0, AES_256_KEY_LENGTH);
|
|
applyHmacSha256Derivation();
|
|
// update the key creation time
|
|
creationTime = now();
|
|
|
|
TraceEvent("BlobCipherKey")
|
|
.detail("DomainId", domainId)
|
|
.detail("BaseCipherId", baseCipherId)
|
|
.detail("BaseCipherLen", baseCipherLen)
|
|
.detail("RandomSalt", randomSalt)
|
|
.detail("CreationTime", creationTime);
|
|
}
|
|
|
|
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);
|
|
StringRef digest = hmacGen.digest(&buf[0], baseCipherLen + sizeof(EncryptCipherRandomSalt), arena);
|
|
std::copy(digest.begin(), digest.end(), cipher.get());
|
|
if (digest.size() < AES_256_KEY_LENGTH) {
|
|
memcpy(cipher.get() + digest.size(), buf, AES_256_KEY_LENGTH - digest.size());
|
|
}
|
|
}
|
|
|
|
void BlobCipherKey::reset() {
|
|
memset(baseCipher.get(), 0, baseCipherLen);
|
|
memset(cipher.get(), 0, AES_256_KEY_LENGTH);
|
|
}
|
|
|
|
// BlobKeyIdCache class methods
|
|
|
|
BlobCipherKeyIdCache::BlobCipherKeyIdCache()
|
|
: domainId(ENCRYPT_INVALID_DOMAIN_ID), latestBaseCipherKeyId(ENCRYPT_INVALID_CIPHER_KEY_ID),
|
|
latestRandomSalt(ENCRYPT_INVALID_RANDOM_SALT) {}
|
|
|
|
BlobCipherKeyIdCache::BlobCipherKeyIdCache(EncryptCipherDomainId dId)
|
|
: domainId(dId), latestBaseCipherKeyId(ENCRYPT_INVALID_CIPHER_KEY_ID), latestRandomSalt(ENCRYPT_INVALID_RANDOM_SALT) {
|
|
TraceEvent("Init_BlobCipherKeyIdCache").detail("DomainId", domainId);
|
|
}
|
|
|
|
BlobCipherKeyIdCacheKey BlobCipherKeyIdCache::getCacheKey(const EncryptCipherBaseKeyId& baseCipherKeyId,
|
|
const EncryptCipherRandomSalt& salt) {
|
|
if (baseCipherKeyId == ENCRYPT_INVALID_CIPHER_KEY_ID || salt == ENCRYPT_INVALID_RANDOM_SALT) {
|
|
throw encrypt_invalid_id();
|
|
}
|
|
return std::make_pair(baseCipherKeyId, salt);
|
|
}
|
|
|
|
Reference<BlobCipherKey> BlobCipherKeyIdCache::getLatestCipherKey() {
|
|
if (keyIdCache.empty()) {
|
|
// Cache is empty, nothing more to do.
|
|
throw encrypt_key_not_found();
|
|
}
|
|
|
|
// Ensure latestCipher details sanity
|
|
ASSERT_GT(latestBaseCipherKeyId, ENCRYPT_INVALID_CIPHER_KEY_ID);
|
|
ASSERT_GT(latestRandomSalt, ENCRYPT_INVALID_RANDOM_SALT);
|
|
|
|
return getCipherByBaseCipherId(latestBaseCipherKeyId, latestRandomSalt);
|
|
}
|
|
|
|
Reference<BlobCipherKey> BlobCipherKeyIdCache::getCipherByBaseCipherId(const EncryptCipherBaseKeyId& baseCipherKeyId,
|
|
const EncryptCipherRandomSalt& salt) {
|
|
BlobCipherKeyIdCacheMapCItr itr = keyIdCache.find(getCacheKey(baseCipherKeyId, salt));
|
|
if (itr == keyIdCache.end()) {
|
|
TraceEvent("CipherByBaseCipherId_KeyMissing")
|
|
.detail("DomainId", domainId)
|
|
.detail("BaseCipherId", baseCipherKeyId)
|
|
.detail("Salt", salt);
|
|
throw encrypt_key_not_found();
|
|
}
|
|
return itr->second;
|
|
}
|
|
|
|
void BlobCipherKeyIdCache::insertBaseCipherKey(const EncryptCipherBaseKeyId& baseCipherId,
|
|
const uint8_t* baseCipher,
|
|
int baseCipherLen) {
|
|
ASSERT_GT(baseCipherId, ENCRYPT_INVALID_CIPHER_KEY_ID);
|
|
|
|
// BaseCipherKeys are immutable, given the routine invocation updates 'latestCipher',
|
|
// ensure no key-tampering is done
|
|
try {
|
|
Reference<BlobCipherKey> cipherKey = getLatestCipherKey();
|
|
if (cipherKey.isValid() && cipherKey->getBaseCipherId() == baseCipherId) {
|
|
if (memcmp(cipherKey->rawBaseCipher(), baseCipher, baseCipherLen) == 0) {
|
|
TraceEvent("InsertBaseCipherKey_AlreadyPresent")
|
|
.detail("BaseCipherKeyId", baseCipherId)
|
|
.detail("DomainId", domainId);
|
|
// Key is already present; nothing more to do.
|
|
return;
|
|
} else {
|
|
TraceEvent("InsertBaseCipherKey_UpdateCipher")
|
|
.detail("BaseCipherKeyId", baseCipherId)
|
|
.detail("DomainId", domainId);
|
|
throw encrypt_update_cipher();
|
|
}
|
|
}
|
|
} catch (Error& e) {
|
|
if (e.code() != error_code_encrypt_key_not_found) {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
Reference<BlobCipherKey> cipherKey =
|
|
makeReference<BlobCipherKey>(domainId, baseCipherId, baseCipher, baseCipherLen);
|
|
BlobCipherKeyIdCacheKey cacheKey = getCacheKey(cipherKey->getBaseCipherId(), cipherKey->getSalt());
|
|
keyIdCache.emplace(cacheKey, cipherKey);
|
|
|
|
// Update the latest BaseCipherKeyId for the given encryption domain
|
|
latestBaseCipherKeyId = baseCipherId;
|
|
latestRandomSalt = cipherKey->getSalt();
|
|
}
|
|
|
|
void BlobCipherKeyIdCache::insertBaseCipherKey(const EncryptCipherBaseKeyId& baseCipherId,
|
|
const uint8_t* baseCipher,
|
|
int baseCipherLen,
|
|
const EncryptCipherRandomSalt& salt) {
|
|
ASSERT_GT(baseCipherId, ENCRYPT_INVALID_CIPHER_KEY_ID);
|
|
ASSERT_GT(salt, ENCRYPT_INVALID_RANDOM_SALT);
|
|
|
|
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) {
|
|
TraceEvent("InsertBaseCipherKey_AlreadyPresent")
|
|
.detail("BaseCipherKeyId", baseCipherId)
|
|
.detail("DomainId", domainId);
|
|
// Key is already present; nothing more to do.
|
|
return;
|
|
} else {
|
|
TraceEvent("InsertBaseCipherKey_UpdateCipher")
|
|
.detail("BaseCipherKeyId", baseCipherId)
|
|
.detail("DomainId", domainId);
|
|
throw encrypt_update_cipher();
|
|
}
|
|
}
|
|
|
|
Reference<BlobCipherKey> cipherKey =
|
|
makeReference<BlobCipherKey>(domainId, baseCipherId, baseCipher, baseCipherLen, salt);
|
|
keyIdCache.emplace(cacheKey, cipherKey);
|
|
}
|
|
|
|
void BlobCipherKeyIdCache::cleanup() {
|
|
for (auto& keyItr : keyIdCache) {
|
|
keyItr.second->reset();
|
|
}
|
|
|
|
keyIdCache.clear();
|
|
}
|
|
|
|
std::vector<Reference<BlobCipherKey>> BlobCipherKeyIdCache::getAllCipherKeys() {
|
|
std::vector<Reference<BlobCipherKey>> cipherKeys;
|
|
for (auto& keyItr : keyIdCache) {
|
|
cipherKeys.push_back(keyItr.second);
|
|
}
|
|
return cipherKeys;
|
|
}
|
|
|
|
// BlobCipherKeyCache class methods
|
|
|
|
void BlobCipherKeyCache::insertCipherKey(const EncryptCipherDomainId& domainId,
|
|
const EncryptCipherBaseKeyId& baseCipherId,
|
|
const uint8_t* baseCipher,
|
|
int baseCipherLen) {
|
|
if (domainId == ENCRYPT_INVALID_DOMAIN_ID || baseCipherId == ENCRYPT_INVALID_CIPHER_KEY_ID) {
|
|
throw encrypt_invalid_id();
|
|
}
|
|
|
|
try {
|
|
auto domainItr = domainCacheMap.find(domainId);
|
|
if (domainItr == domainCacheMap.end()) {
|
|
// Add mapping to track new encryption domain
|
|
Reference<BlobCipherKeyIdCache> keyIdCache = makeReference<BlobCipherKeyIdCache>(domainId);
|
|
keyIdCache->insertBaseCipherKey(baseCipherId, baseCipher, baseCipherLen);
|
|
domainCacheMap.emplace(domainId, keyIdCache);
|
|
} else {
|
|
// Track new baseCipher keys
|
|
Reference<BlobCipherKeyIdCache> keyIdCache = domainItr->second;
|
|
keyIdCache->insertBaseCipherKey(baseCipherId, baseCipher, baseCipherLen);
|
|
}
|
|
|
|
TraceEvent("InsertCipherKey").detail("DomainId", domainId).detail("BaseCipherKeyId", baseCipherId);
|
|
} catch (Error& e) {
|
|
TraceEvent("InsertCipherKey_Failed").detail("BaseCipherKeyId", baseCipherId).detail("DomainId", domainId);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
void BlobCipherKeyCache::insertCipherKey(const EncryptCipherDomainId& domainId,
|
|
const EncryptCipherBaseKeyId& baseCipherId,
|
|
const uint8_t* baseCipher,
|
|
int baseCipherLen,
|
|
const EncryptCipherRandomSalt& salt) {
|
|
if (domainId == ENCRYPT_INVALID_DOMAIN_ID || baseCipherId == ENCRYPT_INVALID_CIPHER_KEY_ID ||
|
|
salt == ENCRYPT_INVALID_RANDOM_SALT) {
|
|
throw encrypt_invalid_id();
|
|
}
|
|
|
|
try {
|
|
auto domainItr = domainCacheMap.find(domainId);
|
|
if (domainItr == domainCacheMap.end()) {
|
|
// Add mapping to track new encryption domain
|
|
Reference<BlobCipherKeyIdCache> keyIdCache = makeReference<BlobCipherKeyIdCache>(domainId);
|
|
keyIdCache->insertBaseCipherKey(baseCipherId, baseCipher, baseCipherLen, salt);
|
|
domainCacheMap.emplace(domainId, keyIdCache);
|
|
} else {
|
|
// Track new baseCipher keys
|
|
Reference<BlobCipherKeyIdCache> keyIdCache = domainItr->second;
|
|
keyIdCache->insertBaseCipherKey(baseCipherId, baseCipher, baseCipherLen, salt);
|
|
}
|
|
|
|
TraceEvent("InsertCipherKey")
|
|
.detail("DomainId", domainId)
|
|
.detail("BaseCipherKeyId", baseCipherId)
|
|
.detail("Salt", salt);
|
|
} catch (Error& e) {
|
|
TraceEvent("InsertCipherKey_Failed")
|
|
.detail("BaseCipherKeyId", baseCipherId)
|
|
.detail("DomainId", domainId)
|
|
.detail("Salt", salt);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
Reference<BlobCipherKey> BlobCipherKeyCache::getLatestCipherKey(const EncryptCipherDomainId& domainId) {
|
|
auto domainItr = domainCacheMap.find(domainId);
|
|
if (domainItr == domainCacheMap.end()) {
|
|
TraceEvent("GetLatestCipherKey_DomainNotFound").detail("DomainId", domainId);
|
|
throw encrypt_key_not_found();
|
|
}
|
|
|
|
Reference<BlobCipherKeyIdCache> keyIdCache = domainItr->second;
|
|
Reference<BlobCipherKey> cipherKey = keyIdCache->getLatestCipherKey();
|
|
if ((now() - cipherKey->getCreationTime()) > FLOW_KNOBS->ENCRYPT_CIPHER_KEY_CACHE_TTL) {
|
|
TraceEvent("GetLatestCipherKey_ExpiredTTL")
|
|
.detail("DomainId", domainId)
|
|
.detail("BaseCipherId", cipherKey->getBaseCipherId());
|
|
throw encrypt_key_ttl_expired();
|
|
}
|
|
|
|
return cipherKey;
|
|
}
|
|
|
|
Reference<BlobCipherKey> BlobCipherKeyCache::getCipherKey(const EncryptCipherDomainId& domainId,
|
|
const EncryptCipherBaseKeyId& baseCipherId,
|
|
const EncryptCipherRandomSalt& salt) {
|
|
auto domainItr = domainCacheMap.find(domainId);
|
|
if (domainItr == domainCacheMap.end()) {
|
|
TraceEvent("GetCipherKey_MissingDomainId").detail("DomainId", domainId);
|
|
throw encrypt_key_not_found();
|
|
}
|
|
|
|
Reference<BlobCipherKeyIdCache> keyIdCache = domainItr->second;
|
|
return keyIdCache->getCipherByBaseCipherId(baseCipherId, salt);
|
|
}
|
|
|
|
void BlobCipherKeyCache::resetEncryptDomainId(const EncryptCipherDomainId domainId) {
|
|
auto domainItr = domainCacheMap.find(domainId);
|
|
if (domainItr == domainCacheMap.end()) {
|
|
throw encrypt_key_not_found();
|
|
}
|
|
|
|
Reference<BlobCipherKeyIdCache> keyIdCache = domainItr->second;
|
|
keyIdCache->cleanup();
|
|
TraceEvent("ResetEncryptDomainId").detail("DomainId", domainId);
|
|
}
|
|
|
|
void BlobCipherKeyCache::cleanup() noexcept {
|
|
Reference<BlobCipherKeyCache> instance = BlobCipherKeyCache::getInstance();
|
|
for (auto& domainItr : instance->domainCacheMap) {
|
|
Reference<BlobCipherKeyIdCache> keyIdCache = domainItr.second;
|
|
keyIdCache->cleanup();
|
|
TraceEvent("BlobCipherKeyCache_Cleanup").detail("DomainId", domainItr.first);
|
|
}
|
|
|
|
instance->domainCacheMap.clear();
|
|
}
|
|
|
|
std::vector<Reference<BlobCipherKey>> BlobCipherKeyCache::getAllCiphers(const EncryptCipherDomainId& domainId) {
|
|
auto domainItr = domainCacheMap.find(domainId);
|
|
if (domainItr == domainCacheMap.end()) {
|
|
return {};
|
|
}
|
|
|
|
Reference<BlobCipherKeyIdCache> keyIdCache = domainItr->second;
|
|
return keyIdCache->getAllCipherKeys();
|
|
}
|
|
|
|
// EncryptBlobCipherAes265Ctr class methods
|
|
|
|
EncryptBlobCipherAes265Ctr::EncryptBlobCipherAes265Ctr(Reference<BlobCipherKey> tCipherKey,
|
|
Reference<BlobCipherKey> hCipherKey,
|
|
const uint8_t* cipherIV,
|
|
const int ivLen,
|
|
const EncryptAuthTokenMode mode)
|
|
: ctx(EVP_CIPHER_CTX_new()), textCipherKey(tCipherKey), headerCipherKey(hCipherKey), authTokenMode(mode) {
|
|
ASSERT(isEncryptHeaderAuthTokenModeValid(mode));
|
|
ASSERT_EQ(ivLen, AES_256_IV_LENGTH);
|
|
|
|
memcpy(&iv[0], cipherIV, ivLen);
|
|
|
|
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(), cipherIV) != 1) {
|
|
throw encrypt_ops_error();
|
|
}
|
|
}
|
|
|
|
Reference<EncryptBuf> EncryptBlobCipherAes265Ctr::encrypt(const uint8_t* plaintext,
|
|
const int plaintextLen,
|
|
BlobCipherEncryptHeader* header,
|
|
Arena& arena) {
|
|
TEST(true); // Encrypting data with BlobCipher
|
|
|
|
memset(reinterpret_cast<uint8_t*>(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.
|
|
|
|
const int allocSize = authTokenMode == ENCRYPT_HEADER_AUTH_TOKEN_MODE_SINGLE
|
|
? plaintextLen + AES_BLOCK_SIZE + sizeof(BlobCipherEncryptHeader)
|
|
: plaintextLen + AES_BLOCK_SIZE;
|
|
Reference<EncryptBuf> encryptBuf = makeReference<EncryptBuf>(allocSize, arena);
|
|
uint8_t* ciphertext = encryptBuf->begin();
|
|
int bytes{ 0 };
|
|
if (EVP_EncryptUpdate(ctx, ciphertext, &bytes, plaintext, plaintextLen) != 1) {
|
|
TraceEvent("Encrypt_UpdateFailed")
|
|
.detail("BaseCipherId", textCipherKey->getBaseCipherId())
|
|
.detail("EncryptDomainId", textCipherKey->getDomainId());
|
|
throw encrypt_ops_error();
|
|
}
|
|
|
|
int finalBytes{ 0 };
|
|
if (EVP_EncryptFinal_ex(ctx, ciphertext + bytes, &finalBytes) != 1) {
|
|
TraceEvent("Encrypt_FinalFailed")
|
|
.detail("BaseCipherId", textCipherKey->getBaseCipherId())
|
|
.detail("EncryptDomainId", textCipherKey->getDomainId());
|
|
throw encrypt_ops_error();
|
|
}
|
|
|
|
if ((bytes + finalBytes) != plaintextLen) {
|
|
TraceEvent("Encrypt_UnexpectedCipherLen")
|
|
.detail("PlaintextLen", plaintextLen)
|
|
.detail("EncryptedBufLen", bytes + finalBytes);
|
|
throw encrypt_ops_error();
|
|
}
|
|
|
|
// 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;
|
|
|
|
// Populate cipherText encryption-key details
|
|
header->cipherTextDetails.baseCipherId = textCipherKey->getBaseCipherId();
|
|
header->cipherTextDetails.encryptDomainId = textCipherKey->getDomainId();
|
|
header->cipherTextDetails.salt = textCipherKey->getSalt();
|
|
memcpy(&header->cipherTextDetails.iv[0], &iv[0], AES_256_IV_LENGTH);
|
|
|
|
if (authTokenMode == ENCRYPT_HEADER_AUTH_TOKEN_MODE_NONE) {
|
|
// No header 'authToken' generation needed.
|
|
} else {
|
|
// Populate header encryption-key details
|
|
header->cipherHeaderDetails.encryptDomainId = headerCipherKey->getDomainId();
|
|
header->cipherHeaderDetails.baseCipherId = headerCipherKey->getBaseCipherId();
|
|
header->cipherHeaderDetails.salt = headerCipherKey->getSalt();
|
|
|
|
// Populate header authToken details
|
|
if (header->flags.authTokenMode == ENCRYPT_HEADER_AUTH_TOKEN_MODE_SINGLE) {
|
|
ASSERT_GE(allocSize, (bytes + finalBytes + sizeof(BlobCipherEncryptHeader)));
|
|
ASSERT_GE(encryptBuf->getLogicalSize(), (bytes + finalBytes + sizeof(BlobCipherEncryptHeader)));
|
|
|
|
memcpy(&ciphertext[bytes + finalBytes],
|
|
reinterpret_cast<const uint8_t*>(header),
|
|
sizeof(BlobCipherEncryptHeader));
|
|
StringRef authToken = computeAuthToken(ciphertext,
|
|
bytes + finalBytes + sizeof(BlobCipherEncryptHeader),
|
|
headerCipherKey->rawCipher(),
|
|
AES_256_KEY_LENGTH,
|
|
arena);
|
|
memcpy(&header->singleAuthToken.authToken[0], authToken.begin(), AUTH_TOKEN_SIZE);
|
|
} else {
|
|
ASSERT_EQ(header->flags.authTokenMode, ENCRYPT_HEADER_AUTH_TOKEN_MODE_MULTI);
|
|
|
|
StringRef cipherTextAuthToken =
|
|
computeAuthToken(ciphertext,
|
|
bytes + finalBytes,
|
|
reinterpret_cast<const uint8_t*>(&header->cipherTextDetails.salt),
|
|
sizeof(EncryptCipherRandomSalt),
|
|
arena);
|
|
memcpy(&header->multiAuthTokens.cipherTextAuthToken[0], cipherTextAuthToken.begin(), AUTH_TOKEN_SIZE);
|
|
StringRef headerAuthToken = computeAuthToken(reinterpret_cast<const uint8_t*>(header),
|
|
sizeof(BlobCipherEncryptHeader),
|
|
headerCipherKey->rawCipher(),
|
|
AES_256_KEY_LENGTH,
|
|
arena);
|
|
memcpy(&header->multiAuthTokens.headerAuthToken[0], headerAuthToken.begin(), AUTH_TOKEN_SIZE);
|
|
}
|
|
}
|
|
|
|
encryptBuf->setLogicalSize(plaintextLen);
|
|
return encryptBuf;
|
|
}
|
|
|
|
EncryptBlobCipherAes265Ctr::~EncryptBlobCipherAes265Ctr() {
|
|
if (ctx != nullptr) {
|
|
EVP_CIPHER_CTX_free(ctx);
|
|
}
|
|
}
|
|
|
|
// DecryptBlobCipherAes256Ctr class methods
|
|
|
|
DecryptBlobCipherAes256Ctr::DecryptBlobCipherAes256Ctr(Reference<BlobCipherKey> tCipherKey,
|
|
Reference<BlobCipherKey> hCipherKey,
|
|
const uint8_t* iv)
|
|
: ctx(EVP_CIPHER_CTX_new()), textCipherKey(tCipherKey), headerCipherKey(hCipherKey),
|
|
headerAuthTokenValidationDone(false), 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();
|
|
}
|
|
}
|
|
|
|
void DecryptBlobCipherAes256Ctr::verifyHeaderAuthToken(const BlobCipherEncryptHeader& header, Arena& arena) {
|
|
if (header.flags.authTokenMode != ENCRYPT_HEADER_AUTH_TOKEN_MODE_MULTI) {
|
|
// NoneAuthToken mode; no authToken is generated; nothing to do
|
|
// SingleAuthToken mode; verification will happen as part of decryption.
|
|
return;
|
|
}
|
|
|
|
ASSERT_EQ(header.flags.authTokenMode, ENCRYPT_HEADER_AUTH_TOKEN_MODE_MULTI);
|
|
|
|
BlobCipherEncryptHeader headerCopy;
|
|
memcpy(reinterpret_cast<uint8_t*>(&headerCopy),
|
|
reinterpret_cast<const uint8_t*>(&header),
|
|
sizeof(BlobCipherEncryptHeader));
|
|
memset(reinterpret_cast<uint8_t*>(&headerCopy.multiAuthTokens.headerAuthToken), 0, AUTH_TOKEN_SIZE);
|
|
StringRef computedHeaderAuthToken = computeAuthToken(reinterpret_cast<const uint8_t*>(&headerCopy),
|
|
sizeof(BlobCipherEncryptHeader),
|
|
headerCipherKey->rawCipher(),
|
|
AES_256_KEY_LENGTH,
|
|
arena);
|
|
if (memcmp(&header.multiAuthTokens.headerAuthToken[0], computedHeaderAuthToken.begin(), AUTH_TOKEN_SIZE) != 0) {
|
|
TraceEvent("VerifyEncryptBlobHeader_AuthTokenMismatch")
|
|
.detail("HeaderVersion", header.flags.headerVersion)
|
|
.detail("HeaderMode", header.flags.encryptMode)
|
|
.detail("MultiAuthHeaderAuthToken",
|
|
StringRef(arena, &header.multiAuthTokens.headerAuthToken[0], AUTH_TOKEN_SIZE).toString())
|
|
.detail("ComputedHeaderAuthToken", computedHeaderAuthToken.toString());
|
|
throw encrypt_header_authtoken_mismatch();
|
|
}
|
|
|
|
headerAuthTokenValidationDone = true;
|
|
}
|
|
|
|
void DecryptBlobCipherAes256Ctr::verifyHeaderSingleAuthToken(const uint8_t* ciphertext,
|
|
const int ciphertextLen,
|
|
const BlobCipherEncryptHeader& header,
|
|
uint8_t* buff,
|
|
Arena& arena) {
|
|
// Header authToken not set for single auth-token mode.
|
|
ASSERT(!headerAuthTokenValidationDone);
|
|
|
|
// prepare the payload {cipherText + encryptionHeader}
|
|
memcpy(&buff[0], ciphertext, ciphertextLen);
|
|
memcpy(&buff[ciphertextLen], reinterpret_cast<const uint8_t*>(&header), sizeof(BlobCipherEncryptHeader));
|
|
// ensure the 'authToken' is reset before computing the 'authentication token'
|
|
BlobCipherEncryptHeader* eHeader = (BlobCipherEncryptHeader*)(&buff[ciphertextLen]);
|
|
memset(reinterpret_cast<uint8_t*>(&eHeader->singleAuthToken), 0, 2 * AUTH_TOKEN_SIZE);
|
|
|
|
StringRef computed = computeAuthToken(
|
|
buff, ciphertextLen + sizeof(BlobCipherEncryptHeader), headerCipherKey->rawCipher(), AES_256_KEY_LENGTH, arena);
|
|
if (memcmp(&header.singleAuthToken.authToken[0], computed.begin(), AUTH_TOKEN_SIZE) != 0) {
|
|
TraceEvent("VerifyEncryptBlobHeader_AuthTokenMismatch")
|
|
.detail("HeaderVersion", header.flags.headerVersion)
|
|
.detail("HeaderMode", header.flags.encryptMode)
|
|
.detail("SingleAuthToken",
|
|
StringRef(arena, &header.singleAuthToken.authToken[0], AUTH_TOKEN_SIZE).toString())
|
|
.detail("ComputedSingleAuthToken", computed.toString());
|
|
throw encrypt_header_authtoken_mismatch();
|
|
}
|
|
}
|
|
|
|
void DecryptBlobCipherAes256Ctr::verifyHeaderMultiAuthToken(const uint8_t* ciphertext,
|
|
const int ciphertextLen,
|
|
const BlobCipherEncryptHeader& header,
|
|
uint8_t* buff,
|
|
Arena& arena) {
|
|
if (!headerAuthTokenValidationDone) {
|
|
verifyHeaderAuthToken(header, arena);
|
|
}
|
|
StringRef computedCipherTextAuthToken =
|
|
computeAuthToken(ciphertext,
|
|
ciphertextLen,
|
|
reinterpret_cast<const uint8_t*>(&header.cipherTextDetails.salt),
|
|
sizeof(EncryptCipherRandomSalt),
|
|
arena);
|
|
if (memcmp(&header.multiAuthTokens.cipherTextAuthToken[0], computedCipherTextAuthToken.begin(), AUTH_TOKEN_SIZE) !=
|
|
0) {
|
|
TraceEvent("VerifyEncryptBlobHeader_AuthTokenMismatch")
|
|
.detail("HeaderVersion", header.flags.headerVersion)
|
|
.detail("HeaderMode", header.flags.encryptMode)
|
|
.detail("MultiAuthCipherTextAuthToken",
|
|
StringRef(arena, &header.multiAuthTokens.cipherTextAuthToken[0], AUTH_TOKEN_SIZE).toString())
|
|
.detail("ComputedCipherTextAuthToken", computedCipherTextAuthToken.toString());
|
|
throw encrypt_header_authtoken_mismatch();
|
|
}
|
|
}
|
|
|
|
void DecryptBlobCipherAes256Ctr::verifyAuthTokens(const uint8_t* ciphertext,
|
|
const int ciphertextLen,
|
|
const BlobCipherEncryptHeader& header,
|
|
uint8_t* buff,
|
|
Arena& arena) {
|
|
if (header.flags.authTokenMode == ENCRYPT_HEADER_AUTH_TOKEN_MODE_SINGLE) {
|
|
verifyHeaderSingleAuthToken(ciphertext, ciphertextLen, header, buff, arena);
|
|
} else {
|
|
ASSERT_EQ(header.flags.authTokenMode, ENCRYPT_HEADER_AUTH_TOKEN_MODE_MULTI);
|
|
verifyHeaderMultiAuthToken(ciphertext, ciphertextLen, header, buff, arena);
|
|
}
|
|
|
|
authTokensValidationDone = true;
|
|
}
|
|
|
|
void DecryptBlobCipherAes256Ctr::verifyEncryptHeaderMetadata(const BlobCipherEncryptHeader& header) {
|
|
// validate header flag sanity
|
|
if (header.flags.headerVersion != EncryptBlobCipherAes265Ctr::ENCRYPT_HEADER_VERSION ||
|
|
header.flags.encryptMode != ENCRYPT_CIPHER_MODE_AES_256_CTR ||
|
|
!isEncryptHeaderAuthTokenModeValid((EncryptAuthTokenMode)header.flags.authTokenMode)) {
|
|
TraceEvent("VerifyEncryptBlobHeader")
|
|
.detail("HeaderVersion", header.flags.headerVersion)
|
|
.detail("ExpectedVersion", EncryptBlobCipherAes265Ctr::ENCRYPT_HEADER_VERSION)
|
|
.detail("EncryptCipherMode", header.flags.encryptMode)
|
|
.detail("ExpectedCipherMode", ENCRYPT_CIPHER_MODE_AES_256_CTR)
|
|
.detail("EncryptHeaderAuthTokenMode", header.flags.authTokenMode);
|
|
throw encrypt_header_metadata_mismatch();
|
|
}
|
|
}
|
|
|
|
Reference<EncryptBuf> DecryptBlobCipherAes256Ctr::decrypt(const uint8_t* ciphertext,
|
|
const int ciphertextLen,
|
|
const BlobCipherEncryptHeader& header,
|
|
Arena& arena) {
|
|
TEST(true); // Decrypting data with BlobCipher
|
|
|
|
verifyEncryptHeaderMetadata(header);
|
|
|
|
if (header.flags.authTokenMode != ENCRYPT_HEADER_AUTH_TOKEN_MODE_NONE && !headerCipherKey.isValid()) {
|
|
TraceEvent("Decrypt_InvalidHeaderCipherKey").detail("AuthTokenMode", header.flags.authTokenMode);
|
|
throw encrypt_ops_error();
|
|
}
|
|
|
|
const int allocSize = header.flags.authTokenMode == ENCRYPT_HEADER_AUTH_TOKEN_MODE_SINGLE
|
|
? ciphertextLen + AES_BLOCK_SIZE + sizeof(BlobCipherEncryptHeader)
|
|
: ciphertextLen + AES_BLOCK_SIZE;
|
|
Reference<EncryptBuf> decrypted = makeReference<EncryptBuf>(allocSize, arena);
|
|
|
|
if (header.flags.authTokenMode != ENCRYPT_HEADER_AUTH_TOKEN_MODE_NONE) {
|
|
verifyAuthTokens(ciphertext, ciphertextLen, header, decrypted->begin(), arena);
|
|
ASSERT(authTokensValidationDone);
|
|
}
|
|
|
|
uint8_t* plaintext = decrypted->begin();
|
|
int bytesDecrypted{ 0 };
|
|
if (!EVP_DecryptUpdate(ctx, plaintext, &bytesDecrypted, ciphertext, ciphertextLen)) {
|
|
TraceEvent("Decrypt_UpdateFailed")
|
|
.detail("BaseCipherId", header.cipherTextDetails.baseCipherId)
|
|
.detail("EncryptDomainId", header.cipherTextDetails.encryptDomainId);
|
|
throw encrypt_ops_error();
|
|
}
|
|
|
|
int finalBlobBytes{ 0 };
|
|
if (EVP_DecryptFinal_ex(ctx, plaintext + bytesDecrypted, &finalBlobBytes) <= 0) {
|
|
TraceEvent("Decrypt_FinalFailed")
|
|
.detail("BaseCipherId", header.cipherTextDetails.baseCipherId)
|
|
.detail("EncryptDomainId", header.cipherTextDetails.encryptDomainId);
|
|
throw encrypt_ops_error();
|
|
}
|
|
|
|
if ((bytesDecrypted + finalBlobBytes) != ciphertextLen) {
|
|
TraceEvent("Encrypt_UnexpectedPlaintextLen")
|
|
.detail("CiphertextLen", ciphertextLen)
|
|
.detail("DecryptedBufLen", bytesDecrypted + finalBlobBytes);
|
|
throw encrypt_ops_error();
|
|
}
|
|
|
|
decrypted->setLogicalSize(ciphertextLen);
|
|
return decrypted;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
StringRef HmacSha256DigestGen::digest(const unsigned char* data, size_t len, Arena& arena) {
|
|
TEST(true); // Digest generation
|
|
unsigned int digestLen = HMAC_size(ctx);
|
|
auto digest = new (arena) unsigned char[digestLen];
|
|
if (HMAC_Update(ctx, data, len) != 1) {
|
|
throw encrypt_ops_error();
|
|
}
|
|
|
|
if (HMAC_Final(ctx, digest, &digestLen) != 1) {
|
|
throw encrypt_ops_error();
|
|
}
|
|
return StringRef(digest, digestLen);
|
|
}
|
|
|
|
StringRef computeAuthToken(const uint8_t* payload,
|
|
const int payloadLen,
|
|
const uint8_t* key,
|
|
const int keyLen,
|
|
Arena& arena) {
|
|
HmacSha256DigestGen hmacGenerator(key, keyLen);
|
|
StringRef digest = hmacGenerator.digest(payload, payloadLen, arena);
|
|
|
|
ASSERT_GE(digest.size(), AUTH_TOKEN_SIZE);
|
|
return digest;
|
|
}
|
|
|
|
// 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
|
|
TEST_CASE("flow/BlobCipher") {
|
|
TraceEvent("BlobCipherTest_Start").log();
|
|
|
|
// Construct a dummy External Key Manager representation and populate with some keys
|
|
class BaseCipher : public ReferenceCounted<BaseCipher>, NonCopyable {
|
|
public:
|
|
EncryptCipherDomainId domainId;
|
|
int len;
|
|
EncryptCipherBaseKeyId keyId;
|
|
std::unique_ptr<uint8_t[]> key;
|
|
EncryptCipherRandomSalt generatedSalt;
|
|
|
|
BaseCipher(const EncryptCipherDomainId& dId, const EncryptCipherBaseKeyId& kId)
|
|
: domainId(dId), len(deterministicRandom()->randomInt(AES_256_KEY_LENGTH / 2, AES_256_KEY_LENGTH + 1)),
|
|
keyId(kId), key(std::make_unique<uint8_t[]>(len)) {
|
|
generateRandomData(key.get(), len);
|
|
}
|
|
};
|
|
|
|
using BaseKeyMap = std::unordered_map<EncryptCipherBaseKeyId, Reference<BaseCipher>>;
|
|
using DomainKeyMap = std::unordered_map<EncryptCipherDomainId, BaseKeyMap>;
|
|
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<BaseCipher>(dId, kId));
|
|
}
|
|
}
|
|
ASSERT_EQ(domainKeyMap.size(), maxDomainId);
|
|
|
|
// insert BlobCipher keys into BlobCipherKeyCache map and validate
|
|
TraceEvent("BlobCipherTest_InsertKeys").log();
|
|
Reference<BlobCipherKeyCache> cipherKeyCache = BlobCipherKeyCache::getInstance();
|
|
for (auto& domainItr : domainKeyMap) {
|
|
for (auto& baseKeyItr : domainItr.second) {
|
|
Reference<BaseCipher> baseCipher = baseKeyItr.second;
|
|
|
|
cipherKeyCache->insertCipherKey(
|
|
baseCipher->domainId, baseCipher->keyId, baseCipher->key.get(), baseCipher->len);
|
|
Reference<BlobCipherKey> fetchedKey = cipherKeyCache->getLatestCipherKey(baseCipher->domainId);
|
|
baseCipher->generatedSalt = fetchedKey->getSalt();
|
|
}
|
|
}
|
|
// insert EncryptHeader BlobCipher key
|
|
Reference<BaseCipher> headerBaseCipher = makeReference<BaseCipher>(ENCRYPT_HEADER_DOMAIN_ID, 1);
|
|
cipherKeyCache->insertCipherKey(
|
|
headerBaseCipher->domainId, headerBaseCipher->keyId, headerBaseCipher->key.get(), headerBaseCipher->len);
|
|
|
|
TraceEvent("BlobCipherTest_InsertKeysDone").log();
|
|
|
|
// validate the cipherKey lookups work as desired
|
|
for (auto& domainItr : domainKeyMap) {
|
|
for (auto& baseKeyItr : domainItr.second) {
|
|
Reference<BaseCipher> baseCipher = baseKeyItr.second;
|
|
Reference<BlobCipherKey> 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
|
|
ASSERT_NE(std::memcmp(cipherKey->rawCipher(), baseCipher->key.get(), cipherKey->getBaseCipherLen()), 0);
|
|
}
|
|
}
|
|
TraceEvent("BlobCipherTest_LooksupDone").log();
|
|
|
|
// Ensure attemtping to insert existing cipherKey (identical) more than once is treated as a NOP
|
|
try {
|
|
Reference<BaseCipher> baseCipher = domainKeyMap[minDomainId][minBaseCipherKeyId];
|
|
cipherKeyCache->insertCipherKey(
|
|
baseCipher->domainId, baseCipher->keyId, baseCipher->key.get(), baseCipher->len);
|
|
} catch (Error& e) {
|
|
throw;
|
|
}
|
|
TraceEvent("BlobCipherTest_ReinsertIdempotentKeyDone").log();
|
|
|
|
// Ensure attemtping to insert an existing cipherKey (modified) fails with appropriate error
|
|
try {
|
|
Reference<BaseCipher> baseCipher = domainKeyMap[minDomainId][minBaseCipherKeyId];
|
|
uint8_t rawCipher[baseCipher->len];
|
|
memcpy(rawCipher, baseCipher->key.get(), baseCipher->len);
|
|
// modify few bytes in the cipherKey
|
|
for (int i = 2; i < 5; i++) {
|
|
rawCipher[i]++;
|
|
}
|
|
cipherKeyCache->insertCipherKey(baseCipher->domainId, baseCipher->keyId, &rawCipher[0], baseCipher->len);
|
|
} catch (Error& e) {
|
|
if (e.code() != error_code_encrypt_update_cipher) {
|
|
throw;
|
|
}
|
|
}
|
|
TraceEvent("BlobCipherTest_ReinsertNonIdempotentKeyDone").log();
|
|
|
|
// Validate Encryption ops
|
|
Reference<BlobCipherKey> cipherKey = cipherKeyCache->getLatestCipherKey(minDomainId);
|
|
Reference<BlobCipherKey> headerCipherKey = cipherKeyCache->getLatestCipherKey(ENCRYPT_HEADER_DOMAIN_ID);
|
|
const int bufLen = deterministicRandom()->randomInt(786, 2127) + 512;
|
|
uint8_t orgData[bufLen];
|
|
generateRandomData(&orgData[0], bufLen);
|
|
|
|
Arena arena;
|
|
uint8_t iv[AES_256_IV_LENGTH];
|
|
generateRandomData(&iv[0], AES_256_IV_LENGTH);
|
|
|
|
BlobCipherEncryptHeader headerCopy;
|
|
// validate basic encrypt followed by decrypt operation for AUTH_MODE_NONE
|
|
{
|
|
TraceEvent("NoneAuthMode_Start").log();
|
|
|
|
EncryptBlobCipherAes265Ctr encryptor(
|
|
cipherKey, Reference<BlobCipherKey>(), iv, AES_256_IV_LENGTH, ENCRYPT_HEADER_AUTH_TOKEN_MODE_NONE);
|
|
BlobCipherEncryptHeader header;
|
|
Reference<EncryptBuf> encrypted = encryptor.encrypt(&orgData[0], bufLen, &header, arena);
|
|
|
|
ASSERT_EQ(encrypted->getLogicalSize(), bufLen);
|
|
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, ENCRYPT_HEADER_AUTH_TOKEN_MODE_NONE);
|
|
|
|
TraceEvent("BlobCipherTest_EncryptDone")
|
|
.detail("HeaderVersion", header.flags.headerVersion)
|
|
.detail("HeaderEncryptMode", header.flags.encryptMode)
|
|
.detail("DomainId", header.cipherTextDetails.encryptDomainId)
|
|
.detail("BaseCipherId", header.cipherTextDetails.baseCipherId);
|
|
|
|
Reference<BlobCipherKey> tCipherKeyKey = cipherKeyCache->getCipherKey(header.cipherTextDetails.encryptDomainId,
|
|
header.cipherTextDetails.baseCipherId,
|
|
header.cipherTextDetails.salt);
|
|
ASSERT(tCipherKeyKey->isEqual(cipherKey));
|
|
DecryptBlobCipherAes256Ctr decryptor(
|
|
tCipherKeyKey, Reference<BlobCipherKey>(), &header.cipherTextDetails.iv[0]);
|
|
Reference<EncryptBuf> decrypted = decryptor.decrypt(encrypted->begin(), bufLen, header, arena);
|
|
|
|
ASSERT_EQ(decrypted->getLogicalSize(), bufLen);
|
|
ASSERT_EQ(memcmp(decrypted->begin(), &orgData[0], bufLen), 0);
|
|
|
|
TraceEvent("BlobCipherTest_DecryptDone").log();
|
|
|
|
// induce encryption header corruption - headerVersion corrupted
|
|
memcpy(reinterpret_cast<uint8_t*>(&headerCopy),
|
|
reinterpret_cast<const uint8_t*>(&header),
|
|
sizeof(BlobCipherEncryptHeader));
|
|
headerCopy.flags.headerVersion += 1;
|
|
try {
|
|
encrypted = encryptor.encrypt(&orgData[0], bufLen, &header, arena);
|
|
DecryptBlobCipherAes256Ctr decryptor(
|
|
tCipherKeyKey, Reference<BlobCipherKey>(), &header.cipherTextDetails.iv[0]);
|
|
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;
|
|
}
|
|
}
|
|
|
|
// induce encryption header corruption - encryptionMode corrupted
|
|
memcpy(reinterpret_cast<uint8_t*>(&headerCopy),
|
|
reinterpret_cast<const uint8_t*>(&header),
|
|
sizeof(BlobCipherEncryptHeader));
|
|
headerCopy.flags.encryptMode += 1;
|
|
try {
|
|
encrypted = encryptor.encrypt(&orgData[0], bufLen, &header, arena);
|
|
DecryptBlobCipherAes256Ctr decryptor(
|
|
tCipherKeyKey, Reference<BlobCipherKey>(), &header.cipherTextDetails.iv[0]);
|
|
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;
|
|
}
|
|
}
|
|
|
|
// induce encrypted buffer payload corruption
|
|
try {
|
|
encrypted = encryptor.encrypt(&orgData[0], bufLen, &header, arena);
|
|
uint8_t temp[bufLen];
|
|
memcpy(encrypted->begin(), &temp[0], bufLen);
|
|
int tIdx = deterministicRandom()->randomInt(0, bufLen - 1);
|
|
temp[tIdx] += 1;
|
|
DecryptBlobCipherAes256Ctr decryptor(
|
|
tCipherKeyKey, Reference<BlobCipherKey>(), &header.cipherTextDetails.iv[0]);
|
|
decrypted = decryptor.decrypt(&temp[0], bufLen, header, arena);
|
|
} catch (Error& e) {
|
|
// No authToken, hence, no corruption detection supported
|
|
ASSERT(false);
|
|
}
|
|
|
|
TraceEvent("NoneAuthMode_Done").log();
|
|
}
|
|
|
|
// validate basic encrypt followed by decrypt operation for AUTH_TOKEN_MODE_SINGLE
|
|
{
|
|
TraceEvent("SingleAuthMode_Start").log();
|
|
|
|
EncryptBlobCipherAes265Ctr encryptor(
|
|
cipherKey, headerCipherKey, iv, AES_256_IV_LENGTH, ENCRYPT_HEADER_AUTH_TOKEN_MODE_SINGLE);
|
|
BlobCipherEncryptHeader header;
|
|
Reference<EncryptBuf> encrypted = encryptor.encrypt(&orgData[0], bufLen, &header, arena);
|
|
|
|
ASSERT_EQ(encrypted->getLogicalSize(), bufLen);
|
|
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, ENCRYPT_HEADER_AUTH_TOKEN_MODE_SINGLE);
|
|
|
|
TraceEvent("BlobCipherTest_EncryptDone")
|
|
.detail("HeaderVersion", header.flags.headerVersion)
|
|
.detail("HeaderEncryptMode", header.flags.encryptMode)
|
|
.detail("DomainId", header.cipherTextDetails.encryptDomainId)
|
|
.detail("BaseCipherId", header.cipherTextDetails.baseCipherId)
|
|
.detail("HeaderAuthToken",
|
|
StringRef(arena, &header.singleAuthToken.authToken[0], AUTH_TOKEN_SIZE).toString());
|
|
|
|
Reference<BlobCipherKey> tCipherKeyKey = cipherKeyCache->getCipherKey(header.cipherTextDetails.encryptDomainId,
|
|
header.cipherTextDetails.baseCipherId,
|
|
header.cipherTextDetails.salt);
|
|
Reference<BlobCipherKey> hCipherKey = cipherKeyCache->getCipherKey(header.cipherHeaderDetails.encryptDomainId,
|
|
header.cipherHeaderDetails.baseCipherId,
|
|
header.cipherHeaderDetails.salt);
|
|
ASSERT(tCipherKeyKey->isEqual(cipherKey));
|
|
DecryptBlobCipherAes256Ctr decryptor(tCipherKeyKey, hCipherKey, &header.cipherTextDetails.iv[0]);
|
|
Reference<EncryptBuf> decrypted = decryptor.decrypt(encrypted->begin(), bufLen, header, arena);
|
|
|
|
ASSERT_EQ(decrypted->getLogicalSize(), bufLen);
|
|
ASSERT_EQ(memcmp(decrypted->begin(), &orgData[0], bufLen), 0);
|
|
|
|
TraceEvent("BlobCipherTest_DecryptDone").log();
|
|
|
|
// induce encryption header corruption - headerVersion corrupted
|
|
encrypted = encryptor.encrypt(&orgData[0], bufLen, &header, arena);
|
|
memcpy(reinterpret_cast<uint8_t*>(&headerCopy),
|
|
reinterpret_cast<const uint8_t*>(&header),
|
|
sizeof(BlobCipherEncryptHeader));
|
|
headerCopy.flags.headerVersion += 1;
|
|
try {
|
|
DecryptBlobCipherAes256Ctr decryptor(tCipherKeyKey, hCipherKey, &header.cipherTextDetails.iv[0]);
|
|
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;
|
|
}
|
|
}
|
|
|
|
// induce encryption header corruption - encryptionMode corrupted
|
|
encrypted = encryptor.encrypt(&orgData[0], bufLen, &header, arena);
|
|
memcpy(reinterpret_cast<uint8_t*>(&headerCopy),
|
|
reinterpret_cast<const uint8_t*>(&header),
|
|
sizeof(BlobCipherEncryptHeader));
|
|
headerCopy.flags.encryptMode += 1;
|
|
try {
|
|
DecryptBlobCipherAes256Ctr decryptor(tCipherKeyKey, hCipherKey, &header.cipherTextDetails.iv[0]);
|
|
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;
|
|
}
|
|
}
|
|
|
|
// induce encryption header corruption - authToken mismatch
|
|
encrypted = encryptor.encrypt(&orgData[0], bufLen, &header, arena);
|
|
memcpy(reinterpret_cast<uint8_t*>(&headerCopy),
|
|
reinterpret_cast<const uint8_t*>(&header),
|
|
sizeof(BlobCipherEncryptHeader));
|
|
int hIdx = deterministicRandom()->randomInt(0, AUTH_TOKEN_SIZE - 1);
|
|
headerCopy.singleAuthToken.authToken[hIdx] += 1;
|
|
try {
|
|
DecryptBlobCipherAes256Ctr decryptor(tCipherKeyKey, hCipherKey, &header.cipherTextDetails.iv[0]);
|
|
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;
|
|
}
|
|
}
|
|
|
|
// induce encrypted buffer payload corruption
|
|
try {
|
|
encrypted = encryptor.encrypt(&orgData[0], bufLen, &header, arena);
|
|
uint8_t temp[bufLen];
|
|
memcpy(encrypted->begin(), &temp[0], bufLen);
|
|
int tIdx = deterministicRandom()->randomInt(0, bufLen - 1);
|
|
temp[tIdx] += 1;
|
|
DecryptBlobCipherAes256Ctr decryptor(tCipherKeyKey, hCipherKey, &header.cipherTextDetails.iv[0]);
|
|
decrypted = decryptor.decrypt(&temp[0], bufLen, header, arena);
|
|
} catch (Error& e) {
|
|
if (e.code() != error_code_encrypt_header_authtoken_mismatch) {
|
|
throw;
|
|
}
|
|
}
|
|
|
|
TraceEvent("SingleAuthMode_Done").log();
|
|
}
|
|
|
|
// validate basic encrypt followed by decrypt operation for AUTH_TOKEN_MODE_MULTI
|
|
{
|
|
TraceEvent("MultiAuthMode_Start").log();
|
|
|
|
EncryptBlobCipherAes265Ctr encryptor(
|
|
cipherKey, headerCipherKey, iv, AES_256_IV_LENGTH, ENCRYPT_HEADER_AUTH_TOKEN_MODE_MULTI);
|
|
BlobCipherEncryptHeader header;
|
|
Reference<EncryptBuf> encrypted = encryptor.encrypt(&orgData[0], bufLen, &header, arena);
|
|
|
|
ASSERT_EQ(encrypted->getLogicalSize(), bufLen);
|
|
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, ENCRYPT_HEADER_AUTH_TOKEN_MODE_MULTI);
|
|
|
|
TraceEvent("BlobCipherTest_EncryptDone")
|
|
.detail("HeaderVersion", header.flags.headerVersion)
|
|
.detail("HeaderEncryptMode", header.flags.encryptMode)
|
|
.detail("DomainId", header.cipherTextDetails.encryptDomainId)
|
|
.detail("BaseCipherId", header.cipherTextDetails.baseCipherId)
|
|
.detail("HeaderAuthToken",
|
|
StringRef(arena, &header.singleAuthToken.authToken[0], AUTH_TOKEN_SIZE).toString());
|
|
|
|
Reference<BlobCipherKey> tCipherKey = cipherKeyCache->getCipherKey(header.cipherTextDetails.encryptDomainId,
|
|
header.cipherTextDetails.baseCipherId,
|
|
header.cipherTextDetails.salt);
|
|
Reference<BlobCipherKey> hCipherKey = cipherKeyCache->getCipherKey(header.cipherHeaderDetails.encryptDomainId,
|
|
header.cipherHeaderDetails.baseCipherId,
|
|
header.cipherHeaderDetails.salt);
|
|
|
|
ASSERT(tCipherKey->isEqual(cipherKey));
|
|
DecryptBlobCipherAes256Ctr decryptor(tCipherKey, hCipherKey, &header.cipherTextDetails.iv[0]);
|
|
Reference<EncryptBuf> decrypted = decryptor.decrypt(encrypted->begin(), bufLen, header, arena);
|
|
|
|
ASSERT_EQ(decrypted->getLogicalSize(), bufLen);
|
|
ASSERT_EQ(memcmp(decrypted->begin(), &orgData[0], bufLen), 0);
|
|
|
|
TraceEvent("BlobCipherTest_DecryptDone").log();
|
|
|
|
// induce encryption header corruption - headerVersion corrupted
|
|
encrypted = encryptor.encrypt(&orgData[0], bufLen, &header, arena);
|
|
memcpy(reinterpret_cast<uint8_t*>(&headerCopy),
|
|
reinterpret_cast<const uint8_t*>(&header),
|
|
sizeof(BlobCipherEncryptHeader));
|
|
headerCopy.flags.headerVersion += 1;
|
|
try {
|
|
DecryptBlobCipherAes256Ctr decryptor(tCipherKey, hCipherKey, &header.cipherTextDetails.iv[0]);
|
|
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;
|
|
}
|
|
}
|
|
|
|
// induce encryption header corruption - encryptionMode corrupted
|
|
encrypted = encryptor.encrypt(&orgData[0], bufLen, &header, arena);
|
|
memcpy(reinterpret_cast<uint8_t*>(&headerCopy),
|
|
reinterpret_cast<const uint8_t*>(&header),
|
|
sizeof(BlobCipherEncryptHeader));
|
|
headerCopy.flags.encryptMode += 1;
|
|
try {
|
|
DecryptBlobCipherAes256Ctr decryptor(tCipherKey, hCipherKey, &header.cipherTextDetails.iv[0]);
|
|
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;
|
|
}
|
|
}
|
|
|
|
// induce encryption header corruption - cipherText authToken mismatch
|
|
encrypted = encryptor.encrypt(&orgData[0], bufLen, &header, arena);
|
|
memcpy(reinterpret_cast<uint8_t*>(&headerCopy),
|
|
reinterpret_cast<const uint8_t*>(&header),
|
|
sizeof(BlobCipherEncryptHeader));
|
|
int hIdx = deterministicRandom()->randomInt(0, AUTH_TOKEN_SIZE - 1);
|
|
headerCopy.multiAuthTokens.cipherTextAuthToken[hIdx] += 1;
|
|
try {
|
|
DecryptBlobCipherAes256Ctr decryptor(tCipherKey, hCipherKey, &header.cipherTextDetails.iv[0]);
|
|
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;
|
|
}
|
|
}
|
|
|
|
// induce encryption header corruption - header authToken mismatch
|
|
encrypted = encryptor.encrypt(&orgData[0], bufLen, &header, arena);
|
|
memcpy(reinterpret_cast<uint8_t*>(&headerCopy),
|
|
reinterpret_cast<const uint8_t*>(&header),
|
|
sizeof(BlobCipherEncryptHeader));
|
|
hIdx = deterministicRandom()->randomInt(0, AUTH_TOKEN_SIZE - 1);
|
|
headerCopy.multiAuthTokens.headerAuthToken[hIdx] += 1;
|
|
try {
|
|
DecryptBlobCipherAes256Ctr decryptor(tCipherKey, hCipherKey, &header.cipherTextDetails.iv[0]);
|
|
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;
|
|
}
|
|
}
|
|
|
|
try {
|
|
encrypted = encryptor.encrypt(&orgData[0], bufLen, &header, arena);
|
|
uint8_t temp[bufLen];
|
|
memcpy(encrypted->begin(), &temp[0], bufLen);
|
|
int tIdx = deterministicRandom()->randomInt(0, bufLen - 1);
|
|
temp[tIdx] += 1;
|
|
DecryptBlobCipherAes256Ctr decryptor(tCipherKey, hCipherKey, &header.cipherTextDetails.iv[0]);
|
|
decrypted = decryptor.decrypt(&temp[0], bufLen, header, arena);
|
|
} catch (Error& e) {
|
|
if (e.code() != error_code_encrypt_header_authtoken_mismatch) {
|
|
throw;
|
|
}
|
|
}
|
|
|
|
TraceEvent("MultiAuthMode_Done").log();
|
|
}
|
|
|
|
// Validate dropping encyrptDomainId cached keys
|
|
const EncryptCipherDomainId candidate = deterministicRandom()->randomInt(minDomainId, maxDomainId);
|
|
cipherKeyCache->resetEncryptDomainId(candidate);
|
|
std::vector<Reference<BlobCipherKey>> cachedKeys = cipherKeyCache->getAllCiphers(candidate);
|
|
ASSERT(cachedKeys.empty());
|
|
|
|
// Validate dropping all cached cipherKeys
|
|
cipherKeyCache->cleanup();
|
|
for (int dId = minDomainId; dId < maxDomainId; dId++) {
|
|
std::vector<Reference<BlobCipherKey>> cachedKeys = cipherKeyCache->getAllCiphers(dId);
|
|
ASSERT(cachedKeys.empty());
|
|
}
|
|
|
|
TraceEvent("BlobCipherTest_Done").log();
|
|
return Void();
|
|
}
|
|
|
|
#endif // ENCRYPTION_ENABLED
|