implement NXDN crypto support; code cleanup;

r05a06_dev
Bryan Biedenkapp 3 days ago
parent 44b5042c35
commit 5c5016b623

@ -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 <algorithm>
#include <cassert>
// ---------------------------------------------------------------------------
// 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<uint32_t> 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<uint32_t>(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<uint32_t>(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<uint8_t[]>(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;
}

@ -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 <random>
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__

@ -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 */

@ -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<uint8_t[]>(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;
}

@ -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

@ -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 <catch2/catch_test_macros.hpp>
#include <cstring>
#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));
}
Loading…
Cancel
Save

Powered by TurnKey Linux.