212 lines
6.1 KiB
PHP
212 lines
6.1 KiB
PHP
<?php
|
|
|
|
namespace App\Support\LegacyCrypto;
|
|
|
|
use RuntimeException;
|
|
|
|
class CiSeedCrypto
|
|
{
|
|
private array $userKeyBytes; // int[16] signed bytes
|
|
private array $iv; // int[16]
|
|
private string $serverEncoding;
|
|
private string $innerEncoding;
|
|
private int $block;
|
|
|
|
public function __construct(string $userKey, array $iv)
|
|
{
|
|
$this->serverEncoding = config('legacy.server_encoding', 'UTF-8');
|
|
$this->innerEncoding = config('legacy.inner_encoding', 'UTF-8');
|
|
$this->block = (int) config('legacy.block', 16);
|
|
|
|
if (count($iv) !== 16) {
|
|
throw new RuntimeException('legacy.iv must be 16 bytes');
|
|
}
|
|
$this->iv = array_values($iv);
|
|
|
|
// 핵심: CI SeedRoundKey는 pbUserKey[0..15]만 사용 → 문자열 키의 "앞 16바이트"만 의미 있음
|
|
$this->userKeyBytes = $this->stringKeyToSigned16Bytes($userKey);
|
|
}
|
|
|
|
public function encrypt(string $plain): string
|
|
{
|
|
$plain = $this->convertEncoding($plain);
|
|
if ($plain === '') return $plain;
|
|
|
|
$planBytes = $this->unpackSignedBytes($plain);
|
|
|
|
$seed = new Seed();
|
|
$pdwRoundKey = null;
|
|
$seed->SeedRoundKey($pdwRoundKey, $this->userKeyBytes);
|
|
|
|
$planLen = count($planBytes);
|
|
$start = 0;
|
|
$end = 0;
|
|
|
|
$cbcBlock = [];
|
|
$this->arrayCopy($this->iv, 0, $cbcBlock, 0, $this->block);
|
|
|
|
$ret = '';
|
|
|
|
while ($end < $planLen) {
|
|
$end = $start + $this->block;
|
|
if ($end > $planLen) $end = $planLen;
|
|
|
|
$cipherBlock = [];
|
|
$this->arrayCopy($planBytes, $start, $cipherBlock, 0, $end - $start);
|
|
|
|
// PKCS#5 padding
|
|
$nPad = $this->block - ($end - $start);
|
|
for ($i = ($end - $start); $i < $this->block; $i++) {
|
|
$cipherBlock[$i] = $nPad;
|
|
}
|
|
|
|
// CBC XOR
|
|
$this->xor16($cipherBlock, $cbcBlock, $cipherBlock);
|
|
|
|
// Encrypt
|
|
$encBlock = null;
|
|
$seed->SeedEncrypt($cipherBlock, $pdwRoundKey, $encBlock);
|
|
|
|
// CBC 갱신
|
|
$this->arrayCopy($encBlock, 0, $cbcBlock, 0, $this->block);
|
|
|
|
foreach ($encBlock as $b) {
|
|
$ret .= bin2hex(chr($b & 0xFF));
|
|
}
|
|
|
|
$start = $end;
|
|
}
|
|
|
|
return $ret;
|
|
}
|
|
|
|
public function decrypt(string $cipherHex): string
|
|
{
|
|
$cipherHex = trim($cipherHex);
|
|
if ($cipherHex === '') return '';
|
|
|
|
if (strlen($cipherHex) % 32 !== 0) {
|
|
throw new RuntimeException('Invalid cipher hex length (must be multiple of 32)');
|
|
}
|
|
|
|
$seed = new Seed();
|
|
$pdwRoundKey = null;
|
|
$seed->SeedRoundKey($pdwRoundKey, $this->userKeyBytes);
|
|
|
|
$cbcBlock = [];
|
|
$this->arrayCopy($this->iv, 0, $cbcBlock, 0, $this->block);
|
|
|
|
$plainBytes = [];
|
|
$blocks = (int)(strlen($cipherHex) / 32);
|
|
|
|
for ($bi = 0; $bi < $blocks; $bi++) {
|
|
$hexBlock = substr($cipherHex, $bi * 32, 32);
|
|
$cipherBlock = $this->hexToBytesSigned($hexBlock); // signed 16 bytes
|
|
|
|
$decBlock = null;
|
|
$seed->SeedDecrypt($cipherBlock, $pdwRoundKey, $decBlock);
|
|
|
|
$plainBlock = [];
|
|
$this->xor16($decBlock, $cbcBlock, $plainBlock);
|
|
|
|
// CBC 갱신
|
|
$this->arrayCopy($cipherBlock, 0, $cbcBlock, 0, $this->block);
|
|
|
|
foreach ($plainBlock as $b) {
|
|
$plainBytes[] = $b;
|
|
}
|
|
}
|
|
|
|
$plainBytes = $this->pkcs5Unpad($plainBytes);
|
|
$plain = $this->packSignedBytes($plainBytes);
|
|
|
|
return $this->convertEncodingBack($plain);
|
|
}
|
|
|
|
/* -------------------- KEY NORMALIZE (핵심) -------------------- */
|
|
|
|
private function stringKeyToSigned16Bytes(string $key): array
|
|
{
|
|
// 레거시(운영 CI)에서 "문자열을 배열처럼 접근 + & 연산"하던 동작을 최대한 재현
|
|
// 문자 1개를 (int)로 캐스팅: 숫자면 0~9, 숫자 아니면 0
|
|
$bytes = [];
|
|
|
|
for ($i = 0; $i < 16; $i++) {
|
|
$ch = $key[$i] ?? "\0"; // 1-char string
|
|
$v = (int) $ch; // 핵심: ord()가 아니라 int 캐스팅
|
|
// signed byte 범위 맞추기(사실 0~9라 필요없지만 안전)
|
|
if ($v > 127) $v -= 256;
|
|
$bytes[] = $v;
|
|
}
|
|
|
|
return $bytes;
|
|
}
|
|
|
|
/* -------------------- UTIL -------------------- */
|
|
|
|
private function convertEncoding(string $s): string
|
|
{
|
|
$out = @iconv($this->serverEncoding, $this->innerEncoding, $s);
|
|
return $out === false ? $s : $out;
|
|
}
|
|
|
|
private function convertEncodingBack(string $s): string
|
|
{
|
|
$out = @iconv($this->innerEncoding, $this->serverEncoding, $s);
|
|
return $out === false ? $s : $out;
|
|
}
|
|
|
|
private function unpackSignedBytes(string $s): array
|
|
{
|
|
return array_values(unpack('c*', $s));
|
|
}
|
|
|
|
private function packSignedBytes(array $bytes): string
|
|
{
|
|
$out = '';
|
|
foreach ($bytes as $b) {
|
|
$out .= chr($b & 0xFF);
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
private function hexToBytesSigned(string $hexBlock): array
|
|
{
|
|
$bin = hex2bin($hexBlock);
|
|
if ($bin === false || strlen($bin) !== 16) {
|
|
throw new RuntimeException('Invalid hex block');
|
|
}
|
|
return $this->unpackSignedBytes($bin);
|
|
}
|
|
|
|
private function pkcs5Unpad(array $bytes): array
|
|
{
|
|
$n = count($bytes);
|
|
if ($n === 0) return $bytes;
|
|
|
|
$pad = $bytes[$n - 1] & 0xFF;
|
|
if ($pad < 1 || $pad > 16) return $bytes;
|
|
|
|
for ($i = 0; $i < $pad; $i++) {
|
|
if (($bytes[$n - 1 - $i] & 0xFF) !== $pad) {
|
|
return $bytes;
|
|
}
|
|
}
|
|
return array_slice($bytes, 0, $n - $pad);
|
|
}
|
|
|
|
private function xor16(array $a, array $b, array &$out): void
|
|
{
|
|
for ($i = 0; $i < 16; $i++) {
|
|
$out[$i] = ($a[$i] ^ $b[$i]);
|
|
}
|
|
}
|
|
|
|
private function arrayCopy(array $src, int $srcPos, array &$dst, int $dstPos, int $length): void
|
|
{
|
|
for ($i = 0; $i < $length; $i++) {
|
|
$dst[$dstPos + $i] = $src[$srcPos + $i];
|
|
}
|
|
}
|
|
}
|