Commit 3356b890 by Mohammad Izzat Johari

refector

parent 0e71f5c2
{
"name": "3fresources/hsmcrypto",
"name": "integration/hsmcrypto",
"description": "Laravel HSM Encryption/Decryption via PKCS#11",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Zee Zaco",
"email": "izzat@3fresources.com"
"email": "izzatjohari@gmail.com"
}
],
"autoload": {
......
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('hsm_log', function (Blueprint $table) {
$table->id();
$table->string('event', 100)->nullable();
$table->string('subject', 100)->nullable();
$table->string('response', 100)->nullable();
$table->integer('causer')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('hsm_log');
}
};
......@@ -9,8 +9,8 @@ class HsmCryptoServiceProvider extends ServiceProvider
public function register()
{
$this->mergeConfigFrom(
__DIR__ . '/../config/hsmcrypto.php',
'hsmcrypto'
__DIR__ . '/../config/hsm.php',
'hsm'
);
$this->app->singleton('hsm-crypto', function () {
......@@ -20,8 +20,16 @@ class HsmCryptoServiceProvider extends ServiceProvider
public function boot()
{
// Auto-load migrations
$this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
// Optionally publish migrations
$this->publishes([
__DIR__ . '/../database/migrations/' => database_path('migrations'),
], 'hsm-crypto-migrations');
$this->publishes([
__DIR__ . '/../config/hsmcrypto.php' => config_path('hsmcrypto.php'),
__DIR__ . '/../config/hsm.php' => config_path('hsm.php'),
], 'config');
}
}
<?php
namespace App\Helpers;
namespace integration\HsmCrypto;
use Illuminate\Support\Facades\Log;
use App\Events\Logger;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\File;
use Workbench\Apim\Model\AttachmentFile;
use integration\HsmCrypto\Models\HsmLog;
use setasign\Fpdi\Fpdi;
class Pkcs11Command
{
......@@ -52,6 +51,16 @@ class Pkcs11Command
return implode("\n", $output);
}
protected function logger(string $event, string $subject, string $response){
$hsm_log = new HsmLog();
$hsm_log->event = $event;
$hsm_log->subject = $subject;
$hsm_log->response = $response;
$hsm_log->causer = auth()->user()->id;
$hsm_log->save();
}
/**
* Check if keypair exists in HSM by listing objects.
*/
......@@ -64,6 +73,8 @@ class Pkcs11Command
. ' --pin "' . $this->pin . '"'
. ' --list-objects';
$this->logger('Retrieve Key', '', 'Retrieve Key Successful');
return $this->runShellCommand($cmd, 'Failed to list objects in HSM');
}
......@@ -106,7 +117,7 @@ class Pkcs11Command
$all_keys = $this->runShellCommand($cmd, 'Failed to list objects in HSM');
preg_match_all('/ID:\s*([0-9A-Fa-f]+)/', $all_keys, $matches);
preg_match_all('/^\s*ID:\s*([0-9A-Fa-f]+)/m', $all_keys, $matches);
$usedIds = collect($matches[1])->unique()->sort()->values();
$nextId = $usedIds->last() ? hexdec($usedIds->last()) + 1 : 1;
return dechex($nextId);
......@@ -127,9 +138,10 @@ class Pkcs11Command
$all_keys = $this->runShellCommand($cmd, 'Failed to list objects in HSM');
preg_match_all('/ID:\s*([0-9A-Fa-f]+)/', $all_keys, $matches);
preg_match_all('/^\s*ID:\s*([0-9A-Fa-f]+)/m', $all_keys, $matches);
$usedIds = collect($matches[1])->unique()->sort()->values();
$nextId = hexdec($usedIds->last());
return dechex($nextId);
}
......@@ -149,6 +161,8 @@ class Pkcs11Command
. ' --id ' . $id
. ' --label "' . $label . '"';
$this->logger('Generate Key', $label, 'Generate Key Successful');
return $this->runShellCommand($cmd, 'Failed to generate RSA keypair');
}
......@@ -168,6 +182,8 @@ class Pkcs11Command
Log::info('Generated AES 256 key', ['key' => base64_encode($aesKey)]);
$this->logger('Generate AES 256 Key', '', 'Generate AES 256 Key Successful');
// Generate IV
$ivLength = openssl_cipher_iv_length('aes-256-gcm');
if ($ivLength === false || $ivLength <= 0) {
......@@ -199,7 +215,7 @@ class Pkcs11Command
}
// Save encrypted PDF
$storagePath = public_path("storage/uploads/encrypt");
$storagePath = public_path("storage/hsm/encrypt");
if (!File::exists($storagePath)) {
File::makeDirectory($storagePath, 0777, true);
}
......@@ -211,36 +227,29 @@ class Pkcs11Command
Log::info('Encrypted PDF saved', ['path' => $encryptedPdf]);
// Encrypt AES Key with HSM (proc_open needed for piping)
$encryptedKey = $this->encryptAesKey($aesKey, $filename);
$this->logger('File Encrypted', $filename, 'File Encrypted Successful');
// Save metadata
$attachment = new AttachmentFile();
$attachment->nama_fail = $filename;
$attachment->dir_key = $encryptedKey;
$attachment->dir_file = $encryptedPdf;
$attachment->dir_iv = base64_encode($iv);
$attachment->dir_tag = base64_encode($tag);
$attachment->save();
Event::dispatch(new Logger("encrypt", $filename, 'success', 'File encrypted successfully', $encryptedPdf));
$encryptedKey = $this->encryptAesKey($aesKey, $filename);
unset($aesKey); // Zeroize key
unset($aesKey);
return [
$response = [
'status' => 'success',
'message' => 'File encrypted successfully',
'file' => $encryptedPdf,
'key' => $encryptedKey
'message' => 'File Encrypted Successful',
'nama_fail' => $filename,
'dir_key' => $encryptedKey,
'dir_file' => $encryptedPdf,
'dir_iv' => base64_encode($iv),
'dir_tag' => base64_encode($tag)
];
return json_encode($response);
} catch (\Throwable $e) {
Log::error('Encryption failed', [
'file' => $filename,
'error' => $e->getMessage()
]);
Event::dispatch(new Logger("encrypt", $filename, 'failed', 'Encryption failed', $e->getMessage()));
throw $e;
}
}
......@@ -251,7 +260,8 @@ class Pkcs11Command
protected function encryptAesKey(string $aesKey, string $filename)
{
$id_key = $this->getkeypairIdByLabel($this->keyLabel);
$storagePath = public_path("storage/uploads/encryptkey");
$storagePath = public_path("storage/hsm/encryptkey");
if (!File::exists($storagePath)) {
File::makeDirectory($storagePath, 0777, true);
}
......@@ -295,11 +305,15 @@ class Pkcs11Command
'stderr' => $stderr,
'code' => $returnCode,
]);
$this->logger('AES Key Encrypted', '', 'AES Key Encrypted Failed');
throw new \RuntimeException("Failed to encrypt AES key: $stderr");
}
Log::info('AES key encrypted', ['path' => $encryptedKeyPath]);
$this->logger('AES Key Encrypted', '', 'AES Key Encrypted Successful');
return $encryptedKeyPath;
}
......@@ -314,7 +328,7 @@ class Pkcs11Command
try {
// Decrypt AES Key first
$storagePath = public_path("storage/uploads/tempkey");
$storagePath = public_path("storage/hsm/tempkey");
if (!File::exists($storagePath)) {
File::makeDirectory($storagePath, 0777, true);
}
......@@ -335,6 +349,8 @@ class Pkcs11Command
// Use runShellCommand for consistent error handling
$this->runShellCommand($cmd, 'Failed to decrypt AES key');
$this->logger('AES Key Decrypted', '', 'AES Key Decrypted Successful');
$aesKey = file_get_contents($decryptedKeyFile);
if ($aesKey === false) {
throw new \Exception('Failed to read decrypted AES key.');
......@@ -361,31 +377,58 @@ class Pkcs11Command
$tag
);
$this->logger('File Decrypted', $filename, 'File Decrypted Successful');
if ($decryptedPdf === false) {
throw new \Exception('Failed to decrypt PDF content.');
}
// Save decrypted PDF
$decryptPath = public_path("storage/uploads/decrypt");
$decryptPath = public_path("storage/hsm/decrypt");
if (!File::exists($decryptPath)) {
File::makeDirectory($decryptPath, 0777, true);
}
$decryptedFile = $decryptPath . '/' . $filename;
if (file_put_contents($decryptedFile, $decryptedPdf) === false) {
throw new \Exception('Failed to save decrypted PDF.');
$fpdi = new Fpdi();
// Write decrypted bytes to a memory stream
$pdfStream = fopen('php://memory', 'r+');
fwrite($pdfStream, $decryptedPdf);
rewind($pdfStream);
// Count pages
$pageCount = $fpdi->setSourceFile($pdfStream);
// 3️⃣ Add watermark & rebuild PDF in memory
for ($i = 1; $i <= $pageCount; $i++) {
$tplIdx = $fpdi->importPage($i);
$size = $fpdi->getTemplateSize($tplIdx);
$orientation = ($size['width'] > $size['height']) ? 'L' : 'P';
$fpdi->AddPage($orientation, [$size['width'], $size['height']]);
$fpdi->useTemplate($tplIdx);
// Watermark text
$fpdi->SetFont('Arial', '', 12);
$fpdi->SetTextColor(150, 150, 150);
$fpdi->SetXY(20, $size['height'] - 20);
$fpdi->Write(0, 'Confidential — ' .
('Guest') .
' — ' . now()->toDateTimeString());
}
Log::info('PDF decrypted successfully', ['file' => $decryptedFile]);
// Output to string (S = string)
$watermarkedPdf = $fpdi->Output('S');
Event::dispatch(new Logger("decrypt", $filename, 'success', 'File decrypted successfully', $decryptedFile));
$response = response($watermarkedPdf, 200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'inline; filename="'.$filename.'"',
'Cache-Control' => 'no-store, no-cache, must-revalidate',
'Pragma' => 'no-cache',
'X-Content-Type-Options' => 'nosniff',
]);
return [
'status' => 'success',
'command' => 'Decryption File successful',
'file' => $decryptedFile,
];
// 4️⃣ Stream PDF to browser (inline)
return $response;
} catch (\Throwable $e) {
Log::error('Decryption failed', [
......@@ -393,8 +436,6 @@ class Pkcs11Command
'error' => $e->getMessage()
]);
Event::dispatch(new Logger("decrypt", $filename, 'failed', 'Decryption failed', $e->getMessage()));
throw $e;
}
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment