You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
dvmhost/src/fne/CryptoContainer.cpp

618 lines
18 KiB

// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Converged FNE Software
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2025 Bryan Biedenkapp, N2PLL
*
*/
#include "common/AESCrypto.h"
#include "common/Log.h"
#include "common/Timer.h"
#include "common/Utils.h"
#include "common/zlib/zlib.h"
#if defined(ENABLE_SSL)
#include "xml/rapidxml.h"
#endif // ENABLE_SSL
#include "CryptoContainer.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
#if defined(ENABLE_SSL)
#include <openssl/bio.h>
#include <openssl/evp.h>
#endif // ENABLE_SSL
using namespace crypto;
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
#define CHUNK 16384
// ---------------------------------------------------------------------------
// Global Functions
// ---------------------------------------------------------------------------
#if defined(ENABLE_SSL)
/**
* @brief Calculates the length of a decoded base64 string.
* @param b64input String containing the base64 encoded data.
* @returns int Length of buffer to contain base64 encoded data.
*/
int calcDecodeLength(const char* b64input)
{
int len = strlen(b64input);
int padding = 0;
// last two chars are =
if (b64input[len-1] == '=' && b64input[len-2] == '=')
padding = 2;
else if (b64input[len-1] == '=') // last char is =
padding = 1;
return (int)len * 0.75 - padding;
}
/**
* @brief Decodes a base64 encoded string.
* @param b64message String containing the base64 encoded data.
* @param buffer Buffer pointer to place encoded data.
* @returns int
*/
int base64Decode(char* b64message, uint8_t** buffer)
{
int decodeLen = calcDecodeLength(b64message), len = 0;
*buffer = (uint8_t*)malloc(decodeLen + 1);
FILE* stream = ::fmemopen(b64message, ::strlen(b64message), "r");
BIO* b64 = BIO_new(BIO_f_base64());
BIO* bio = BIO_new_fp(stream, BIO_NOCLOSE);
bio = BIO_push(b64, bio);
BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL); // do not use newlines to flush buffer
len = BIO_read(bio, *buffer, ::strlen(b64message));
// can test here if len == decodeLen - if not, then return an error
(*buffer)[len] = '\0';
BIO_free_all(bio);
::fclose(stream);
return decodeLen;
}
#endif // ENABLE_SSL
/**
* @brief
* @param buffer
* @param len
* @param target
* @return int
*/
int findFirstChar(const uint8_t* buffer, uint32_t len, char target)
{
for (uint32_t i = 0U; i < len; i++) {
if (buffer[i] == target) {
return (int)i;
}
}
return -1;
}
/**
* @brief
* @param buffer
* @param len
* @param target
* @return int
*/
int findLastChar(const uint8_t* buffer, uint32_t len, char target)
{
if (buffer == nullptr) {
return -1;
}
int lastIndex = -1;
for (uint32_t i = 0U; i < len; i++) {
if (buffer[i] == target) {
lastIndex = i;
}
}
return lastIndex;
}
// ---------------------------------------------------------------------------
// Static Class Members
// ---------------------------------------------------------------------------
std::mutex CryptoContainer::s_mutex;
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/* Initializes a new instance of the CryptoContainer class. */
CryptoContainer::CryptoContainer(const std::string& filename, const std::string& password, uint32_t reloadTime, bool enabled) : Thread(),
m_file(filename),
m_password(password),
m_reloadTime(reloadTime),
m_lastLoadTime(0U),
#if !defined(ENABLE_SSL)
m_enabled(false),
#else
m_enabled(enabled),
#endif // !ENABLE_SSL
m_stop(false),
m_keys()
{
/* stub */
}
/* Finalizes a instance of the CryptoContainer class. */
CryptoContainer::~CryptoContainer() = default;
/* Thread entry point. This function is provided to run the thread for the lookup table. */
void CryptoContainer::entry()
{
if (m_reloadTime == 0U) {
return;
}
Timer timer(1U, 60U * m_reloadTime);
timer.start();
while (!m_stop) {
sleep(1000U);
timer.clock();
if (timer.hasExpired()) {
load();
timer.start();
}
}
}
/* Stops and unloads this lookup table. */
void CryptoContainer::stop(bool noDestroy)
{
if (!m_enabled)
return;
if (m_reloadTime == 0U) {
if (!noDestroy)
delete this;
return;
}
m_stop = true;
wait();
}
/* Reads the lookup table from the specified lookup table file. */
bool CryptoContainer::read()
{
if (!m_enabled)
return false;
bool ret = load();
if (m_reloadTime > 0U)
run();
setName("fne:crypto-lookup-tbl");
return ret;
}
/* Clears all entries from the lookup table. */
void CryptoContainer::clear()
{
std::lock_guard<std::mutex> lock(s_mutex);
m_keys.clear();
}
/* Adds a new entry to the lookup table by the specified unique ID. */
void CryptoContainer::addEntry(EKCKeyItem key)
{
if (key.isInvalid())
return;
EKCKeyItem entry = key;
uint32_t id = entry.id();
uint32_t kId = entry.kId();
std::lock_guard<std::mutex> lock(s_mutex);
auto it = std::find_if(m_keys.begin(), m_keys.end(),
[&](EKCKeyItem& x)
{
return x.id() == id && x.kId() == kId;
});
if (it != m_keys.end()) {
m_keys[it - m_keys.begin()] = entry;
}
else {
m_keys.push_back(entry);
}
}
/* Erases an existing entry from the lookup table by the specified unique ID. */
void CryptoContainer::eraseEntry(uint32_t id)
{
std::lock_guard<std::mutex> lock(s_mutex);
auto it = std::find_if(m_keys.begin(), m_keys.end(),
[&](EKCKeyItem& x) {
return x.id() == id;
});
if (it != m_keys.end()) {
m_keys.erase(it);
}
}
/* Finds a table entry in this lookup table. */
EKCKeyItem CryptoContainer::find(uint32_t kId)
{
EKCKeyItem entry;
std::lock_guard<std::mutex> lock(s_mutex);
auto it = std::find_if(m_keys.begin(), m_keys.end(),
[&](EKCKeyItem& x) {
return x.kId() == kId;
});
if (it != m_keys.end()) {
entry = *it;
} else {
entry = EKCKeyItem();
}
return entry;
}
/* Finds a table entry in this lookup table. */
EKCKeyItem CryptoContainer::findUKEK(uint32_t rsi)
{
EKCKeyItem entry;
std::lock_guard<std::mutex> lock(s_mutex);
/*
** TODO TODO TODO
*/
entry = EKCKeyItem();
return entry;
}
/* Finds a table entry in this lookup table. */
EKCKeyItem CryptoContainer::findLLA(uint32_t rsi)
{
EKCKeyItem entry;
std::lock_guard<std::mutex> lock(s_mutex);
/*
** TODO TODO TODO
*/
entry = EKCKeyItem();
return entry;
}
// ---------------------------------------------------------------------------
// Private Class Members
// ---------------------------------------------------------------------------
/* Loads the table from the passed lookup table file. */
bool CryptoContainer::load()
{
#if !defined(ENABLE_SSL)
return false;
#else
if (!m_enabled) {
return false;
}
if (m_file.length() <= 0) {
return false;
}
if (m_password.length() <= 0) {
return false;
}
FILE* ekcFile = ::fopen(m_file.c_str(), "rb");
if (ekcFile == nullptr) {
LogError(LOG_HOST, "Cannot open the crypto container file - %s", m_file.c_str());
return false;
}
// inflate file
// compression structures
z_stream strm;
strm.zalloc = Z_NULL;
strm.zfree = Z_NULL;
strm.opaque = Z_NULL;
// set input data
strm.avail_in = 0U;
strm.next_in = Z_NULL;
// initialize decompression
int ret = inflateInit2(&strm, 16 + MAX_WBITS);
if (ret != Z_OK) {
LogError(LOG_HOST, "Error initializing ZLIB, ret = %d", ret);
::fclose(ekcFile);
return false;
}
// skip 4 bytes (C# adds a header on the GZIP stream for the decompressed length)
::fseek(ekcFile, 4, SEEK_SET);
// decompress data
std::vector<uint8_t> decompressedData;
uint8_t inBuffer[CHUNK];
uint8_t outBuffer[CHUNK];
do {
strm.avail_in = fread(inBuffer, 1, CHUNK, ekcFile);
if (::ferror(ekcFile)) {
inflateEnd(&strm);
::fclose(ekcFile);
return false;
}
if (strm.avail_in == 0)
break;
strm.next_in = inBuffer;
uint32_t have = 0U;
do {
strm.avail_out = CHUNK;
strm.next_out = outBuffer;
ret = inflate(&strm, Z_NO_FLUSH);
if (ret == Z_DATA_ERROR) {
// deflate stream invalid
LogError(LOG_HOST, "Error decompressing EKC: %s", (strm.msg == NULL) ? "compressed data error" : strm.msg);
inflateEnd(&strm);
::fclose(ekcFile);
return false;
}
if (ret == Z_STREAM_ERROR || ret < 0) {
LogError(LOG_HOST, "Error decompressing EKC, ret = %d", ret);
inflateEnd(&strm);
::fclose(ekcFile);
return false;
}
have = CHUNK - strm.avail_out;
decompressedData.insert(decompressedData.end(), outBuffer, outBuffer + have);
} while (strm.avail_out == 0);
} while (ret != Z_STREAM_END);
// cleanup
inflateEnd(&strm);
::fclose(ekcFile);
try {
// ensure zero termination
decompressedData.push_back(0U);
uint8_t* decompressed = decompressedData.data();
// parse outer container DOM
enum { PARSE_FLAGS = rapidxml::parse_full };
rapidxml::xml_document<> ekcOuterContainer;
ekcOuterContainer.parse<PARSE_FLAGS>(reinterpret_cast<char*>(decompressed));
rapidxml::xml_node<>* outerRoot = ekcOuterContainer.first_node("OuterContainer");
if (outerRoot != nullptr) {
// get EKC version
std::string version = "";
rapidxml::xml_attribute<>* versionAttr = outerRoot->first_attribute("version");
if (versionAttr != nullptr)
version = std::string(versionAttr->value());
// validate EKC version is set and is 1.0
if (version == "") {
::LogError(LOG_HOST, "Error opening EKC: incorrect version, expected 1.0 got none");
return false;
}
if (version != "1.0") {
::LogError(LOG_HOST, "Error opening EKC: incorrect version, expected 1.0 got %s", version.c_str());
return false;
}
// get key derivation node
rapidxml::xml_node<>* keyDerivation = outerRoot->first_node("KeyDerivation");
if (keyDerivation == nullptr) {
::LogError(LOG_HOST, "Error opening EKC: failed to process XML");
return false;
}
// retreive and parse salt
uint8_t* salt = nullptr;
rapidxml::xml_node<>* saltNode = keyDerivation->first_node("Salt");
if (saltNode == nullptr) {
::LogError(LOG_HOST, "Error opening EKC: failed to process XML");
return false;
}
int8_t saltBufLen = base64Decode(saltNode->value(), &salt);
// retrieve interation count
int32_t iterationCount = 0;
rapidxml::xml_node<>* iterNode = keyDerivation->first_node("IterationCount");
if (iterNode == nullptr) {
::LogError(LOG_HOST, "Error opening EKC: failed to process XML");
return false;
}
iterationCount = ::strtoul(iterNode->value(), NULL, 10);
// retrieve key length
int32_t keyLength = 0;
rapidxml::xml_node<>* keyLenNode = keyDerivation->first_node("KeyLength");
if (keyLenNode == nullptr) {
::LogError(LOG_HOST, "Error opening EKC: failed to process XML");
return false;
}
keyLength = ::strtoul(keyLenNode->value(), NULL, 10);
// generate crypto key to decrypt inner container
uint8_t key[EVP_MAX_KEY_LENGTH];
::memset(key, 0x00U, EVP_MAX_KEY_LENGTH);
uint8_t iv[EVP_MAX_IV_LENGTH];
::memset(iv, 0x00U, EVP_MAX_IV_LENGTH);
uint8_t keyIv[EVP_MAX_KEY_LENGTH + EVP_MAX_IV_LENGTH];
::memset(keyIv, 0x00U, EVP_MAX_KEY_LENGTH + EVP_MAX_IV_LENGTH);
if (PKCS5_PBKDF2_HMAC(m_password.c_str(), m_password.size(), salt, saltBufLen, iterationCount, EVP_sha512(), keyLength + EVP_MAX_IV_LENGTH, keyIv)) {
::memcpy(key, keyIv, keyLength);
::memcpy(iv, keyIv + keyLength, EVP_MAX_IV_LENGTH);
}
free(salt); // base64Decode allocates memory with malloc()
// get inner container encrypted data
// bryanb: annoying levels of XML encapsulation...
rapidxml::xml_node<>* encryptedDataNode = outerRoot->first_node("EncryptedData");
if (encryptedDataNode == nullptr) {
::LogError(LOG_HOST, "Error opening EKC: failed to process XML");
return false;
}
rapidxml::xml_node<>* cipherDataNode = encryptedDataNode->first_node("CipherData");
if (cipherDataNode == nullptr) {
::LogError(LOG_HOST, "Error opening EKC: failed to process XML");
return false;
}
rapidxml::xml_node<>* cipherValue = cipherDataNode->first_node("CipherValue");
if (cipherValue == nullptr) {
::LogError(LOG_HOST, "Error opening EKC: failed to process XML");
return false;
}
uint8_t* innerContainerCrypted = nullptr;
int innerContainerLen = base64Decode(cipherValue->value(), &innerContainerCrypted);
// decrypt inner container
AES aes = AES(AESKeyLength::AES_256);
uint8_t* innerContainer = aes.decryptCBC(innerContainerCrypted, innerContainerLen, key, iv);
free(innerContainerCrypted); // base64Decode allocates memory with malloc()
/*
** bryanb: this is probably slightly error prone...
*/
int xmlFirstTagChar = findFirstChar(innerContainer, innerContainerLen, '<');
int xmlLastTagChar = findLastChar(innerContainer, innerContainerLen, '>');
// zero all bytes after the last > character
::memset(innerContainer + xmlLastTagChar + 1U, 0x00U, innerContainerLen - xmlLastTagChar);
rapidxml::xml_document<> ekcInnerContainer;
ekcInnerContainer.parse<PARSE_FLAGS>(reinterpret_cast<char*>(innerContainer + xmlFirstTagChar));
rapidxml::xml_node<>* innerRoot = ekcInnerContainer.first_node("InnerContainer");
if (innerRoot != nullptr) {
// clear table
clear();
std::lock_guard<std::mutex> lock(s_mutex);
// get keys node
rapidxml::xml_node<>* keys = innerRoot->first_node("Keys");
if (keys != nullptr) {
uint32_t i = 0U;
for (rapidxml::xml_node<>* keyNode = keys->first_node("KeyItem"); keyNode; keyNode = keyNode->next_sibling()) {
EKCKeyItem key = EKCKeyItem();
key.id(i);
// get name
rapidxml::xml_node<>* nameNode = keyNode->first_node("Name");
if (nameNode == nullptr) {
continue;
}
key.name(nameNode->value());
// get keyset ID
rapidxml::xml_node<>* keysetIdNode = keyNode->first_node("KeysetId");
if (keysetIdNode == nullptr) {
continue;
}
key.keysetId(::strtoul(keysetIdNode->value(), NULL, 10));
// get SLN
rapidxml::xml_node<>* slnNode = keyNode->first_node("Sln");
if (slnNode == nullptr) {
continue;
}
key.sln(::strtoul(slnNode->value(), NULL, 10));
// get algorithm ID
rapidxml::xml_node<>* algIdNode = keyNode->first_node("AlgorithmId");
if (algIdNode == nullptr) {
continue;
}
key.algId(::strtoul(algIdNode->value(), NULL, 10));
// get key ID
rapidxml::xml_node<>* kIdNode = keyNode->first_node("KeyId");
if (kIdNode == nullptr) {
continue;
}
key.kId(::strtoul(kIdNode->value(), NULL, 10));
// get key material
rapidxml::xml_node<>* keyMatNode = keyNode->first_node("Key");
if (keyMatNode == nullptr) {
continue;
}
key.keyMaterial(keyMatNode->value());
::LogInfoEx(LOG_HOST, "Key NAME: %s SLN: %u ALGID: $%02X, KID: $%04X", key.name().c_str(), key.sln(), key.algId(), key.kId());
m_keys.push_back(key);
i++;
}
}
}
delete[] innerContainer;
}
} catch(const std::exception& e) {
::LogError(LOG_HOST, "Error opening EKC: %s", e.what());
return false;
}
if (m_keys.size() == 0U) {
::LogError(LOG_HOST, "No encryption keys defined!");
return false;
}
size_t size = m_keys.size();
if (size == 0U)
return false;
uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
m_lastLoadTime = now;
LogInfoEx(LOG_HOST, "Loaded %lu entries into crypto lookup table", size);
return true;
#endif // !ENABLE_SSL
}

Powered by TurnKey Linux.