diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..71fabaf --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-and-test: + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Configure + run: cmake -S . -B build -DBUILD_TESTING=ON -DCMAKE_BUILD_TYPE=Release + + - name: Build + run: cmake --build build --parallel + + - name: Test + run: ctest --test-dir build --output-on-failure diff --git a/CMakeLists.txt b/CMakeLists.txt index 96ac741..ee5d72e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,16 +2,21 @@ cmake_minimum_required(VERSION 3.25) project(DesfireCrypto) set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) -add_executable(DesfireCrypto - - # INCLUDE +add_library(desfire_crypto include/aes/AES.cpp - include/aes/AES.h include/desfire_crypto/DesfireCrypto.cpp - include/desfire_crypto/DesfireCrypto.h +) +target_include_directories(desfire_crypto PUBLIC include) - # MAIN - src/main.cpp +add_executable(DesfireCrypto src/main.cpp) +target_link_libraries(DesfireCrypto PRIVATE desfire_crypto) -) +include(CTest) +if(BUILD_TESTING) + add_executable(desfire_crypto_tests tests/desfire_crypto_test.cpp) + target_link_libraries(desfire_crypto_tests PRIVATE desfire_crypto) + add_test(NAME desfire_crypto_tests COMMAND desfire_crypto_tests) +endif() diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b7d182d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Govind Yadav + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index cb90cb0..1fa96b7 100644 --- a/README.md +++ b/README.md @@ -1,56 +1,75 @@ # DesfireCrypto -DesfireCrypto is a C++ class that provides cryptographic functionalities for DESFire cards. It includes encryption, decryption, initialization of the Cipher-based Message Authentication Code (CMAC), and other utility functions. +A small C++17 implementation of the cryptographic building blocks used by MIFARE DESFire integrations: AES-CBC encryption and decryption, AES-CMAC subkey generation, DESFire-style truncated CMAC output, CRC-32, and byte-vector helpers. -## Functions +The repository is intentionally compact so the byte-level operations remain inspectable. -### generateSubkeys -```cpp -void generateSubkeys(); -``` -This function generates key0, key1, and key2 for DESFire. It performs the following steps: -1. Generate key0, key1, and key2. -2. Encrypt 16 bytes of 0x00 with the key. -3. Left shift key0 by 1 bit and store it in key1. -4. If the Most Significant Bit (MSB) of key0 is 0x80, then key1 = (key0 << 1) ^ 0x87. -5. Left shift key1 by 1 bit and store it in key2. -6. If the MSB of key1 is 0x80, then key2 = (key1 << 1) ^ 0x87. - -### setIv -```cpp -void setIv(const vector &_iv); -``` -This function sets the Initialization Vector (IV) for encryption and decryption. The IV is obtained during authentication. +## What it provides -### encryptAes -```cpp -vector encryptAes(vector &data, const vector &key, const vector &iv); -``` -This function encrypts the provided data using AES-128 algorithm. It takes the data, encryption key, and IV as inputs and returns the encrypted data as a vector of bytes. +- AES-128 encryption and decryption +- AES-CMAC generation with the first eight bytes returned for DESFire workflows +- CMAC support for empty, single-block, and multi-block messages +- Configurable session IV +- DESFire CRC-32 helper +- CMake library, example executable, and CTest target -### decryptAes -```cpp -vector decryptAes(vector &data, const vector &key, const vector &iv); -``` -This function decrypts the provided data using AES-128 algorithm. It takes the data, decryption key, and IV as inputs and returns the decrypted data as a vector of bytes. +## Build and test -### initCMAC -```cpp -void initCMAC(const vector &_key, const vector &_iv); +Requirements: a C++17 compiler and CMake 3.25 or newer. + +```bash +cmake -S . -B build +cmake --build build +ctest --test-dir build --output-on-failure ``` -This function initializes the Cipher-based Message Authentication Code (CMAC) with the provided key and IV. It is called during the authentication process. -### getCMAC +The tests use the AES-CMAC examples from NIST SP 800-38B, truncated to the eight-byte value returned by `getCMAC()`. + +## Example + ```cpp -vector getCMAC(const vector &_data); +#include +#include + +#include "desfire_crypto/DesfireCrypto.h" + +int main() { + DesfireCrypto crypto; + + std::vector key = { + 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, + 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c, + }; + std::vector iv(16, 0x00); + std::vector message = { + 0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96, + 0xe9, 0x3d, 0x7e, 0x11, 0x73, 0x93, 0x17, 0x2a, + }; + + crypto.initCMAC(key, iv); + crypto.generateSubkeys(); + const auto cmac = crypto.getCMAC(message); +} ``` -This function calculates the CMAC for the provided data. It performs the following steps: -1. Checks if padding is required (`isPaddingRequired = dataLen % AES_BLOCK_SIZE != 0`). -2. Calculates the number of blocks (`numberOfBlocks = isPaddingRequired ? dataLen / AES_BLOCK_SIZE + 1 : dataLen / AES_BLOCK_SIZE`). -3. Splits the data into blocks, each of size 16 bytes (for AES). -4. If padding is required, adds padding to the last block (`lastBlock.push_back(0x80); lastBlock.resize(AES_BLOCK_SIZE, 0x00);`). -5. If `isPaddingRequired` is true, XORs the last block with key2; otherwise, XORs it with key1. -6. For each block, XORs it with the previous IV obtained in the last process (`xorVec(blocks[i], iv, blocks[i]);`). -7. Encrypts the XORed block with AES-128 using the key and IV (`aes.EncryptCBC(blocks[i], key, iv);`). The IV used for encryption should be all zeros. -8. Updates the IV with the encrypted block. -9. Returns the last block of the IV as DesfireCrypto, as only the first 8 \ No newline at end of file + +`getCMAC()` updates the object's IV as blocks are processed. Create a new instance or call `setIv()` when starting an independent calculation. + +## API overview + +| Method | Purpose | +| --- | --- | +| `initCMAC(key, iv)` | Initialize AES-CMAC state with a 16-byte key and IV | +| `generateSubkeys()` | Derive the two AES-CMAC subkeys | +| `getCMAC(data)` | Return the first eight bytes of the calculated CMAC | +| `setIv(iv)` | Replace the current session IV | +| `encryptAes(data, key, iv)` | AES-CBC encryption | +| `decryptAes(data, key, iv)` | AES-CBC decryption | +| `crc32(data, length, output)` | Calculate the four-byte CRC value | + +## Security status + +This implementation has not received an independent security audit. Validate it against the requirements and test vectors for your card/application profile before using it in production or for key-management operations. Do not log keys, derived subkeys, or session IVs. + +## License + +[MIT](LICENSE) © Govind Yadav diff --git a/include/desfire_crypto/DesfireCrypto.cpp b/include/desfire_crypto/DesfireCrypto.cpp index e1fc72d..71885fc 100644 --- a/include/desfire_crypto/DesfireCrypto.cpp +++ b/include/desfire_crypto/DesfireCrypto.cpp @@ -16,49 +16,41 @@ void DesfireCrypto::generateSubkeys() { leftShift(key1, key2); if (key1[0] & 0x80) key2[key2.size() - 1] ^= 0x87U; - - vecPrint("key0", key0); - vecPrint("key1", key1); - vecPrint("key2", key2); - } vector DesfireCrypto::getCMAC(const vector& data) { - const int AES_BLOCK_SIZE = 16; - bool isPaddingRequired = (data.size() % AES_BLOCK_SIZE != 0); - int numberOfBlocks = isPaddingRequired ? data.size() / AES_BLOCK_SIZE + 1 : data.size() / AES_BLOCK_SIZE; + constexpr size_t AES_BLOCK_SIZE = 16; + const bool hasCompleteFinalBlock = !data.empty() && data.size() % AES_BLOCK_SIZE == 0; + const size_t numberOfBlocks = max(1, (data.size() + AES_BLOCK_SIZE - 1) / AES_BLOCK_SIZE); vector cmac(AES_BLOCK_SIZE / 2, 0x00); vector> blocks; + blocks.reserve(numberOfBlocks); - for (int i = 0; i < numberOfBlocks; ++i) { - int start = i * AES_BLOCK_SIZE; - int end = min(start + AES_BLOCK_SIZE, static_cast(data.size())); + for (size_t i = 0; i < numberOfBlocks; ++i) { + const size_t start = min(i * AES_BLOCK_SIZE, data.size()); + const size_t end = min(start + AES_BLOCK_SIZE, data.size()); vector block(data.begin() + start, data.begin() + end); blocks.push_back(block); } - if (isPaddingRequired) { + if (!hasCompleteFinalBlock) { vector& lastBlock = blocks.back(); - if (lastBlock.size() < AES_BLOCK_SIZE) { - lastBlock.push_back(0x80); - lastBlock.resize(AES_BLOCK_SIZE, 0x00); - } + lastBlock.push_back(0x80); + lastBlock.resize(AES_BLOCK_SIZE, 0x00); xorVec(lastBlock, key2, lastBlock); } else { xorVec(blocks.back(), key1, blocks.back()); } - int offset = 0; vector tempIv(AES_BLOCK_SIZE, 0x00); - while (offset < numberOfBlocks) { - vector temp = blocks[offset]; + for (size_t blockIndex = 0; blockIndex < numberOfBlocks; ++blockIndex) { + vector temp = blocks[blockIndex]; vector xorTemp(AES_BLOCK_SIZE, 0x00); xorVec(iv, temp, xorTemp); temp = encryptAes(xorTemp, key, tempIv); iv = temp; - offset += AES_BLOCK_SIZE; } cmac = {iv.begin(), iv.begin() + AES_BLOCK_SIZE / 2}; diff --git a/include/desfire_crypto/DesfireCrypto.h b/include/desfire_crypto/DesfireCrypto.h index d768c53..818976c 100644 --- a/include/desfire_crypto/DesfireCrypto.h +++ b/include/desfire_crypto/DesfireCrypto.h @@ -126,11 +126,12 @@ class DesfireCrypto { */ static void xorVec(const vector &vector1, const vector &vector2, vector &result) { size_t size = min(vector1.size(), vector2.size()); - result.clear(); - result.reserve(size); + vector output; + output.reserve(size); for (size_t i = 0; i < size; ++i) { - result.push_back(vector1[i] ^ vector2[i]); + output.push_back(vector1[i] ^ vector2[i]); } + result.swap(output); } /** diff --git a/tests/desfire_crypto_test.cpp b/tests/desfire_crypto_test.cpp new file mode 100644 index 0000000..cd131e9 --- /dev/null +++ b/tests/desfire_crypto_test.cpp @@ -0,0 +1,64 @@ +#include +#include +#include +#include +#include + +#include "desfire_crypto/DesfireCrypto.h" + +namespace { + +std::vector fromHex(const std::string& hex) { + std::vector bytes; + bytes.reserve(hex.size() / 2); + for (size_t i = 0; i < hex.size(); i += 2) { + bytes.push_back(static_cast(std::stoul(hex.substr(i, 2), nullptr, 16))); + } + return bytes; +} + +void expectCmac(const std::string& name, const std::string& messageHex, const std::string& expectedHex) { + DesfireCrypto crypto; + auto key = fromHex("2b7e151628aed2a6abf7158809cf4f3c"); + std::vector iv(16, 0x00); + const auto message = fromHex(messageHex); + + crypto.initCMAC(key, iv); + crypto.generateSubkeys(); + const auto actual = crypto.getCMAC(message); + const auto expected = fromHex(expectedHex); + + if (actual != expected) { + std::cerr << name << " failed" << std::endl; + std::exit(EXIT_FAILURE); + } +} + +} // namespace + +int main() { + expectCmac("empty message", "", "bb1d6929e9593728"); + expectCmac( + "one complete block", + "6bc1bee22e409f96e93d7e117393172a", + "070a16b46b4d4144" + ); + expectCmac( + "partial final block", + "6bc1bee22e409f96e93d7e117393172a" + "ae2d8a571e03ac9c9eb76fac45af8e51" + "30c81c46a35ce411", + "dfa66747de9ae630" + ); + expectCmac( + "four complete blocks", + "6bc1bee22e409f96e93d7e117393172a" + "ae2d8a571e03ac9c9eb76fac45af8e51" + "30c81c46a35ce411e5fbc1191a0a52ef" + "f69f2445df4f9b17ad2b417be66c3710", + "51f0bebf7e3b9d92" + ); + + std::cout << "All AES-CMAC vectors passed" << std::endl; + return EXIT_SUCCESS; +}