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/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 d79427f..0000000 --- a/example-app/app/Http/Controllers/SDMController.php +++ /dev/null @@ -1,320 +0,0 @@ -getSDM(); - - $result = $sdm->validatePlainSun( - uid: $params['uid'], - readCtr: $params['ctr'], - sdmmac: $params['sdmmac'], - sdmFileReadKey: $this->getMacKey($params['uid']) - ); - - return view('info', [ - 'encryptionMode' => $result['encryption_mode']->name, - 'uid' => $result['uid'], - 'readCtr' => $result['read_ctr'], - 'fileData' => null, - 'fileDataUtf8' => null, - ]); - } catch (ValidationException $e) { - return $this->errorResponse($e->getMessage(), 403); - } catch (\InvalidArgumentException $e) { - return $this->errorResponse($e->getMessage(), 400); - } - } - - /** - * Plain SUN message validation (JSON API). - */ - public function apiTagPlainText(Request $request): JsonResponse - { - 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 $this->jsonResponse([ - 'encryption_mode' => $result['encryption_mode']->name, - 'uid' => bin2hex($result['uid']), - 'read_ctr' => $result['read_ctr'], - ]); - } catch (ValidationException $e) { - return $this->jsonErrorResponse($e->getMessage(), 403); - } catch (\InvalidArgumentException $e) { - return $this->jsonErrorResponse($e->getMessage(), 400); - } - } - - /** - * SUN message decryption (HTML). - */ - public function tag(Request $request) - { - return $this->processEncryptedTag($request, false); - } - - /** - * SUN message decryption (JSON API). - */ - public function apiTag(Request $request): JsonResponse - { - return $this->processEncryptedTagApi($request, false); - } - - /** - * Tamper-tag SUN message decryption (HTML). - */ - public function tagTamper(Request $request) - { - return $this->processEncryptedTag($request, true); - } - - /** - * Tamper-tag SUN message decryption (JSON API). - */ - public function apiTagTamper(Request $request): JsonResponse - { - return $this->processEncryptedTagApi($request, true); - } - - /** - * Process encrypted tag (common logic for tag and tagTamper). - */ - 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 $this->errorResponse('LRP mode is required', 400); - } - - $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'] - ); - - $viewData = [ - 'piccDataTag' => $result['picc_data_tag'], - 'encryptionMode' => $result['encryption_mode']->name, - 'uid' => $result['uid'], - 'readCtr' => $result['read_ctr'], - 'fileData' => $result['file_data'], - 'fileDataUtf8' => $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']; - } - } - - return view('info', $viewData); - } catch (ValidationException $e) { - return $this->errorResponse($e->getMessage(), 403); - } catch (DecryptionException $e) { - return $this->errorResponse($e->getMessage(), 400); - } catch (\InvalidArgumentException $e) { - return $this->errorResponse($e->getMessage(), 400); - } catch (\RuntimeException $e) { - return $this->errorResponse($e->getMessage(), 501); - } - } - - /** - * Process encrypted tag API (common logic for API routes). - */ - private function processEncryptedTagApi(Request $request, bool $isTamperTag): JsonResponse - { - try { - $params = ParameterParser::parseEncryptedParams($request); - - // Check for LRP mode requirement - if (config('sdm.require_lrp') && $params['mode'] !== 'LRP') { - return $this->jsonErrorResponse('LRP mode is required', 400); - } - - $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' => bin2hex($result['picc_data_tag']), - 'encryption_mode' => $result['encryption_mode']->name, - 'uid' => bin2hex($result['uid']), - 'read_ctr' => $result['read_ctr'], - ]; - - 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']); - if ($tamperInfo) { - $responseData['tamper_status'] = $tamperInfo['status']; - } - } - - return $this->jsonResponse($responseData); - } catch (ValidationException $e) { - return $this->jsonErrorResponse($e->getMessage(), 403); - } catch (DecryptionException $e) { - return $this->jsonErrorResponse($e->getMessage(), 400); - } catch (\InvalidArgumentException $e) { - return $this->jsonErrorResponse($e->getMessage(), 400); - } catch (\RuntimeException $e) { - return $this->jsonErrorResponse($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); - } - - /** - * Return error view. - */ - private function errorResponse(string $message, int $status = 400) - { - return response()->view('error', ['message' => $message], $status); - } - - /** - * 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); - } - - /** - * 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/Controllers/TagController.php b/example-app/app/Http/Controllers/TagController.php new file mode 100644 index 0000000..f3b8a8e --- /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 @@ +headers->set('Accept', 'application/json'); + + return $next($request); + } +} diff --git a/example-app/app/Http/Responses/ErrorResponse.php b/example-app/app/Http/Responses/ErrorResponse.php new file mode 100644 index 0000000..e683b33 --- /dev/null +++ b/example-app/app/Http/Responses/ErrorResponse.php @@ -0,0 +1,31 @@ +wantsJson()) { + return response()->json( + ['error' => $this->message], + $this->status, + [], + JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES + ); + } + + return response()->view('error', ['message' => $this->message], $this->status); + } +} diff --git a/example-app/app/Http/Responses/ValidResponse.php b/example-app/app/Http/Responses/ValidResponse.php new file mode 100644 index 0000000..218ed3c --- /dev/null +++ b/example-app/app/Http/Responses/ValidResponse.php @@ -0,0 +1,69 @@ +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; + } +} 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 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..e778527 --- /dev/null +++ b/example-app/routes/api.php @@ -0,0 +1,15 @@ +