From 5c5016b6235fce3a6ec42f260749078037cabd05 Mon Sep 17 00:00:00 2001 From: Bryan Biedenkapp Date: Fri, 25 Sep 2026 21:22:28 -0400 Subject: [PATCH] implement NXDN crypto support; code cleanup; --- src/common/nxdn/Crypto.cpp | 391 ++++++++++++++++++++++++++++++++++ src/common/nxdn/Crypto.h | 183 ++++++++++++++++ src/common/nxdn/NXDNDefines.h | 23 +- src/common/p25/Crypto.cpp | 64 +++++- src/common/p25/Crypto.h | 25 ++- tests/nxdn/Crypto_Tests.cpp | 137 ++++++++++++ 6 files changed, 805 insertions(+), 18 deletions(-) create mode 100644 src/common/nxdn/Crypto.cpp create mode 100644 src/common/nxdn/Crypto.h create mode 100644 tests/nxdn/Crypto_Tests.cpp diff --git a/src/common/nxdn/Crypto.cpp b/src/common/nxdn/Crypto.cpp new file mode 100644 index 00000000..67e527a7 --- /dev/null +++ b/src/common/nxdn/Crypto.cpp @@ -0,0 +1,391 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Common Library + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2026 C. Lovell, Dev_Ranger + * Copyright (C) 2026 Bryan Biedenkapp, N2PLL + * + */ +#include "Defines.h" +#include "nxdn/NXDNDefines.h" +#include "nxdn/Crypto.h" +#include "AESCrypto.h" +#include "DESCrypto.h" +#include "Log.h" + +using namespace ::crypto; +using namespace nxdn::defines; +using namespace nxdn::crypto; + +#include +#include + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +#define KEYSTREAM_LENGTH_BYTES 196U + +// --------------------------------------------------------------------------- +// Public Class Members +// --------------------------------------------------------------------------- + +/* Initializes a new instance of the NXDNCrypto class. */ + +NXDNCrypto::NXDNCrypto() : + m_tekCipherType(CIPHER_TYPE_NONE), + m_tekKeyId(0U), + m_tekLength(0U), + m_keystream(nullptr), + m_mi(new uint8_t[MI_LENGTH_BYTES]), + m_tek(nullptr), + m_random() +{ + ::memset(m_mi, 0x00U, MI_LENGTH_BYTES); + std::random_device rd; + m_random.seed(rd()); +} + +/* Finalizes a instance of the NXDNCrypto class. */ + +NXDNCrypto::~NXDNCrypto() +{ + if (m_keystream != nullptr) { + ::memset(m_keystream, 0x00U, KEYSTREAM_LENGTH_BYTES); + delete[] m_keystream; + } + + ::memset(m_mi, 0x00U, MI_LENGTH_BYTES); + delete[] m_mi; +} + +/* Helper to generate a new initial seed MI. */ + +void NXDNCrypto::generateMI() +{ + do { + std::uniform_int_distribution dist(0U, 255U); + for (uint8_t i = 0U; i < MI_LENGTH_BYTES; i++) + m_mi[i] = (uint8_t)dist(m_random); + } while (!hasValidMI()); +} + +/* Helper given the last MI, generate the next MI using LFSR. */ + +void NXDNCrypto::generateNextMI() +{ + if (!hasValidMI()) + return; + + uint8_t nextMI[MI_LENGTH_BYTES]; + ::memcpy(nextMI, m_mi, MI_LENGTH_BYTES); + + for (uint8_t cycle = 0U; cycle < 64U; cycle++) { + // Calculate bit 0 for the next cycle. + uint8_t carry = ((nextMI[0U] >> 7U) ^ (nextMI[0U] >> 5U) ^ + (nextMI[2U] >> 5U) ^ (nextMI[3U] >> 5U) ^ + (nextMI[4U] >> 2U) ^ (nextMI[6U] >> 6U)) & 0x01U; + + // Shift each byte and carry in the high bit from the next byte. + uint8_t i; + for (i = 0U; i < MI_LENGTH_BYTES - 1U; i++) + nextMI[i] = ((nextMI[i] & 0x7FU) << 1U) | (nextMI[i + 1U] >> 7U); + + nextMI[i] = ((nextMI[i] & 0x7FU) << 1U) | carry; + } + + ::memcpy(m_mi, nextMI, MI_LENGTH_BYTES); +} + +/* Helper to check if there is a valid encryption keystream. */ + +bool NXDNCrypto::hasValidKeystream() const +{ + return m_tek != nullptr && m_tekLength > 0U && m_keystream != nullptr; +} + +/* Helper to generate the encryption keystream. */ + +void NXDNCrypto::generateKeystream() +{ + if (m_tek == nullptr || m_tekLength == 0U) + return; + if (m_tekCipherType != CIPHER_TYPE_EHR && !hasValidMI()) + return; + if (m_keystream == nullptr) + m_keystream = new uint8_t[KEYSTREAM_LENGTH_BYTES]; + + ::memset(m_keystream, 0x00U, KEYSTREAM_LENGTH_BYTES); + switch (m_tekCipherType) { + case CIPHER_TYPE_EHR: + { + uint16_t lfsr = (uint16_t(m_tek[0U]) << 8U) | m_tek[1U]; + for (uint32_t bit = 0U; bit < uint32_t(EHR_WORDS_PER_SESSION) * AMBE_LENGTH_BITS; bit++) { + m_keystream[bit >> 3U] |= (lfsr & 1U) << (7U - (bit & 7U)); + lfsr = (lfsr >> 1U) | (((lfsr ^ (lfsr >> 1U)) & 1U) << 14U); + } + } + break; + case CIPHER_TYPE_DES: + { + DES des; + uint8_t input[DES_KEY_LENGTH_BYTES]; + ::memcpy(input, m_mi, sizeof(input)); + + uint8_t* out = des.encryptBlock(input, m_tek.get()); + ::memcpy(input, out, sizeof(input)); + delete[] out; + + for (uint32_t off = 0U; off < KEYSTREAM_LENGTH_BYTES; off += sizeof(input)) { + out = des.encryptBlock(input, m_tek.get()); + ::memcpy(input, out, sizeof(input)); + ::memcpy(m_keystream + off, out, std::min(sizeof(input), KEYSTREAM_LENGTH_BYTES - off)); + delete[] out; + } + } + break; + case CIPHER_TYPE_AES: + { + AES aes(AESKeyLength::AES_256); + uint8_t input[16U]; + ::memcpy(input, m_mi, MI_LENGTH_BYTES); + + uint64_t next = 0U; + for (uint8_t i = 0U; i < MI_LENGTH_BYTES; i++) + next = (next << 8U) | m_mi[i]; + + for (uint8_t i = 0U; i < 64U; i++) + stepLFSR(next); + + for (int8_t i = 15; i >= int8_t(MI_LENGTH_BYTES); i--) { + input[i] = (uint8_t)(next & 0xFFU); + next >>= 8U; + } + + uint8_t* out = aes.encryptECB(input, sizeof(input), m_tek.get()); + ::memcpy(input, out, sizeof(input)); + delete[] out; + + for (uint32_t off = 0U; off < KEYSTREAM_LENGTH_BYTES; off += sizeof(input)) { + out = aes.encryptECB(input, sizeof(input), m_tek.get()); + ::memcpy(input, out, sizeof(input)); + ::memcpy(m_keystream + off, out, std::min(sizeof(input), KEYSTREAM_LENGTH_BYTES - off)); + delete[] out; + } + } + break; + default: + LogError(LOG_NXDN, "unsupported crypto algorithm, cipherType = $%02X", m_tekCipherType); + if (m_keystream != nullptr) { + delete[] m_keystream; + m_keystream = nullptr; + } + break; + } +} + +/* Helper to reset the encryption keystream. */ + +void NXDNCrypto::resetKeystream() +{ + clearMI(); + + if (m_keystream != nullptr) { + ::memset(m_keystream, 0x00U, KEYSTREAM_LENGTH_BYTES); + delete[] m_keystream; + m_keystream = nullptr; + } +} + +/* Helper to crypt an AMBE word using the EHR algorithm. */ + +void NXDNCrypto::cryptEHR_AMBE(uint8_t* ambe, uint8_t n) const +{ + if (m_tekCipherType == CIPHER_TYPE_EHR) + cryptAMBE(ambe, n, EHR_WORDS_PER_SESSION); +} + +/* Helper to crypt an AMBE word using the DES algorithm. */ + +void NXDNCrypto::cryptDES_AMBE(uint8_t* ambe, uint8_t n) const +{ + if (m_tekCipherType == CIPHER_TYPE_DES) + cryptAMBE(ambe, n, BLOCK_WORDS_PER_SESSION); +} + +/* Helper to crypt an AMBE word using the AES algorithm. */ + +void NXDNCrypto::cryptAES_AMBE(uint8_t* ambe, uint8_t n) const +{ + if (m_tekCipherType == CIPHER_TYPE_AES) + cryptAMBE(ambe, n, BLOCK_WORDS_PER_SESSION); +} + +/* Helper to check if there is a valid encryption message indicator. */ + +bool NXDNCrypto::hasValidMI() const +{ + for (uint8_t i = 0U; i < MI_LENGTH_BYTES; i++) { + if (m_mi[i] != 0x00U) + return true; + } + + return false; +} + +/* Helper to set the encryption message indicator. */ + +void NXDNCrypto::setMI(const uint8_t* mi) +{ + assert(mi != nullptr); + ::memcpy(m_mi, mi, MI_LENGTH_BYTES); +} + +/* Helper to get the encryption message indicator. */ + +void NXDNCrypto::getMI(uint8_t* mi) const +{ + assert(mi != nullptr); + ::memcpy(mi, m_mi, MI_LENGTH_BYTES); +} + +/* Helper to clear the encryption message indicator. */ + +void NXDNCrypto::clearMI() +{ + ::memset(m_mi, 0x00U, MI_LENGTH_BYTES); +} + +/* Helper to set the encryption key. */ + +void NXDNCrypto::setKey(const uint8_t* key, uint8_t len) +{ + assert(key != nullptr); + + uint8_t expected = m_tekCipherType == CIPHER_TYPE_EHR ? EHR_KEY_LENGTH_BYTES : + m_tekCipherType == CIPHER_TYPE_DES ? DES_KEY_LENGTH_BYTES : + m_tekCipherType == CIPHER_TYPE_AES ? AES_KEY_LENGTH_BYTES : 0U; + + bool valid = expected > 0U && len == expected && m_tekKeyId > 0U && m_tekKeyId <= 63U; + if (valid && m_tekCipherType == CIPHER_TYPE_EHR) { + uint16_t value = (uint16_t(key[0U]) << 8U) | key[1U]; + valid = value > 0U && value <= 0x7FFFU; + } + + if (valid && m_tekCipherType == CIPHER_TYPE_DES) + valid = !isWeakDESKey(key); + + if (!valid) { + LogError(LOG_NXDN, "invalid crypto key, cipherType = $%02X, keyId = $%02X, len = %u", m_tekCipherType, m_tekKeyId, len); + clearKey(); + return; + } + + clearKey(); + m_tek = std::make_unique(len); + ::memcpy(m_tek.get(), key, len); + + m_tekLength = len; +} + +/* Helper to get the encryption key. */ + +void NXDNCrypto::getKey(uint8_t* key) const +{ + assert(key != nullptr); + + if (m_tek != nullptr) + ::memcpy(key, m_tek.get(), m_tekLength); +} + +/* Helper to clear the stored encryption key. */ + +void NXDNCrypto::clearKey() +{ + if (m_tek != nullptr) { + ::memset(m_tek.get(), 0U, m_tekLength); + m_tek.reset(); + } + + m_tekLength = 0U; + if (m_keystream != nullptr) { + ::memset(m_keystream, 0U, KEYSTREAM_LENGTH_BYTES); + delete[] m_keystream; + m_keystream = nullptr; + } +} + +// --------------------------------------------------------------------------- +// Private Class Members +// --------------------------------------------------------------------------- + +/* Crypt AMBE data using the keystream. */ + +void NXDNCrypto::cryptAMBE(uint8_t* ambe, uint8_t word, uint8_t words) const +{ + assert(ambe != nullptr); + if (m_keystream == nullptr || word >= words) + return; + + for (uint32_t bit = 0U; bit < AMBE_LENGTH_BITS; bit++) { + uint32_t pos = uint32_t(word) * AMBE_LENGTH_BITS + bit; + ambe[bit] ^= (m_keystream[pos >> 3U] >> (7U - (pos & 7U))) & 1U; + } +} + +/* Helper to step the linear feedback shift register (LFSR). */ + +uint64_t NXDNCrypto::stepLFSR(uint64_t& lfsr) +{ + uint64_t ovBit = (lfsr >> 63U) & 0x01U; + + // compute feedback bit using polynomial: x^64 + x^62 + x^46 + x^38 + x^27 + x^15 + 1 + uint64_t fbBit = ((lfsr >> 63U) ^ (lfsr >> 61U) ^ (lfsr >> 45U) ^ (lfsr >> 37U) ^ + (lfsr >> 26U) ^ (lfsr >> 14U)) & 0x01U; + + // shift LFSR left and insert feedback bit + lfsr = (lfsr << 1) | fbBit; + return ovBit; +} + +/* Helper to check for weak DES keys. */ + +bool NXDNCrypto::isWeakDESKey(const uint8_t* key) +{ + static const uint8_t WEAK_KEYS[][DES_KEY_LENGTH_BYTES] = { + { 0x01U, 0x01U, 0x01U, 0x01U, 0x01U, 0x01U, 0x01U, 0x01U }, + { 0xFEU, 0xFEU, 0xFEU, 0xFEU, 0xFEU, 0xFEU, 0xFEU, 0xFEU }, + { 0xE0U, 0xE0U, 0xE0U, 0xE0U, 0xF1U, 0xF1U, 0xF1U, 0xF1U }, + { 0x1FU, 0x1FU, 0x1FU, 0x1FU, 0x0EU, 0x0EU, 0x0EU, 0x0EU }, + { 0x01U, 0xFEU, 0x01U, 0xFEU, 0x01U, 0xFEU, 0x01U, 0xFEU }, + { 0xFEU, 0x01U, 0xFEU, 0x01U, 0xFEU, 0x01U, 0xFEU, 0x01U }, + { 0x1FU, 0xE0U, 0x1FU, 0xE0U, 0x0EU, 0xF1U, 0x0EU, 0xF1U }, + { 0xE0U, 0x1FU, 0xE0U, 0x1FU, 0xF1U, 0x0EU, 0xF1U, 0x0EU }, + { 0x01U, 0xE0U, 0x01U, 0xE0U, 0x01U, 0xF1U, 0x01U, 0xF1U }, + { 0xE0U, 0x01U, 0xE0U, 0x01U, 0xF1U, 0x01U, 0xF1U, 0x01U }, + { 0x1FU, 0xFEU, 0x1FU, 0xFEU, 0x0EU, 0xFEU, 0x0EU, 0xFEU }, + { 0xFEU, 0x1FU, 0xFEU, 0x1FU, 0xFEU, 0x0EU, 0xFEU, 0x0EU }, + { 0x01U, 0x1FU, 0x01U, 0x1FU, 0x01U, 0x0EU, 0x01U, 0x0EU }, + { 0x1FU, 0x01U, 0x1FU, 0x01U, 0x0EU, 0x01U, 0x0EU, 0x01U }, + { 0xE0U, 0xFEU, 0xE0U, 0xFEU, 0xF1U, 0xFEU, 0xF1U, 0xFEU }, + { 0xFEU, 0xE0U, 0xFEU, 0xE0U, 0xFEU, 0xF1U, 0xFEU, 0xF1U } + }; + + for (const auto& weakKey : WEAK_KEYS) { + bool match = true; + for (uint8_t i = 0U; i < DES_KEY_LENGTH_BYTES; i++) { + // DES parity bits do not contribute to the effective 56-bit key + if ((key[i] & 0xFEU) != (weakKey[i] & 0xFEU)) { + match = false; + break; + } + } + + if (match) + return true; + } + + return false; +} diff --git a/src/common/nxdn/Crypto.h b/src/common/nxdn/Crypto.h new file mode 100644 index 00000000..beb67de6 --- /dev/null +++ b/src/common/nxdn/Crypto.h @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Common Library + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2026 C. Lovell, Dev_Ranger + * Copyright (C) 2026 Bryan Biedenkapp, N2PLL + * + */ +/** + * @defgroup nxdn_crypto NXDN Cryptography + * @brief Defines and implements cryptography routines for NXDN. + * @ingroup nxdn + * + * @file Crypto.h + * @ingroup nxdn + * @file Crypto.cpp + * @ingroup nxdn + */ +#if !defined(__NXDN_CRYPTO_H__) +#define __NXDN_CRYPTO_H__ + +#include "common/Defines.h" +#include "common/nxdn/NXDNDefines.h" + +#include + +namespace nxdn +{ + namespace crypto + { + // --------------------------------------------------------------------------- + // Class Declaration + // --------------------------------------------------------------------------- + + /** + * @brief Generates and applies NXDN voice keystreams. + * @ingroup nxdn_crypto + */ + class DVM_COMMON_API NXDNCrypto { + public: + /** + * @brief Initializes a new instance of the NXDNCrypto class. + */ + NXDNCrypto(); + /** + * @brief Finalizes a instance of the NXDNCrypto class. + */ + ~NXDNCrypto(); + + NXDNCrypto(const NXDNCrypto&) = delete; + NXDNCrypto& operator=(const NXDNCrypto&) = delete; + + /** + * @brief Helper to generate a new initial seed MI. + */ + void generateMI(); + /** + * @brief Helper given the last MI, generate the next MI using LFSR. + */ + void generateNextMI(); + + /** + * @brief Helper to check if there is a valid encryption keystream. + * @return bool True, if there is a valid keystream, otherwise false. + */ + bool hasValidKeystream() const; + /** + * @brief Helper to generate the encryption keystream. + */ + void generateKeystream(); + /** + * @brief Helper to reset the encryption keystream. + */ + void resetKeystream(); + + /** + * @brief Helper to crypt an AMBE word using the EHR algorithm. + * @param ambe Buffer containing the AMBE word to crypt. + * @param n Index of the AMBE word within the session. + */ + void cryptEHR_AMBE(uint8_t* ambe, uint8_t n) const; + /** + * @brief Helper to crypt an AMBE word using the DES algorithm. + * @param ambe Buffer containing the AMBE word to crypt. + * @param n Index of the AMBE word within the session. + */ + void cryptDES_AMBE(uint8_t* ambe, uint8_t n) const; + /** + * @brief Helper to crypt an AMBE word using the AES algorithm. + * @param ambe Buffer containing the AMBE word to crypt. + * @param n Index of the AMBE word within the session. + */ + void cryptAES_AMBE(uint8_t* ambe, uint8_t n) const; + + /** + * @brief Helper to check if there is a valid encryption message indicator. + * @return bool True, if there is a valid encryption message indicator, otherwise false. + */ + bool hasValidMI() const; + /** + * @brief Sets the encryption message indicator. + * @param[in] mi Buffer containing the 9-byte Message Indicator. + */ + void setMI(const uint8_t* mi); + /** + * @brief Gets the encryption message indicator. + * @param[out] mi Buffer containing the 9-byte Message Indicator. + */ + void getMI(uint8_t* mi) const; + /** + * @brief Clears the encryption message indicator. + */ + void clearMI(); + + /** + * @brief Sets the encryption key. + * @param[in] key Buffer containing the encryption key. + * @param[in] len Length of the key. + */ + void setKey(const uint8_t* key, uint8_t len); + /** + * @brief Gets the encryption key, + * @param[out] key Buffer containing the encryption key. + */ + void getKey(uint8_t* key) const; + /** + * @brief Clears the stored encryption key. + */ + void clearKey(); + + public: + /** + * @brief Traffic Cipher Type. + */ + DECLARE_PROPERTY(uint8_t, tekCipherType, TEKCipherType); + /** + * @brief Traffic Encryption Key ID. + */ + DECLARE_PROPERTY(uint8_t, tekKeyId, TEKKeyId); + + /** + * @brief Traffic Encryption Key Length. + */ + DECLARE_RO_PROPERTY(uint8_t, tekLength, TEKKeyLength); + + private: + uint8_t* m_keystream; + + uint8_t* m_mi; + + UInt8Array m_tek; + + std::mt19937 m_random; + + /** + * @brief Helper to crypt AMBE audio. + * @param ambe Buffer containing AMBE to crypt. + * @param wordIndex Index of the word within the session. + * @param wordsPerSession Number of words per session. + */ + void cryptAMBE(uint8_t* ambe, uint8_t wordIndex, uint8_t wordsPerSession) const; + + /** + * @brief Helper to step the linear feedback shift register (LFSR). + * @note This uses the polynomial: x^64 + x^62 + x^46 + x^38 + x^27 + x^15 + 1 + * @param lfsr Linear feedback shift register value. + * @return uint64_t The next LFSR value. + */ + static uint64_t stepLFSR(uint64_t& lfsr); + + /** + * @brief Helper to check if a DES key is weak. + * @param key DES key to check. + * @returns bool True If the key is weak, otherwise false. + */ + static bool isWeakDESKey(const uint8_t* key); + }; + } // namespace crypto +} // namespace nxdn + +#endif // __NXDN_CRYPTO_H__ diff --git a/src/common/nxdn/NXDNDefines.h b/src/common/nxdn/NXDNDefines.h index 487d7da5..a2749862 100644 --- a/src/common/nxdn/NXDNDefines.h +++ b/src/common/nxdn/NXDNDefines.h @@ -116,7 +116,15 @@ namespace nxdn const uint32_t MI_LENGTH_BYTES = 8U; const uint32_t RAW_AMBE_LENGTH_BYTES = 9U; + const uint8_t AMBE_LENGTH_BITS = 49U; + const uint32_t PCKT_INFO_LENGTH_BYTES = 3U; + + const uint8_t EHR_KEY_LENGTH_BYTES = 2U; + const uint8_t DES_KEY_LENGTH_BYTES = 8U; + const uint8_t AES_KEY_LENGTH_BYTES = 32U; + const uint8_t EHR_WORDS_PER_SESSION = 16U; + const uint8_t BLOCK_WORDS_PER_SESSION = 32U; /** @} */ /** @name Thresholds */ @@ -130,6 +138,17 @@ namespace nxdn const uint32_t MAX_NXDN_VOICE_ERRORS_STEAL = 94U; /** @} */ + /** @name Encryption Algorithms */ + /** @brief Unencrypted */ + const uint8_t CIPHER_TYPE_NONE = 0x00U; + /** @brief EHR */ + const uint8_t CIPHER_TYPE_EHR = 0x01U; + /** @brief DES-OFB */ + const uint8_t CIPHER_TYPE_DES = 0x02U; + /** @brief AES-256 */ + const uint8_t CIPHER_TYPE_AES = 0x03U; + /** @} */ + /** @brief Link Information Channel - RF Channel Type */ namespace RFChannelType { /** @brief Link Information Channel - RF Channel Type */ @@ -191,10 +210,6 @@ namespace nxdn }; } - /** @name Encryption Algorithms */ - const uint8_t CIPHER_TYPE_NONE = 0x00U; //!< Unencrypted - /** @} */ - /** @brief Location Category */ namespace LocationCategory { /** @brief Location Category */ diff --git a/src/common/p25/Crypto.cpp b/src/common/p25/Crypto.cpp index 5c525fbf..c4a96b0a 100644 --- a/src/common/p25/Crypto.cpp +++ b/src/common/p25/Crypto.cpp @@ -68,6 +68,8 @@ P25Crypto::~P25Crypto() { if (m_keystream != nullptr) delete[] m_keystream; + + ::memset(m_mi, 0x00U, MI_LENGTH_BYTES); delete[] m_mi; } @@ -111,7 +113,7 @@ void P25Crypto::generateNextMI() /* Helper to check if there is a valid encryption keystream. */ -bool P25Crypto::hasValidKeystream() +bool P25Crypto::hasValidKeystream() const { if (m_tek == nullptr) return false; @@ -217,6 +219,10 @@ void P25Crypto::generateKeystream() break; default: LogError(LOG_P25, "unsupported crypto algorithm, algId = $%02X", m_tekAlgoId); + if (m_keystream != nullptr) { + delete[] m_keystream; + m_keystream = nullptr; + } break; } } @@ -743,7 +749,7 @@ void P25Crypto::cryptARC4_IMBE(uint8_t* imbe, DUID::E duid) /* Helper to check if there is a valid encryption message indicator. */ -bool P25Crypto::hasValidMI() +bool P25Crypto::hasValidMI() const { bool hasMI = false; for (uint8_t i = 0; i < MI_LENGTH_BYTES; i++) { @@ -785,13 +791,18 @@ void P25Crypto::setKey(const uint8_t* key, uint8_t len) { assert(key != nullptr); - m_tekLength = len; - if (m_tek != nullptr) - m_tek.reset(); + if (m_tekAlgoId == ALGO_DES && isWeakDESKey(key)) { + LogError(LOG_P25, "invalid DES crypto key, algoId = $%02X, keyId = $%02X, len = %u", m_tekAlgoId, m_tekKeyId, len); + clearKey(); + return; + } + clearKey(); m_tek = std::make_unique(len); ::memset(m_tek.get(), 0x00U, m_tekLength); ::memcpy(m_tek.get(), key, len); + + m_tekLength = len; } /* Gets the encryption key. */ @@ -800,7 +811,8 @@ void P25Crypto::getKey(uint8_t* key) const { assert(key != nullptr); - ::memcpy(key, m_tek.get(), m_tekLength); + if (m_tek != nullptr) + ::memcpy(key, m_tek.get(), m_tekLength); } /* Clears the stored encryption key. */ @@ -869,3 +881,43 @@ uint8_t* P25Crypto::expandMIToIV() return iv; } + +/* Helper to check for weak DES keys. */ + +bool P25Crypto::isWeakDESKey(const uint8_t* key) +{ + static const uint8_t WEAK_KEYS[][DES_ENC_KEY_LENGTH_BYTES] = { + { 0x01U, 0x01U, 0x01U, 0x01U, 0x01U, 0x01U, 0x01U, 0x01U }, + { 0xFEU, 0xFEU, 0xFEU, 0xFEU, 0xFEU, 0xFEU, 0xFEU, 0xFEU }, + { 0xE0U, 0xE0U, 0xE0U, 0xE0U, 0xF1U, 0xF1U, 0xF1U, 0xF1U }, + { 0x1FU, 0x1FU, 0x1FU, 0x1FU, 0x0EU, 0x0EU, 0x0EU, 0x0EU }, + { 0x01U, 0xFEU, 0x01U, 0xFEU, 0x01U, 0xFEU, 0x01U, 0xFEU }, + { 0xFEU, 0x01U, 0xFEU, 0x01U, 0xFEU, 0x01U, 0xFEU, 0x01U }, + { 0x1FU, 0xE0U, 0x1FU, 0xE0U, 0x0EU, 0xF1U, 0x0EU, 0xF1U }, + { 0xE0U, 0x1FU, 0xE0U, 0x1FU, 0xF1U, 0x0EU, 0xF1U, 0x0EU }, + { 0x01U, 0xE0U, 0x01U, 0xE0U, 0x01U, 0xF1U, 0x01U, 0xF1U }, + { 0xE0U, 0x01U, 0xE0U, 0x01U, 0xF1U, 0x01U, 0xF1U, 0x01U }, + { 0x1FU, 0xFEU, 0x1FU, 0xFEU, 0x0EU, 0xFEU, 0x0EU, 0xFEU }, + { 0xFEU, 0x1FU, 0xFEU, 0x1FU, 0xFEU, 0x0EU, 0xFEU, 0x0EU }, + { 0x01U, 0x1FU, 0x01U, 0x1FU, 0x01U, 0x0EU, 0x01U, 0x0EU }, + { 0x1FU, 0x01U, 0x1FU, 0x01U, 0x0EU, 0x01U, 0x0EU, 0x01U }, + { 0xE0U, 0xFEU, 0xE0U, 0xFEU, 0xF1U, 0xFEU, 0xF1U, 0xFEU }, + { 0xFEU, 0xE0U, 0xFEU, 0xE0U, 0xFEU, 0xF1U, 0xFEU, 0xF1U } + }; + + for (const auto& weakKey : WEAK_KEYS) { + bool match = true; + for (uint8_t i = 0U; i < DES_ENC_KEY_LENGTH_BYTES; i++) { + // DES parity bits do not contribute to the effective 56-bit key + if ((key[i] & 0xFEU) != (weakKey[i] & 0xFEU)) { + match = false; + break; + } + } + + if (match) + return true; + } + + return false; +} diff --git a/src/common/p25/Crypto.h b/src/common/p25/Crypto.h index 14e9d29b..9a06dfa9 100644 --- a/src/common/p25/Crypto.h +++ b/src/common/p25/Crypto.h @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-only /* * Digital Voice Modem - Common Library - * MIT Open Source. Use is subject to license terms. + * 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 @@ -35,7 +35,7 @@ namespace p25 // --------------------------------------------------------------------------- /** - * @brief Project 25 Cryptography. + * @brief Generates and applies P25 voice and data keystreams. * @ingroup p25_crypto */ class DVM_COMMON_API P25Crypto { @@ -49,9 +49,11 @@ namespace p25 */ ~P25Crypto(); + P25Crypto(const P25Crypto&) = delete; + P25Crypto& operator=(const P25Crypto&) = delete; + /** - * @brief Helper given to generate a new initial seed MI. - * @param mi + * @brief Helper to generate a new initial seed MI. */ void generateMI(); /** @@ -63,7 +65,7 @@ namespace p25 * @brief Helper to check if there is a valid encryption keystream. * @return bool True, if there is a valid keystream, otherwise false. */ - bool hasValidKeystream(); + bool hasValidKeystream() const; /** * @brief Helper to generate the encryption keystream. */ @@ -160,7 +162,7 @@ namespace p25 * @brief Helper to check if there is a valid encryption message indicator. * @return bool True, if there is a valid encryption message indicator, otherwise false. */ - bool hasValidMI(); + bool hasValidMI() const; /** * @brief Sets the encryption message indicator. * @param[in] mi Buffer containing the 9-byte Message Indicator. @@ -178,13 +180,13 @@ namespace p25 /** * @brief Sets the encryption key. - * @param[in] mi Buffer containing the encryption key. + * @param[in] key Buffer containing the encryption key. * @param[in] len Length of the key. */ void setKey(const uint8_t* key, uint8_t len); /** * @brief Gets the encryption key, - * @param[out] mi Buffer containing the encryption key. + * @param[out] key Buffer containing the encryption key. */ void getKey(uint8_t* key) const; /** @@ -229,6 +231,13 @@ namespace p25 * @return uint8_t* Buffer containing expanded 16-byte IV. */ uint8_t* expandMIToIV(); + + /** + * @brief Helper to check if a DES key is weak. + * @param key DES key to check. + * @returns bool True If the key is weak, otherwise false. + */ + static bool isWeakDESKey(const uint8_t* key); }; } // namespace crypto } // namespace p25 diff --git a/tests/nxdn/Crypto_Tests.cpp b/tests/nxdn/Crypto_Tests.cpp new file mode 100644 index 00000000..54aacafd --- /dev/null +++ b/tests/nxdn/Crypto_Tests.cpp @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Test Suite + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2026 Bryan Biedenkapp, N2PLL + * + */ + +#include +#include + +#include "common/nxdn/Crypto.h" + +using namespace nxdn::crypto; +using namespace nxdn::defines; + +/** + * @brief Crypt an AMBE word using the specified cipher type. + * @param crypto The NXDN crypto instance to use. + * @param cipher The cipher type to use. + * @param word The AMBE word to crypt. + * @param index The index of the AMBE word within the session. + */ +static void cryptWord(NXDNCrypto& crypto, uint8_t cipher, uint8_t* word, uint8_t index) +{ + if (cipher == CIPHER_TYPE_EHR) + crypto.cryptEHR_AMBE(word, index); + else if (cipher == CIPHER_TYPE_DES) + crypto.cryptDES_AMBE(word, index); + else if (cipher == CIPHER_TYPE_AES) + crypto.cryptAES_AMBE(word, index); +} + +/** + * @brief Require that the first AMBE word produced by the given cipher matches the expected bit pattern. + * @param crypto The NXDN crypto instance to use. + * @param cipher The cipher type to test. + * @param expected The expected bit pattern for the first AMBE word. + */ +static void requireWord(NXDNCrypto& crypto, uint8_t cipher, const char* expected) +{ + uint8_t word[AMBE_LENGTH_BITS] = {}; + cryptWord(crypto, cipher, word, 0U); + for (uint8_t i = 0U; i < AMBE_LENGTH_BITS; i++) + REQUIRE(word[i] == (uint8_t)(expected[i] - '0')); + + // XOR with the same indexed stream is its own inverse. + cryptWord(crypto, cipher, word, 0U); + for (uint8_t bit : word) + REQUIRE(bit == 0U); +} + +TEST_CASE("NXDN EHR known-answer vector", "[nxdn][crypto][ehr]") +{ + const uint8_t key[] = { 0x12U, 0x34U }; + NXDNCrypto crypto; + crypto.setTEKCipherType(CIPHER_TYPE_EHR); + crypto.setTEKKeyId(1U); + crypto.setKey(key, sizeof(key)); + crypto.generateKeystream(); + REQUIRE(crypto.hasValidKeystream()); + requireWord(crypto, CIPHER_TYPE_EHR, "0010110001001000111010011011001001110101101011010"); + uint8_t word[AMBE_LENGTH_BITS] = {}; + crypto.cryptEHR_AMBE(word, EHR_WORDS_PER_SESSION); + for (uint8_t bit : word) + REQUIRE(bit == 0U); +} + +TEST_CASE("NXDN DES-OFB known-answer vector", "[nxdn][crypto][des]") +{ + const uint8_t key[] = { 0x13U, 0x34U, 0x57U, 0x79U, 0x9BU, 0xBCU, 0xDFU, 0xF1U }; + const uint8_t mi[] = { 0x01U, 0x23U, 0x45U, 0x67U, 0x89U, 0xABU, 0xCDU, 0xEFU }; + NXDNCrypto crypto; + crypto.setTEKCipherType(CIPHER_TYPE_DES); + crypto.setTEKKeyId(2U); + crypto.setKey(key, sizeof(key)); + crypto.generateKeystream(); + REQUIRE_FALSE(crypto.hasValidKeystream()); + crypto.setMI(mi); + crypto.generateKeystream(); + REQUIRE(crypto.hasValidKeystream()); + requireWord(crypto, CIPHER_TYPE_DES, "0110011110101110011110100010100101100001110111111"); +} + +TEST_CASE("NXDN AES-256-OFB known-answer vector", "[nxdn][crypto][aes]") +{ + uint8_t key[AES_KEY_LENGTH_BYTES]; + for (uint8_t i = 0U; i < sizeof(key); i++) + key[i] = i; + + const uint8_t mi[] = { 0x01U, 0x23U, 0x45U, 0x67U, 0x89U, 0xABU, 0xCDU, 0xEFU }; + NXDNCrypto crypto; + crypto.setTEKCipherType(CIPHER_TYPE_AES); + crypto.setTEKKeyId(3U); + crypto.setKey(key, sizeof(key)); + crypto.setMI(mi); + crypto.generateKeystream(); + REQUIRE(crypto.hasValidKeystream()); + requireWord(crypto, CIPHER_TYPE_AES, "0011110010110110000110001010000001001010001110011"); + crypto.generateNextMI(); + + uint8_t next[MI_LENGTH_BYTES]; + crypto.getMI(next); + const uint8_t expected[] = { 0x20U, 0xB1U, 0x25U, 0xE7U, 0x79U, 0xD0U, 0xF3U, 0x4EU }; + REQUIRE(::memcmp(next, expected, sizeof(next)) == 0); +} + +TEST_CASE("NXDN crypto rejects invalid keys", "[nxdn][crypto]") +{ + NXDNCrypto crypto; + const uint8_t zeroEhr[] = { 0x00U, 0x00U }; + const uint8_t highEhr[] = { 0x80U, 0x01U }; + const uint8_t validEhr[] = { 0x00U, 0x01U }; + const uint8_t weakDes[] = { 0x01U, 0x01U, 0x01U, 0x01U, 0x01U, 0x01U, 0x01U, 0x01U }; + crypto.setTEKCipherType(CIPHER_TYPE_EHR); + crypto.setTEKKeyId(1U); + crypto.setKey(zeroEhr, sizeof(zeroEhr)); + REQUIRE(crypto.getTEKKeyLength() == 0U); + crypto.setKey(highEhr, sizeof(highEhr)); + REQUIRE(crypto.getTEKKeyLength() == 0U); + crypto.setTEKKeyId(0U); + crypto.setKey(validEhr, sizeof(validEhr)); + REQUIRE(crypto.getTEKKeyLength() == 0U); + crypto.setTEKKeyId(64U); + crypto.setKey(validEhr, sizeof(validEhr)); + REQUIRE(crypto.getTEKKeyLength() == 0U); + crypto.setTEKCipherType(CIPHER_TYPE_DES); + crypto.setTEKKeyId(1U); + crypto.setKey(weakDes, sizeof(weakDes)); + REQUIRE(crypto.getTEKKeyLength() == 0U); + crypto.setTEKCipherType(CIPHER_TYPE_EHR); + crypto.setTEKKeyId(63U); + crypto.setKey(validEhr, sizeof(validEhr)); + REQUIRE(crypto.getTEKKeyLength() == sizeof(validEhr)); +}