Created
August 28, 2026 10:44
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?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; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?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'), | |
| ]; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?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)); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?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