Completed
Push — master ( 946999...5f7661 )
by Marcos
04:12
created

EncryptService::handleCredential()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 18
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 12
c 0
b 0
f 0
nc 5
nop 2
dl 0
loc 18
rs 9.2
1
<?php
2
/**
3
 * Nextcloud - passman
4
 *
5
 * @copyright Copyright (c) 2016, Sander Brand ([email protected])
6
 * @copyright Copyright (c) 2016, Marcos Zuriaga Miguel ([email protected])
7
 * @license GNU AGPL version 3 or any later version
8
 *
9
 * This program is free software: you can redistribute it and/or modify
10
 * it under the terms of the GNU Affero General Public License as
11
 * published by the Free Software Foundation, either version 3 of the
12
 * License, or (at your option) any later version.
13
 *
14
 * This program is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17
 * GNU Affero General Public License for more details.
18
 *
19
 * You should have received a copy of the GNU Affero General Public License
20
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
21
 *
22
 */
23
24
namespace OCA\Passman\Service;
25
26
27
// Class copied from http://stackoverflow.com/questions/5089841/two-way-encryption-i-need-to-store-passwords-that-can-be-retrieved?answertab=votes#tab-top
28
// Upgraded to use openssl
29
use Icewind\SMB\Exception\Exception;
30
use OCA\Passman\Db\Credential;
31
use OCA\Passman\Db\File;
32
33
/**
34
 * A class to handle secure encryption and decryption of arbitrary data
35
 *
36
 *  Note that this is not just straight encryption. It also has a few other
37
 *  features in it to make the encrypted data far more secure.  Note that any
38
 *  other implementations used to decrypt data will have to do the same exact
39
 *  operations.
40
 *
41
 * Security Benefits:
42
 *
43
 * - Uses Key stretching
44
 * - Hides the Initialization Vector
45
 * - Does HMAC verification of source data
46
 *
47
 */
