Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.50% covered (success)
97.50%
39 / 40
87.50% covered (warning)
87.50%
7 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
IdUtils
97.50% covered (success)
97.50%
39 / 40
87.50% covered (warning)
87.50%
7 / 8
12
0.00% covered (danger)
0.00%
0 / 1
 toExternalId
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 serializeId
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 encryptId
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 toInternalId
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 decryptId
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 deserializeId
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 crc16
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 fromEnv
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace Olz\Utils;
4
5class 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}