devsofttech_hms_ci4/app/Libraries/FhirEncryptionService.php
DevSoftTech Server a56e8a5776 feat: SNOMED background coding pipeline + ABDM Coding Panel
- Add opd_coding_queue + opd_snomed_suggestions migrations
- Add ProcessSnomedCoding spark command (cron: snomed:process-coding)
  Extracts clinical phrases, matches local SNOMED FULLTEXT, saves suggestions
- Add AbdmCodingPanel controller: index/review/confirm/reject/correct/mark_fhir_ready/tip_check
- Add coding_panel + coding_panel_review views (Bootstrap 5, AJAX actions)
- Inject queue trigger in opd_prescription_save (fail-open, ON DUPLICATE KEY)
- Add consult form tip badge: polls tip_check 4s after save, shows review link
- Add AbdmCodingPanel routes behind abdm permission filter
- Various ABDM gateway, FHIR, ABHA, SNOMED library updates
2026-05-19 20:40:27 +05:30

134 lines
4.0 KiB
PHP

<?php
namespace App\Libraries;
/**
* FHIR Payload Encryption Service
*
* Provides AES-256-GCM authenticated encryption for FHIR bundle JSON
* payloads stored at rest in the health_records table.
*
* Key configuration — add to .env:
* ABDM_FHIR_ENCRYPTION_KEY = <64-char lowercase hex string — 32 random bytes>
*
* Generate a key with:
* php -r "echo bin2hex(random_bytes(32)) . PHP_EOL;"
*
* If ABDM_FHIR_ENCRYPTION_KEY is absent the service falls back to an HMAC
* of the application's existing encryption.key with a fixed domain label.
* This guarantees the service always works, but a dedicated key is strongly
* recommended for production.
*/
class FhirEncryptionService
{
private const CIPHER = 'aes-256-gcm';
private const IV_LEN = 12; // 96-bit IV (recommended for GCM)
private const TAG_LEN = 16; // 128-bit authentication tag
private string $key;
public function __construct()
{
// Prefer getenv() so the value is available before CI4 bootstraps .env
$keyHex = trim((string) getenv('ABDM_FHIR_ENCRYPTION_KEY'));
if ($keyHex === '') {
$keyHex = trim((string) env('ABDM_FHIR_ENCRYPTION_KEY', ''));
}
if ($keyHex !== '' && ctype_xdigit($keyHex) && strlen($keyHex) >= 64) {
// Decode exactly 32 bytes from the first 64 hex chars
$this->key = (string) hex2bin(substr($keyHex, 0, 64));
} else {
// Fallback: derive a 32-byte key from the CI4 application key
$appKey = trim((string) env('encryption.key', (string) env('app.key', '')));
$this->key = hash_hmac('sha256', 'abdm-fhir-records-v1', $appKey, true);
}
}
/**
* Encrypt plaintext with AES-256-GCM.
*
* Output format (base64-encoded): IV (12 B) | TAG (16 B) | CIPHERTEXT
*
* @throws \RuntimeException on OpenSSL failure
*/
public function encrypt(string $plaintext): string
{
if ($plaintext === '') {
return '';
}
$iv = random_bytes(self::IV_LEN);
$tag = '';
$ciphertext = openssl_encrypt(
$plaintext,
self::CIPHER,
$this->key,
OPENSSL_RAW_DATA,
$iv,
$tag,
'',
self::TAG_LEN
);
if ($ciphertext === false) {
throw new \RuntimeException(
'FHIR payload encryption failed: ' . (openssl_error_string() ?: 'unknown OpenSSL error')
);
}
return base64_encode($iv . $tag . $ciphertext);
}
/**
* Decrypt a payload produced by encrypt().
*
* The GCM authentication tag is verified automatically; a mismatch
* (indicating tampering or a wrong key) throws a RuntimeException.
*
* @throws \RuntimeException on decryption or authentication failure
*/
public function decrypt(string $encoded): string
{
if ($encoded === '') {
return '';
}
$raw = base64_decode($encoded, true);
if ($raw === false || strlen($raw) < self::IV_LEN + self::TAG_LEN + 1) {
throw new \RuntimeException('Invalid encrypted FHIR payload: data is truncated or corrupt');
}
$iv = substr($raw, 0, self::IV_LEN);
$tag = substr($raw, self::IV_LEN, self::TAG_LEN);
$ciphertext = substr($raw, self::IV_LEN + self::TAG_LEN);
$plaintext = openssl_decrypt(
$ciphertext,
self::CIPHER,
$this->key,
OPENSSL_RAW_DATA,
$iv,
$tag
);
if ($plaintext === false) {
throw new \RuntimeException(
'FHIR payload decryption failed: authentication tag mismatch or corrupt data'
);
}
return $plaintext;
}
/**
* Returns true when the current PHP build supports AES-256-GCM.
*/
public static function isSupported(): bool
{
return in_array(self::CIPHER, openssl_get_cipher_methods(), true);
}
}