1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* Copyright © 2017 Toan Nguyen. All rights reserved. |
4
|
|
|
* |
5
|
|
|
* For the full copyright and license information, please view the LICENSE |
6
|
|
|
* file that was distributed with this source code. |
7
|
|
|
*/ |
8
|
|
|
|
9
|
|
|
namespace Gojira\Framework\Encryption; |
10
|
|
|
|
11
|
|
|
use Gojira\Framework\App\Configuration\Configuration; |
12
|
|
|
use Gojira\Framework\Math\Random; |
13
|
|
|
|
14
|
|
|
/** |
15
|
|
|
* Class Encryptor provides basic logic for hashing strings and encrypting/decrypting misc data |
16
|
|
|
* |
17
|
|
|
* @package Gojira\Framework\Encryption |
18
|
|
|
* @author Toan Nguyen <[email protected]> |
19
|
|
|
*/ |
20
|
|
|
class Encryptor implements EncryptorInterface |
21
|
|
|
{ |
22
|
|
|
/** |
23
|
|
|
* Array key of encryption key in deployment config |
24
|
|
|
*/ |
25
|
|
|
const PARAM_CRYPT_KEY = 'options/encryption_key'; |
26
|
|
|
|
27
|
|
|
/**#@+ |
28
|
|
|
* Cipher versions |
29
|
|
|
*/ |
30
|
|
|
const CIPHER_BLOWFISH = 0; |
31
|
|
|
|
32
|
|
|
const CIPHER_RIJNDAEL_128 = 1; |
33
|
|
|
|
34
|
|
|
const CIPHER_RIJNDAEL_256 = 2; |
35
|
|
|
|
36
|
|
|
const CIPHER_LATEST = 2; |
37
|
|
|
/**#@-*/ |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* Indicate cipher |
41
|
|
|
* |
42
|
|
|
* @var int |
43
|
|
|
*/ |
44
|
|
|
protected $cipher = self::CIPHER_LATEST; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* Version of encryption key |
48
|
|
|
* |
49
|
|
|
* @var int |
50
|
|
|
*/ |
51
|
|
|
protected $keyVersion; |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* Array of encryption keys |
55
|
|
|
* |
56
|
|
|
* @var string[] |
57
|
|
|
*/ |
58
|
|
|
protected $keys = []; |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* Random generator |
62
|
|
|
* |
63
|
|
|
* @var Random |
64
|
|
|
*/ |
65
|
|
|
protected $random; |
66
|
|
|
|
67
|
|
|
/** |
68
|
|
|
* @param Random $random |
69
|
|
|
* @param Configuration $configuration |
70
|
|
|
*/ |
71
|
|
|
public function __construct( |
72
|
|
|
Random $random, |
73
|
|
|
Configuration $configuration |
74
|
|
|
) { |
75
|
|
|
$this->random = $random; |
76
|
|
|
|
77
|
|
|
// load all possible keys |
78
|
|
|
$this->keys = preg_split('/\s+/s', trim($configuration->getData(self::PARAM_CRYPT_KEY))); |
79
|
|
|
$this->keyVersion = count($this->keys) - 1; |
80
|
|
|
} |
81
|
|
|
|
82
|
|
|
/** |
83
|
|
|
* Check whether specified cipher version is supported |
84
|
|
|
* |
85
|
|
|
* Returns matched supported version or throws exception |
86
|
|
|
* |
87
|
|
|
* @param int $version |
88
|
|
|
* |
89
|
|
|
* @return int |
90
|
|
|
* @throws \Exception |
91
|
|
|
*/ |
92
|
|
|
public function validateCipher($version) |
93
|
|
|
{ |
94
|
|
|
$types = [self::CIPHER_BLOWFISH, self::CIPHER_RIJNDAEL_128, self::CIPHER_RIJNDAEL_256]; |
95
|
|
|
|
96
|
|
|
$version = (int)$version; |
97
|
|
|
if (!in_array($version, $types, true)) { |
98
|
|
|
throw new \Exception((string)new \Gojira\Framework\Phrase('Not supported cipher version')); |
99
|
|
|
} |
100
|
|
|
return $version; |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
/** |
104
|
|
|
* Prepend key and cipher versions to encrypted data after encrypting |
105
|
|
|
* |
106
|
|
|
* @param string $data |
107
|
|
|
* |
108
|
|
|
* @return string |
109
|
|
|
*/ |
110
|
|
|
public function encrypt($data) |
111
|
|
|
{ |
112
|
|
|
$crypt = $this->getCrypt(); |
113
|
|
|
if (null === $crypt) { |
114
|
|
|
return $data; |
115
|
|
|
} |
116
|
|
|
return $this->keyVersion . ':' . $this->cipher . ':' . (MCRYPT_MODE_CBC === |
117
|
|
|
$crypt->getMode() ? $crypt->getInitVector() . ':' : '') . base64_encode( |
118
|
|
|
$crypt->encrypt((string)$data) |
119
|
|
|
); |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
/** |
123
|
|
|
* Look for key and crypt versions in encrypted data before decrypting |
124
|
|
|
* |
125
|
|
|
* Unsupported/unspecified key version silently fallback to the oldest we have |
126
|
|
|
* Unsupported cipher versions eventually throw exception |
127
|
|
|
* Unspecified cipher version fallback to the oldest we support |
128
|
|
|
* |
129
|
|
|
* @param string $data |
130
|
|
|
* |
131
|
|
|
* @return string |
132
|
|
|
*/ |
133
|
|
|
public function decrypt($data) |
134
|
|
|
{ |
135
|
|
|
if ($data) { |
136
|
|
|
$parts = explode(':', $data, 4); |
137
|
|
|
$partsCount = count($parts); |
138
|
|
|
|
139
|
|
|
$initVector = false; |
140
|
|
|
// specified key, specified crypt, specified iv |
141
|
|
|
if (4 === $partsCount) { |
142
|
|
|
list($keyVersion, $cryptVersion, $iv, $data) = $parts; |
|
|
|
|
143
|
|
|
$initVector = $iv ? $iv : false; |
144
|
|
|
$keyVersion = (int)$keyVersion; |
145
|
|
|
$cryptVersion = self::CIPHER_RIJNDAEL_256; |
146
|
|
|
// specified key, specified crypt |
147
|
|
|
} elseif (3 === $partsCount) { |
148
|
|
|
list($keyVersion, $cryptVersion, $data) = $parts; |
149
|
|
|
$keyVersion = (int)$keyVersion; |
150
|
|
|
$cryptVersion = (int)$cryptVersion; |
151
|
|
|
// no key version = oldest key, specified crypt |
152
|
|
|
} elseif (2 === $partsCount) { |
153
|
|
|
list($cryptVersion, $data) = $parts; |
154
|
|
|
$keyVersion = 0; |
155
|
|
|
$cryptVersion = (int)$cryptVersion; |
156
|
|
|
// no key version = oldest key, no crypt version = oldest crypt |
157
|
|
|
} elseif (1 === $partsCount) { |
158
|
|
|
$keyVersion = 0; |
159
|
|
|
$cryptVersion = self::CIPHER_BLOWFISH; |
160
|
|
|
// not supported format |
161
|
|
|
} else { |
162
|
|
|
return ''; |
163
|
|
|
} |
164
|
|
|
// no key for decryption |
165
|
|
|
if (!isset($this->keys[$keyVersion])) { |
166
|
|
|
return ''; |
167
|
|
|
} |
168
|
|
|
$crypt = $this->getCrypt($this->keys[$keyVersion], $cryptVersion, $initVector); |
169
|
|
|
if (null === $crypt) { |
170
|
|
|
return ''; |
171
|
|
|
} |
172
|
|
|
return trim($crypt->decrypt(base64_decode((string)$data))); |
173
|
|
|
} |
174
|
|
|
return ''; |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
/** |
178
|
|
|
* Return crypt model, instantiate if it is empty |
179
|
|
|
* |
180
|
|
|
* @param string|null $key NULL value means usage of the default key specified on constructor |
181
|
|
|
* |
182
|
|
|
* @return \Gojira\Framework\Encryption\Crypt |
183
|
|
|
* @throws \Exception |
184
|
|
|
*/ |
185
|
|
|
public function validateKey($key) |
186
|
|
|
{ |
187
|
|
|
if (preg_match('/\s/s', $key)) { |
188
|
|
|
throw new \Exception((string)new \Gojira\Framework\Phrase('The encryption key format is invalid.')); |
189
|
|
|
} |
190
|
|
|
return $this->getCrypt($key); |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
/** |
194
|
|
|
* Attempt to append new key & version |
195
|
|
|
* |
196
|
|
|
* @param string $key |
197
|
|
|
* |
198
|
|
|
* @return $this |
199
|
|
|
*/ |
200
|
|
|
public function setNewKey($key) |
201
|
|
|
{ |
202
|
|
|
$this->validateKey($key); |
203
|
|
|
$this->keys[] = $key; |
204
|
|
|
$this->keyVersion += 1; |
205
|
|
|
return $this; |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
/** |
209
|
|
|
* Export current keys as string |
210
|
|
|
* |
211
|
|
|
* @return string |
212
|
|
|
*/ |
213
|
|
|
public function exportKeys() |
214
|
|
|
{ |
215
|
|
|
return implode("\n", $this->keys); |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
/** |
219
|
|
|
* Initialize crypt module if needed |
220
|
|
|
* |
221
|
|
|
* By default initializes with latest key and crypt versions |
222
|
|
|
* |
223
|
|
|
* @param string $key |
224
|
|
|
* @param int $cipherVersion |
225
|
|
|
* @param bool $initVector |
226
|
|
|
* |
227
|
|
|
* @return Crypt|null |
228
|
|
|
*/ |
229
|
|
|
protected function getCrypt($key = null, $cipherVersion = null, $initVector = true) |
230
|
|
|
{ |
231
|
|
|
if (null === $key && null === $cipherVersion) { |
232
|
|
|
$cipherVersion = self::CIPHER_RIJNDAEL_256; |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
if (null === $key) { |
236
|
|
|
$key = $this->keys[$this->keyVersion]; |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
if (!$key) { |
240
|
|
|
return null; |
241
|
|
|
} |
242
|
|
|
|
243
|
|
|
if (null === $cipherVersion) { |
244
|
|
|
$cipherVersion = $this->cipher; |
245
|
|
|
} |
246
|
|
|
$cipherVersion = $this->validateCipher($cipherVersion); |
247
|
|
|
|
248
|
|
|
if ($cipherVersion === self::CIPHER_RIJNDAEL_128) { |
249
|
|
|
$cipher = MCRYPT_RIJNDAEL_128; |
250
|
|
|
$mode = MCRYPT_MODE_ECB; |
251
|
|
|
} elseif ($cipherVersion === self::CIPHER_RIJNDAEL_256) { |
252
|
|
|
$cipher = MCRYPT_RIJNDAEL_256; |
253
|
|
|
$mode = MCRYPT_MODE_CBC; |
254
|
|
|
} else { |
255
|
|
|
$cipher = MCRYPT_BLOWFISH; |
256
|
|
|
$mode = MCRYPT_MODE_ECB; |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
return new Crypt($key, $cipher, $mode, $initVector); |
260
|
|
|
} |
261
|
|
|
} |
262
|
|
|
|
This checks looks for assignemnts to variables using the
list(...)
function, where not all assigned variables are subsequently used.Consider the following code example.
Only the variables
$a
and$c
are used. There was no need to assign$b
.Instead, the list call could have been.