From 0562ae6ab2f64f34a49c42f38b356fdb4b80c181 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Nov 2025 20:50:28 +0000 Subject: [PATCH 1/9] Refactor error handling to use ErrorResponse class - Create ErrorResponse class implementing Illuminate\Contracts\Support\Responsable - Replace errorResponse() and jsonErrorResponse() methods with unified ErrorResponse - ErrorResponse automatically detects JSON vs HTML requests using wantsJson() - Remove deprecated error response helper methods from SDMController --- .../app/Http/Controllers/SDMController.php | 49 +++++++------------ .../app/Http/Responses/ErrorResponse.php | 33 +++++++++++++ 2 files changed, 50 insertions(+), 32 deletions(-) create mode 100644 example-app/app/Http/Responses/ErrorResponse.php diff --git a/example-app/app/Http/Controllers/SDMController.php b/example-app/app/Http/Controllers/SDMController.php index 7b75cf2..ee6506e 100644 --- a/example-app/app/Http/Controllers/SDMController.php +++ b/example-app/app/Http/Controllers/SDMController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers; use App\Helpers\ParameterParser; +use App\Http\Responses\ErrorResponse; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\View\View; @@ -55,9 +56,9 @@ public function tagPlainText(Request $request) 'fileDataUtf8' => null, ]); } catch (ValidationException $e) { - return $this->errorResponse($e->getMessage(), 403); + return new ErrorResponse($e->getMessage(), 403); } catch (\InvalidArgumentException $e) { - return $this->errorResponse($e->getMessage(), 400); + return new ErrorResponse($e->getMessage(), 400); } } @@ -84,9 +85,9 @@ public function apiTagPlainText(Request $request): JsonResponse 'read_ctr' => $result['read_ctr'], ]); } catch (ValidationException $e) { - return $this->jsonErrorResponse($e->getMessage(), 403); + return new ErrorResponse($e->getMessage(), 403); } catch (\InvalidArgumentException $e) { - return $this->jsonErrorResponse($e->getMessage(), 400); + return new ErrorResponse($e->getMessage(), 400); } } @@ -132,12 +133,12 @@ private function processEncryptedTag(Request $request, bool $isTamperTag) // Check for LRP mode requirement if (config('sdm.require_lrp') && $params['mode'] !== 'LRP') { - return $this->errorResponse('LRP mode is required', 400); + return new ErrorResponse('LRP mode is required', 400); } // Check if LRP mode is requested but not supported if ($params['mode'] === 'LRP') { - return $this->errorResponse('LRP mode is not yet supported in the php-sdm library', 501); + return new ErrorResponse('LRP mode is not yet supported in the php-sdm library', 501); } $sdm = $this->getSDM(); @@ -171,13 +172,13 @@ private function processEncryptedTag(Request $request, bool $isTamperTag) return view('info', $viewData); } catch (ValidationException $e) { - return $this->errorResponse($e->getMessage(), 403); + return new ErrorResponse($e->getMessage(), 403); } catch (DecryptionException $e) { - return $this->errorResponse($e->getMessage(), 400); + return new ErrorResponse($e->getMessage(), 400); } catch (\InvalidArgumentException $e) { - return $this->errorResponse($e->getMessage(), 400); + return new ErrorResponse($e->getMessage(), 400); } catch (\RuntimeException $e) { - return $this->errorResponse($e->getMessage(), 501); + return new ErrorResponse($e->getMessage(), 501); } } @@ -191,12 +192,12 @@ private function processEncryptedTagApi(Request $request, bool $isTamperTag): Js // Check for LRP mode requirement if (config('sdm.require_lrp') && $params['mode'] !== 'LRP') { - return $this->jsonErrorResponse('LRP mode is required', 400); + return new ErrorResponse('LRP mode is required', 400); } // Check if LRP mode is requested but not supported if ($params['mode'] === 'LRP') { - return $this->jsonErrorResponse('LRP mode is not yet supported in the php-sdm library', 501); + return new ErrorResponse('LRP mode is not yet supported in the php-sdm library', 501); } $sdm = $this->getSDM(); @@ -232,13 +233,13 @@ private function processEncryptedTagApi(Request $request, bool $isTamperTag): Js return $this->jsonResponse($responseData); } catch (ValidationException $e) { - return $this->jsonErrorResponse($e->getMessage(), 403); + return new ErrorResponse($e->getMessage(), 403); } catch (DecryptionException $e) { - return $this->jsonErrorResponse($e->getMessage(), 400); + return new ErrorResponse($e->getMessage(), 400); } catch (\InvalidArgumentException $e) { - return $this->jsonErrorResponse($e->getMessage(), 400); + return new ErrorResponse($e->getMessage(), 400); } catch (\RuntimeException $e) { - return $this->jsonErrorResponse($e->getMessage(), 501); + return new ErrorResponse($e->getMessage(), 501); } } @@ -304,14 +305,6 @@ private function getMacKey(string $uid): string return $kdf->deriveTagKey($masterKey, $uid, 2); } - /** - * Return error view. - */ - private function errorResponse(string $message, int $status = 400) - { - return response()->view('error', ['message' => $message], $status); - } - /** * Return JSON response with pretty printing. */ @@ -319,12 +312,4 @@ private function jsonResponse(array $data, int $status = 200): JsonResponse { return response()->json($data, $status, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); } - - /** - * Return JSON error response. - */ - private function jsonErrorResponse(string $message, int $status = 400): JsonResponse - { - return $this->jsonResponse(['error' => $message], $status); - } } diff --git a/example-app/app/Http/Responses/ErrorResponse.php b/example-app/app/Http/Responses/ErrorResponse.php new file mode 100644 index 0000000..0e5338e --- /dev/null +++ b/example-app/app/Http/Responses/ErrorResponse.php @@ -0,0 +1,33 @@ +wantsJson()) { + return response()->json( + ['error' => $this->message], + $this->status, + [], + JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES + ); + } + + return response()->view('error', ['message' => $this->message], $this->status); + } +} From b11b53686af890a7d0e7e001467c2cdc0298d6e1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Nov 2025 20:55:08 +0000 Subject: [PATCH 2/9] Add ForceJsonResponseMiddleware for API routes - Create ForceJsonResponseMiddleware to force JSON responses - Middleware sets Accept header to application/json for all API routes - Create routes/api.php for API endpoints - Move API routes from web.php to api.php - Register API routes with /api prefix in bootstrap/app.php - Apply ForceJsonResponseMiddleware to all API routes - API routes now automatically return JSON via wantsJson() --- .../ForceJsonResponseMiddleware.php | 25 +++++++++++++++++++ example-app/bootstrap/app.php | 6 ++++- example-app/routes/api.php | 13 ++++++++++ example-app/routes/web.php | 3 --- 4 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 example-app/app/Http/Middleware/ForceJsonResponseMiddleware.php create mode 100644 example-app/routes/api.php diff --git a/example-app/app/Http/Middleware/ForceJsonResponseMiddleware.php b/example-app/app/Http/Middleware/ForceJsonResponseMiddleware.php new file mode 100644 index 0000000..23a6dcb --- /dev/null +++ b/example-app/app/Http/Middleware/ForceJsonResponseMiddleware.php @@ -0,0 +1,25 @@ +headers->set('Accept', 'application/json'); + + return $next($request); + } +} diff --git a/example-app/bootstrap/app.php b/example-app/bootstrap/app.php index c183276..f000855 100644 --- a/example-app/bootstrap/app.php +++ b/example-app/bootstrap/app.php @@ -1,5 +1,6 @@ withRouting( web: __DIR__.'/../routes/web.php', + api: __DIR__.'/../routes/api.php', commands: __DIR__.'/../routes/console.php', health: '/up', ) ->withMiddleware(function (Middleware $middleware): void { - // + $middleware->api(prepend: [ + ForceJsonResponseMiddleware::class, + ]); }) ->withExceptions(function (Exceptions $exceptions): void { // diff --git a/example-app/routes/api.php b/example-app/routes/api.php new file mode 100644 index 0000000..acc835f --- /dev/null +++ b/example-app/routes/api.php @@ -0,0 +1,13 @@ + Date: Sun, 23 Nov 2025 21:01:19 +0000 Subject: [PATCH 3/9] Create ValidResponse class to unify successful responses - Create ValidResponse class implementing Responsable - Automatically converts data format based on request type: * JSON: converts binary fields to hex, keeps snake_case keys * HTML: converts keys to camelCase, keeps binary as-is - Replace all view('info', ...) calls with ValidResponse - Replace all jsonResponse(...) calls with ValidResponse - Remove jsonResponse() helper method - Remove JsonResponse import (no longer needed) - Update return type hints to allow Responsable returns - Unify data preparation using snake_case format with binary data --- .../app/Http/Controllers/SDMController.php | 62 +++++++--------- .../app/Http/Responses/ValidResponse.php | 71 +++++++++++++++++++ 2 files changed, 97 insertions(+), 36 deletions(-) create mode 100644 example-app/app/Http/Responses/ValidResponse.php diff --git a/example-app/app/Http/Controllers/SDMController.php b/example-app/app/Http/Controllers/SDMController.php index ee6506e..bf458be 100644 --- a/example-app/app/Http/Controllers/SDMController.php +++ b/example-app/app/Http/Controllers/SDMController.php @@ -6,7 +6,7 @@ use App\Helpers\ParameterParser; use App\Http\Responses\ErrorResponse; -use Illuminate\Http\JsonResponse; +use App\Http\Responses\ValidResponse; use Illuminate\Http\Request; use Illuminate\View\View; use KDuma\SDM\Exceptions\DecryptionException; @@ -48,12 +48,12 @@ public function tagPlainText(Request $request) sdmFileReadKey: $this->getMacKey($params['uid']) ); - return view('info', [ - 'encryptionMode' => $result['encryption_mode']->value, + return new ValidResponse([ + 'encryption_mode' => $result['encryption_mode']->value, 'uid' => $result['uid'], - 'readCtr' => $result['read_ctr'], - 'fileData' => null, - 'fileDataUtf8' => null, + 'read_ctr' => $result['read_ctr'], + 'file_data' => null, + 'file_data_utf8' => null, ]); } catch (ValidationException $e) { return new ErrorResponse($e->getMessage(), 403); @@ -65,7 +65,7 @@ public function tagPlainText(Request $request) /** * Plain SUN message validation (JSON API). */ - public function apiTagPlainText(Request $request): JsonResponse + public function apiTagPlainText(Request $request) { try { $params = ParameterParser::parsePlainParams($request); @@ -79,9 +79,9 @@ public function apiTagPlainText(Request $request): JsonResponse sdmFileReadKey: $this->getMacKey($params['uid']) ); - return $this->jsonResponse([ + return new ValidResponse([ 'encryption_mode' => $result['encryption_mode']->value, - 'uid' => bin2hex($result['uid']), + 'uid' => $result['uid'], 'read_ctr' => $result['read_ctr'], ]); } catch (ValidationException $e) { @@ -102,7 +102,7 @@ public function tag(Request $request) /** * SUN message decryption (JSON API). */ - public function apiTag(Request $request): JsonResponse + public function apiTag(Request $request) { return $this->processEncryptedTagApi($request, false); } @@ -118,7 +118,7 @@ public function tagTamper(Request $request) /** * Tamper-tag SUN message decryption (JSON API). */ - public function apiTagTamper(Request $request): JsonResponse + public function apiTagTamper(Request $request) { return $this->processEncryptedTagApi($request, true); } @@ -152,25 +152,25 @@ private function processEncryptedTag(Request $request, bool $isTamperTag) encFileData: $params['enc_file_data'] ); - $viewData = [ - 'piccDataTag' => $result['picc_data_tag'], - 'encryptionMode' => $result['encryption_mode']->value, + $responseData = [ + 'picc_data_tag' => $result['picc_data_tag'], + 'encryption_mode' => $result['encryption_mode']->value, 'uid' => $result['uid'], - 'readCtr' => $result['read_ctr'], - 'fileData' => $result['file_data'], - 'fileDataUtf8' => $result['file_data'] ? $this->convertToUtf8($result['file_data']) : null, + 'read_ctr' => $result['read_ctr'], + 'file_data' => $result['file_data'], + 'file_data_utf8' => $result['file_data'] ? $this->convertToUtf8($result['file_data']) : null, ]; // Add tamper status if this is a tamper tag if ($isTamperTag && $result['file_data']) { $tamperInfo = ParameterParser::interpretTamperStatus($result['file_data']); if ($tamperInfo) { - $viewData['tamperStatus'] = $tamperInfo['status']; - $viewData['tamperColor'] = $tamperInfo['color']; + $responseData['tamper_status'] = $tamperInfo['status']; + $responseData['tamper_color'] = $tamperInfo['color']; } } - return view('info', $viewData); + return new ValidResponse($responseData); } catch (ValidationException $e) { return new ErrorResponse($e->getMessage(), 403); } catch (DecryptionException $e) { @@ -185,7 +185,7 @@ private function processEncryptedTag(Request $request, bool $isTamperTag) /** * Process encrypted tag API (common logic for API routes). */ - private function processEncryptedTagApi(Request $request, bool $isTamperTag): JsonResponse + private function processEncryptedTagApi(Request $request, bool $isTamperTag) { try { $params = ParameterParser::parseEncryptedParams($request); @@ -212,17 +212,14 @@ private function processEncryptedTagApi(Request $request, bool $isTamperTag): Js ); $responseData = [ - 'picc_data_tag' => bin2hex($result['picc_data_tag']), + 'picc_data_tag' => $result['picc_data_tag'], 'encryption_mode' => $result['encryption_mode']->value, - 'uid' => bin2hex($result['uid']), + 'uid' => $result['uid'], 'read_ctr' => $result['read_ctr'], + 'file_data' => $result['file_data'], + 'file_data_utf8' => $result['file_data'] ? $this->convertToUtf8($result['file_data']) : null, ]; - if ($result['file_data']) { - $responseData['file_data'] = bin2hex($result['file_data']); - $responseData['file_data_utf8'] = $this->convertToUtf8($result['file_data']); - } - // Add tamper status if this is a tamper tag if ($isTamperTag && $result['file_data']) { $tamperInfo = ParameterParser::interpretTamperStatus($result['file_data']); @@ -231,7 +228,7 @@ private function processEncryptedTagApi(Request $request, bool $isTamperTag): Js } } - return $this->jsonResponse($responseData); + return new ValidResponse($responseData); } catch (ValidationException $e) { return new ErrorResponse($e->getMessage(), 403); } catch (DecryptionException $e) { @@ -305,11 +302,4 @@ private function getMacKey(string $uid): string return $kdf->deriveTagKey($masterKey, $uid, 2); } - /** - * Return JSON response with pretty printing. - */ - private function jsonResponse(array $data, int $status = 200): JsonResponse - { - return response()->json($data, $status, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); - } } diff --git a/example-app/app/Http/Responses/ValidResponse.php b/example-app/app/Http/Responses/ValidResponse.php new file mode 100644 index 0000000..5f91a96 --- /dev/null +++ b/example-app/app/Http/Responses/ValidResponse.php @@ -0,0 +1,71 @@ +wantsJson()) { + return response()->json( + $this->convertBinaryToHex($this->data), + $this->status, + [], + JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES + ); + } + + return response()->view('info', $this->convertKeysToCamelCase($this->data), $this->status); + } + + /** + * Convert binary fields to hex for JSON responses. + */ + private function convertBinaryToHex(array $data): array + { + $result = []; + + foreach ($data as $key => $value) { + if (in_array($key, self::BINARY_FIELDS) && is_string($value)) { + $result[$key] = bin2hex($value); + } else { + $result[$key] = $value; + } + } + + return $result; + } + + /** + * Convert snake_case keys to camelCase for view responses. + */ + private function convertKeysToCamelCase(array $data): array + { + $result = []; + + foreach ($data as $key => $value) { + $camelKey = lcfirst(str_replace('_', '', ucwords($key, '_'))); + $result[$camelKey] = $value; + } + + return $result; + } +} From c9b74dc1e08b10cac4e94c3c11c808b0279ed8f1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Nov 2025 21:04:36 +0000 Subject: [PATCH 4/9] Fix encryption_mode to return enum name instead of numeric value --- example-app/app/Http/Controllers/SDMController.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/example-app/app/Http/Controllers/SDMController.php b/example-app/app/Http/Controllers/SDMController.php index bf458be..919a73f 100644 --- a/example-app/app/Http/Controllers/SDMController.php +++ b/example-app/app/Http/Controllers/SDMController.php @@ -49,7 +49,7 @@ public function tagPlainText(Request $request) ); return new ValidResponse([ - 'encryption_mode' => $result['encryption_mode']->value, + 'encryption_mode' => $result['encryption_mode']->name, 'uid' => $result['uid'], 'read_ctr' => $result['read_ctr'], 'file_data' => null, @@ -80,7 +80,7 @@ public function apiTagPlainText(Request $request) ); return new ValidResponse([ - 'encryption_mode' => $result['encryption_mode']->value, + 'encryption_mode' => $result['encryption_mode']->name, 'uid' => $result['uid'], 'read_ctr' => $result['read_ctr'], ]); @@ -154,7 +154,7 @@ private function processEncryptedTag(Request $request, bool $isTamperTag) $responseData = [ 'picc_data_tag' => $result['picc_data_tag'], - 'encryption_mode' => $result['encryption_mode']->value, + 'encryption_mode' => $result['encryption_mode']->name, 'uid' => $result['uid'], 'read_ctr' => $result['read_ctr'], 'file_data' => $result['file_data'], @@ -213,7 +213,7 @@ private function processEncryptedTagApi(Request $request, bool $isTamperTag) $responseData = [ 'picc_data_tag' => $result['picc_data_tag'], - 'encryption_mode' => $result['encryption_mode']->value, + 'encryption_mode' => $result['encryption_mode']->name, 'uid' => $result['uid'], 'read_ctr' => $result['read_ctr'], 'file_data' => $result['file_data'], From f7cebf3d9a4ff4c4d4669e28758c599586d060cf Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Nov 2025 21:07:59 +0000 Subject: [PATCH 5/9] Enable LRP mode support in example app --- .../app/Http/Controllers/SDMController.php | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/example-app/app/Http/Controllers/SDMController.php b/example-app/app/Http/Controllers/SDMController.php index 919a73f..a296bbf 100644 --- a/example-app/app/Http/Controllers/SDMController.php +++ b/example-app/app/Http/Controllers/SDMController.php @@ -131,16 +131,6 @@ private function processEncryptedTag(Request $request, bool $isTamperTag) try { $params = ParameterParser::parseEncryptedParams($request); - // Check for LRP mode requirement - if (config('sdm.require_lrp') && $params['mode'] !== 'LRP') { - return new ErrorResponse('LRP mode is required', 400); - } - - // Check if LRP mode is requested but not supported - if ($params['mode'] === 'LRP') { - return new ErrorResponse('LRP mode is not yet supported in the php-sdm library', 501); - } - $sdm = $this->getSDM(); $result = $sdm->decryptSunMessage( @@ -190,16 +180,6 @@ private function processEncryptedTagApi(Request $request, bool $isTamperTag) try { $params = ParameterParser::parseEncryptedParams($request); - // Check for LRP mode requirement - if (config('sdm.require_lrp') && $params['mode'] !== 'LRP') { - return new ErrorResponse('LRP mode is required', 400); - } - - // Check if LRP mode is requested but not supported - if ($params['mode'] === 'LRP') { - return new ErrorResponse('LRP mode is not yet supported in the php-sdm library', 501); - } - $sdm = $this->getSDM(); $result = $sdm->decryptSunMessage( From 847c16e6d0cdfb8e20623fdd9e929d1378a1c85c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Nov 2025 21:09:29 +0000 Subject: [PATCH 6/9] Simplify static page routes to use Route::view - Replace Route::get with Route::view for main page and WebNFC - Remove index() and webnfc() controller methods - Remove unused Illuminate\View\View import --- .../app/Http/Controllers/SDMController.php | 17 ----------------- example-app/routes/web.php | 4 ++-- 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/example-app/app/Http/Controllers/SDMController.php b/example-app/app/Http/Controllers/SDMController.php index a296bbf..26e6ee2 100644 --- a/example-app/app/Http/Controllers/SDMController.php +++ b/example-app/app/Http/Controllers/SDMController.php @@ -8,29 +8,12 @@ use App\Http\Responses\ErrorResponse; use App\Http\Responses\ValidResponse; use Illuminate\Http\Request; -use Illuminate\View\View; use KDuma\SDM\Exceptions\DecryptionException; use KDuma\SDM\Exceptions\ValidationException; use KDuma\SDM\SDM; class SDMController extends Controller { - /** - * Main landing page. - */ - public function index(): View - { - return view('main'); - } - - /** - * WebNFC interface page. - */ - public function webnfc(): View - { - return view('webnfc'); - } - /** * Plain SUN message validation (HTML). */ diff --git a/example-app/routes/web.php b/example-app/routes/web.php index f5e1633..c0d6066 100644 --- a/example-app/routes/web.php +++ b/example-app/routes/web.php @@ -4,10 +4,10 @@ use Illuminate\Support\Facades\Route; // Main page -Route::get('/', [SDMController::class, 'index']); +Route::view('/', 'main'); // WebNFC interface -Route::get('/webnfc', [SDMController::class, 'webnfc']); +Route::view('/webnfc', 'webnfc'); // Plain SUN message validation Route::get('/tagpt', [SDMController::class, 'tagPlainText']); From 0e94fa90272dc855d74a1308f6acf2cc41ada7c2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Nov 2025 21:13:55 +0000 Subject: [PATCH 7/9] Merge duplicate controller methods for web and API routes - Remove apiTagPlainText, apiTag, apiTagTamper methods - Remove processEncryptedTagApi method - Update API routes to use the same controller methods as web routes - ValidResponse automatically handles JSON vs HTML based on request - Simplifies controller by removing duplicate code --- .../app/Http/Controllers/SDMController.php | 98 +------------------ example-app/routes/api.php | 6 +- 2 files changed, 6 insertions(+), 98 deletions(-) diff --git a/example-app/app/Http/Controllers/SDMController.php b/example-app/app/Http/Controllers/SDMController.php index 26e6ee2..fcf2778 100644 --- a/example-app/app/Http/Controllers/SDMController.php +++ b/example-app/app/Http/Controllers/SDMController.php @@ -15,7 +15,7 @@ class SDMController extends Controller { /** - * Plain SUN message validation (HTML). + * Plain SUN message validation. */ public function tagPlainText(Request $request) { @@ -46,36 +46,7 @@ public function tagPlainText(Request $request) } /** - * Plain SUN message validation (JSON API). - */ - public function apiTagPlainText(Request $request) - { - try { - $params = ParameterParser::parsePlainParams($request); - - $sdm = $this->getSDM(); - - $result = $sdm->validatePlainSun( - uid: $params['uid'], - readCtr: $params['ctr'], - sdmmac: $params['sdmmac'], - sdmFileReadKey: $this->getMacKey($params['uid']) - ); - - return new ValidResponse([ - 'encryption_mode' => $result['encryption_mode']->name, - 'uid' => $result['uid'], - 'read_ctr' => $result['read_ctr'], - ]); - } catch (ValidationException $e) { - return new ErrorResponse($e->getMessage(), 403); - } catch (\InvalidArgumentException $e) { - return new ErrorResponse($e->getMessage(), 400); - } - } - - /** - * SUN message decryption (HTML). + * SUN message decryption. */ public function tag(Request $request) { @@ -83,29 +54,13 @@ public function tag(Request $request) } /** - * SUN message decryption (JSON API). - */ - public function apiTag(Request $request) - { - return $this->processEncryptedTagApi($request, false); - } - - /** - * Tamper-tag SUN message decryption (HTML). + * Tamper-tag SUN message decryption. */ public function tagTamper(Request $request) { return $this->processEncryptedTag($request, true); } - /** - * Tamper-tag SUN message decryption (JSON API). - */ - public function apiTagTamper(Request $request) - { - return $this->processEncryptedTagApi($request, true); - } - /** * Process encrypted tag (common logic for tag and tagTamper). */ @@ -155,53 +110,6 @@ private function processEncryptedTag(Request $request, bool $isTamperTag) } } - /** - * Process encrypted tag API (common logic for API routes). - */ - private function processEncryptedTagApi(Request $request, bool $isTamperTag) - { - try { - $params = ParameterParser::parseEncryptedParams($request); - - $sdm = $this->getSDM(); - - $result = $sdm->decryptSunMessage( - paramMode: \KDuma\SDM\ParamMode::SEPARATED, - sdmMetaReadKey: $this->getEncKey(), - sdmFileReadKey: fn(string $uid) => $this->getMacKey($uid), - piccEncData: $params['picc_data'], - sdmmac: $params['sdmmac'], - encFileData: $params['enc_file_data'] - ); - - $responseData = [ - 'picc_data_tag' => $result['picc_data_tag'], - 'encryption_mode' => $result['encryption_mode']->name, - 'uid' => $result['uid'], - 'read_ctr' => $result['read_ctr'], - 'file_data' => $result['file_data'], - 'file_data_utf8' => $result['file_data'] ? $this->convertToUtf8($result['file_data']) : null, - ]; - - // Add tamper status if this is a tamper tag - if ($isTamperTag && $result['file_data']) { - $tamperInfo = ParameterParser::interpretTamperStatus($result['file_data']); - if ($tamperInfo) { - $responseData['tamper_status'] = $tamperInfo['status']; - } - } - - return new ValidResponse($responseData); - } catch (ValidationException $e) { - return new ErrorResponse($e->getMessage(), 403); - } catch (DecryptionException $e) { - return new ErrorResponse($e->getMessage(), 400); - } catch (\InvalidArgumentException $e) { - return new ErrorResponse($e->getMessage(), 400); - } catch (\RuntimeException $e) { - return new ErrorResponse($e->getMessage(), 501); - } - } /** * Get SDM instance. diff --git a/example-app/routes/api.php b/example-app/routes/api.php index acc835f..a9e4543 100644 --- a/example-app/routes/api.php +++ b/example-app/routes/api.php @@ -4,10 +4,10 @@ use Illuminate\Support\Facades\Route; // Plain SUN message validation -Route::get('/tagpt', [SDMController::class, 'apiTagPlainText']); +Route::get('/tagpt', [SDMController::class, 'tagPlainText']); // SUN message decryption -Route::get('/tag', [SDMController::class, 'apiTag']); +Route::get('/tag', [SDMController::class, 'tag']); // Tamper-tag SUN message decryption -Route::get('/tagtt', [SDMController::class, 'apiTagTamper']); +Route::get('/tagtt', [SDMController::class, 'tagTamper']); From b7d11afbaa821c1f3d3c06e821447ed60cb32e5b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Nov 2025 21:22:22 +0000 Subject: [PATCH 8/9] Refactor to invokable controllers with base class - Create BaseSDMController with shared SDM methods * getSDM(), getMasterKey(), getEncKey(), getMacKey() - Create TagPlainTextController (invokable) for plain SUN validation - Create TagController (invokable) for encrypted SUN decryption * Includes convertToUtf8() method * Protected isTamperTag() method for extension - Create TagTamperController extending TagController * Overrides isTamperTag() to return true - Update routes to use invokable controllers (ControllerClass::class) - Remove old monolithic SDMController - Each controller is focused on a single responsibility --- .../Http/Controllers/BaseSDMController.php | 57 ++++++ .../app/Http/Controllers/SDMController.php | 176 ------------------ .../app/Http/Controllers/TagController.php | 87 +++++++++ .../Controllers/TagPlainTextController.php | 45 +++++ .../Http/Controllers/TagTamperController.php | 16 ++ example-app/routes/api.php | 10 +- example-app/routes/web.php | 10 +- 7 files changed, 217 insertions(+), 184 deletions(-) create mode 100644 example-app/app/Http/Controllers/BaseSDMController.php delete mode 100644 example-app/app/Http/Controllers/SDMController.php create mode 100644 example-app/app/Http/Controllers/TagController.php create mode 100644 example-app/app/Http/Controllers/TagPlainTextController.php create mode 100644 example-app/app/Http/Controllers/TagTamperController.php diff --git a/example-app/app/Http/Controllers/BaseSDMController.php b/example-app/app/Http/Controllers/BaseSDMController.php new file mode 100644 index 0000000..444f774 --- /dev/null +++ b/example-app/app/Http/Controllers/BaseSDMController.php @@ -0,0 +1,57 @@ +getMasterKey(); + $kdf = app(\KDuma\SDM\KeyDerivation::class); + + return $kdf->deriveUndiversifiedKey($masterKey, 1); + } + + /** + * Get MAC key for a specific UID. + */ + protected function getMacKey(string $uid): string + { + $masterKey = $this->getMasterKey(); + $kdf = app(\KDuma\SDM\KeyDerivation::class); + + return $kdf->deriveTagKey($masterKey, $uid, 2); + } +} diff --git a/example-app/app/Http/Controllers/SDMController.php b/example-app/app/Http/Controllers/SDMController.php deleted file mode 100644 index fcf2778..0000000 --- a/example-app/app/Http/Controllers/SDMController.php +++ /dev/null @@ -1,176 +0,0 @@ -getSDM(); - - $result = $sdm->validatePlainSun( - uid: $params['uid'], - readCtr: $params['ctr'], - sdmmac: $params['sdmmac'], - sdmFileReadKey: $this->getMacKey($params['uid']) - ); - - return new ValidResponse([ - 'encryption_mode' => $result['encryption_mode']->name, - 'uid' => $result['uid'], - 'read_ctr' => $result['read_ctr'], - 'file_data' => null, - 'file_data_utf8' => null, - ]); - } catch (ValidationException $e) { - return new ErrorResponse($e->getMessage(), 403); - } catch (\InvalidArgumentException $e) { - return new ErrorResponse($e->getMessage(), 400); - } - } - - /** - * SUN message decryption. - */ - public function tag(Request $request) - { - return $this->processEncryptedTag($request, false); - } - - /** - * Tamper-tag SUN message decryption. - */ - public function tagTamper(Request $request) - { - return $this->processEncryptedTag($request, true); - } - - /** - * Process encrypted tag (common logic for tag and tagTamper). - */ - private function processEncryptedTag(Request $request, bool $isTamperTag) - { - try { - $params = ParameterParser::parseEncryptedParams($request); - - $sdm = $this->getSDM(); - - $result = $sdm->decryptSunMessage( - paramMode: \KDuma\SDM\ParamMode::SEPARATED, - sdmMetaReadKey: $this->getEncKey(), - sdmFileReadKey: fn(string $uid) => $this->getMacKey($uid), - piccEncData: $params['picc_data'], - sdmmac: $params['sdmmac'], - encFileData: $params['enc_file_data'] - ); - - $responseData = [ - 'picc_data_tag' => $result['picc_data_tag'], - 'encryption_mode' => $result['encryption_mode']->name, - 'uid' => $result['uid'], - 'read_ctr' => $result['read_ctr'], - 'file_data' => $result['file_data'], - 'file_data_utf8' => $result['file_data'] ? $this->convertToUtf8($result['file_data']) : null, - ]; - - // Add tamper status if this is a tamper tag - if ($isTamperTag && $result['file_data']) { - $tamperInfo = ParameterParser::interpretTamperStatus($result['file_data']); - if ($tamperInfo) { - $responseData['tamper_status'] = $tamperInfo['status']; - $responseData['tamper_color'] = $tamperInfo['color']; - } - } - - return new ValidResponse($responseData); - } catch (ValidationException $e) { - return new ErrorResponse($e->getMessage(), 403); - } catch (DecryptionException $e) { - return new ErrorResponse($e->getMessage(), 400); - } catch (\InvalidArgumentException $e) { - return new ErrorResponse($e->getMessage(), 400); - } catch (\RuntimeException $e) { - return new ErrorResponse($e->getMessage(), 501); - } - } - - - /** - * Get SDM instance. - */ - private function getSDM(?string $uid = null): SDM - { - $factory = app('sdm.factory'); - - return $factory($uid); - } - - /** - * Convert binary data to UTF-8 string safely. - */ - private function convertToUtf8(string $data): string - { - // Check if data is already valid UTF-8 - if (mb_check_encoding($data, 'UTF-8')) { - return $data; - } - - // Treat as ISO-8859-1 (Latin1) and convert to UTF-8 - // This ensures every byte is mapped to a valid character - return mb_convert_encoding($data, 'UTF-8', 'ISO-8859-1'); - } - - /** - * Get master key from configuration. - */ - private function getMasterKey(): string - { - $masterKeyHex = config('sdm.master_key'); - $masterKey = hex2bin($masterKeyHex); - - if ($masterKey === false) { - throw new \InvalidArgumentException('Invalid master key format'); - } - - return $masterKey; - } - - /** - * Get encryption key. - */ - private function getEncKey(): string - { - $masterKey = $this->getMasterKey(); - $kdf = app(\KDuma\SDM\KeyDerivation::class); - - return $kdf->deriveUndiversifiedKey($masterKey, 1); - } - - /** - * Get MAC key for a specific UID. - */ - private function getMacKey(string $uid): string - { - $masterKey = $this->getMasterKey(); - $kdf = app(\KDuma\SDM\KeyDerivation::class); - - return $kdf->deriveTagKey($masterKey, $uid, 2); - } - -} diff --git a/example-app/app/Http/Controllers/TagController.php b/example-app/app/Http/Controllers/TagController.php new file mode 100644 index 0000000..f1d90a2 --- /dev/null +++ b/example-app/app/Http/Controllers/TagController.php @@ -0,0 +1,87 @@ +getSDM(); + + $result = $sdm->decryptSunMessage( + paramMode: \KDuma\SDM\ParamMode::SEPARATED, + sdmMetaReadKey: $this->getEncKey(), + sdmFileReadKey: fn(string $uid) => $this->getMacKey($uid), + piccEncData: $params['picc_data'], + sdmmac: $params['sdmmac'], + encFileData: $params['enc_file_data'] + ); + + $responseData = [ + 'picc_data_tag' => $result['picc_data_tag'], + 'encryption_mode' => $result['encryption_mode']->name, + 'uid' => $result['uid'], + 'read_ctr' => $result['read_ctr'], + 'file_data' => $result['file_data'], + 'file_data_utf8' => $result['file_data'] ? $this->convertToUtf8($result['file_data']) : null, + ]; + + // Add tamper status if this is a tamper tag + if ($this->isTamperTag() && $result['file_data']) { + $tamperInfo = ParameterParser::interpretTamperStatus($result['file_data']); + if ($tamperInfo) { + $responseData['tamper_status'] = $tamperInfo['status']; + $responseData['tamper_color'] = $tamperInfo['color']; + } + } + + return new ValidResponse($responseData); + } catch (ValidationException $e) { + return new ErrorResponse($e->getMessage(), 403); + } catch (DecryptionException $e) { + return new ErrorResponse($e->getMessage(), 400); + } catch (\InvalidArgumentException $e) { + return new ErrorResponse($e->getMessage(), 400); + } catch (\RuntimeException $e) { + return new ErrorResponse($e->getMessage(), 501); + } + } + + /** + * Determine if this is a tamper tag. + */ + protected function isTamperTag(): bool + { + return false; + } + + /** + * Convert binary data to UTF-8 string safely. + */ + protected function convertToUtf8(string $data): string + { + // Check if data is already valid UTF-8 + if (mb_check_encoding($data, 'UTF-8')) { + return $data; + } + + // Treat as ISO-8859-1 (Latin1) and convert to UTF-8 + // This ensures every byte is mapped to a valid character + return mb_convert_encoding($data, 'UTF-8', 'ISO-8859-1'); + } +} diff --git a/example-app/app/Http/Controllers/TagPlainTextController.php b/example-app/app/Http/Controllers/TagPlainTextController.php new file mode 100644 index 0000000..f9124d7 --- /dev/null +++ b/example-app/app/Http/Controllers/TagPlainTextController.php @@ -0,0 +1,45 @@ +getSDM(); + + $result = $sdm->validatePlainSun( + uid: $params['uid'], + readCtr: $params['ctr'], + sdmmac: $params['sdmmac'], + sdmFileReadKey: $this->getMacKey($params['uid']) + ); + + return new ValidResponse([ + 'encryption_mode' => $result['encryption_mode']->name, + 'uid' => $result['uid'], + 'read_ctr' => $result['read_ctr'], + 'file_data' => null, + 'file_data_utf8' => null, + ]); + } catch (ValidationException $e) { + return new ErrorResponse($e->getMessage(), 403); + } catch (\InvalidArgumentException $e) { + return new ErrorResponse($e->getMessage(), 400); + } + } +} diff --git a/example-app/app/Http/Controllers/TagTamperController.php b/example-app/app/Http/Controllers/TagTamperController.php new file mode 100644 index 0000000..e3f1213 --- /dev/null +++ b/example-app/app/Http/Controllers/TagTamperController.php @@ -0,0 +1,16 @@ + Date: Sun, 23 Nov 2025 22:26:44 +0100 Subject: [PATCH 9/9] Codestyle --- example-app/app/Helpers/ParameterParser.php | 8 ++++---- example-app/app/Http/Controllers/TagController.php | 2 +- example-app/app/Http/Responses/ErrorResponse.php | 4 +--- example-app/app/Http/Responses/ValidResponse.php | 4 +--- example-app/app/Providers/SDMServiceProvider.php | 2 +- 5 files changed, 8 insertions(+), 12 deletions(-) diff --git a/example-app/app/Helpers/ParameterParser.php b/example-app/app/Helpers/ParameterParser.php index 0c65d19..41c80fe 100644 --- a/example-app/app/Helpers/ParameterParser.php +++ b/example-app/app/Helpers/ParameterParser.php @@ -60,7 +60,7 @@ public static function parseEncryptedParams(Request $request): array } $encFileDataBin = null; - if (!empty($encFileData)) { + if (! empty($encFileData)) { if (strlen($encFileData) % 2 !== 0) { throw new \InvalidArgumentException( sprintf('Invalid %s parameter: must have even length', $paramNames['enc_file_data']) @@ -154,7 +154,7 @@ private static function parseBulkMode(string $bulkParam, string $sdmmacParamName } // Remove the SDMMAC parameter suffix if present - $sdmmacSuffix = '&' . $sdmmacParamName . '='; + $sdmmacSuffix = '&'.$sdmmacParamName.'='; $bulkParam = str_replace($sdmmacSuffix, '', $bulkParam); // Validate hex string length is even @@ -197,7 +197,7 @@ private static function parseBulkMode(string $bulkParam, string $sdmmacParamName /** * Detect encryption mode based on PICC data length. * - * @param string $piccData Binary PICC data + * @param string $piccData Binary PICC data * @return string 'AES' or 'LRP' */ private static function detectEncryptionMode(string $piccData): string @@ -216,7 +216,7 @@ private static function detectEncryptionMode(string $piccData): string /** * Interpret tamper tag status from file data. * - * @param string $fileData Binary file data + * @param string $fileData Binary file data * @return array{status: string, color: string}|null */ public static function interpretTamperStatus(string $fileData): ?array diff --git a/example-app/app/Http/Controllers/TagController.php b/example-app/app/Http/Controllers/TagController.php index f1d90a2..f3b8a8e 100644 --- a/example-app/app/Http/Controllers/TagController.php +++ b/example-app/app/Http/Controllers/TagController.php @@ -26,7 +26,7 @@ public function __invoke(Request $request) $result = $sdm->decryptSunMessage( paramMode: \KDuma\SDM\ParamMode::SEPARATED, sdmMetaReadKey: $this->getEncKey(), - sdmFileReadKey: fn(string $uid) => $this->getMacKey($uid), + sdmFileReadKey: fn (string $uid) => $this->getMacKey($uid), piccEncData: $params['picc_data'], sdmmac: $params['sdmmac'], encFileData: $params['enc_file_data'] diff --git a/example-app/app/Http/Responses/ErrorResponse.php b/example-app/app/Http/Responses/ErrorResponse.php index 0e5338e..e683b33 100644 --- a/example-app/app/Http/Responses/ErrorResponse.php +++ b/example-app/app/Http/Responses/ErrorResponse.php @@ -6,7 +6,6 @@ use Illuminate\Contracts\Support\Responsable; use Illuminate\Http\JsonResponse; -use Illuminate\Http\Request; use Illuminate\Http\Response; class ErrorResponse implements Responsable @@ -14,8 +13,7 @@ class ErrorResponse implements Responsable public function __construct( private readonly string $message, private readonly int $status = 400 - ) { - } + ) {} public function toResponse($request): Response|JsonResponse { diff --git a/example-app/app/Http/Responses/ValidResponse.php b/example-app/app/Http/Responses/ValidResponse.php index 5f91a96..218ed3c 100644 --- a/example-app/app/Http/Responses/ValidResponse.php +++ b/example-app/app/Http/Responses/ValidResponse.php @@ -6,7 +6,6 @@ use Illuminate\Contracts\Support\Responsable; use Illuminate\Http\JsonResponse; -use Illuminate\Http\Request; use Illuminate\Http\Response; class ValidResponse implements Responsable @@ -19,8 +18,7 @@ class ValidResponse implements Responsable public function __construct( private readonly array $data, private readonly int $status = 200 - ) { - } + ) {} public function toResponse($request): Response|JsonResponse { diff --git a/example-app/app/Providers/SDMServiceProvider.php b/example-app/app/Providers/SDMServiceProvider.php index e906721..5bc5ac1 100644 --- a/example-app/app/Providers/SDMServiceProvider.php +++ b/example-app/app/Providers/SDMServiceProvider.php @@ -17,7 +17,7 @@ public function register(): void { // Register KeyDerivation as a singleton $this->app->singleton(KeyDerivation::class, function ($app) { - return new KeyDerivation(); + return new KeyDerivation; }); // Register a factory for creating SDM instances