Commit add2a314 by Mohammad Izzat Johari

init commit

parents
{
"name": "integration/hsm-crypto",
"description": "Laravel HSM Encryption/Decryption via PKCS#11",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Zee Zaco",
"email": "izzat@3fresources.com"
}
],
"autoload": {
"psr-4": {
"integration\\HsmCrypto\\": "src/"
}
},
"require": {
"php": "^8.1",
"illuminate/support": "^10.0"
},
"extra": {
"laravel": {
"providers": [
"integration\\HsmCrypto\\HsmCryptoServiceProvider"
],
"aliases": {
"HsmCrypto": "integration\\HsmCrypto\\Facades\\HsmCrypto"
}
}
}
}
<?php
return [
'pkcs11_tool' => env('HSM_PKCS11_TOOL', ''),
'module' => env('HSM_MODULE', ''),
'pin' => env('HSM_USER_PIN', ''),
'key_label' => "key_master_".date("Y")
];
<?php
namespace integration\HsmCrypto\Facades;
use Illuminate\Support\Facades\Facade;
class HsmCrypto extends Facade
{
protected static function getFacadeAccessor()
{
return 'hsm-crypto';
}
}
<?php
namespace integration\HsmCrypto;
use Illuminate\Support\ServiceProvider;
class HsmCryptoServiceProvider extends ServiceProvider
{
public function register()
{
$this->mergeConfigFrom(
__DIR__ . '/../config/hsmcrypto.php',
'hsmcrypto'
);
$this->app->singleton('hsm-crypto', function () {
return new Pkcs11Command();
});
}
public function boot()
{
$this->publishes([
__DIR__ . '/../config/hsmcrypto.php' => config_path('hsmcrypto.php'),
], 'config');
}
}
<?php
namespace App\Helpers;
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;
class Pkcs11Command
{
protected $pkcs11_tool;
protected $module;
protected $pin;
protected $keyLabel;
protected $slotId;
public function __construct()
{
$this->pkcs11_tool = config('hsm.pkcs11_tool');
$this->module = config('hsm.module');
$this->pin = config('hsm.pin');
$this->keyLabel = config('hsm.key_label');
$this->slotId = "1";
}
/**
* Run a shell command and capture errors.
*/
protected function runShellCommand(string $cmd, string $errorMessage)
{
Log::debug("Executing command: $cmd");
$output = [];
$returnCode = 0;
exec($cmd . ' 2>&1', $output, $returnCode);
if ($returnCode !== 0) {
$errorOutput = implode("\n", $output);
Log::error("$errorMessage", [
'command' => $cmd,
'output' => $errorOutput,
'code' => $returnCode,
]);
throw new \RuntimeException("$errorMessage: $errorOutput");
}
return implode("\n", $output);
}
/**
* Check if keypair exists in HSM by listing objects.
*/
public function getKeypair()
{
$cmd = '"' . $this->pkcs11_tool . '"'
. ' --module "' . $this->module . '"'
. ' --slot ' . $this->slotId
. ' --login'
. ' --pin "' . $this->pin . '"'
. ' --list-objects';
return $this->runShellCommand($cmd, 'Failed to list objects in HSM');
}
/**
* Check if label keypair exists in HSM by listing objects.
*/
public function getkeypairIdByLabel($label)
{
$cmd = '"' . $this->pkcs11_tool . '"'
. ' --module "' . $this->module . '"'
. ' --slot ' . $this->slotId
. ' --login'
. ' --pin "' . $this->pin . '"'
. ' --list-objects'
. ' --label "'. $label . '"';
$keys = $this->runShellCommand($cmd, 'Failed to list objects in HSM');
$key_exists = str_contains($keys, $label);
if(!$key_exists){
$latest_id = $this->getNewKeypairId();
$this->generateKeypair($latest_id, $label);
}
return $this->getKeypairIdLatest();
}
/**
* Generate RSA keypair.
*/
public function getNewKeypairId()
{
$cmd = '"' . $this->pkcs11_tool . '"'
. ' --module "' . $this->module . '"'
. ' --slot ' . $this->slotId
. ' --login'
. ' --pin "' . $this->pin . '"'
. ' --list-objects';
$all_keys = $this->runShellCommand($cmd, 'Failed to list objects in HSM');
preg_match_all('/ID:\s*([0-9A-Fa-f]+)/', $all_keys, $matches);
$usedIds = collect($matches[1])->unique()->sort()->values();
$nextId = $usedIds->last() ? hexdec($usedIds->last()) + 1 : 1;
return dechex($nextId);
}
/**
* Generate RSA keypair.
*/
public function getKeypairIdLatest()
{
$cmd = '"' . $this->pkcs11_tool . '"'
. ' --module "' . $this->module . '"'
. ' --slot ' . $this->slotId
. ' --login'
. ' --pin "' . $this->pin . '"'
. ' --list-objects';
$all_keys = $this->runShellCommand($cmd, 'Failed to list objects in HSM');
preg_match_all('/ID:\s*([0-9A-Fa-f]+)/', $all_keys, $matches);
$usedIds = collect($matches[1])->unique()->sort()->values();
$nextId = hexdec($usedIds->last());
return dechex($nextId);
}
/**
* Generate RSA keypair.
*/
public function generateKeypair(string $id, string $label)
{
$cmd = '"' . $this->pkcs11_tool . '"'
. ' --module "' . $this->module . '"'
. ' --slot ' . $this->slotId
. ' --login'
. ' --pin "' . $this->pin . '"'
. ' --keypairgen'
. ' --key-type rsa:2048'
. ' --id ' . $id
. ' --label "' . $label . '"';
return $this->runShellCommand($cmd, 'Failed to generate RSA keypair');
}
/**
* Encrypt PDF file and AES key.
*/
public function encryptPkcs11($file)
{
$filename = $file->getClientOriginalName();
try {
// Generate AES key
$aesKey = openssl_random_pseudo_bytes(32);
if ($aesKey === false) {
throw new \Exception('Failed to generate AES key.');
}
Log::info('Generated AES 256 key', ['key' => base64_encode($aesKey)]);
// Generate IV
$ivLength = openssl_cipher_iv_length('aes-256-gcm');
if ($ivLength === false || $ivLength <= 0) {
throw new \Exception('Failed to get IV length.');
}
$iv = openssl_random_pseudo_bytes($ivLength);
if ($iv === false) {
throw new \Exception('Failed to generate IV.');
}
// Encrypt PDF
$pdfData = file_get_contents($file);
if ($pdfData === false) {
throw new \Exception('Failed to read PDF file.');
}
$tag = null;
$ciphertext = openssl_encrypt(
$pdfData,
'aes-256-gcm',
$aesKey,
OPENSSL_RAW_DATA,
$iv,
$tag
);
if ($ciphertext === false) {
throw new \Exception('AES encryption failed.');
}
// Save encrypted PDF
$storagePath = public_path("storage/uploads/encrypt");
if (!File::exists($storagePath)) {
File::makeDirectory($storagePath, 0777, true);
}
$encryptedPdf = $storagePath . '/' . Str::beforeLast($filename, '.') . '.aes';
if (file_put_contents($encryptedPdf, $ciphertext) === false) {
throw new \Exception("Failed to save encrypted PDF: $encryptedPdf");
}
Log::info('Encrypted PDF saved', ['path' => $encryptedPdf]);
// Encrypt AES Key with HSM (proc_open needed for piping)
$encryptedKey = $this->encryptAesKey($aesKey, $filename);
// 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));
unset($aesKey); // Zeroize key
return [
'status' => 'success',
'message' => 'File encrypted successfully',
'file' => $encryptedPdf,
'key' => $encryptedKey
];
} 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;
}
}
/**
* Encrypt AES key using HSM.
*/
protected function encryptAesKey(string $aesKey, string $filename)
{
$id_key = $this->getkeypairIdByLabel($this->keyLabel);
$storagePath = public_path("storage/uploads/encryptkey");
if (!File::exists($storagePath)) {
File::makeDirectory($storagePath, 0777, true);
}
$encryptedKeyPath = $storagePath . '/' . Str::beforeLast($filename, '.') . '.key.enc';
$cmd = '"' . $this->pkcs11_tool . '"'
. ' --module "' . $this->module . '"'
. ' --slot ' . $this->slotId
. ' --login'
. ' --pin "' . $this->pin . '"'
. ' --id ' . $id_key
. ' --encrypt'
. ' --mechanism RSA-PKCS-OAEP'
. ' --output-file "' . $encryptedKeyPath . '"';
$pipes = [];
$process = proc_open($cmd, [
0 => ["pipe", "r"], // stdin
1 => ["pipe", "w"], // stdout
2 => ["pipe", "w"], // stderr
], $pipes);
if (!is_resource($process)) {
throw new \RuntimeException("Failed to start pkcs11-tool process.");
}
fwrite($pipes[0], $aesKey);
fclose($pipes[0]);
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$returnCode = proc_close($process);
if ($returnCode !== 0) {
Log::error("Failed to encrypt AES key", [
'stdout' => $stdout,
'stderr' => $stderr,
'code' => $returnCode,
]);
throw new \RuntimeException("Failed to encrypt AES key: $stderr");
}
Log::info('AES key encrypted', ['path' => $encryptedKeyPath]);
return $encryptedKeyPath;
}
/**
* Decrypt encrypted PDF and AES key.
*/
public function decryptPkcs11($AttachmentFile)
{
$id_key = $this->getkeypairIdByLabel($this->keyLabel);
$filename = $AttachmentFile->nama_fail;
try {
// Decrypt AES Key first
$storagePath = public_path("storage/uploads/tempkey");
if (!File::exists($storagePath)) {
File::makeDirectory($storagePath, 0777, true);
}
$decryptedKeyFile = $storagePath . '/' . $filename . '.bin';
$cmd = '"' . $this->pkcs11_tool . '"'
. ' --module "' . $this->module . '"'
. ' --slot ' . $this->slotId
. ' --login'
. ' --pin "' . $this->pin . '"'
. ' --id ' . $id_key
. ' --decrypt'
. ' --mechanism RSA-PKCS-OAEP'
. ' --input-file "' . $AttachmentFile->dir_key . '"'
. ' --output-file "' . $decryptedKeyFile . '"';
// Use runShellCommand for consistent error handling
$this->runShellCommand($cmd, 'Failed to decrypt AES key');
$aesKey = file_get_contents($decryptedKeyFile);
if ($aesKey === false) {
throw new \Exception('Failed to read decrypted AES key.');
}
// Clean up decrypted key file for security
@unlink($decryptedKeyFile);
// Decrypt the PDF content
$encryptedPdf = file_get_contents($AttachmentFile->dir_file);
$iv = base64_decode($AttachmentFile->dir_iv);
$tag = base64_decode($AttachmentFile->dir_tag);
if ($encryptedPdf === false || $iv === false || $tag === false) {
throw new \Exception('Failed to load encrypted file, IV, or tag.');
}
$decryptedPdf = openssl_decrypt(
$encryptedPdf,
'aes-256-gcm',
$aesKey,
OPENSSL_RAW_DATA,
$iv,
$tag
);
if ($decryptedPdf === false) {
throw new \Exception('Failed to decrypt PDF content.');
}
// Save decrypted PDF
$decryptPath = public_path("storage/uploads/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.');
}
Log::info('PDF decrypted successfully', ['file' => $decryptedFile]);
Event::dispatch(new Logger("decrypt", $filename, 'success', 'File decrypted successfully', $decryptedFile));
return [
'status' => 'success',
'command' => 'Decryption File successful',
'file' => $decryptedFile,
];
} catch (\Throwable $e) {
Log::error('Decryption failed', [
'file' => $filename,
'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