Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
21 changes: 13 additions & 8 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -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.
109 changes: 64 additions & 45 deletions README.md
Original file line number Diff line number Diff line change
@@ -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<uint8_t> &_iv);
```
This function sets the Initialization Vector (IV) for encryption and decryption. The IV is obtained during authentication.
## What it provides

### encryptAes
```cpp
vector<uint8_t> encryptAes(vector<uint8_t> &data, const vector<uint8_t> &key, const vector<uint8_t> &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<uint8_t> decryptAes(vector<uint8_t> &data, const vector<uint8_t> &key, const vector<uint8_t> &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<uint8_t> &_key, const vector<uint8_t> &_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<uint8_t> getCMAC(const vector<uint8_t> &_data);
#include <cstdint>
#include <vector>

#include "desfire_crypto/DesfireCrypto.h"

int main() {
DesfireCrypto crypto;

std::vector<uint8_t> key = {
0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6,
0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c,
};
std::vector<uint8_t> iv(16, 0x00);
std::vector<uint8_t> 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

`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
32 changes: 12 additions & 20 deletions include/desfire_crypto/DesfireCrypto.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint8_t> DesfireCrypto::getCMAC(const vector<uint8_t>& 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<size_t>(1, (data.size() + AES_BLOCK_SIZE - 1) / AES_BLOCK_SIZE);

vector<uint8_t> cmac(AES_BLOCK_SIZE / 2, 0x00);
vector<vector<uint8_t>> 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<int>(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<uint8_t> block(data.begin() + start, data.begin() + end);
blocks.push_back(block);
}

if (isPaddingRequired) {
if (!hasCompleteFinalBlock) {
vector<uint8_t>& 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<unsigned char> tempIv(AES_BLOCK_SIZE, 0x00);
while (offset < numberOfBlocks) {
vector<unsigned char> temp = blocks[offset];
for (size_t blockIndex = 0; blockIndex < numberOfBlocks; ++blockIndex) {
vector<unsigned char> temp = blocks[blockIndex];
vector<unsigned char> 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};
Expand Down
7 changes: 4 additions & 3 deletions include/desfire_crypto/DesfireCrypto.h
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,12 @@ class DesfireCrypto {
*/
static void xorVec(const vector<uint8_t> &vector1, const vector<uint8_t> &vector2, vector<uint8_t> &result) {
size_t size = min(vector1.size(), vector2.size());
result.clear();
result.reserve(size);
vector<uint8_t> 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);
}

/**
Expand Down
64 changes: 64 additions & 0 deletions tests/desfire_crypto_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#include <cstdint>
#include <cstdlib>
#include <iostream>
#include <string>
#include <vector>

#include "desfire_crypto/DesfireCrypto.h"

namespace {

std::vector<uint8_t> fromHex(const std::string& hex) {
std::vector<uint8_t> bytes;
bytes.reserve(hex.size() / 2);
for (size_t i = 0; i < hex.size(); i += 2) {
bytes.push_back(static_cast<uint8_t>(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<uint8_t> 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;
}
Loading