Skip to content

Instantly share code, notes, and snippets.

@alucard001
Created August 28, 2026 10:44
Show Gist options
  • Select an option

  • Save alucard001/845a691ddd12f4d2054e1267d15fbd14 to your computer and use it in GitHub Desktop.

Select an option

Save alucard001/845a691ddd12f4d2054e1267d15fbd14 to your computer and use it in GitHub Desktop.
SM4 encrypt/decrypt in Laravel. Create your own PDF and Image file when you do your own testing.
<?php
namespace App\Helpers;
use RuntimeException;
class Sm4
{
public static function encrypt(string $plainText): string
{
$cipher = static::cipher();
$key = static::key();
$iv = static::iv($cipher);
$encrypted = $iv === null
? openssl_encrypt($plainText, $cipher, $key, OPENSSL_RAW_DATA)
: openssl_encrypt($plainText, $cipher, $key, OPENSSL_RAW_DATA, $iv);
if ($encrypted === false) {
throw new RuntimeException(sprintf('Unable to encrypt the payload with cipher [%s].', $cipher));
}
return static::encode($encrypted);
}
public static function decrypt(string $payload): string
{
$cipher = static::cipher();
$key = static::key();
$iv = static::iv($cipher);
$binary = static::decode($payload);
$decrypted = $iv === null
? openssl_decrypt($binary, $cipher, $key, OPENSSL_RAW_DATA)
: openssl_decrypt($binary, $cipher, $key, OPENSSL_RAW_DATA, $iv);
if ($decrypted === false) {
throw new RuntimeException(sprintf('Unable to decrypt the payload with cipher [%s].', $cipher));
}
return $decrypted;
}
protected static function cipher(): string
{
$cipher = strtolower(trim((string) config('sm4.cipher', 'sm4-cbc')));
if ($cipher === '') {
throw new RuntimeException('The SM4 cipher must be configured before encrypting or decrypting payloads.');
}
$supportedCiphers = array_map('strtolower', openssl_get_cipher_methods());
if (! in_array($cipher, $supportedCiphers, true)) {
throw new RuntimeException(sprintf('SM4 cipher [%s] is not supported by the current OpenSSL build.', $cipher));
}
return $cipher;
}
protected static function key(): string
{
$key = (string) config('sm4.key', '');
if (strlen($key) !== 16) {
throw new RuntimeException('The SM4 key must be exactly 16 bytes.');
}
return $key;
}
protected static function iv(string $cipher): ?string
{
$ivLength = openssl_cipher_iv_length($cipher);
if ($ivLength <= 0) {
return null;
}
$iv = (string) config('sm4.iv', '');
if (strlen($iv) !== $ivLength) {
throw new RuntimeException(sprintf('The SM4 IV must be exactly %d bytes for cipher [%s].', $ivLength, $cipher));
}
return $iv;
}
protected static function encode(string $payload): string
{
return match (static::encoding()) {
'base64' => base64_encode($payload),
'hex' => strtolower(bin2hex($payload)),
default => throw new RuntimeException('Unsupported SM4 request encoding.'),
};
}
protected static function decode(string $payload): string
{
$payload = trim($payload);
if (static::encoding() === 'hex') {
if ($payload !== '' && preg_match('/\A[0-9a-fA-F]+\z/', $payload) === 1 && strlen($payload) % 2 === 0) {
$binary = hex2bin($payload);
if ($binary !== false) {
return $binary;
}
}
throw new RuntimeException('The SM4 payload is not valid hexadecimal content.');
}
$binary = base64_decode($payload, true);
if ($binary === false) {
throw new RuntimeException('The SM4 payload is not valid Base64 content.');
}
return $binary;
}
protected static function encoding(): string
{
$encoding = strtolower(trim((string) config('sm4.request_encoding', 'base64')));
if (! in_array($encoding, ['base64', 'hex'], true)) {
throw new RuntimeException(sprintf('Unsupported SM4 request encoding [%s].', $encoding));
}
return $encoding;
}
}
<?php
return [
'cipher' => env('SM4_CIPHER', 'sm4-cbc'),
// Enter your own SM4_KEY and IV
'key' => env('SM4_KEY', '0123456789abcdef'),
'iv' => env('SM4_IV', 'fedcba9876543210'),
'request_encoding' => env('SM4_REQUEST_ENCODING', 'base64'),
];
<?php
namespace Tests\Feature\Helpers;
use App\Helpers\Sm4;
use Tests\TestCase;
class Sm4Test extends TestCase
{
private function validConfig(): array
{
return [
'sm4.cipher' => 'sm4-cbc',
'sm4.key' => '0123456789abcdef',
'sm4.iv' => 'fedcba9876543210',
'sm4.request_encoding' => 'base64',
];
}
public function test_encrypt_and_decrypt_1mb_pdf_base64(): void
{
config($this->validConfig());
$fixture = base_path('tests/Fixtures/sample-1mb.pdf');
$this->assertFileExists($fixture);
$this->assertSame(1024 * 1024, filesize($fixture));
$base64 = base64_encode(file_get_contents($fixture));
$encrypted = Sm4::encrypt($base64);
$this->assertSame($base64, Sm4::decrypt($encrypted));
}
public function test_encrypt_and_decrypt_1mb_image_base64(): void
{
config($this->validConfig());
$fixture = base_path('tests/Fixtures/sample-1mb.png');
$this->assertFileExists($fixture);
$this->assertSame(1024 * 1024, filesize($fixture));
$base64 = base64_encode(file_get_contents($fixture));
$encrypted = Sm4::encrypt($base64);
$this->assertSame($base64, Sm4::decrypt($encrypted));
}
}
<?php
namespace Tests\Unit\Helpers;
use App\Helpers\Sm4;
use RuntimeException;
use Tests\TestCase;
class Sm4Test extends TestCase
{
private function validConfig(): array
{
return [
'sm4.cipher' => 'sm4-cbc',
'sm4.key' => '0123456789abcdef',
'sm4.iv' => 'fedcba9876543210',
'sm4.request_encoding' => 'base64',
];
}
public function test_encrypt_and_decrypt_roundtrip(): void
{
config($this->validConfig());
$plainText = 'hello sm4';
$encrypted = Sm4::encrypt($plainText);
$this->assertNotSame($plainText, $encrypted);
$this->assertSame($plainText, Sm4::decrypt($encrypted));
}
public function test_encrypt_returns_base64_ciphertext(): void
{
config($this->validConfig());
$encrypted = Sm4::encrypt('hello');
$this->assertNotFalse(base64_decode($encrypted, true));
}
public function test_encrypt_and_decrypt_support_hex_encoding(): void
{
config(array_merge($this->validConfig(), ['sm4.request_encoding' => 'hex']));
$plainText = 'hello hex';
$encrypted = Sm4::encrypt($plainText);
$this->assertMatchesRegularExpression('/\A[0-9a-f]+\z/', $encrypted);
$this->assertSame($plainText, Sm4::decrypt($encrypted));
}
public function test_encrypt_throws_when_key_is_not_16_bytes(): void
{
config(array_merge($this->validConfig(), ['sm4.key' => 'short']));
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('must be exactly 16 bytes');
Sm4::encrypt('hello');
}
public function test_encrypt_throws_when_iv_is_not_valid_length(): void
{
config(array_merge($this->validConfig(), ['sm4.iv' => 'short']));
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('must be exactly 16 bytes');
Sm4::encrypt('hello');
}
public function test_encrypt_throws_when_cipher_is_unsupported(): void
{
config(array_merge($this->validConfig(), ['sm4.cipher' => 'sm4-not-a-cipher']));
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('not supported');
Sm4::encrypt('hello');
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment