1+ <?php
2+
3+ namespace BaseFrame \Crypt \Crypter ;
4+
5+ /**
6+ * Шифровальщик, использующий CBC алгоритм. Вектор шифрования и ключ шифрования получаются
7+ * из pbkdf2 исходного ключа шифрования, что совместимо с консольным OpenSSL.
8+ */
9+ class OpenSSLDerivedCBC implements \BaseFrame \Crypt \Crypter {
10+
11+ /** @var bool флаг инициализации шифровальщика */
12+ protected bool $ _is_initialized = false ;
13+ protected string $ _algo = \BaseFrame \Crypt \Crypter \OpenSSL \Algo::AES256CBC ->value ;
14+
15+ /**
16+ * Класс шифрования данных с использование OpenSSL.
17+ */
18+ protected function __construct (
19+ protected ?string $ _key ,
20+ protected int $ _options ,
21+ protected mixed $ _init_fn ,
22+ ) {
23+
24+ // не делаем никаких проверок, если есть функция инициализации
25+ if (!is_null ($ _init_fn )) {
26+ return ;
27+ }
28+
29+ // проверяем, что указанная длина ключа подходит для шифрования выбранным алгоритмом,
30+ // чтобы не было ситуаций, когда в php недостающие байты нулями заполнились, а в go все сломалось
31+ if (is_null ($ _key ) || strlen ($ _key ) !== openssl_cipher_key_length ($ this ->_algo )) {
32+ throw new \RuntimeException ("passed incorrect key " );
33+ }
34+
35+ // поскольку функция инициализации не передана,
36+ // то шифровальщик считаем проинициализированным
37+ $ this ->_is_initialized = true ;
38+ }
39+
40+ /**
41+ * Создает экземпляр класса шифрования.
42+ */
43+ public static function instance (?string $ key , int $ options = OPENSSL_RAW_DATA | OPENSSL_DONT_ZERO_PAD_KEY , ?\Closure $ init_fn = null ):static {
44+
45+ return new static ($ key , $ options , $ init_fn );
46+ }
47+
48+ /**
49+ * Функция инициализации, чтобы не дергать по 10 раз какую-то сложную логику
50+ */
51+ public function init ():static {
52+
53+ if ($ this ->_is_initialized ) {
54+ return $ this ;
55+ }
56+
57+ $ this ->_is_initialized = true ;
58+
59+ if (!is_null ($ this ->_init_fn )) {
60+ $ this ->_init_fn ->call ($ this );
61+ }
62+
63+ // проверим, что после инициализации ключ у нас не пустой,
64+ // поскольку изначально мы допускаем возможность передать
65+ // пустой ключ при наличии функции инициализации
66+ if (is_null ($ this ->_key ) || strlen ($ this ->_key ) !== openssl_cipher_key_length ($ this ->_algo )) {
67+ throw new \RuntimeException ("key is incorrect after initialization " );
68+ }
69+
70+ return $ this ;
71+ }
72+
73+ /**
74+ * Зашифровывает данные. Работает аналогично команде:
75+ * echo <in> | openssl aes-256-cbc -pbkdf2 -k <key> -out <out>
76+ */
77+ public function encrypt (string $ raw ):string {
78+
79+ if (!$ this ->_is_initialized ) {
80+ throw new \RuntimeException ("cryptor is not initialized, call init before use " );
81+ }
82+
83+ // для CBC размер блока и длина вектора инициализации
84+ // совпадают, для остальных, увы, нужно считать
85+ $ block_length = openssl_cipher_iv_length ($ this ->_algo );
86+
87+ // первые 8 байт — просто байты, будем вставлять строку Salted__
88+ $ salt = random_bytes ($ block_length - strlen ("Salted__ " ));
89+ $ pb = openssl_pbkdf2 ($ this ->_key , $ salt , 48 , 10000 , "sha256 " );
90+
91+ // получаем ключ, которым будем шифровать и вектор инициализации,
92+ // вот так сложно все, но это так работает openssl через терминал
93+ $ pass = substr ($ pb , 0 , 32 );
94+ $ iv = substr ($ pb , 32 , 16 );
95+
96+ // соль добавляем в результат шифрования в качестве префикса
97+ $ result = "Salted__ " . $ salt . openssl_encrypt ($ raw , $ this ->_algo , $ pass , $ this ->_options , $ iv );
98+
99+ if ($ result === false ) {
100+ throw new \RuntimeException ("encryption failed " );
101+ }
102+
103+ // важно, что эта штука вернет байты, а не строку, смотри не сломай ничего
104+ return $ result ;
105+ }
106+
107+ /**
108+ * Расшифровывает данные. Работает аналогично команде:
109+ * openssl aes-256-cbc -pbkdf2 -d -k <key> -in <in> -out <out>
110+ */
111+ public function decrypt (string $ encrypted ):string {
112+
113+ if (!$ this ->_is_initialized ) {
114+ throw new \RuntimeException ("cryptor is not initialized, call init before use " );
115+ }
116+
117+ // первые 8 байт — просто байты, вторые 8 байт — соль
118+ // остальное _ зашифрованные данные
119+ $ salt = substr ($ encrypted , 8 , 8 );
120+ $ encrypted = substr ($ encrypted , 16 );
121+
122+ // есть ключ, есть соль, повторяем логику как при шифровании
123+ $ pb = openssl_pbkdf2 ($ this ->_key , $ salt , 48 , 10000 , "sha256 " );
124+ $ pass = substr ($ pb , 0 , 32 );
125+ $ iv = substr ($ pb , 32 , 16 );
126+
127+ $ result = openssl_decrypt ($ encrypted , $ this ->_algo , $ pass , $ this ->_options , $ iv );
128+
129+ if ($ result === false ) {
130+ throw new \RuntimeException ("decryption failed " );
131+ }
132+
133+ return $ result ;
134+ }
135+ }
0 commit comments