diff --git a/API.md b/API.md new file mode 100644 index 0000000..2b16f35 --- /dev/null +++ b/API.md @@ -0,0 +1,904 @@ +# API Reference + +Complete API documentation for the PHP SDM library. + +## Table of Contents + +- [SDM Class](#sdm-class) +- [KeyDerivation Class](#keyderivation-class) +- [Enums](#enums) +- [Exceptions](#exceptions) +- [Cipher Classes](#cipher-classes) + +--- + +## SDM Class + +**Namespace:** `KDuma\SDM` + +**Implements:** `SDMInterface` + +Main class for NTAG 424 DNA Secure Dynamic Messaging operations. + +### Constructor + +```php +public function __construct( + string $encKey, + string $macKey, + string $sdmmacParam = '' +) +``` + +Creates a new SDM instance. + +**Parameters:** +- `$encKey` - Encryption key for PICC data (binary, 16 bytes) +- `$macKey` - MAC calculation key (binary, 16 bytes) +- `$sdmmacParam` - SDMMAC parameter name for separated mode (default: empty string) + +**Example:** + +```php +$sdm = new SDM( + encKey: hex2bin('C9EB67DF090AFF47C3B19A2516680B9D'), + macKey: hex2bin('1234567890ABCDEF1234567890ABCDEF'), + sdmmacParam: 'cmac' +); +``` + +--- + +### decrypt() + +```php +public function decrypt( + string $encData, + string $encFileData, + string $cmac +): array +``` + +Decrypt and validate an SDM message (convenience wrapper). + +**Parameters:** +- `$encData` - Encrypted PICC data (binary, 16 or 24 bytes) +- `$encFileData` - Encrypted file data (binary, multiple of 16 bytes, or empty) +- `$cmac` - CMAC for authentication (binary, 8 bytes) + +**Returns:** Array with decrypted data: +```php +[ + 'picc_data_tag' => string, // 1 byte: PICCDataTag configuration + 'uid' => string, // 7 bytes: Tag UID + 'read_ctr' => int, // Read counter value (0-16777215) + 'file_data' => ?string, // Decrypted file data or null + 'encryption_mode' => EncMode // AES or LRP +] +``` + +**Throws:** +- `DecryptionException` - If decryption fails +- `ValidationException` - If CMAC validation fails + +**Example:** + +```php +$result = $sdm->decrypt( + encData: hex2bin('EF963FF7828658A599F3041510671E88'), + encFileData: hex2bin('CEE9A53E3E463EF1F459635736738962'), + cmac: hex2bin('94EED9EE65337086') +); + +echo "UID: " . bin2hex($result['uid']) . "\n"; +echo "Counter: " . $result['read_ctr'] . "\n"; +``` + +--- + +### validate() + +```php +public function validate(string $data, string $cmac): bool +``` + +Validate plain SUN message authentication (convenience wrapper). + +**Parameters:** +- `$data` - Plain SUN data: UID (7 bytes) + ReadCtr (3 bytes), exactly 10 bytes +- `$cmac` - SDMMAC to validate against (binary, 8 bytes) + +**Returns:** `true` if valid, `false` otherwise + +**Example:** + +```php +$data = hex2bin('041E3C8A2D6B80000006'); // UID + counter +$cmac = hex2bin('4B00064004B0B3D3'); + +if ($sdm->validate($data, $cmac)) { + echo "Valid SUN message"; +} +``` + +**Note:** This method assumes AES mode. For LRP mode or detailed results, use `validatePlainSun()`. + +--- + +### decryptSunMessage() + +```php +public function decryptSunMessage( + ParamMode $paramMode, + string $sdmMetaReadKey, + callable $sdmFileReadKey, + string $piccEncData, + string $sdmmac, + ?string $encFileData = null +): array +``` + +Decrypt SUN message for NTAG 424 DNA (advanced method). + +**Parameters:** +- `$paramMode` - Type of dynamic URL encoding (SEPARATED or BULK) +- `$sdmMetaReadKey` - SUN decryption key for PICC data (binary, 16 bytes) +- `$sdmFileReadKey` - Callable that returns MAC key: `function(string $uid): string` +- `$piccEncData` - Encrypted PICC data (binary, 16 or 24 bytes) +- `$sdmmac` - SDMMAC of the SUN message (binary, 8 bytes) +- `$encFileData` - Optional encrypted file data (binary, multiple of 16 bytes) + +**Returns:** Array with decrypted data (same as `decrypt()`) + +**Throws:** +- `DecryptionException` - If SUN message is invalid +- `ValidationException` - If MAC is invalid + +**Example:** + +```php +use KDuma\SDM\ParamMode; + +$result = $sdm->decryptSunMessage( + paramMode: ParamMode::SEPARATED, + sdmMetaReadKey: $encKey, + sdmFileReadKey: function(string $uid) use ($kdf, $masterKey): string { + return $kdf->deriveTagKey($masterKey, $uid, 2); + }, + piccEncData: hex2bin($piccData), + sdmmac: hex2bin($cmac), + encFileData: hex2bin($encFileData) +); +``` + +--- + +### validatePlainSun() + +```php +public function validatePlainSun( + string $uid, + string $readCtr, + string $sdmmac, + string $sdmFileReadKey, + ?EncMode $mode = null +): array +``` + +Validate plain (unencrypted) SUN message authentication. + +**Parameters:** +- `$uid` - Tag's unique identifier (binary, 7 bytes) +- `$readCtr` - SDM read counter (binary, 3 bytes, little-endian) +- `$sdmmac` - SDMMAC to validate against (binary, 8 bytes) +- `$sdmFileReadKey` - MAC calculation key (binary, 16 bytes) +- `$mode` - Encryption mode (EncMode::AES or EncMode::LRP, default: AES) + +**Returns:** Array with validation result: +```php +[ + 'encryption_mode' => EncMode, // AES or LRP + 'uid' => string, // 7 bytes: Tag UID + 'read_ctr' => int // Read counter value +] +``` + +**Throws:** +- `ValidationException` - If MAC is invalid or input data is malformed + +**Example:** + +```php +use KDuma\SDM\EncMode; + +$result = $sdm->validatePlainSun( + uid: hex2bin('041E3C8A2D6B80'), + readCtr: hex2bin('000006'), + sdmmac: hex2bin('4B00064004B0B3D3'), + sdmFileReadKey: $macKey, + mode: EncMode::AES +); +``` + +--- + +### calculateSdmmac() + +```php +public function calculateSdmmac( + ParamMode $paramMode, + string $sdmFileReadKey, + string $piccData, + ?string $encFileData = null, + ?EncMode $mode = null +): string +``` + +Calculate SDMMAC for NTAG 424 DNA. + +**Parameters:** +- `$paramMode` - Type of dynamic URL encoding (SEPARATED or BULK) +- `$sdmFileReadKey` - MAC calculation key (binary, 16 bytes) +- `$piccData` - UID + SDMReadCtr (binary, 10 bytes) +- `$encFileData` - Optional encrypted file data (binary) +- `$mode` - Encryption mode (EncMode::AES or EncMode::LRP, default: AES) + +**Returns:** Calculated SDMMAC (binary, 8 bytes) + +**Example:** + +```php +use KDuma\SDM\ParamMode; +use KDuma\SDM\EncMode; + +$piccData = $uid . strrev($readCtr); // UID + reversed counter +$sdmmac = $sdm->calculateSdmmac( + paramMode: ParamMode::SEPARATED, + sdmFileReadKey: $macKey, + piccData: $piccData, + encFileData: $encFileData, + mode: EncMode::AES +); +``` + +--- + +### decryptFileData() + +```php +public function decryptFileData( + string $sdmFileReadKey, + string $piccData, + string $readCtr, + string $encFileData, + ?EncMode $mode = null +): string +``` + +Decrypt SDMEncFileData for NTAG 424 DNA. + +**Parameters:** +- `$sdmFileReadKey` - SUN decryption key (binary, 16 bytes) +- `$piccData` - PICCDataTag + UID + SDMReadCtr (binary) +- `$readCtr` - SDM read counter (binary, 3 bytes, little-endian) +- `$encFileData` - Encrypted file data (binary, multiple of 16 bytes) +- `$mode` - Encryption mode (EncMode::AES or EncMode::LRP, default: AES) + +**Returns:** Decrypted file data (binary) + +**Example:** + +```php +use KDuma\SDM\EncMode; + +$fileData = $sdm->decryptFileData( + sdmFileReadKey: $fileKey, + piccData: $uid . strrev($readCtr), + readCtr: $readCtr, + encFileData: $encryptedData, + mode: EncMode::AES +); +``` + +--- + +### getEncryptionMode() + +```php +public function getEncryptionMode(string $piccEncData): EncMode +``` + +Get encryption mode from PICC encrypted data length. + +**Parameters:** +- `$piccEncData` - Encrypted PICC data (binary) + +**Returns:** Detected encryption mode (EncMode::AES or EncMode::LRP) + +**Throws:** +- `DecryptionException` - If unsupported encryption mode (not 16 or 24 bytes) + +**Example:** + +```php +$mode = $sdm->getEncryptionMode($piccEncData); + +if ($mode === EncMode::AES) { + echo "Using AES mode"; +} else { + echo "Using LRP mode"; +} +``` + +--- + +## KeyDerivation Class + +**Namespace:** `KDuma\SDM` + +Key derivation functions for NTAG 424 DNA based on NIST SP 800-108. + +### Constructor + +```php +public function __construct() +``` + +Creates a new KeyDerivation instance. + +**Example:** + +```php +$kdf = new KeyDerivation(); +``` + +--- + +### deriveUndiversifiedKey() + +```php +public function deriveUndiversifiedKey( + string $masterKey, + int $keyNumber +): string +``` + +Derive an undiversified key from a master key. + +**Parameters:** +- `$masterKey` - Master key (binary, 16-32 bytes) +- `$keyNumber` - Key number (must be 1) + +**Returns:** Derived key (binary, 16 bytes) + +**Throws:** +- `InvalidArgumentException` - If key number is not 1 or master key length is invalid + +**Example:** + +```php +$masterKey = hex2bin('C9EB67DF090AFF47C3B19A2516680B9D'); +$encKey = $kdf->deriveUndiversifiedKey($masterKey, 1); +``` + +**Note:** Returns factory key unchanged if master key is all zeros. + +--- + +### deriveTagKey() + +```php +public function deriveTagKey( + string $masterKey, + string $uid, + int $keyNumber +): string +``` + +Derive a tag-specific (UID-diversified) key from a master key. + +**Parameters:** +- `$masterKey` - Master key (binary, 16-32 bytes) +- `$uid` - UID of the tag (binary, exactly 7 bytes) +- `$keyNumber` - Key number (1 or 2) + +**Returns:** Derived key (binary, 16 bytes) + +**Throws:** +- `InvalidArgumentException` - If UID length is not 7 bytes, key number is invalid, or master key length is invalid + +**Example:** + +```php +$uid = hex2bin('04E12AB3CD5E80'); +$fileKey = $kdf->deriveTagKey($masterKey, $uid, 1); // File read key +$macKey = $kdf->deriveTagKey($masterKey, $uid, 2); // MAC key +``` + +**Key Numbers:** +- `1` - File read key (K_SDMFileReadKey) +- `2` - MAC key (K_SDMFileReadMACKey) + +**Note:** Returns factory key unchanged if master key is all zeros. + +--- + +## Enums + +### EncMode + +**Namespace:** `KDuma\SDM` + +Encryption mode for NTAG 424 DNA SDM. + +**Values:** + +```php +enum EncMode: int +{ + case AES = 0; // AES-128 mode (16-byte PICC data) + case LRP = 1; // Leakage Resilient Primitive (24-byte PICC data) +} +``` + +**Usage:** + +```php +use KDuma\SDM\EncMode; + +$mode = EncMode::AES; +echo $mode->name; // "AES" +echo $mode->value; // 0 + +if ($mode === EncMode::AES) { + // Handle AES mode +} +``` + +--- + +### ParamMode + +**Namespace:** `KDuma\SDM` + +Parameter mode for dynamic URL encoding. + +**Values:** + +```php +enum ParamMode: int +{ + case SEPARATED = 0; // Each parameter has its own name + case BULK = 1; // All parameters concatenated +} +``` + +**Usage:** + +```php +use KDuma\SDM\ParamMode; + +$mode = ParamMode::SEPARATED; +echo $mode->name; // "SEPARATED" +echo $mode->value; // 0 +``` + +**Examples:** + +- **SEPARATED:** `?picc_data=xxx&enc=yyy&cmac=zzz` +- **BULK:** `?data=xxxyyyzzz` + +--- + +## Exceptions + +All exceptions extend from the base `SDMException` class. + +### SDMException + +**Namespace:** `KDuma\SDM\Exceptions` + +Base exception for all SDM-related errors. + +```php +class SDMException extends \Exception +{ +} +``` + +--- + +### DecryptionException + +**Namespace:** `KDuma\SDM\Exceptions` + +Thrown when decryption fails. + +```php +class DecryptionException extends SDMException +{ +} +``` + +**Common scenarios:** +- Invalid encryption key +- Malformed encrypted data +- Incorrect data length +- Unsupported encryption mode +- Failed to decrypt PICC data + +**Example:** + +```php +use KDuma\SDM\Exceptions\DecryptionException; + +try { + $result = $sdm->decrypt($encData, $encFileData, $cmac); +} catch (DecryptionException $e) { + error_log("Decryption failed: " . $e->getMessage()); +} +``` + +--- + +### ValidationException + +**Namespace:** `KDuma\SDM\Exceptions` + +Thrown when CMAC validation fails. + +```php +class ValidationException extends SDMException +{ +} +``` + +**Common scenarios:** +- Invalid CMAC +- Tampered data +- Wrong MAC key +- Incorrect parameter mode + +**Example:** + +```php +use KDuma\SDM\Exceptions\ValidationException; + +try { + $result = $sdm->decrypt($encData, $encFileData, $cmac); +} catch (ValidationException $e) { + error_log("Validation failed - possible tampering: " . $e->getMessage()); +} +``` + +--- + +## Cipher Classes + +Internal cipher implementations (not typically used directly). + +### AESCipher + +**Namespace:** `KDuma\SDM\Cipher` + +**Implements:** `CipherInterface` + +AES-128 cipher implementation for SDM operations. + +#### encrypt() + +```php +public function encrypt( + string $data, + string $key, + string $iv +): string +``` + +Encrypt data using AES-128-CBC. + +**Parameters:** +- `$data` - Data to encrypt (binary) +- `$key` - Encryption key (binary, 16 bytes) +- `$iv` - Initialization vector (binary, 16 bytes) + +**Returns:** Encrypted data (binary) + +--- + +#### decrypt() + +```php +public function decrypt( + string $data, + string $key, + string $iv +): string +``` + +Decrypt data using AES-128-CBC. + +**Parameters:** +- `$data` - Data to decrypt (binary) +- `$key` - Decryption key (binary, 16 bytes) +- `$iv` - Initialization vector (binary, 16 bytes) + +**Returns:** Decrypted data (binary) + +--- + +#### cmac() + +```php +public function cmac( + string $data, + string $key +): string +``` + +Calculate AES-CMAC (RFC 4493). + +**Parameters:** +- `$data` - Data to authenticate (binary or string) +- `$key` - CMAC key (binary, 16 bytes) + +**Returns:** CMAC value (binary, 16 bytes) + +**Note:** The `$key` parameter is ignored when using OpenSSL implementation. The key from constructor or first call is used. + +--- + +#### encryptECB() + +```php +public function encryptECB( + string $data, + string $key +): string +``` + +Encrypt data using AES-128-ECB (no IV). + +**Parameters:** +- `$data` - Data to encrypt (binary, must be multiple of 16 bytes) +- `$key` - Encryption key (binary, 16 bytes) + +**Returns:** Encrypted data (binary) + +--- + +### LRPCipher + +**Namespace:** `KDuma\SDM\Cipher` + +**Implements:** `CipherInterface` + +Leakage Resilient Primitive cipher implementation. + +#### Constructor + +```php +public function __construct( + string $key, + int $updateMode, + ?string $counter = null, + bool $padCounter = true +) +``` + +**Parameters:** +- `$key` - LRP key (binary, 16 bytes) +- `$updateMode` - Update mode (0 for CMAC, 1 for encryption) +- `$counter` - Optional counter/IV (binary, 1-16 bytes) +- `$padCounter` - Whether to pad counter to 16 bytes (default: true) + +--- + +#### encrypt() + +```php +public function encrypt( + string $data, + string $key, + string $iv +): string +``` + +Encrypt data using LRP. + +**Parameters:** +- `$data` - Data to encrypt (binary) +- `$key` - Encryption key (binary, 16 bytes) +- `$iv` - Counter/IV (binary, 1-16 bytes) + +**Returns:** Encrypted data (binary) + +--- + +#### decrypt() + +```php +public function decrypt( + string $data, + string $key, + string $iv +): string +``` + +Decrypt data using LRP. + +**Parameters:** +- `$data` - Data to decrypt (binary) +- `$key` - Decryption key (binary, 16 bytes) +- `$iv` - Counter/IV (binary, 1-16 bytes) + +**Returns:** Decrypted data (binary) + +--- + +#### cmac() + +```php +public function cmac( + string $data, + string $key +): string +``` + +Calculate LRP CMAC. + +**Parameters:** +- `$data` - Data to authenticate (binary or string) +- `$key` - CMAC key (binary, 16 bytes) + +**Returns:** CMAC value (binary, 16 bytes) + +**Note:** The `$key` parameter is ignored. The key from constructor is used. + +--- + +## Type Definitions + +### SDMInterface + +```php +interface SDMInterface +{ + public function decrypt( + string $encData, + string $encFileData, + string $cmac + ): array; + + public function validate( + string $data, + string $cmac + ): bool; +} +``` + +--- + +### CipherInterface + +```php +interface CipherInterface +{ + public function encrypt( + string $data, + string $key, + string $iv + ): string; + + public function decrypt( + string $data, + string $key, + string $iv + ): string; + + public function cmac( + string $data, + string $key + ): string; +} +``` + +--- + +## Constants + +### SDM Class Constants + +Internal constants used for protocol implementation: + +```php +// Session vector prefixes +private const SV2_PREFIX_CMAC = "\x3C\xC3\x00\x01\x00\x80"; +private const SV1_PREFIX_ENC = "\xC3\x3C\x00\x01\x00\x80"; + +// LRP protocol +private const LRP_PROTOCOL_PREFIX = "\x00\x01\x00\x80"; +private const LRP_STREAM_TRAILER = "\x1E\xE1"; + +// PICCDataTag bit masks +private const PICC_UID_MIRROR_MASK = 0x80; +private const PICC_READ_CTR_MASK = 0x40; +private const PICC_UID_LENGTH_MASK = 0x0F; +private const PICC_SUPPORTED_UID_LENGTH = 0x07; +``` + +### KeyDerivation Class Constants + +Diversification constants from NTAG 424 DNA specification: + +```php +private const DIV_CONST1 = '50494343446174614b6579'; // "PICCDataKey" +private const DIV_CONST2 = '536c6f744d61737465724b6579'; // "SlotMasterKey" +private const DIV_CONST3 = '446976426173654b6579'; // "DivBaseKey" +``` + +--- + +## Data Formats + +### Binary Data + +All binary data in the API uses PHP binary strings (8-bit clean strings). + +**Converting hex to binary:** + +```php +$binary = hex2bin('041E3C8A2D6B80'); +``` + +**Converting binary to hex:** + +```php +$hex = bin2hex($binary); +``` + +### Read Counter Format + +**In encrypted PICC data:** 3 bytes, little-endian + +```php +$readCtr = "\x06\x00\x00"; // Counter value 6 +``` + +**In return values:** Integer (0-16777215) + +```php +$result['read_ctr']; // 6 +``` + +### UID Format + +**Always 7 bytes for NTAG 424 DNA:** + +```php +$uid = hex2bin('041E3C8A2D6B80'); // 7 bytes +``` + +### CMAC Format + +**Always 8 bytes:** + +```php +$cmac = hex2bin('94EED9EE65337086'); // 8 bytes +``` + +### Encrypted Data Lengths + +**PICC encrypted data:** +- AES mode: 16 bytes +- LRP mode: 24 bytes (8-byte random + 16-byte encrypted) + +**File encrypted data:** +- Multiple of 16 bytes (AES block size) +- Can be empty (null) + +--- + +For usage examples, see [DOCUMENTATION.md](DOCUMENTATION.md). + +For the example application, see [example-app/README.md](example-app/README.md). diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md new file mode 100644 index 0000000..a4f2226 --- /dev/null +++ b/DOCUMENTATION.md @@ -0,0 +1,715 @@ +# PHP SDM - Complete Documentation + +This document provides detailed information on how to use the PHP SDM library for NTAG 424 DNA Secure Dynamic Messaging. + +## Table of Contents + +1. [Introduction](#introduction) +2. [Installation](#installation) +3. [Core Concepts](#core-concepts) +4. [Key Derivation](#key-derivation) +5. [Decrypting SDM Messages](#decrypting-sdm-messages) +6. [Validating Plain SUN Messages](#validating-plain-sun-messages) +7. [Encryption Modes](#encryption-modes) +8. [Parameter Modes](#parameter-modes) +9. [Advanced Usage](#advanced-usage) +10. [Error Handling](#error-handling) +11. [Security Considerations](#security-considerations) +12. [Examples](#examples) + +## Introduction + +NTAG 424 DNA is an NFC tag that supports Secure Dynamic Messaging (SDM), which allows tags to generate cryptographically authenticated URLs that change with each scan. This library provides PHP implementations of: + +- **SDM message decryption** - Decrypt encrypted PICC data and file data +- **CMAC validation** - Verify message authentication codes +- **Key derivation** - Generate session keys from master keys +- **UID diversification** - Tag-specific key derivation + +The library follows the specifications in: +- NXP AN12196 (NTAG 424 DNA features) +- NXP AN12304 (Leakage Resilient Primitive) +- NIST SP 800-108 (Key Derivation Functions) + +## Installation + +Install via Composer: + +```bash +composer require kduma/php-sdm +``` + +## Core Concepts + +### NTAG 424 DNA + +NTAG 424 DNA tags can be configured to generate dynamic URLs that include: + +1. **Encrypted PICC Data** - Contains UID, read counter, and configuration +2. **Encrypted File Data** - Optional application-specific data +3. **SDMMAC** - Message authentication code (CMAC) + +### SUN Messages + +Secure Unique NFC (SUN) messages come in two types: + +1. **Plain SUN** - Unencrypted UID and read counter with CMAC +2. **Encrypted SUN** - Encrypted PICC data with optional file data and CMAC + +### Encryption Modes + +NTAG 424 DNA supports two encryption modes: + +- **AES** - Standard AES-128 encryption (16-byte encrypted PICC data) +- **LRP** - Leakage Resilient Primitive (24-byte encrypted PICC data) + +## Key Derivation + +The library implements NIST SP 800-108 key derivation with UID diversification. + +### Master Key Setup + +Your master key should be a 16-32 byte binary string, typically derived from your NTAG configuration: + +```php +use KDuma\SDM\KeyDerivation; + +$masterKey = hex2bin('C9EB67DF090AFF47C3B19A2516680B9D'); +$kdf = new KeyDerivation(); +``` + +### Undiversified Keys + +Used for PICC data encryption (not UID-specific): + +```php +// Derive encryption key (key number 1) +$encKey = $kdf->deriveUndiversifiedKey($masterKey, 1); +``` + +**Note:** Only key number 1 is supported for undiversified keys. + +### UID-Diversified Keys + +Used for file data encryption and CMAC (tag-specific): + +```php +// UID is 7 bytes +$uid = hex2bin('04E12AB3CD5E80'); + +// Derive MAC key (key number 2) +$macKey = $kdf->deriveTagKey($masterKey, $uid, 2); + +// Or derive file read key (key number 1) +$fileKey = $kdf->deriveTagKey($masterKey, $uid, 1); +``` + +**Key Numbers:** +- `1` - File read key +- `2` - MAC key + +### Factory Keys + +The library recognizes factory keys (all zeros) and returns them unchanged: + +```php +$factoryKey = str_repeat("\x00", 16); +$result = $kdf->deriveUndiversifiedKey($factoryKey, 1); +// Returns: "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +``` + +## Decrypting SDM Messages + +### Basic Decryption + +The simplest way to decrypt an SDM message: + +```php +use KDuma\SDM\SDM; + +$sdm = new SDM( + encKey: $encKey, + macKey: $macKey +); + +// Decrypt the message +$result = $sdm->decrypt( + encData: hex2bin('EF963FF7828658A599F3041510671E88'), + encFileData: hex2bin('CEE9A53E3E463EF1F459635736738962'), + cmac: hex2bin('94EED9EE65337086') +); + +// Result contains: +// [ +// 'picc_data_tag' => "\xC7", +// 'uid' => "\x04\x1E\x3C\x8A\x2D\x6B\x80", +// 'read_ctr' => 6, +// 'file_data' => "\x43\x43", +// 'encryption_mode' => EncMode::AES +// ] +``` + +### Advanced Decryption with Callable Keys + +For more control, use `decryptSunMessage()` with a callable for dynamic key derivation: + +```php +use KDuma\SDM\ParamMode; +use KDuma\SDM\KeyDerivation; + +$kdf = new KeyDerivation(); +$masterKey = hex2bin('your_master_key'); + +// Create SDM instance with undiversified key +$sdm = new SDM( + encKey: $kdf->deriveUndiversifiedKey($masterKey, 1), + macKey: '', // Not used when providing callable +); + +$result = $sdm->decryptSunMessage( + paramMode: ParamMode::SEPARATED, + sdmMetaReadKey: $kdf->deriveUndiversifiedKey($masterKey, 1), + sdmFileReadKey: function(string $uid) use ($kdf, $masterKey): string { + // Derive tag-specific key after UID is decrypted + return $kdf->deriveTagKey($masterKey, $uid, 2); + }, + piccEncData: hex2bin($piccData), + sdmmac: hex2bin($cmac), + encFileData: hex2bin($encFileData) +); +``` + +### Without File Data + +If the tag doesn't use encrypted file data: + +```php +$result = $sdm->decrypt( + encData: hex2bin($piccData), + encFileData: hex2bin(''), // Empty string + cmac: hex2bin($cmac) +); + +// file_data will be null +``` + +## Validating Plain SUN Messages + +Plain SUN messages contain unencrypted UID and read counter with CMAC validation. + +### Simple Validation + +```php +use KDuma\SDM\SDM; + +$sdm = new SDM( + encKey: $encKey, + macKey: $macKey +); + +// Data is UID (7 bytes) + read counter (3 bytes) +$data = hex2bin('041E3C8A2D6B80000006'); +$cmac = hex2bin('4B00064004B0B3D3'); + +$isValid = $sdm->validate($data, $cmac); +// Returns: true or false +``` + +### Detailed Validation + +For more information about the validated message: + +```php +use KDuma\SDM\EncMode; + +$result = $sdm->validatePlainSun( + uid: hex2bin('041E3C8A2D6B80'), + readCtr: hex2bin('000006'), + sdmmac: hex2bin('4B00064004B0B3D3'), + sdmFileReadKey: $macKey, + mode: EncMode::AES +); + +// Result contains: +// [ +// 'encryption_mode' => EncMode::AES, +// 'uid' => "\x04\x1E\x3C\x8A\x2D\x6B\x80", +// 'read_ctr' => 6 +// ] +``` + +**Note:** Read counter in `validatePlainSun()` is in little-endian format, while the returned integer is the actual counter value. + +## Encryption Modes + +### Detecting Encryption Mode + +The library automatically detects the encryption mode from PICC data length: + +```php +$mode = $sdm->getEncryptionMode($piccEncData); + +if ($mode === EncMode::AES) { + // 16-byte PICC data - AES mode +} elseif ($mode === EncMode::LRP) { + // 24-byte PICC data - LRP mode +} +``` + +### AES Mode + +Standard AES-128 encryption: + +- PICC encrypted data: 16 bytes +- Uses AES-CBC with zero IV for PICC data +- Session keys derived via AES-CMAC + +```php +$result = $sdm->decryptSunMessage( + // ... parameters ... + piccEncData: hex2bin('EF963FF7828658A599F3041510671E88'), // 16 bytes +); +// encryption_mode will be EncMode::AES +``` + +### LRP Mode + +Leakage Resilient Primitive for enhanced security: + +- PICC encrypted data: 24 bytes (8-byte random + 16-byte encrypted data) +- Uses LRP cipher with PICC random as counter +- Session keys derived via LRP CMAC + +```php +$result = $sdm->decryptSunMessage( + // ... parameters ... + piccEncData: hex2bin('1234567890ABCDEF...'), // 24 bytes +); +// encryption_mode will be EncMode::LRP +``` + +**Note:** LRP mode is detected automatically. No manual mode selection is needed. + +## Parameter Modes + +SDM URLs can encode parameters in two ways: + +### Separated Mode + +Each parameter has its own name: + +``` +https://example.com/tag?picc_data=xxx&enc=yyy&cmac=zzz +``` + +```php +use KDuma\SDM\ParamMode; + +$result = $sdm->decryptSunMessage( + paramMode: ParamMode::SEPARATED, + // ... other parameters ... +); +``` + +### Bulk Mode + +All parameters concatenated without names: + +``` +https://example.com/tag?data=xxxyyyzzz +``` + +```php +use KDuma\SDM\ParamMode; + +$result = $sdm->decryptSunMessage( + paramMode: ParamMode::BULK, + // ... other parameters ... +); +``` + +**Note:** Parameter mode affects CMAC calculation. Use the mode that matches your tag configuration. + +## Advanced Usage + +### Custom SDMMAC Parameter Name + +If your SDM URL uses a custom parameter name for CMAC: + +```php +$sdm = new SDM( + encKey: $encKey, + macKey: $macKey, + sdmmacParam: 'mac' // Default is 'cmac' +); +``` + +This affects CMAC calculation for separated mode with file data. + +### Manual CMAC Calculation + +Calculate SDMMAC manually: + +```php +use KDuma\SDM\ParamMode; +use KDuma\SDM\EncMode; + +$piccData = $uid . strrev($readCtr); // UID + reversed read counter + +$sdmmac = $sdm->calculateSdmmac( + paramMode: ParamMode::SEPARATED, + sdmFileReadKey: $macKey, + piccData: $piccData, + encFileData: $encFileData, // or null if not used + mode: EncMode::AES +); +``` + +### Manual File Data Decryption + +Decrypt file data separately: + +```php +$fileData = $sdm->decryptFileData( + sdmFileReadKey: $fileKey, + piccData: $uid . strrev($readCtr), + readCtr: $readCtr, // 3-byte little-endian + encFileData: $encryptedData, + mode: EncMode::AES +); +``` + +## Error Handling + +The library throws specific exceptions for different error conditions: + +### DecryptionException + +Thrown when decryption fails: + +```php +use KDuma\SDM\Exceptions\DecryptionException; + +try { + $result = $sdm->decrypt($encData, $encFileData, $cmac); +} catch (DecryptionException $e) { + // Invalid encrypted data, wrong key, or malformed input + echo "Decryption failed: " . $e->getMessage(); +} +``` + +**Common causes:** +- Invalid encryption key +- Malformed encrypted data +- Incorrect data length +- Unsupported encryption mode + +### ValidationException + +Thrown when CMAC validation fails: + +```php +use KDuma\SDM\Exceptions\ValidationException; + +try { + $result = $sdm->decrypt($encData, $encFileData, $cmac); +} catch (ValidationException $e) { + // Invalid CMAC - message has been tampered with + echo "Validation failed: " . $e->getMessage(); +} +``` + +**Common causes:** +- Wrong MAC key +- Tampered data +- Incorrect CMAC value +- Mismatched parameter mode + +### InvalidArgumentException + +Thrown for invalid input parameters: + +```php +try { + $key = $kdf->deriveTagKey($masterKey, $uid, 3); // Invalid key number +} catch (\InvalidArgumentException $e) { + echo "Invalid parameter: " . $e->getMessage(); +} +``` + +### Exception Hierarchy + +``` +\Exception +└── KDuma\SDM\Exceptions\SDMException + ├── DecryptionException + └── ValidationException +``` + +## Security Considerations + +### Key Storage + +- Never hardcode keys in source code +- Store master keys in secure configuration files or environment variables +- Use key derivation instead of storing multiple keys +- Rotate keys periodically + +### CMAC Validation + +- Always validate CMAC before trusting decrypted data +- Use constant-time comparison (`hash_equals()`) to prevent timing attacks +- The library uses `hash_equals()` internally + +### Read Counter + +- Verify read counter increments to prevent replay attacks +- Store last seen counter per UID +- Reject messages with lower or equal counters + +### UID Verification + +- Verify UID matches expected tag when applicable +- Use UID diversification for tag-specific keys +- Check UID length (must be 7 bytes for NTAG 424 DNA) + +### Factory Keys + +- Never use factory keys (all zeros) in production +- The library detects factory keys but does not prevent their use +- Change default keys before deployment + +## Examples + +### Example 1: Web Application URL Handler + +```php +deriveUndiversifiedKey($masterKey, 1); + + // Create SDM instance + $sdm = new SDM($encKey, ''); + + // Decrypt message with dynamic MAC key derivation + $result = $sdm->decryptSunMessage( + paramMode: \KDuma\SDM\ParamMode::SEPARATED, + sdmMetaReadKey: $encKey, + sdmFileReadKey: fn($uid) => $kdf->deriveTagKey($masterKey, $uid, 2), + piccEncData: hex2bin($piccData), + sdmmac: hex2bin($cmac), + encFileData: $encFileData ? hex2bin($encFileData) : null + ); + + // Display results + echo "UID: " . bin2hex($result['uid']) . "\n"; + echo "Read Counter: " . $result['read_ctr'] . "\n"; + echo "Encryption: " . $result['encryption_mode']->name . "\n"; + + if ($result['file_data']) { + echo "File Data: " . bin2hex($result['file_data']) . "\n"; + } + +} catch (ValidationException $e) { + http_response_code(403); + echo "Invalid signature: " . $e->getMessage(); +} catch (DecryptionException $e) { + http_response_code(400); + echo "Decryption error: " . $e->getMessage(); +} +``` + +### Example 2: Plain SUN Validation with Counter Tracking + +```php +counters[$uidHex])) { + $lastCounter = $this->counters[$uidHex]; + + // Reject if counter didn't increment + if ($readCtr <= $lastCounter) { + return false; + } + } + + // Update counter + $this->counters[$uidHex] = $readCtr; + return true; + } +} + +$tracker = new CounterTracker(); +$masterKey = hex2bin(getenv('SDM_MASTER_KEY')); +$kdf = new KeyDerivation(); + +// Parse parameters +$uid = hex2bin($_GET['uid']); +$readCtr = hex2bin($_GET['ctr']); +$cmac = hex2bin($_GET['cmac']); + +// Derive MAC key +$macKey = $kdf->deriveTagKey($masterKey, $uid, 2); +$sdm = new SDM('', $macKey); + +try { + // Validate CMAC + $result = $sdm->validatePlainSun( + uid: $uid, + readCtr: $readCtr, + sdmmac: $cmac, + sdmFileReadKey: $macKey + ); + + // Check counter + if (!$tracker->validateAndTrack($result['uid'], $result['read_ctr'])) { + http_response_code(403); + echo "Replay attack detected!"; + exit; + } + + echo "Valid scan from UID: " . bin2hex($result['uid']) . "\n"; + echo "Counter: " . $result['read_ctr'] . "\n"; + +} catch (\Exception $e) { + http_response_code(400); + echo "Validation failed: " . $e->getMessage(); +} +``` + +### Example 3: Tamper Tag Detection + +```php + 'Invalid', 'color' => 'gray']; + } + + $statusBytes = substr($fileData, 0, 2); + + return match ($statusBytes) { + "\xCC\xCC" => ['status' => 'Secure', 'color' => 'green'], + "\x0C\xCC" => ['status' => 'Tampered (Closed)', 'color' => 'red'], + "\x0C\x0C" => ['status' => 'Tampered (Open)', 'color' => 'red'], + "\xCD\xCD" => ['status' => 'Uninitialized', 'color' => 'orange'], + default => ['status' => 'Unknown', 'color' => 'gray'], + }; +} + +$masterKey = hex2bin(getenv('SDM_MASTER_KEY')); +$kdf = new KeyDerivation(); +$encKey = $kdf->deriveUndiversifiedKey($masterKey, 1); + +$sdm = new SDM($encKey, ''); + +$result = $sdm->decryptSunMessage( + paramMode: \KDuma\SDM\ParamMode::SEPARATED, + sdmMetaReadKey: $encKey, + sdmFileReadKey: fn($uid) => $kdf->deriveTagKey($masterKey, $uid, 2), + piccEncData: hex2bin($_GET['picc_data']), + sdmmac: hex2bin($_GET['cmac']), + encFileData: hex2bin($_GET['enc']) +); + +if ($result['file_data']) { + $tamperStatus = interpretTamperStatus($result['file_data']); + echo "Tamper Status: " . $tamperStatus['status'] . "\n"; + echo "UID: " . bin2hex($result['uid']) . "\n"; + echo "Counter: " . $result['read_ctr'] . "\n"; +} +``` + +### Example 4: Batch Validation + +```php +deriveUndiversifiedKey($masterKey, 1); + +$sdm = new SDM($encKey, ''); + +$scans = [ + [ + 'picc_data' => 'EF963FF7828658A599F3041510671E88', + 'enc' => 'CEE9A53E3E463EF1F459635736738962', + 'cmac' => '94EED9EE65337086', + ], + // ... more scans +]; + +$results = []; + +foreach ($scans as $scan) { + try { + $result = $sdm->decryptSunMessage( + paramMode: ParamMode::SEPARATED, + sdmMetaReadKey: $encKey, + sdmFileReadKey: fn($uid) => $kdf->deriveTagKey($masterKey, $uid, 2), + piccEncData: hex2bin($scan['picc_data']), + sdmmac: hex2bin($scan['cmac']), + encFileData: hex2bin($scan['enc']) + ); + + $results[] = [ + 'success' => true, + 'uid' => bin2hex($result['uid']), + 'counter' => $result['read_ctr'], + 'mode' => $result['encryption_mode']->name, + ]; + + } catch (\Exception $e) { + $results[] = [ + 'success' => false, + 'error' => $e->getMessage(), + ]; + } +} + +echo json_encode($results, JSON_PRETTY_PRINT); +``` + +--- + +For API reference, see [API.md](API.md). + +For the example Laravel application, see [example-app/README.md](example-app/README.md). diff --git a/README.md b/README.md index adbc55b..e1b1897 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,18 @@ -# PHP SDM - NTAG DNA 424 Implementation +# PHP SDM - NTAG 424 DNA Implementation -PHP implementation for NTAG DNA 424 Secure Dynamic Messaging (SDM). +A PHP library for decrypting and validating NTAG 424 DNA Secure Dynamic Messaging (SDM) data. -## Features - -- NTAG DNA 424 SDM message decryption -- CMAC validation -- AES encryption/decryption -- PICC data parsing -- SUN message handling +[![PHP Version](https://img.shields.io/badge/php-%5E8.3-blue)](https://www.php.net/) +[![License](https://img.shields.io/badge/license-MIT-green)](LICENSE) -## Requirements +## Features -- PHP 8.3 or higher +- **NTAG 424 DNA SDM** support with AES and LRP encryption modes +- **CMAC validation** for message authentication +- **Key derivation** with UID diversification (NIST SP 800-108) +- **Plain and encrypted SUN messages** +- **Tamper detection** support for TagTamper variant +- **Example Laravel app** included ## Installation @@ -20,68 +20,82 @@ PHP implementation for NTAG DNA 424 Secure Dynamic Messaging (SDM). composer require kduma/php-sdm ``` -## Usage +## Quick Start ```php use KDuma\SDM\SDM; +use KDuma\SDM\KeyDerivation; // Initialize with your keys -$sdm = new SDM( - encKey: 'your_encryption_key', - macKey: 'your_mac_key' -); +$masterKey = hex2bin('your_master_key_here'); +$kdf = new KeyDerivation(); + +// Derive encryption and MAC keys +$encKey = $kdf->deriveUndiversifiedKey($masterKey, 1); +$macKey = $kdf->deriveTagKey($masterKey, $uid, 2); + +$sdm = new SDM($encKey, $macKey); // Decrypt SDM message -$result = $sdm->decrypt($encData, $encFileData, $cmac); +$result = $sdm->decrypt( + encData: hex2bin($piccData), + encFileData: hex2bin($encFileData), + cmac: hex2bin($cmac) +); -// Validate CMAC -$isValid = $sdm->validate($data, $cmac); +// Or validate plain SUN message +$isValid = $sdm->validate( + data: hex2bin($uid . $readCtr), + cmac: hex2bin($cmac) +); ``` -## Development +## Documentation + +- **[Documentation](DOCUMENTATION.md)** - Detailed usage guide and examples +- **[API Reference](API.md)** - Complete API documentation +- **[Example App](example-app/README.md)** - Laravel example application guide + +## Requirements + +- PHP 8.3 or higher +- OpenSSL extension (for AES operations) + +## Example Application -### Install dependencies +The library includes a full Laravel application demonstrating real-world usage: ```bash +cd example-app composer install +php artisan serve ``` -### Run tests +Visit `http://localhost:8000` for a working demo with WebNFC support. -```bash -composer test -``` +See [example-app/README.md](example-app/README.md) for details. -### Run static analysis +## Development ```bash -composer phpstan -``` +# Install dependencies +composer install -### Fix code style +# Run tests +composer test -```bash +# Run static analysis +composer phpstan + +# Fix code style composer cs-fix ``` -## Structure +## References -``` -src/ -├── Cipher/ # Cryptographic operations -│ ├── CipherInterface.php -│ └── AESCipher.php -├── Exceptions/ # Custom exceptions -│ ├── SDMException.php -│ ├── DecryptionException.php -│ └── ValidationException.php -├── PICC/ # PICC data structures -│ └── PICCData.php -├── SUN/ # SUN message handling -│ └── SUNMessage.php -├── SDMInterface.php -└── SDM.php -``` +- [NXP AN12196](https://www.nxp.com/docs/en/application-note/AN12196.pdf) - NTAG 424 DNA and TagTamper Features +- [NXP AN12304](https://www.nxp.com/docs/en/application-note/AN12304.pdf) - Leakage Resilient Primitive (LRP) +- [NIST SP 800-108](https://csrc.nist.gov/publications/detail/sp/800-108/rev-1/final) - Key Derivation Functions ## License diff --git a/example-app/README.md b/example-app/README.md index 7e5acde..32abfd2 100644 --- a/example-app/README.md +++ b/example-app/README.md @@ -1,18 +1,35 @@ # SDM Backend Server - Laravel Example Application -This Laravel application demonstrates the usage of the [kduma/php-sdm](https://github.com/kduma-autoid/php-sdm) library for processing NTAG 424 DNA Secure Dynamic Messaging (SDM) data. +This Laravel application demonstrates real-world usage of the [kduma/php-sdm](https://github.com/kduma-autoid/php-sdm) library for processing NTAG 424 DNA Secure Dynamic Messaging (SDM) data. This implementation is a PHP/Laravel port of the Python Flask [nfc-developer/sdm-backend](https://github.com/nfc-developer/sdm-backend) application. +## Table of Contents + +1. [Features](#features) +2. [Requirements](#requirements) +3. [Installation](#installation) +4. [Configuration](#configuration) +5. [Routes and Endpoints](#routes-and-endpoints) +6. [Usage Examples](#usage-examples) +7. [Architecture](#architecture) +8. [WebNFC Interface](#webnfc-interface) +9. [Demo Mode](#demo-mode) +10. [Response Formats](#response-formats) +11. [Deployment](#deployment) +12. [Testing](#testing) +13. [References](#references) + ## Features -- **Plain SUN Message Validation** - Validate plaintext UID with mirrored Read Counter and CMAC +- **Plain SUN Message Validation** - Validate plaintext UID with mirrored read counter and CMAC - **Encrypted SUN Message Decryption** - Decrypt and validate encrypted PICC and file data - **Tamper-Tag Support** - Detect and report tamper status from TagTamper variant tags - **Dual Output** - HTML views for browsers and JSON API endpoints for programmatic access - **WebNFC Interface** - Browser-based NFC tag scanning (Chrome for Android only) - **Demo Mode** - Automatic example data when using factory keys (all zeros) -- **LRP Preparation** - Detection and graceful handling of LRP mode (not yet supported in library) +- **AES and LRP Support** - Automatic detection and handling of both encryption modes +- **Key Derivation** - Proper UID diversification using NIST SP 800-108 ## Requirements @@ -22,49 +39,61 @@ This implementation is a PHP/Laravel port of the Python Flask [nfc-developer/sdm ## Installation -1. **Navigate to the example-app directory:** - ```bash - cd example-app - ``` +### Step 1: Navigate to Example App + +```bash +cd example-app +``` + +### Step 2: Install Dependencies + +```bash +composer install +``` + +Dependencies are already configured in `composer.json` and include the parent library via path repository. -2. **Install dependencies** (already done during creation): - ```bash - composer install - ``` +### Step 3: Environment Setup -3. **Configure environment:** - The `.env` file is already configured with demo mode enabled (all-zeros master key). +The `.env` file is already configured with demo mode enabled (all-zeros master key). + +For production use with real tags, edit `.env`: + +```bash +SDM_MASTER_KEY=your_32_character_hex_master_key +``` + +### Step 4: Start Development Server + +```bash +php artisan serve +``` - To use your own keys, edit `.env` and set: - ```bash - SDM_MASTER_KEY=your_hex_master_key_here - ``` +The application will be available at `http://localhost:8000`. -4. **Start the development server:** - ```bash - php artisan serve - ``` +### Step 5: Test the Application -5. **Visit the application:** - Open your browser to `http://localhost:8000` +Open your browser to: +- `http://localhost:8000` - Main landing page with examples +- `http://localhost:8000/webnfc` - WebNFC scanning interface (Chrome on Android only) ## Configuration -All SDM-related configuration is in `config/sdm.php` and can be overridden via `.env`: +All SDM-related configuration is in `config/sdm.php` and can be overridden via `.env`. -### Master Key +### Master Key Configuration ```bash SDM_MASTER_KEY=00000000000000000000000000000000 ``` -- **32-character hexadecimal string** (16 bytes) -- **Demo Mode:** When set to all zeros, the application shows example URLs and GitHub attribution -- **Production:** Use your actual master key derived from your NTAG 424 DNA tag configuration +- **Format:** 32-character hexadecimal string (16 bytes) +- **Demo Mode:** All zeros activates demo mode with example URLs +- **Production:** Use your actual master key from NTAG 424 DNA tag configuration -### Parameter Names +### URL Parameter Names -Customize the URL parameter names used in SDM URLs: +Customize the parameter names used in SDM URLs: ```bash SDM_ENC_PICC_DATA_PARAM=picc_data @@ -74,150 +103,746 @@ SDM_CTR_PARAM=ctr SDM_SDMMAC_PARAM=cmac ``` -### LRP Mode (Future Feature) +These should match your NTAG 424 DNA tag configuration. + +### LRP Mode Requirement ```bash SDM_REQUIRE_LRP=false ``` -When enabled, the application will enforce LRP encryption mode and reject AES requests. -**Note:** LRP mode is not yet implemented in the php-sdm library. +When enabled, the application enforces LRP encryption mode and rejects AES requests. + +**Note:** LRP mode is fully supported in the library and application. + +### Configuration File -## Routes +**File:** `example-app/config/sdm.php` + +```php +return [ + 'master_key' => env('SDM_MASTER_KEY', '00000000000000000000000000000000'), + 'enc_picc_data_param' => env('SDM_ENC_PICC_DATA_PARAM', 'picc_data'), + 'enc_file_data_param' => env('SDM_ENC_FILE_DATA_PARAM', 'enc'), + 'uid_param' => env('SDM_UID_PARAM', 'uid'), + 'ctr_param' => env('SDM_CTR_PARAM', 'ctr'), + 'sdmmac_param' => env('SDM_SDMMAC_PARAM', 'cmac'), + 'require_lrp' => env('SDM_REQUIRE_LRP', false), + 'is_demo_mode' => env('SDM_MASTER_KEY', '00000000000000000000000000000000') === '00000000000000000000000000000000', +]; +``` + +## Routes and Endpoints ### Web Routes (HTML Output) -| Route | Description | -|-------|-------------| -| `GET /` | Main landing page with example URLs | -| `GET /tagpt` | Plain SUN message validation | -| `GET /tag` | SUN message decryption | -| `GET /tagtt` | Tamper-tag SUN message decryption | -| `GET /webnfc` | WebNFC interface for browser-based scanning | +| Route | Description | Parameters | +|-------|-------------|------------| +| `GET /` | Main landing page | None | +| `GET /webnfc` | WebNFC scanning interface | None | +| `GET /tagpt` | Plain SUN validation | `uid`, `ctr`, `cmac` | +| `GET /tag` | Encrypted SUN decryption | `picc_data`, `cmac`, `enc` (optional) | +| `GET /tagtt` | Tamper-tag decryption | `picc_data`, `cmac`, `enc` | ### API Routes (JSON Output) -| Route | Description | -|-------|-------------| -| `GET /api/tagpt` | Plain SUN validation (JSON) | -| `GET /api/tag` | SUN decryption (JSON) | -| `GET /api/tagtt` | Tamper-tag decryption (JSON) | +| Route | Description | Parameters | +|-------|-------------|------------| +| `GET /api/tagpt` | Plain SUN validation (JSON) | `uid`, `ctr`, `cmac` | +| `GET /api/tag` | SUN decryption (JSON) | `picc_data`, `cmac`, `enc` (optional) | +| `GET /api/tagtt` | Tamper-tag decryption (JSON) | `picc_data`, `cmac`, `enc` | + +### Route Definitions + +**Web Routes File:** `example-app/routes/web.php` + +```php +use App\Http\Controllers\TagController; +use App\Http\Controllers\TagPlainTextController; +use App\Http\Controllers\TagTamperController; + +// Main page +Route::view('/', 'main'); + +// WebNFC interface +Route::view('/webnfc', 'webnfc'); + +// Plain SUN message validation +Route::get('/tagpt', TagPlainTextController::class); + +// SUN message decryption +Route::get('/tag', TagController::class); + +// Tamper-tag SUN message decryption +Route::get('/tagtt', TagTamperController::class); +``` + +**API Routes File:** `example-app/routes/api.php` + +```php +use App\Http\Controllers\TagController; +use App\Http\Controllers\TagPlainTextController; +use App\Http\Controllers\TagTamperController; + +// Plain SUN message validation +Route::get('/tagpt', TagPlainTextController::class); + +// SUN message decryption +Route::get('/tag', TagController::class); + +// Tamper-tag SUN message decryption +Route::get('/tagtt', TagTamperController::class); +``` + +**Note:** All controllers are invokable (single `__invoke()` method), and API routes automatically return JSON via middleware. ## Usage Examples -### Plain SUN Validation +### Example 1: Plain SUN Validation + +Validate a plain SUN message with unencrypted UID and read counter. + +**HTML Output:** ```bash -# HTML output curl "http://localhost:8000/tagpt?uid=041E3C8A2D6B80&ctr=000006&cmac=4B00064004B0B3D3" +``` -# JSON output +**JSON Output:** + +```bash curl "http://localhost:8000/api/tagpt?uid=041E3C8A2D6B80&ctr=000006&cmac=4B00064004B0B3D3" ``` -### Encrypted SUN Message (AES) +**Response (JSON):** -```bash -# HTML output -curl "http://localhost:8000/tag?picc_data=EF963FF7828658A599F3041510671E88&cmac=94EED9EE65337086" +```json +{ + "encryption_mode": "AES", + "uid": "041e3c8a2d6b80", + "read_ctr": 6 +} +``` + +### Example 2: Encrypted SUN (AES Mode) + +Decrypt an encrypted SUN message without file data. -# JSON output +**Request:** + +```bash curl "http://localhost:8000/api/tag?picc_data=EF963FF7828658A599F3041510671E88&cmac=94EED9EE65337086" ``` -### Encrypted with File Data +**Response:** + +```json +{ + "picc_data_tag": "c7", + "encryption_mode": "AES", + "uid": "041e3c8a2d6b80", + "read_ctr": 6 +} +``` + +### Example 3: Encrypted SUN with File Data + +Decrypt SUN message including encrypted file data. + +**Request:** ```bash -curl "http://localhost:8000/tag?picc_data=FD91EC264309878BE6345CBE53BADF40&enc=CEE9A53E3E463EF1F459635736738962&cmac=ECC1E7F6C6C73BF6" +curl "http://localhost:8000/api/tag?picc_data=FD91EC264309878BE6345CBE53BADF40&enc=CEE9A53E3E463EF1F459635736738962&cmac=ECC1E7F6C6C73BF6" ``` -### Tamper-Tag Variant +**Response:** + +```json +{ + "picc_data_tag": "c7", + "encryption_mode": "AES", + "uid": "041e3c8a2d6b80", + "read_ctr": 23, + "file_data": "4343", + "file_data_utf8": "CC" +} +``` + +### Example 4: Tamper-Tag Detection + +Detect tamper status from TagTamper variant tags. + +**Request:** ```bash -curl "http://localhost:8000/tagtt?picc_data=FDD387BF32A33A7C40CF259675B3A1E2&enc=EA050C282D8E9043E28F7A171464D697&cmac=758110182134ECE9" +curl "http://localhost:8000/api/tagtt?picc_data=FDD387BF32A33A7C40CF259675B3A1E2&enc=EA050C282D8E9043E28F7A171464D697&cmac=758110182134ECE9" +``` + +**Response:** + +```json +{ + "picc_data_tag": "c7", + "encryption_mode": "AES", + "uid": "041e3c8a2d6b80", + "read_ctr": 42, + "file_data": "4343", + "file_data_utf8": "CC", + "tamper_status": "Secure" +} +``` + +### Example 5: LRP Mode Detection + +The application automatically detects and handles LRP mode. + +**Request:** + +```bash +# LRP mode has 24-byte picc_data (vs 16 bytes for AES) +curl "http://localhost:8000/api/tag?picc_data=1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF&cmac=..." +``` + +**Response:** + +```json +{ + "encryption_mode": "LRP", + ... +} ``` -## JSON Response Format +### Example 6: Error Handling + +**Invalid CMAC:** + +```bash +curl "http://localhost:8000/api/tag?picc_data=EF963FF7828658A599F3041510671E88&cmac=0000000000000000" +``` -All API endpoints return prettified JSON: +**Response (403 Forbidden):** ```json { - "picc_data_tag": "c7", - "encryption_mode": "AES", - "uid": "041e3c8a2d6b80", - "read_ctr": 6, - "file_data": "4343", - "file_data_utf8": "CC", - "tamper_status": "Secure" + "error": "Message is not properly signed - invalid MAC" +} +``` + +**Invalid Data:** + +```bash +curl "http://localhost:8000/api/tag?picc_data=INVALID&cmac=94EED9EE65337086" +``` + +**Response (400 Bad Request):** + +```json +{ + "error": "Invalid encrypted PICC data length - expected 16 bytes (AES) or 24 bytes (LRP), got 3 bytes. This may indicate malformed or truncated input data." } ``` ## Architecture +The example application follows modern Laravel best practices with a clean, maintainable architecture. + +### Design Principles + +1. **Invokable Controllers** - Each controller has a single responsibility via the `__invoke()` method +2. **Responsable Pattern** - Response objects handle both JSON and HTML output automatically +3. **Controller Inheritance** - Common functionality in `BaseSDMController` prevents code duplication +4. **Middleware for API Routes** - `ForceJsonResponseMiddleware` ensures API routes always return JSON +5. **Separated Route Files** - Web and API routes in separate files for clarity +6. **Static Routes** - Simple pages use `Route::view()` instead of controllers + +### Key Architectural Features + +**Automatic Response Format Detection:** +- Controllers return `ValidResponse` or `ErrorResponse` objects +- Response objects check `$request->wantsJson()` to determine output format +- Binary data automatically converted to hex for JSON, kept binary for HTML views +- Keys automatically converted to camelCase for Blade views + +**Controller Hierarchy:** +``` +BaseSDMController (abstract) + ├─ TagPlainTextController (invokable) + ├─ TagController (invokable) + │ └─ TagTamperController (invokable, extends TagController) +``` + +**Response Flow:** +- Controllers catch exceptions and return `ErrorResponse` with appropriate status codes +- Successful operations return `ValidResponse` with decrypted data +- Response objects handle all formatting logic + +### Directory Structure + +``` +example-app/ +├── app/ +│ ├── Helpers/ +│ │ └── ParameterParser.php # URL parameter parsing +│ ├── Http/ +│ │ ├── Controllers/ +│ │ │ ├── BaseSDMController.php # Abstract base controller +│ │ │ ├── TagController.php # Encrypted SUN handler +│ │ │ ├── TagPlainTextController.php # Plain SUN handler +│ │ │ └── TagTamperController.php # Tamper-tag handler +│ │ ├── Middleware/ +│ │ │ └── ForceJsonResponseMiddleware.php # API JSON middleware +│ │ └── Responses/ +│ │ ├── ValidResponse.php # Success response handler +│ │ └── ErrorResponse.php # Error response handler +│ └── Providers/ +│ └── SDMServiceProvider.php # Key derivation service +├── config/ +│ └── sdm.php # SDM configuration +├── resources/ +│ └── views/ +│ ├── layouts/ +│ │ └── app.blade.php # Base layout +│ ├── main.blade.php # Landing page +│ ├── info.blade.php # Result display +│ ├── error.blade.php # Error display +│ └── webnfc.blade.php # WebNFC interface +└── routes/ + ├── web.php # Web route definitions + └── api.php # API route definitions +``` + ### Key Components -- **`config/sdm.php`** - Configuration file with demo mode detection -- **`app/Providers/SDMServiceProvider.php`** - Service provider for key derivation and SDM instances -- **`app/Helpers/ParameterParser.php`** - Parameter parsing (bulk vs separated modes) -- **`app/Http/Controllers/SDMController.php`** - All route handlers -- **`resources/views/`** - Blade templates (layout, main, info, webnfc, error) +#### BaseSDMController + +**File:** `app/Http/Controllers/BaseSDMController.php` + +Abstract base controller providing common functionality for all SDM controllers. + +**Protected Methods:** +- `getSDM(?string $uid)` - Get SDM instance from factory +- `getMasterKey()` - Get master key from configuration +- `getEncKey()` - Derive encryption key for PICC data +- `getMacKey(string $uid)` - Derive MAC key for specific UID + +#### TagController + +**File:** `app/Http/Controllers/TagController.php` + +Invokable controller handling encrypted SUN message decryption (routes `/tag` and `/api/tag`). + +**Method:** +- `__invoke(Request)` - Process encrypted SUN messages, decrypt data, return response + +**Protected Methods:** +- `isTamperTag()` - Returns false (overridden in TagTamperController) +- `convertToUtf8(string)` - Convert binary data to UTF-8 safely + +#### TagPlainTextController + +**File:** `app/Http/Controllers/TagPlainTextController.php` + +Invokable controller handling plain SUN message validation (routes `/tagpt` and `/api/tagpt`). + +**Method:** +- `__invoke(Request)` - Validate plain SUN messages with CMAC + +#### TagTamperController + +**File:** `app/Http/Controllers/TagTamperController.php` + +Invokable controller extending TagController for tamper-tag handling (routes `/tagtt` and `/api/tagtt`). + +**Method:** +- `isTamperTag()` - Overrides parent to return true, enabling tamper status interpretation + +#### ValidResponse + +**File:** `app/Http/Responses/ValidResponse.php` -### Key Derivation +Responsable class for successful SDM operations, automatically handles JSON and HTML responses. -The application uses two-level key derivation: +**Features:** +- Converts binary fields to hex for JSON responses +- Converts snake_case keys to camelCase for Blade views +- Returns appropriate response based on request Accept header -1. **Undiversified Key** - Derived from master key for PICC data decryption -2. **Tag-Specific Key** - Derived from master key + UID for file data and CMAC +#### ErrorResponse + +**File:** `app/Http/Responses/ErrorResponse.php` + +Responsable class for error responses, automatically handles JSON and HTML responses. + +**Features:** +- Returns JSON with error message for API requests +- Returns error Blade view for web requests +- Supports custom HTTP status codes + +#### ParameterParser + +**File:** `app/Helpers/ParameterParser.php` + +Parses and validates URL parameters. + +**Static Methods:** +- `parsePlainParams(Request)` - Parse plain SUN parameters +- `parseEncryptedParams(Request)` - Parse encrypted SUN parameters +- `interpretTamperStatus(string)` - Interpret tamper tag file data + +#### SDMServiceProvider + +**File:** `app/Providers/SDMServiceProvider.php` + +Service provider for key derivation and SDM instance creation. + +**Provides:** +- `KeyDerivation` singleton +- `sdm.factory` - Factory callable for creating SDM instances + +#### ForceJsonResponseMiddleware + +**File:** `app/Http/Middleware/ForceJsonResponseMiddleware.php` + +Middleware that forces JSON responses for API routes by setting the Accept header. + +### Key Derivation Flow ```php +// 1. Get master key from config $masterKey = hex2bin(config('sdm.master_key')); -$kdf = new KeyDerivation(); -// For PICC data encryption +// 2. Derive undiversified key for PICC data +$kdf = app(KeyDerivation::class); $encKey = $kdf->deriveUndiversifiedKey($masterKey, 1); -// For file data and CMAC (per-tag) -$macKey = $kdf->deriveTagKey($masterKey, $uid, 2); +// 3. Create SDM instance +$sdm = new SDM($encKey, ''); + +// 4. Decrypt with dynamic MAC key derivation +$result = $sdm->decryptSunMessage( + sdmMetaReadKey: $encKey, + sdmFileReadKey: function(string $uid) use ($kdf, $masterKey): string { + // Derive tag-specific key after UID is decrypted + return $kdf->deriveTagKey($masterKey, $uid, 2); + }, + // ... other parameters +); +``` + +### Request Flow + +``` +Client Request + ↓ +Route (web.php or api.php) + ↓ +[API: ForceJsonResponseMiddleware] + ↓ +Invokable Controller + (TagController / TagPlainTextController / TagTamperController) + ↓ +ParameterParser (validate & parse) + ↓ +BaseSDMController (key derivation) + ↓ +SDM Library (decrypt/validate) + ↓ +Responsable Object + (ValidResponse or ErrorResponse) + ↓ + Checks request->wantsJson() + ↓ + ├─ JSON: Convert binary to hex, return JSON + └─ HTML: Convert keys to camelCase, render Blade view +``` + +## WebNFC Interface + +The application includes a browser-based NFC scanning interface using the WebNFC API. + +**Route:** `GET /webnfc` + +**File:** `resources/views/webnfc.blade.php` + +### Features + +- Real-time NFC tag scanning +- Automatic URL parsing from NDEF records +- Parameter extraction and validation +- Live result display +- Support for all SDM message types + +### Browser Support + +**Supported:** +- Chrome for Android (version 89+) + +**Not Supported:** +- iOS (no WebNFC support) +- Desktop browsers (limited WebNFC support) + +### Usage + +1. Open `http://localhost:8000/webnfc` on Chrome for Android +2. Click "Scan NFC Tag" button +3. Hold NTAG 424 DNA tag near device +4. View decrypted results + +### Security Note + +WebNFC requires HTTPS in production. Use `http://localhost` for development only. + +## Demo Mode + +Demo mode is automatically activated when using factory keys (all zeros). + +### Features + +- Example URLs displayed on landing page +- Attribution to NXP and nfc-developer +- Sample data for testing +- All functionality works normally + +### Activating Demo Mode + +```bash +SDM_MASTER_KEY=00000000000000000000000000000000 +``` + +### Detecting Demo Mode + +```php +if (config('sdm.is_demo_mode')) { + // Show example URLs and attribution +} +``` + +### Production Mode + +Set a real master key to disable demo mode: + +```bash +SDM_MASTER_KEY=C9EB67DF090AFF47C3B19A2516680B9D +``` + +## Response Formats + +### HTML Response + +Returns Blade view with formatted data. + +**Success View:** `resources/views/info.blade.php` + +**Error View:** `resources/views/error.blade.php` + +**Variables:** +- `$encryptionMode` - "AES" or "LRP" +- `$uid` - Binary UID (displayed as hex) +- `$readCtr` - Integer read counter +- `$fileData` - Binary file data (displayed as hex) +- `$fileDataUtf8` - UTF-8 converted file data +- `$tamperStatus` - Tamper status (TagTamper only) +- `$tamperColor` - Color for tamper status (TagTamper only) + +### JSON Response + +Returns pretty-printed JSON with appropriate HTTP status codes. + +**Success (200 OK):** + +```json +{ + "picc_data_tag": "c7", + "encryption_mode": "AES", + "uid": "041e3c8a2d6b80", + "read_ctr": 6, + "file_data": "4343", + "file_data_utf8": "CC" +} +``` + +**Validation Error (403 Forbidden):** + +```json +{ + "error": "Message is not properly signed - invalid MAC" +} +``` + +**Decryption Error (400 Bad Request):** + +```json +{ + "error": "Invalid encrypted PICC data length - expected 16 bytes (AES) or 24 bytes (LRP), got 8 bytes. This may indicate malformed or truncated input data." +} +``` + +**Invalid Input (400 Bad Request):** + +```json +{ + "error": "Missing required parameter: picc_data" +} +``` + +**LRP Required (501 Not Implemented):** + +```json +{ + "error": "LRP mode is required" +} ``` ### Tamper Status Interpretation -The application interprets the first 2 bytes of file data as tamper status: +**File Data Format:** First 2 bytes indicate tamper status -| Bytes | Status | Color | -|-------|--------|-------| -| `CC` | Secure | Green | -| `OC` | Tampered (Closed) | Red | -| `OO` | Tampered (Open) | Red | -| `II` | Uninitialized | Orange | -| `NT` | Not TagTamper | Orange | +| Bytes | Status | Color | Description | +|-------|--------|-------|-------------| +| `CC CC` | Secure | Green | Tag has not been tampered with | +| `0C CC` | Tampered (Closed) | Red | Loop was opened and closed | +| `0C 0C` | Tampered (Open) | Red | Loop is currently open | +| `CD CD` | Uninitialized | Orange | TagTamper not initialized | +| Other | Unknown | Gray | Not a TagTamper variant | -## Development +**Implementation:** -### Running Tests +```php +function interpretTamperStatus(string $fileData): ?array +{ + if (strlen($fileData) < 2) { + return null; + } + + $statusBytes = substr($fileData, 0, 2); + + return match ($statusBytes) { + "\xCC\xCC" => ['status' => 'Secure', 'color' => 'green'], + "\x0C\xCC" => ['status' => 'Tampered (Closed)', 'color' => 'red'], + "\x0C\x0C" => ['status' => 'Tampered (Open)', 'color' => 'red'], + "\xCD\xCD" => ['status' => 'Uninitialized', 'color' => 'orange'], + default => ['status' => 'Not TagTamper', 'color' => 'orange'], + }; +} +``` + +## Deployment + +### Production Requirements + +- PHP 8.3 or higher +- Composer +- Web server (Apache, Nginx, etc.) +- HTTPS (required for WebNFC) +- Environment variables configured + +### Step 1: Install Dependencies ```bash -php artisan test +composer install --no-dev --optimize-autoloader ``` -### Code Style +### Step 2: Configure Environment + +Create `.env` file with production settings: ```bash -composer cs-fix +APP_ENV=production +APP_DEBUG=false +APP_KEY=base64:your_app_key_here + +SDM_MASTER_KEY=your_32_character_hex_master_key + +# Optional customization +SDM_ENC_PICC_DATA_PARAM=picc_data +SDM_ENC_FILE_DATA_PARAM=enc +SDM_SDMMAC_PARAM=cmac +SDM_REQUIRE_LRP=false +``` + +### Step 3: Optimize Laravel + +```bash +php artisan config:cache +php artisan route:cache +php artisan view:cache ``` -### Static Analysis +### Step 4: Set Permissions ```bash -composer phpstan +chmod -R 755 storage bootstrap/cache +``` + +### Step 5: Configure Web Server + +**Nginx Example:** + +```nginx +server { + listen 443 ssl http2; + server_name your-domain.com; + + ssl_certificate /path/to/cert.pem; + ssl_certificate_key /path/to/key.pem; + + root /path/to/example-app/public; + index index.php; + + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + location ~ \.php$ { + fastcgi_pass unix:/var/run/php/php8.3-fpm.sock; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; + include fastcgi_params; + } +} +``` + +### Security Considerations + +1. **HTTPS Required** - WebNFC only works over HTTPS +2. **Key Security** - Store master key in environment variables, never in code +3. **Input Validation** - All parameters are validated before processing +4. **CMAC Verification** - Always verify CMAC before trusting decrypted data +5. **Error Messages** - Production errors should not reveal sensitive information + +### Monitoring + +Log all validation failures for security monitoring: + +```php +// In SDMController +catch (ValidationException $e) { + Log::warning('CMAC validation failed', [ + 'uid' => $uid, + 'ip' => $request->ip(), + 'error' => $e->getMessage(), + ]); + return $this->jsonErrorResponse($e->getMessage(), 403); +} +``` + +--- + +## Testing + +Run the included tests: + +```bash +php artisan test ``` ## References - [NXP AN12196](https://www.nxp.com/docs/en/application-note/AN12196.pdf) - NTAG 424 DNA and TagTamper Features +- [NXP AN12304](https://www.nxp.com/docs/en/application-note/AN12304.pdf) - Leakage Resilient Primitive - [kduma/php-sdm](https://github.com/kduma-autoid/php-sdm) - PHP SDM Library - [nfc-developer/sdm-backend](https://github.com/nfc-developer/sdm-backend) - Original Python Implementation +- [WebNFC API](https://developer.mozilla.org/en-US/docs/Web/API/Web_NFC_API) - WebNFC Documentation ## License