Skip to content

Commit 020304f

Browse files
Merge pull request #5 from getCompassUtils/release_v4.4.3
Release v4.4.3
2 parents c9743c4 + 0fc2887 commit 020304f

File tree

20 files changed

+1432
-19
lines changed

20 files changed

+1432
-19
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.idea
22
composer.phar
33
composer.lock
4-
vendor
4+
vendor
5+
/dev/

src/Crypt/Crypter.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace BaseFrame\Crypt;
4+
5+
/**
6+
* Класс шифрования данных.
7+
* Пришлось сделать еще один класс, потому что дефолтный не поддерживает случайный iv.
8+
*/
9+
interface Crypter {
10+
11+
/**
12+
* Функция инициализации, чтобы не дергать по 10 раз
13+
*/
14+
public function init():static;
15+
16+
/**
17+
* Зашифровывает данные. Можно передать вектор инициализации,
18+
* но лучше использовать случайный, создающийся если его не передать.
19+
*/
20+
public function encrypt(string $raw):string;
21+
22+
/**
23+
* Расшифровывает данные. Важно — может расшифровать только то, что сам зашифровал —
24+
* требует наличия инициализирующего вектора перед зашифрованным значением.
25+
*/
26+
public function decrypt(string $encrypted):string;
27+
}

src/Crypt/Crypter/OpenSSL.php

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

src/Crypt/Crypter/OpenSSL/Algo.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace BaseFrame\Crypt\Crypter\OpenSSL;
4+
5+
/**
6+
* Поддерживаемые OpenSSL алгоритмы шифрования.
7+
*/
8+
enum Algo : string {
9+
10+
case AES256CBC = "AES-256-CBC";
11+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
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+
}

src/Crypt/CrypterProvider.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
namespace BaseFrame\Crypt;
4+
5+
/**
6+
* Класс-хранилище для шифровальщиков..
7+
*/
8+
class CrypterProvider {
9+
10+
/** @var \BaseFrame\Crypt\Crypter[] хранилище шифровальщиков */
11+
protected static array $_store = [];
12+
13+
/**
14+
* Добавляет новый шифровальщик в хранилище.
15+
*/
16+
public static function add(string $uniq_key, Crypter $crypter):void {
17+
18+
if (isset(static::$_store[$uniq_key])) {
19+
throw new \RuntimeException("passed key $uniq_key already defined");
20+
}
21+
22+
static::$_store[$uniq_key] = $crypter;
23+
}
24+
25+
/**
26+
* Достает из хранилища шифровальщик по ключу.
27+
*/
28+
public static function get(string $uniq_key):Crypter {
29+
30+
if (!isset(static::$_store[$uniq_key])) {
31+
throw new \RuntimeException("crypter with uniq $uniq_key is not defined");
32+
}
33+
34+
return static::$_store[$uniq_key]->init();
35+
}
36+
}

0 commit comments

Comments
 (0)