Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 40 |
|
0.00% |
0 / 8 |
CRAP | |
0.00% |
0 / 1 |
IdUtils | |
0.00% |
0 / 40 |
|
0.00% |
0 / 8 |
156 | |
0.00% |
0 / 1 |
toExternalId | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
serializeId | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
encryptId | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
toInternalId | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
decryptId | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
deserializeId | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
crc16 | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
fromEnv | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace Olz\Utils; |
4 | |
5 | class IdUtils { |
6 | use WithUtilsTrait; |
7 | |
8 | protected string $base64Iv = '9V0IXtcQo5o='; |
9 | protected string $algo = 'des-ede-cbc'; // Find one using `composer get_id_algos` |
10 | |
11 | public function toExternalId(int $internal_id, string $type = ''): string { |
12 | $serialized_id = $this->serializeId($internal_id, $type); |
13 | return $this->encryptId($serialized_id); |
14 | } |
15 | |
16 | protected function serializeId(int $internal_id, string $type): string { |
17 | if ($internal_id < 0) { |
18 | throw new \Exception("Internal ID must be positive"); |
19 | } |
20 | $type_hash_hex = str_pad(dechex($this->crc16($type)), 4, '0', STR_PAD_LEFT); |
21 | $id_hex = str_pad(dechex($internal_id), 10, '0', STR_PAD_LEFT); |
22 | if (strlen($id_hex) > 10) { |
23 | throw new \Exception("Internal ID must be at most 40 bits"); |
24 | } |
25 | $type_id_hex = "{$type_hash_hex}{$id_hex}"; |
26 | $type_id_bin = hex2bin($type_id_hex); |
27 | $this->generalUtils()->checkNotFalse($type_id_bin, "hex2bin({$type_id_hex}) failed"); |
28 | return $type_id_bin; |
29 | } |
30 | |
31 | protected function encryptId(string $serialized_id): string { |
32 | $plaintext = $serialized_id; |
33 | $key = $this->envUtils()->getIdEncryptionKey(); |
34 | $iv = base64_decode($this->base64Iv); |
35 | $ciphertext = @openssl_encrypt($plaintext, $this->algo, $key, OPENSSL_RAW_DATA, $iv, $tag); |
36 | $this->generalUtils()->checkNotFalse($ciphertext, fn () => "Could not encrypt ID: ".openssl_error_string()); |
37 | return $this->generalUtils()->base64EncodeUrl($ciphertext); |
38 | } |
39 | |
40 | public function toInternalId(string $external_id, string $type = ''): int { |
41 | $serialized_id = $this->decryptId($external_id); |
42 | $this->generalUtils()->checkNotNull($serialized_id, "ID decryption failed: {$external_id}"); |
43 | return $this->deserializeId($serialized_id, $type); |
44 | } |
45 | |
46 | protected function decryptId(string $encrypted_id): ?string { |
47 | $ciphertext = $this->generalUtils()->base64DecodeUrl($encrypted_id); |
48 | $key = $this->envUtils()->getIdEncryptionKey(); |
49 | $iv = base64_decode($this->base64Iv); |
50 | $plaintext = openssl_decrypt($ciphertext, $this->algo, $key, OPENSSL_RAW_DATA, $iv); |
51 | $this->generalUtils()->checkNotFalse($plaintext, "Could not decrypt ID: {$encrypted_id}"); |
52 | return $plaintext; |
53 | } |
54 | |
55 | protected function deserializeId(string $serialized_id, string $type): int { |
56 | $expected_type_hash_hex = str_pad(dechex($this->crc16($type)), 4, '0', STR_PAD_LEFT); |
57 | $serialized_id_hex = bin2hex($serialized_id); |
58 | $actual_type_hash_hex = substr($serialized_id_hex, 0, 4); |
59 | if ($actual_type_hash_hex !== $expected_type_hash_hex) { |
60 | throw new \Exception("Invalid serialized ID: Type mismatch {$actual_type_hash_hex} vs. {$expected_type_hash_hex}"); |
61 | } |
62 | return intval(hexdec(substr($serialized_id_hex, 4))); |
63 | } |
64 | |
65 | protected function crc16(string $data): int { |
66 | $crc = 0xFFFF; |
67 | for ($i = 0; $i < strlen($data); $i++) { |
68 | $x = (($crc >> 8) ^ ord($data[$i])) & 0xFF; |
69 | $x ^= $x >> 4; |
70 | $crc = (($crc << 8) ^ ($x << 12) ^ ($x << 5) ^ $x) & 0xFFFF; |
71 | } |
72 | return $crc; |
73 | } |
74 | |
75 | public static function fromEnv(): self { |
76 | return new self(); |
77 | } |
78 | } |