Encryption::getKey()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 2
rs 10
1
<?php
2
namespace Ubiquity\security\data;
3
4
use Ubiquity\exceptions\EncryptException;
5
use Ubiquity\exceptions\DecryptException;
6
use Ubiquity\exceptions\EncryptionKeyException;
7
8
/**
9
 * Ubiquity\security\data$Encryption
10
 * This class is part of Ubiquity
11
 * Inspired from illuminate/encryption package
12
 *
13
 * @author jc
14
 * @version 1.0.0
15
 *
16
 *
17
 */
18
class Encryption {
19
20
	const AES128 = 'AES-128-CBC';
21
22
	const AES192 = 'AES-192-CBC';
23
24
	const AES256 = 'AES-256-CBC';
25
26
	private static $acceptedCiphers = [
27
		32 => self::AES128,
28
		48 => self::AES192,
29
		64 => self::AES256
30
	];
31
32
	/**
33
	 * The encryption key.
34
	 *
35
	 * @var string
36
	 */
37
	protected $key;
38
39
	/**
40
	 * The algorithm used for encryption.
41
	 *
42
	 * @var string
43
	 */
44
	protected $cipher;
45
46
	/**
47
	 * Create a new encrypter instance.
48
	 *
49
	 * @param string $key
50
	 * @param string $cipher
51
	 * @return void
52
	 *
53
	 * @throws \RuntimeException
54
	 */
55
	public function __construct(?string $key = null, ?string $cipher = null) {
56
		$this->key = $key;
57
		$this->cipher = $cipher;
58
	}
59
60
	public function initializeKeyAndCipher() {
61
		if (isset($this->key) && ! isset($this->cipher)) {
62
			$this->cipher = self::getCipherFromKey($this->key);
63
		} elseif (! isset($this->key)) {
64
			$this->cipher ??= self::AES128;
65
			$this->key = self::generateKey($this->cipher);
66
		}
67
		if (! self::isValidKey($this->key, $this->cipher)) {
68
			throw new EncryptionKeyException("The encryption key size is not valid for {$this->cipher}.");
69
		}
70
	}
71
72
	public static function getCipherFromKey(string $key) {
73
		$size = \strlen($key);
74
		if (isset(self::$acceptedCiphers[$size])) {
75
			return self::$acceptedCiphers[$size];
76
		}
77
		throw new EncryptionKeyException("The encryption key has not a valid size ({$size})");
78
	}
79
80
	/**
81
	 * Create a MAC for the given value.
82
	 *
83
	 * @param string $iv
84
	 * @param mixed $value
85
	 * @return string
86
	 */
87
	protected function hash($iv, $value): string {
88
		return \hash_hmac('sha256', $iv . $value, $this->key);
89
	}
90
91
	/**
92
	 * Get the JSON array from the given payload.
93
	 *
94
	 * @param string $payload
95
	 * @return array
96
	 */
97
	protected function getJsonPayload($payload) {
98
		$payload = \json_decode(\base64_decode($payload), true);
99
		if (! $this->isValidPayload($payload)) {
100
			throw new DecryptException('The payload is invalid.');
101
		}
102
		if (! $this->isValidMac($payload)) {
103
			throw new DecryptException('The MAC control is invalid.');
104
		}
105
106
		return $payload;
107
	}
108
109
	/**
110
	 * Check that the encryption payload is valid.
111
	 *
112
	 * @param mixed $payload
113
	 * @return bool
114
	 */
115
	protected function isValidPayload($payload) {
116
		return \is_array($payload) && isset($payload['iv'], $payload['value'], $payload['mac']) && \strlen(\base64_decode($payload['iv'], true)) === \openssl_cipher_iv_length($this->cipher);
117
	}
118
119
	/**
120
	 * Check if the MAC for the given payload is valid.
121
	 *
122
	 * @param array $payload
123
	 * @return bool
124
	 */
125
	protected function isValidMac(array $payload) {
126
		$calculated = $this->calculateMac($payload, $bytes = random_bytes(16));
127
		return \hash_equals(\hash_hmac('sha256', $payload['mac'], $bytes, true), $calculated);
128
	}
129
130
	/**
131
	 * Calculate the hash of the given payload.
132
	 *
133
	 * @param array $payload
134
	 * @param string $bytes
135
	 * @return string
136
	 */
137
	protected function calculateMac($payload, $bytes) {
138
		return \hash_hmac('sha256', $this->hash($payload['iv'], $payload['value']), $bytes, true);
139
	}
140
141
	/**
142
	 * Encrypt the given value.
143
	 *
144
	 * @param mixed $value
145
	 * @param bool $serialize
146
	 * @return string
147
	 *
148
	 */
149
	public function encrypt($value, $serialize = true): string {
150
		$iv = \random_bytes(\openssl_cipher_iv_length($this->cipher));
151
		$value = \openssl_encrypt($serialize ? \serialize($value) : $value, $this->cipher, $this->key, 0, $iv);
152
		if ($value === false) {
153
			throw new EncryptException('Could not encrypt the data with openssl.');
154
		}
155
156
		$mac = $this->hash($iv = base64_encode($iv), $value);
157
		$json = \json_encode(\compact('iv', 'value', 'mac'), JSON_UNESCAPED_SLASHES);
158
		if (\json_last_error() !== \JSON_ERROR_NONE) {
159
			throw new EncryptException('Could not json_encode the data.');
160
		}
161
162
		return \base64_encode($json);
163
	}
164
165
	/**
166
	 * Encrypt a string without serialization.
167
	 *
168
	 * @param string $value
169
	 * @return string
170
	 */
171
	public function encryptString(string $value): string {
172
		return $this->encrypt($value, false);
173
	}
174
175
	/**
176
	 * Decrypt the given value.
177
	 *
178
	 * @param string $payload
179
	 * @param bool $unserialize
180
	 * @return mixed
181
	 *
182
	 */
183
	public function decrypt(string $payload, $unserialize = true) {
184
		$payload = $this->getJsonPayload($payload);
185
		$iv = base64_decode($payload['iv']);
186
		$decrypted = \openssl_decrypt($payload['value'], $this->cipher, $this->key, 0, $iv);
187
188
		if ($decrypted === false) {
189
			throw new DecryptException('Could not decrypt the data.');
190
		}
191
192
		return $unserialize ? unserialize($decrypted) : $decrypted;
193
	}
194
195
	/**
196
	 * Decrypt the given string without unserialization.
197
	 *
198
	 * @param string $payload
199
	 * @return string
200
	 *
201
	 */
202
	public function decryptString($payload) {
203
		return $this->decrypt($payload, false);
204
	}
205
206
	/**
207
	 * Check if the given key and cipher combination is valid.
208
	 *
209
	 * @param string $key
210
	 * @param string $cipher
211
	 * @return bool
212
	 */
213
	public static function isValidKey(string $key, string $cipher): bool {
214
		$length = \strlen($key);
215
		return isset(self::$acceptedCiphers[$length]) && self::$acceptedCiphers[$length] === $cipher;
216
	}
217
218
	/**
219
	 * Generate a new key for the given cipher.
220
	 *
221
	 * @param string $cipher
222
	 * @return string
223
	 */
224
	public static function generateKey(string $cipher): string {
225
		$sizeMethods = \array_flip(self::$acceptedCiphers);
226
		return \bin2hex(\random_bytes($sizeMethods[$cipher] / 2));
227
	}
228
229
	/**
230
	 *
231
	 * @return string
232
	 */
233
	public function getKey() {
234
		return $this->key;
235
	}
236
237
	/**
238
	 *
239
	 * @return string
240
	 */
241
	public function getCipher() {
242
		return $this->cipher;
243
	}
244
245
	public static function getMethods(?bool $aliases = null): array {
246
		return \openssl_get_cipher_methods($aliases);
0 ignored issues
show
Bug introduced by
It seems like $aliases can also be of type null; however, parameter $aliases of openssl_get_cipher_methods() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

246
		return \openssl_get_cipher_methods(/** @scrutinizer ignore-type */ $aliases);
Loading history...
247
	}
248
}
249
250