48
class EncryptService {
49
50
	/**
51
	 * Supported cipher algorithms accompanied by their key/block sizes in bytes
52
	 *
53
	 * OpenSSL has no equivalent of mcrypt_get_key_size() and mcrypt_get_block_size() hence sizes stored here.
54
	 *
55
	 * @var array
56
	 */
57
	const SUPPORTED_ALGORITHMS = array(
58
		'aes-256-cbc' => array('name' => 'AES-256', 'keySize' => 32, 'blockSize' => 32),
59
		'bf' => array('name' => 'BF', 'keySize' => 16, 'blockSize' => 8),
60
		'des' => array('name' => 'DES', 'keySize' => 7, 'blockSize' => 8),
61
		'des-ede3' => array('name' => 'DES-EDE3', 'keySize' => 21, 'blockSize' => 8), // 3 different 56-bit keys
62
		'cast5' => array('name' => 'CAST5', 'keySize' => 16, 'blockSize' => 8),
63
	);
64
65
	const OP_ENCRYPT = 'encrypt';
66
	const OP_DECRYPT = 'decrypt';
67
68
	// The fields of a credential which are encrypted
69
	public $encrypted_credential_fields = array(
70
		'description', 'username', 'password', 'files', 'custom_fields', 'otp', 'email', 'tags', 'url'
71
	);
72
73
	// Contains the server key
74
	private $server_key;
75
76
	/**
77
	 * @var string $cipher The openssl cipher to use for this instance
78
	 */
79
	protected $cipher = '';
80
81
	/**
82
	 * @var int $rounds The number of rounds to feed into PBKDF2 for key generation
83
	 */
84
	protected $rounds = 100;
85
86
	/**
87
	 * Constructor!
88
	 *
89
	 * @param SettingsService $settings
90
	 */
91
	public function __construct(SettingsService $settings) {
92
		$this->cipher = $settings->getAppSetting('server_side_encryption', 'aes-256-cbc');
93
		$password_salt = \OC::$server->getConfig()->getSystemValue('passwordsalt', '');
94
		$secret = \OC::$server->getConfig()->getSystemValue('secret', '');
95
		$this->server_key = $password_salt . $secret;
96
		$this->rounds = $settings->getAppSetting('rounds_pbkdf2_stretching', 100);
97
	}
98
99
	/**
100
	 * Create an encryption key. Based on given parameters
101
	 *
102
	 * @param string $userKey The user key to use. This should be specific to this user.
103
	 * @param string $serverKey The server key
104
	 * @param string $userSuppliedKey A key from the credential (eg guid, name or tags)
105
	 * @return string
106
	 */
107
108
	public static function makeKey($userKey, $serverKey, $userSuppliedKey) {
109
		$key = hash_hmac('sha512', $userKey, $serverKey);
110
		$key = hash_hmac('sha512', $key, $userSuppliedKey);
111
		return $key;
112
	}
113
114
	/**
115
	 * Get the maximum key size for the selected cipher and mode of operation
116
	 *
117
	 * @return int Value is in bytes
118
	 */
119
	public function getKeySize() {
120
		return EncryptService::SUPPORTED_ALGORITHMS[$this->cipher]['keySize'];
121
	}
122
123
	/**
124
	 * Decrypt the data with the provided key
125
	 *
126
	 * @param string $data_hex The encrypted datat to decrypt
127
	 * @param string $key The key to use for decryption
128
	 *
129
	 * @returns string|false The returned string if decryption is successful
130
	 *                           false if it is not
131
	 */
132
	public function decrypt($data_hex, $key) {
133
134
		if (!function_exists('hex2bin')) {
135
			function hex2bin($str) {
136
				$sbin = "";
137
				$len = strlen($str);
138
				for ($i = 0; $i < $len; $i += 2) {
139
					$sbin .= pack("H*", substr($str, $i, 2));
140
				}
141
142
				return $sbin;
143
			}
144
		}
145
146
		$data = hex2bin($data_hex);
147
148
		$salt = substr($data, 0, 128);
149
		$enc = substr($data, 128, -64);
150
		$mac = substr($data, -64);
151
152
		list ($cipherKey, $macKey, $iv) = $this->getKeys($salt, $key);
153
154
		if (!$this->hash_equals(hash_hmac('sha512', $enc, $macKey, true), $mac)) {
155
			return false;
156
		}
157
158
		$dec = openssl_decrypt($enc, $this->cipher, $cipherKey, true, $iv);
159
		$data = $this->unpad($dec);
160
161
		return $data;
162
	}
163
164
	/**
165
	 * Encrypt the supplied data using the supplied key
166
	 *
167
	 * @param string $data The data to encrypt
168
	 * @param string $key The key to encrypt with
169
	 *
170
	 * @returns string The encrypted data
171
	 */
172
	public function encrypt($data, $key) {
173
		if (function_exists('random_bytes')) {
174
			$salt = random_bytes(128);
175
		} else {
176
			$salt = openssl_random_pseudo_bytes(128);
177
		}
178
		list ($cipherKey, $macKey, $iv) = $this->getKeys($salt, $key);
179
		$data = $this->pad($data);
180
		$enc = openssl_encrypt($data, $this->cipher, $cipherKey, true, $iv);
181
		$mac = hash_hmac('sha512', $enc, $macKey, true);
182
		$data = bin2hex($salt . $enc . $mac);
183
		return $data;
184
185
	}
186
187
	/**
188
	 * Generates a set of keys given a random salt and a master key
189
	 *
190
	 * @param string $salt A random string to change the keys each encryption
191
	 * @param string $key The supplied key to encrypt with
192
	 *
193
	 * @returns array An array of keys (a cipher key, a mac key, and a IV)
194
	 */
195
	protected function getKeys($salt, $key) {
196
		$ivSize = openssl_cipher_iv_length($this->cipher);
197
		$keySize = openssl_cipher_iv_length($this->cipher);
198
		$length = 2 * $keySize + $ivSize;
199
200
		$key = $this->pbkdf2('sha512', $key, $salt, $this->rounds, $length);
201
202
		$cipherKey = substr($key, 0, $keySize);
203
		$macKey = substr($key, $keySize, $keySize);
204
		$iv = substr($key, 2 * $keySize);
205
		return array($cipherKey, $macKey, $iv);
206
	}
207
208
	protected function hash_equals($a, $b) {
209
		if (function_exists('random_bytes')) {
210
			$key = random_bytes(128);
211
		} else {
212
			$key = openssl_random_pseudo_bytes(128);
213
		}
214
		return hash_hmac('sha512', $a, $key) === hash_hmac('sha512', $b, $key);
215
	}
216
217
	/**
218
	 * Stretch the key using the PBKDF2 algorithm
219
	 *
220
	 * @see http://en.wikipedia.org/wiki/PBKDF2
221
	 *
222
	 * @param string $algo The algorithm to use
223
	 * @param string $key The key to stretch
224
	 * @param string $salt A random salt
225
	 * @param int $rounds The number of rounds to derive
226
	 * @param int $length The length of the output key
227
	 *
228
	 * @returns string The derived key.
229
	 */
230
	protected function pbkdf2($algo, $key, $salt, $rounds, $length) {
231
		$size = strlen(hash($algo, '', true));
232
		$len = ceil($length / $size);
233
		$result = '';
234
		for ($i = 1; $i <= $len; $i++) {
235
			$tmp = hash_hmac($algo, $salt . pack('N', $i), $key, true);
236
			$res = $tmp;
237
			for ($j = 1; $j < $rounds; $j++) {
238
				$tmp = hash_hmac($algo, $tmp, $key, true);
239
				$res ^= $tmp;
240
			}
241
			$result .= $res;
242
		}
243
		return substr($result, 0, $length);
244
	}
245
246
	/**
247
	 * Pad the data with a random char chosen by the pad amount.
248
	 *
249
	 * @param $data
250
	 * @return string
251
	 */
252
	protected function pad($data) {
253
		$length = $this->getKeySize();
254
		$padAmount = $length - strlen($data) % $length;
255
		if ($padAmount === 0) {
256
			$padAmount = $length;
257
		}
258
		return $data . str_repeat(chr($padAmount), $padAmount);
259
	}
260
261
262
	/**
263
	 * Unpad the the data
264
	 *
265
	 * @param $data
266
	 * @return bool|string
267
	 */
268
	protected function unpad($data) {
269
		$length = $this->getKeySize();
270
		$last = ord($data[strlen($data) - 1]);
271
		if ($last > $length) return false;
272
		if (substr($data, -1 * $last) !== str_repeat(chr($last), $last)) {
273
			return false;
274
		}
275
		return substr($data, 0, -1 * $last);
276
	}
277
278
279
	/**
280
	 * Encrypt a credential
281
	 *
282
	 * @param Credential|array $credential the credential to decrypt
283
	 * @return Credential|array
284
	 */
285
	public function decryptCredential($credential) {
286
		return $this->handleCredential($credential, EncryptService::OP_DECRYPT);
287
	}
288
289
	/**
290
	 * Encrypt a credential
291
	 *
292
	 * @param Credential|array $credential the credential to encrypt
293
	 * @return Credential|array
294
	 * @throws \Exception
295
	 */
296
	public function encryptCredential($credential) {
297
		return $this->handleCredential($credential, EncryptService::OP_ENCRYPT);
298
	}
299
300
301
	private function extractKeysFromCredential($credential) {
302
		$userKey = '';
303
		$userSuppliedKey = '';
304
		if ($credential instanceof Credential) {
305
			$userSuppliedKey = $credential->getLabel();
306
			$sk = $credential->getSharedKey();
307
			$userKey = (isset($sk)) ? $sk : $credential->getUserId();
308
		}
309
		if (is_array($credential)) {
310
			$userSuppliedKey = $credential['label'];
311
			$userKey = (isset($credential['shared_key'])) ? $credential['shared_key'] : $credential['user_id'];
312
		}
313
		return array($userKey, $userSuppliedKey);
314
	}
315
316
	/**
317
	 * Handles the encryption / decryption of a credential
318
	 *
319
	 * @param Credential|array $credential the credential to encrypt
320
	 * @return Credential|array
321
	 * @throws \Exception
322
	 */
323
	private function handleCredential($credential, $service_function) {
324
		list($userKey, $userSuppliedKey) = $this->extractKeysFromCredential($credential);
325
326
		$key = $this->makeKey($userKey, $this->server_key, $userSuppliedKey);
327
		foreach ($this->encrypted_credential_fields as $field) {
328
			if ($credential instanceof Credential) {
329
				$field = str_replace(' ', '', str_replace('_', ' ', ucwords($field, '_')));
330
				$set = 'set' . $field;
331
				$get = 'get' . $field;
332
				$credential->{$set}($this->{$service_function}($credential->{$get}(), $key));
333
			}
334
335
			if (is_array($credential)) {
336
				$credential[$field] = $this->{$service_function}($credential[$field], $key);
337
			}
338
		}
339
		return $credential;
340
	}
341
342
	/**
343
	 * Encrypt a file
344
	 *
345
	 * @param  File|array $file
346
	 * @return File|array
347
	 */
348
349
	public function encryptFile($file) {
350
		return $this->handleFile($file, EncryptService::OP_ENCRYPT);
351
	}
352
353
	/**
354
	 * Decrypt a file
355
	 *
356
	 * @param  File|array $file
357
	 * @return File|array
358
	 */
359
360
	public function decryptFile($file) {
361
		return $this->handleFile($file, EncryptService::OP_DECRYPT);
362
	}
363
364
	/**
365
	 * Handles the encryption / decryption of a File
366
	 *
367
	 * @param File|array $file the credential to encrypt
368
	 * @return File|array
369
	 * @throws \Exception
370
	 */
371
	private function handleFile($file, $service_function) {
372
		$userKey = '';
373
		$userSuppliedKey = '';
374
		if ($file instanceof File) {
375
			$userSuppliedKey = $file->getFilename();
376
			$userKey = md5($file->getMimetype());
377
		}
378
379
		if (is_array($file)) {
380
			$userSuppliedKey = $file['size'];
381
			$userKey = md5($file['mimetype']);
382
		}
383
384
		$key = $this->makeKey($userKey, $this->server_key, $userSuppliedKey);
385
386
387
		if ($file instanceof File) {
388
			$file->setFilename($this->{$service_function}($file->getFilename(), $key));
389
			$file->setFileData($this->{$service_function}($file->getFileData(), $key));
390
		}
391
392
		if (is_array($file)) {
393
			$file['filename'] = $this->{$service_function}($file['filename'], $key);
394
			$file['file_data'] = $this->{$service_function}($file['file_data'], $key);
395
		}
396
397
		return $file;
398
	}
399
}