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]; } } }