1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* @copyright Copyright (c) 2016, ownCloud, Inc. |
4
|
|
|
* |
5
|
|
|
* @author Bjoern Schiessle <[email protected]> |
6
|
|
|
* @author Björn Schießle <[email protected]> |
7
|
|
|
* @author Clark Tomlinson <[email protected]> |
8
|
|
|
* @author Joas Schilling <[email protected]> |
9
|
|
|
* @author Lukas Reschke <[email protected]> |
10
|
|
|
* @author Morris Jobke <[email protected]> |
11
|
|
|
* @author Thomas Müller <[email protected]> |
12
|
|
|
* |
13
|
|
|
* @license AGPL-3.0 |
14
|
|
|
* |
15
|
|
|
* This code is free software: you can redistribute it and/or modify |
16
|
|
|
* it under the terms of the GNU Affero General Public License, version 3, |
17
|
|
|
* as published by the Free Software Foundation. |
18
|
|
|
* |
19
|
|
|
* This program is distributed in the hope that it will be useful, |
20
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
21
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
22
|
|
|
* GNU Affero General Public License for more details. |
23
|
|
|
* |
24
|
|
|
* You should have received a copy of the GNU Affero General Public License, version 3, |
25
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/> |
26
|
|
|
* |
27
|
|
|
*/ |
28
|
|
|
|
29
|
|
|
namespace OCA\Encryption\Crypto; |
30
|
|
|
|
31
|
|
|
|
32
|
|
|
use OC\Encryption\Exceptions\DecryptionFailedException; |
33
|
|
|
use OC\Encryption\Exceptions\EncryptionFailedException; |
34
|
|
|
use OCA\Encryption\Exceptions\MultiKeyDecryptException; |
35
|
|
|
use OCA\Encryption\Exceptions\MultiKeyEncryptException; |
36
|
|
|
use OCP\Encryption\Exceptions\GenericEncryptionException; |
37
|
|
|
use OCP\IConfig; |
38
|
|
|
use OCP\IL10N; |
39
|
|
|
use OCP\ILogger; |
40
|
|
|
use OCP\IUserSession; |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* Class Crypt provides the encryption implementation of the default Nextcloud |
44
|
|
|
* encryption module. As default AES-256-CTR is used, it does however offer support |
45
|
|
|
* for the following modes: |
46
|
|
|
* |
47
|
|
|
* - AES-256-CTR |
48
|
|
|
* - AES-128-CTR |
49
|
|
|
* - AES-256-CFB |
50
|
|
|
* - AES-128-CFB |
51
|
|
|
* |
52
|
|
|
* For integrity protection Encrypt-Then-MAC using HMAC-SHA256 is used. |
53
|
|
|
* |
54
|
|
|
* @package OCA\Encryption\Crypto |
55
|
|
|
*/ |
56
|
|
|
class Crypt { |
57
|
|
|
|
58
|
|
|
const DEFAULT_CIPHER = 'AES-256-CTR'; |
59
|
|
|
// default cipher from old Nextcloud versions |
60
|
|
|
const LEGACY_CIPHER = 'AES-128-CFB'; |
61
|
|
|
|
62
|
|
|
// default key format, old Nextcloud version encrypted the private key directly |
63
|
|
|
// with the user password |
64
|
|
|
const LEGACY_KEY_FORMAT = 'password'; |
65
|
|
|
|
66
|
|
|
const HEADER_START = 'HBEGIN'; |
67
|
|
|
const HEADER_END = 'HEND'; |
68
|
|
|
|
69
|
|
|
/** @var ILogger */ |
70
|
|
|
private $logger; |
71
|
|
|
|
72
|
|
|
/** @var string */ |
73
|
|
|
private $user; |
74
|
|
|
|
75
|
|
|
/** @var IConfig */ |
76
|
|
|
private $config; |
77
|
|
|
|
78
|
|
|
/** @var array */ |
79
|
|
|
private $supportedKeyFormats; |
80
|
|
|
|
81
|
|
|
/** @var IL10N */ |
82
|
|
|
private $l; |
83
|
|
|
|
84
|
|
|
/** @var array */ |
85
|
|
|
private $supportedCiphersAndKeySize = [ |
86
|
|
|
'AES-256-CTR' => 32, |
87
|
|
|
'AES-128-CTR' => 16, |
88
|
|
|
'AES-256-CFB' => 32, |
89
|
|
|
'AES-128-CFB' => 16, |
90
|
|
|
]; |
91
|
|
|
|
92
|
|
|
/** |
93
|
|
|
* @param ILogger $logger |
94
|
|
|
* @param IUserSession $userSession |
95
|
|
|
* @param IConfig $config |
96
|
|
|
* @param IL10N $l |
97
|
|
|
*/ |
98
|
|
|
public function __construct(ILogger $logger, IUserSession $userSession, IConfig $config, IL10N $l) { |
99
|
|
|
$this->logger = $logger; |
100
|
|
|
$this->user = $userSession && $userSession->isLoggedIn() ? $userSession->getUser()->getUID() : '"no user given"'; |
101
|
|
|
$this->config = $config; |
102
|
|
|
$this->l = $l; |
103
|
|
|
$this->supportedKeyFormats = ['hash', 'password']; |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
/** |
107
|
|
|
* create new private/public key-pair for user |
108
|
|
|
* |
109
|
|
|
* @return array|bool |
110
|
|
|
*/ |
111
|
|
|
public function createKeyPair() { |
112
|
|
|
|
113
|
|
|
$log = $this->logger; |
114
|
|
|
$res = $this->getOpenSSLPKey(); |
115
|
|
|
|
116
|
|
|
if (!$res) { |
117
|
|
|
$log->error("Encryption Library couldn't generate users key-pair for {$this->user}", |
118
|
|
|
['app' => 'encryption']); |
119
|
|
|
|
120
|
|
|
if (openssl_error_string()) { |
121
|
|
|
$log->error('Encryption library openssl_pkey_new() fails: ' . openssl_error_string(), |
122
|
|
|
['app' => 'encryption']); |
123
|
|
|
} |
124
|
|
|
} elseif (openssl_pkey_export($res, |
125
|
|
|
$privateKey, |
126
|
|
|
null, |
127
|
|
|
$this->getOpenSSLConfig())) { |
128
|
|
|
$keyDetails = openssl_pkey_get_details($res); |
129
|
|
|
$publicKey = $keyDetails['key']; |
130
|
|
|
|
131
|
|
|
return [ |
132
|
|
|
'publicKey' => $publicKey, |
133
|
|
|
'privateKey' => $privateKey |
134
|
|
|
]; |
135
|
|
|
} |
136
|
|
|
$log->error('Encryption library couldn\'t export users private key, please check your servers OpenSSL configuration.' . $this->user, |
137
|
|
|
['app' => 'encryption']); |
138
|
|
|
if (openssl_error_string()) { |
139
|
|
|
$log->error('Encryption Library:' . openssl_error_string(), |
140
|
|
|
['app' => 'encryption']); |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
return false; |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
/** |
147
|
|
|
* Generates a new private key |
148
|
|
|
* |
149
|
|
|
* @return resource |
150
|
|
|
*/ |
151
|
|
|
public function getOpenSSLPKey() { |
152
|
|
|
$config = $this->getOpenSSLConfig(); |
153
|
|
|
return openssl_pkey_new($config); |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
/** |
157
|
|
|
* get openSSL Config |
158
|
|
|
* |
159
|
|
|
* @return array |
160
|
|
|
*/ |
161
|
|
|
private function getOpenSSLConfig() { |
162
|
|
|
$config = ['private_key_bits' => 4096]; |
163
|
|
|
$config = array_merge( |
164
|
|
|
$config, |
165
|
|
|
$this->config->getSystemValue('openssl', []) |
166
|
|
|
); |
167
|
|
|
return $config; |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
/** |
171
|
|
|
* @param string $plainContent |
172
|
|
|
* @param string $passPhrase |
173
|
|
|
* @param int $version |
174
|
|
|
* @param int $position |
175
|
|
|
* @return false|string |
176
|
|
|
* @throws EncryptionFailedException |
177
|
|
|
*/ |
178
|
|
|
public function symmetricEncryptFileContent($plainContent, $passPhrase, $version, $position) { |
179
|
|
|
|
180
|
|
|
if (!$plainContent) { |
181
|
|
|
$this->logger->error('Encryption Library, symmetrical encryption failed no content given', |
182
|
|
|
['app' => 'encryption']); |
183
|
|
|
return false; |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
$iv = $this->generateIv(); |
187
|
|
|
|
188
|
|
|
$encryptedContent = $this->encrypt($plainContent, |
189
|
|
|
$iv, |
190
|
|
|
$passPhrase, |
191
|
|
|
$this->getCipher()); |
192
|
|
|
|
193
|
|
|
// Create a signature based on the key as well as the current version |
194
|
|
|
$sig = $this->createSignature($encryptedContent, $passPhrase.$version.$position); |
195
|
|
|
|
196
|
|
|
// combine content to encrypt the IV identifier and actual IV |
197
|
|
|
$catFile = $this->concatIV($encryptedContent, $iv); |
198
|
|
|
$catFile = $this->concatSig($catFile, $sig); |
199
|
|
|
return $this->addPadding($catFile); |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
/** |
203
|
|
|
* generate header for encrypted file |
204
|
|
|
* |
205
|
|
|
* @param string $keyFormat (can be 'hash' or 'password') |
206
|
|
|
* @return string |
207
|
|
|
* @throws \InvalidArgumentException |
208
|
|
|
*/ |
209
|
|
|
public function generateHeader($keyFormat = 'hash') { |
210
|
|
|
|
211
|
|
|
if (in_array($keyFormat, $this->supportedKeyFormats, true) === false) { |
212
|
|
|
throw new \InvalidArgumentException('key format "' . $keyFormat . '" is not supported'); |
213
|
|
|
} |
214
|
|
|
|
215
|
|
|
$cipher = $this->getCipher(); |
216
|
|
|
|
217
|
|
|
$header = self::HEADER_START |
218
|
|
|
. ':cipher:' . $cipher |
219
|
|
|
. ':keyFormat:' . $keyFormat |
220
|
|
|
. ':' . self::HEADER_END; |
221
|
|
|
|
222
|
|
|
return $header; |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
/** |
226
|
|
|
* @param string $plainContent |
227
|
|
|
* @param string $iv |
228
|
|
|
* @param string $passPhrase |
229
|
|
|
* @param string $cipher |
230
|
|
|
* @return string |
231
|
|
|
* @throws EncryptionFailedException |
232
|
|
|
*/ |
233
|
|
|
private function encrypt($plainContent, $iv, $passPhrase = '', $cipher = self::DEFAULT_CIPHER) { |
234
|
|
|
$encryptedContent = openssl_encrypt($plainContent, |
235
|
|
|
$cipher, |
236
|
|
|
$passPhrase, |
237
|
|
|
false, |
238
|
|
|
$iv); |
239
|
|
|
|
240
|
|
|
if (!$encryptedContent) { |
241
|
|
|
$error = 'Encryption (symmetric) of content failed'; |
242
|
|
|
$this->logger->error($error . openssl_error_string(), |
243
|
|
|
['app' => 'encryption']); |
244
|
|
|
throw new EncryptionFailedException($error); |
245
|
|
|
} |
246
|
|
|
|
247
|
|
|
return $encryptedContent; |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
/** |
251
|
|
|
* return Cipher either from config.php or the default cipher defined in |
252
|
|
|
* this class |
253
|
|
|
* |
254
|
|
|
* @return string |
255
|
|
|
*/ |
256
|
|
|
public function getCipher() { |
257
|
|
|
$cipher = $this->config->getSystemValue('cipher', self::DEFAULT_CIPHER); |
258
|
|
|
if (!isset($this->supportedCiphersAndKeySize[$cipher])) { |
259
|
|
|
$this->logger->warning( |
260
|
|
|
sprintf( |
261
|
|
|
'Unsupported cipher (%s) defined in config.php supported. Falling back to %s', |
262
|
|
|
$cipher, |
263
|
|
|
self::DEFAULT_CIPHER |
264
|
|
|
), |
265
|
|
|
['app' => 'encryption']); |
266
|
|
|
$cipher = self::DEFAULT_CIPHER; |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
// Workaround for OpenSSL 0.9.8. Fallback to an old cipher that should work. |
270
|
|
|
if(OPENSSL_VERSION_NUMBER < 0x1000101f) { |
271
|
|
|
if($cipher === 'AES-256-CTR' || $cipher === 'AES-128-CTR') { |
272
|
|
|
$cipher = self::LEGACY_CIPHER; |
273
|
|
|
} |
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
return $cipher; |
277
|
|
|
} |
278
|
|
|
|
279
|
|
|
/** |
280
|
|
|
* get key size depending on the cipher |
281
|
|
|
* |
282
|
|
|
* @param string $cipher |
283
|
|
|
* @return int |
284
|
|
|
* @throws \InvalidArgumentException |
285
|
|
|
*/ |
286
|
|
|
protected function getKeySize($cipher) { |
287
|
|
|
if(isset($this->supportedCiphersAndKeySize[$cipher])) { |
288
|
|
|
return $this->supportedCiphersAndKeySize[$cipher]; |
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
throw new \InvalidArgumentException( |
292
|
|
|
sprintf( |
293
|
|
|
'Unsupported cipher (%s) defined.', |
294
|
|
|
$cipher |
295
|
|
|
) |
296
|
|
|
); |
297
|
|
|
} |
298
|
|
|
|
299
|
|
|
/** |
300
|
|
|
* get legacy cipher |
301
|
|
|
* |
302
|
|
|
* @return string |
303
|
|
|
*/ |
304
|
|
|
public function getLegacyCipher() { |
305
|
|
|
return self::LEGACY_CIPHER; |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
/** |
309
|
|
|
* @param string $encryptedContent |
310
|
|
|
* @param string $iv |
311
|
|
|
* @return string |
312
|
|
|
*/ |
313
|
|
|
private function concatIV($encryptedContent, $iv) { |
314
|
|
|
return $encryptedContent . '00iv00' . $iv; |
315
|
|
|
} |
316
|
|
|
|
317
|
|
|
/** |
318
|
|
|
* @param string $encryptedContent |
319
|
|
|
* @param string $signature |
320
|
|
|
* @return string |
321
|
|
|
*/ |
322
|
|
|
private function concatSig($encryptedContent, $signature) { |
323
|
|
|
return $encryptedContent . '00sig00' . $signature; |
324
|
|
|
} |
325
|
|
|
|
326
|
|
|
/** |
327
|
|
|
* Note: This is _NOT_ a padding used for encryption purposes. It is solely |
328
|
|
|
* used to achieve the PHP stream size. It has _NOTHING_ to do with the |
329
|
|
|
* encrypted content and is not used in any crypto primitive. |
330
|
|
|
* |
331
|
|
|
* @param string $data |
332
|
|
|
* @return string |
333
|
|
|
*/ |
334
|
|
|
private function addPadding($data) { |
335
|
|
|
return $data . 'xxx'; |
336
|
|
|
} |
337
|
|
|
|
338
|
|
|
/** |
339
|
|
|
* generate password hash used to encrypt the users private key |
340
|
|
|
* |
341
|
|
|
* @param string $password |
342
|
|
|
* @param string $cipher |
343
|
|
|
* @param string $uid only used for user keys |
344
|
|
|
* @return string |
345
|
|
|
*/ |
346
|
|
|
protected function generatePasswordHash($password, $cipher, $uid = '') { |
347
|
|
|
$instanceId = $this->config->getSystemValue('instanceid'); |
348
|
|
|
$instanceSecret = $this->config->getSystemValue('secret'); |
349
|
|
|
$salt = hash('sha256', $uid . $instanceId . $instanceSecret, true); |
350
|
|
|
$keySize = $this->getKeySize($cipher); |
351
|
|
|
|
352
|
|
|
$hash = hash_pbkdf2( |
353
|
|
|
'sha256', |
354
|
|
|
$password, |
355
|
|
|
$salt, |
356
|
|
|
100000, |
357
|
|
|
$keySize, |
358
|
|
|
true |
359
|
|
|
); |
360
|
|
|
|
361
|
|
|
return $hash; |
362
|
|
|
} |
363
|
|
|
|
364
|
|
|
/** |
365
|
|
|
* encrypt private key |
366
|
|
|
* |
367
|
|
|
* @param string $privateKey |
368
|
|
|
* @param string $password |
369
|
|
|
* @param string $uid for regular users, empty for system keys |
370
|
|
|
* @return false|string |
371
|
|
|
*/ |
372
|
|
|
public function encryptPrivateKey($privateKey, $password, $uid = '') { |
373
|
|
|
$cipher = $this->getCipher(); |
374
|
|
|
$hash = $this->generatePasswordHash($password, $cipher, $uid); |
375
|
|
|
$encryptedKey = $this->symmetricEncryptFileContent( |
376
|
|
|
$privateKey, |
377
|
|
|
$hash, |
378
|
|
|
0, |
379
|
|
|
0 |
380
|
|
|
); |
381
|
|
|
|
382
|
|
|
return $encryptedKey; |
383
|
|
|
} |
384
|
|
|
|
385
|
|
|
/** |
386
|
|
|
* @param string $privateKey |
387
|
|
|
* @param string $password |
388
|
|
|
* @param string $uid for regular users, empty for system keys |
389
|
|
|
* @return false|string |
390
|
|
|
*/ |
391
|
|
|
public function decryptPrivateKey($privateKey, $password = '', $uid = '') { |
392
|
|
|
|
393
|
|
|
$header = $this->parseHeader($privateKey); |
394
|
|
|
|
395
|
|
|
if (isset($header['cipher'])) { |
396
|
|
|
$cipher = $header['cipher']; |
397
|
|
|
} else { |
398
|
|
|
$cipher = self::LEGACY_CIPHER; |
399
|
|
|
} |
400
|
|
|
|
401
|
|
|
if (isset($header['keyFormat'])) { |
402
|
|
|
$keyFormat = $header['keyFormat']; |
403
|
|
|
} else { |
404
|
|
|
$keyFormat = self::LEGACY_KEY_FORMAT; |
405
|
|
|
} |
406
|
|
|
|
407
|
|
|
if ($keyFormat === 'hash') { |
408
|
|
|
$password = $this->generatePasswordHash($password, $cipher, $uid); |
409
|
|
|
} |
410
|
|
|
|
411
|
|
|
// If we found a header we need to remove it from the key we want to decrypt |
412
|
|
|
if (!empty($header)) { |
413
|
|
|
$privateKey = substr($privateKey, |
414
|
|
|
strpos($privateKey, |
415
|
|
|
self::HEADER_END) + strlen(self::HEADER_END)); |
416
|
|
|
} |
417
|
|
|
|
418
|
|
|
$plainKey = $this->symmetricDecryptFileContent( |
419
|
|
|
$privateKey, |
420
|
|
|
$password, |
421
|
|
|
$cipher, |
422
|
|
|
0 |
423
|
|
|
); |
424
|
|
|
|
425
|
|
|
if ($this->isValidPrivateKey($plainKey) === false) { |
426
|
|
|
return false; |
427
|
|
|
} |
428
|
|
|
|
429
|
|
|
return $plainKey; |
430
|
|
|
} |
431
|
|
|
|
432
|
|
|
/** |
433
|
|
|
* check if it is a valid private key |
434
|
|
|
* |
435
|
|
|
* @param string $plainKey |
436
|
|
|
* @return bool |
437
|
|
|
*/ |
438
|
|
|
protected function isValidPrivateKey($plainKey) { |
439
|
|
|
$res = openssl_get_privatekey($plainKey); |
440
|
|
|
if (is_resource($res)) { |
441
|
|
|
$sslInfo = openssl_pkey_get_details($res); |
442
|
|
|
if (isset($sslInfo['key'])) { |
443
|
|
|
return true; |
444
|
|
|
} |
445
|
|
|
} |
446
|
|
|
|
447
|
|
|
return false; |
448
|
|
|
} |
449
|
|
|
|
450
|
|
|
/** |
451
|
|
|
* @param string $keyFileContents |
452
|
|
|
* @param string $passPhrase |
453
|
|
|
* @param string $cipher |
454
|
|
|
* @param int $version |
455
|
|
|
* @param int $position |
456
|
|
|
* @return string |
457
|
|
|
* @throws DecryptionFailedException |
458
|
|
|
*/ |
459
|
|
|
public function symmetricDecryptFileContent($keyFileContents, $passPhrase, $cipher = self::DEFAULT_CIPHER, $version = 0, $position = 0) { |
460
|
|
|
$catFile = $this->splitMetaData($keyFileContents, $cipher); |
461
|
|
|
|
462
|
|
|
if ($catFile['signature'] !== false) { |
463
|
|
|
$this->checkSignature($catFile['encrypted'], $passPhrase.$version.$position, $catFile['signature']); |
464
|
|
|
} |
465
|
|
|
|
466
|
|
|
return $this->decrypt($catFile['encrypted'], |
467
|
|
|
$catFile['iv'], |
468
|
|
|
$passPhrase, |
469
|
|
|
$cipher); |
470
|
|
|
} |
471
|
|
|
|
472
|
|
|
/** |
473
|
|
|
* check for valid signature |
474
|
|
|
* |
475
|
|
|
* @param string $data |
476
|
|
|
* @param string $passPhrase |
477
|
|
|
* @param string $expectedSignature |
478
|
|
|
* @throws GenericEncryptionException |
479
|
|
|
*/ |
480
|
|
|
private function checkSignature($data, $passPhrase, $expectedSignature) { |
481
|
|
|
$signature = $this->createSignature($data, $passPhrase); |
482
|
|
|
if (!hash_equals($expectedSignature, $signature)) { |
483
|
|
|
throw new GenericEncryptionException('Bad Signature', $this->l->t('Bad Signature')); |
484
|
|
|
} |
485
|
|
|
} |
486
|
|
|
|
487
|
|
|
/** |
488
|
|
|
* create signature |
489
|
|
|
* |
490
|
|
|
* @param string $data |
491
|
|
|
* @param string $passPhrase |
492
|
|
|
* @return string |
493
|
|
|
*/ |
494
|
|
|
private function createSignature($data, $passPhrase) { |
495
|
|
|
$passPhrase = hash('sha512', $passPhrase . 'a', true); |
496
|
|
|
return hash_hmac('sha256', $data, $passPhrase); |
497
|
|
|
} |
498
|
|
|
|
499
|
|
|
|
500
|
|
|
/** |
501
|
|
|
* remove padding |
502
|
|
|
* |
503
|
|
|
* @param string $padded |
504
|
|
|
* @param bool $hasSignature did the block contain a signature, in this case we use a different padding |
505
|
|
|
* @return string|false |
506
|
|
|
*/ |
507
|
|
|
private function removePadding($padded, $hasSignature = false) { |
508
|
|
|
if ($hasSignature === false && substr($padded, -2) === 'xx') { |
509
|
|
|
return substr($padded, 0, -2); |
510
|
|
|
} elseif ($hasSignature === true && substr($padded, -3) === 'xxx') { |
511
|
|
|
return substr($padded, 0, -3); |
512
|
|
|
} |
513
|
|
|
return false; |
514
|
|
|
} |
515
|
|
|
|
516
|
|
|
/** |
517
|
|
|
* split meta data from encrypted file |
518
|
|
|
* Note: for now, we assume that the meta data always start with the iv |
519
|
|
|
* followed by the signature, if available |
520
|
|
|
* |
521
|
|
|
* @param string $catFile |
522
|
|
|
* @param string $cipher |
523
|
|
|
* @return array |
524
|
|
|
*/ |
525
|
|
|
private function splitMetaData($catFile, $cipher) { |
526
|
|
|
if ($this->hasSignature($catFile, $cipher)) { |
527
|
|
|
$catFile = $this->removePadding($catFile, true); |
528
|
|
|
$meta = substr($catFile, -93); |
529
|
|
|
$iv = substr($meta, strlen('00iv00'), 16); |
530
|
|
|
$sig = substr($meta, 22 + strlen('00sig00')); |
531
|
|
|
$encrypted = substr($catFile, 0, -93); |
532
|
|
|
} else { |
533
|
|
|
$catFile = $this->removePadding($catFile); |
534
|
|
|
$meta = substr($catFile, -22); |
535
|
|
|
$iv = substr($meta, -16); |
536
|
|
|
$sig = false; |
537
|
|
|
$encrypted = substr($catFile, 0, -22); |
538
|
|
|
} |
539
|
|
|
|
540
|
|
|
return [ |
541
|
|
|
'encrypted' => $encrypted, |
542
|
|
|
'iv' => $iv, |
543
|
|
|
'signature' => $sig |
544
|
|
|
]; |
545
|
|
|
} |
546
|
|
|
|
547
|
|
|
/** |
548
|
|
|
* check if encrypted block is signed |
549
|
|
|
* |
550
|
|
|
* @param string $catFile |
551
|
|
|
* @param string $cipher |
552
|
|
|
* @return bool |
553
|
|
|
* @throws GenericEncryptionException |
554
|
|
|
*/ |
555
|
|
|
private function hasSignature($catFile, $cipher) { |
556
|
|
|
$meta = substr($catFile, -93); |
557
|
|
|
$signaturePosition = strpos($meta, '00sig00'); |
558
|
|
|
|
559
|
|
|
// enforce signature for the new 'CTR' ciphers |
560
|
|
|
if ($signaturePosition === false && stripos($cipher, 'ctr') !== false) { |
561
|
|
|
throw new GenericEncryptionException('Missing Signature', $this->l->t('Missing Signature')); |
562
|
|
|
} |
563
|
|
|
|
564
|
|
|
return ($signaturePosition !== false); |
565
|
|
|
} |
566
|
|
|
|
567
|
|
|
|
568
|
|
|
/** |
569
|
|
|
* @param string $encryptedContent |
570
|
|
|
* @param string $iv |
571
|
|
|
* @param string $passPhrase |
572
|
|
|
* @param string $cipher |
573
|
|
|
* @return string |
574
|
|
|
* @throws DecryptionFailedException |
575
|
|
|
*/ |
576
|
|
|
private function decrypt($encryptedContent, $iv, $passPhrase = '', $cipher = self::DEFAULT_CIPHER) { |
577
|
|
|
$plainContent = openssl_decrypt($encryptedContent, |
578
|
|
|
$cipher, |
579
|
|
|
$passPhrase, |
580
|
|
|
false, |
581
|
|
|
$iv); |
582
|
|
|
|
583
|
|
|
if ($plainContent) { |
584
|
|
|
return $plainContent; |
585
|
|
|
} else { |
586
|
|
|
throw new DecryptionFailedException('Encryption library: Decryption (symmetric) of content failed: ' . openssl_error_string()); |
587
|
|
|
} |
588
|
|
|
} |
589
|
|
|
|
590
|
|
|
/** |
591
|
|
|
* @param string $data |
592
|
|
|
* @return array |
593
|
|
|
*/ |
594
|
|
|
protected function parseHeader($data) { |
595
|
|
|
$result = []; |
596
|
|
|
|
597
|
|
|
if (substr($data, 0, strlen(self::HEADER_START)) === self::HEADER_START) { |
598
|
|
|
$endAt = strpos($data, self::HEADER_END); |
599
|
|
|
$header = substr($data, 0, $endAt + strlen(self::HEADER_END)); |
600
|
|
|
|
601
|
|
|
// +1 not to start with an ':' which would result in empty element at the beginning |
602
|
|
|
$exploded = explode(':', |
603
|
|
|
substr($header, strlen(self::HEADER_START) + 1)); |
604
|
|
|
|
605
|
|
|
$element = array_shift($exploded); |
606
|
|
|
|
607
|
|
|
while ($element !== self::HEADER_END) { |
608
|
|
|
$result[$element] = array_shift($exploded); |
609
|
|
|
$element = array_shift($exploded); |
610
|
|
|
} |
611
|
|
|
} |
612
|
|
|
|
613
|
|
|
return $result; |
614
|
|
|
} |
615
|
|
|
|
616
|
|
|
/** |
617
|
|
|
* generate initialization vector |
618
|
|
|
* |
619
|
|
|
* @return string |
620
|
|
|
* @throws GenericEncryptionException |
621
|
|
|
*/ |
622
|
|
|
private function generateIv() { |
623
|
|
|
return random_bytes(16); |
624
|
|
|
} |
625
|
|
|
|
626
|
|
|
/** |
627
|
|
|
* Generate a cryptographically secure pseudo-random 256-bit ASCII key, used |
628
|
|
|
* as file key |
629
|
|
|
* |
630
|
|
|
* @return string |
631
|
|
|
* @throws \Exception |
632
|
|
|
*/ |
633
|
|
|
public function generateFileKey() { |
634
|
|
|
return random_bytes(32); |
635
|
|
|
} |
636
|
|
|
|
637
|
|
|
/** |
638
|
|
|
* @param $encKeyFile |
639
|
|
|
* @param $shareKey |
640
|
|
|
* @param $privateKey |
641
|
|
|
* @return string |
642
|
|
|
* @throws MultiKeyDecryptException |
643
|
|
|
*/ |
644
|
|
|
public function multiKeyDecrypt($encKeyFile, $shareKey, $privateKey) { |
645
|
|
|
if (!$encKeyFile) { |
646
|
|
|
throw new MultiKeyDecryptException('Cannot multikey decrypt empty plain content'); |
647
|
|
|
} |
648
|
|
|
|
649
|
|
|
if (openssl_open($encKeyFile, $plainContent, $shareKey, $privateKey)) { |
650
|
|
|
return $plainContent; |
651
|
|
|
} else { |
652
|
|
|
throw new MultiKeyDecryptException('multikeydecrypt with share key failed:' . openssl_error_string()); |
653
|
|
|
} |
654
|
|
|
} |
655
|
|
|
|
656
|
|
|
/** |
657
|
|
|
* @param string $plainContent |
658
|
|
|
* @param array $keyFiles |
659
|
|
|
* @return array |
660
|
|
|
* @throws MultiKeyEncryptException |
661
|
|
|
*/ |
662
|
|
|
public function multiKeyEncrypt($plainContent, array $keyFiles) { |
663
|
|
|
// openssl_seal returns false without errors if plaincontent is empty |
664
|
|
|
// so trigger our own error |
665
|
|
|
if (empty($plainContent)) { |
666
|
|
|
throw new MultiKeyEncryptException('Cannot multikeyencrypt empty plain content'); |
667
|
|
|
} |
668
|
|
|
|
669
|
|
|
// Set empty vars to be set by openssl by reference |
670
|
|
|
$sealed = ''; |
671
|
|
|
$shareKeys = []; |
672
|
|
|
$mappedShareKeys = []; |
673
|
|
|
|
674
|
|
|
if (openssl_seal($plainContent, $sealed, $shareKeys, $keyFiles)) { |
675
|
|
|
$i = 0; |
676
|
|
|
|
677
|
|
|
// Ensure each shareKey is labelled with its corresponding key id |
678
|
|
|
foreach ($keyFiles as $userId => $publicKey) { |
679
|
|
|
$mappedShareKeys[$userId] = $shareKeys[$i]; |
680
|
|
|
$i++; |
681
|
|
|
} |
682
|
|
|
|
683
|
|
|
return [ |
684
|
|
|
'keys' => $mappedShareKeys, |
685
|
|
|
'data' => $sealed |
686
|
|
|
]; |
687
|
|
|
} else { |
688
|
|
|
throw new MultiKeyEncryptException('multikeyencryption failed ' . openssl_error_string()); |
689
|
|
|
} |
690
|
|
|
} |
691
|
|
|
} |
692
|
|
|
|
693
|
|
|
|