Blame view
sources/apps/files_encryption/lib/crypt.php
17.8 KB
|
03e52840d
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
<?php /** * ownCloud * * @author Sam Tuke, Frank Karlitschek, Robin Appelman * @copyright 2012 Sam Tuke samtuke@owncloud.com, * Robin Appelman icewind@owncloud.com, Frank Karlitschek * frank@owncloud.org * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE * License as published by the Free Software Foundation; either * version 3 of the License, or any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU AFFERO GENERAL PUBLIC LICENSE for more details. * * You should have received a copy of the GNU Affero General Public * License along with this library. If not, see <http://www.gnu.org/licenses/>. * */ namespace OCA\Encryption; |
|
31b7f2792
|
28 |
require_once __DIR__ . '/../3rdparty/Crypt_Blowfish/Blowfish.php'; |
|
03e52840d
|
29 30 31 32 33 34 35 |
/**
* Class for common cryptography functionality
*/
class Crypt {
|
|
31b7f2792
|
36 37 38 39 40 |
const ENCRYPTION_UNKNOWN_ERROR = -1; const ENCRYPTION_NOT_INITIALIZED_ERROR = 1; const ENCRYPTION_PRIVATE_KEY_NOT_VALID_ERROR = 2; const ENCRYPTION_NO_SHARE_KEY_FOUND = 3; |
|
f7d878ff1
|
41 42 43 44 45 |
const BLOCKSIZE = 8192; // block size will always be 8192 for a PHP stream https://bugs.php.net/bug.php?id=21641 const DEFAULT_CIPHER = 'AES-256-CFB'; const HEADERSTART = 'HBEGIN'; const HEADEREND = 'HEND'; |
|
31b7f2792
|
46 |
|
|
03e52840d
|
47 |
/** |
|
6d9380f96
|
48 |
* return encryption mode client or server side encryption |
|
03e52840d
|
49 50 |
* @param string $user name (use system wide setting if name=null) * @return string 'client' or 'server' |
|
6d9380f96
|
51 |
* @note at the moment we only support server side encryption |
|
03e52840d
|
52 53 54 55 56 57 58 59 |
*/
public static function mode($user = null) {
return 'server';
}
/**
|
|
6d9380f96
|
60 |
* Create a new encryption keypair |
|
03e52840d
|
61 62 63 64 65 66 |
* @return array publicKey, privatekey
*/
public static function createKeypair() {
$return = false;
|
|
31b7f2792
|
67 |
$res = Helper::getOpenSSLPkey(); |
|
03e52840d
|
68 69 70 71 72 73 |
if ($res === false) {
\OCP\Util::writeLog('Encryption library', 'couldn\'t generate users key-pair for ' . \OCP\User::getUser(), \OCP\Util::ERROR);
while ($msg = openssl_error_string()) {
\OCP\Util::writeLog('Encryption library', 'openssl_pkey_new() fails: ' . $msg, \OCP\Util::ERROR);
}
|
|
31b7f2792
|
74 |
} elseif (openssl_pkey_export($res, $privateKey, null, Helper::getOpenSSLConfig())) {
|
|
03e52840d
|
75 76 77 78 79 80 81 82 83 84 |
// Get public key
$keyDetails = openssl_pkey_get_details($res);
$publicKey = $keyDetails['key'];
$return = array(
'publicKey' => $publicKey,
'privateKey' => $privateKey
);
} else {
\OCP\Util::writeLog('Encryption library', 'couldn\'t export users private key, please check your servers openSSL configuration.' . \OCP\User::getUser(), \OCP\Util::ERROR);
|
|
31b7f2792
|
85 86 87 |
while($errMsg = openssl_error_string()) {
\OCP\Util::writeLog('Encryption library', $errMsg, \OCP\Util::ERROR);
}
|
|
03e52840d
|
88 89 90 91 92 93 |
} return $return; } /** |
|
6d9380f96
|
94 |
* Add arbitrary padding to encrypted data |
|
03e52840d
|
95 96 97 98 99 100 101 |
* @param string $data data to be padded * @return string padded data * @note In order to end up with data exactly 8192 bytes long we must * add two letters. It is impossible to achieve exactly 8192 length * blocks with encryption alone, hence padding is added to achieve the * required length. */ |
|
31b7f2792
|
102 |
private static function addPadding($data) {
|
|
03e52840d
|
103 104 105 106 107 108 109 110 |
$padded = $data . 'xx'; return $padded; } /** |
|
6d9380f96
|
111 |
* Remove arbitrary padding to encrypted data |
|
03e52840d
|
112 113 114 |
* @param string $padded padded data to remove padding from * @return string unpadded data on success, false on error */ |
|
31b7f2792
|
115 |
private static function removePadding($padded) {
|
|
03e52840d
|
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
if (substr($padded, -2) === 'xx') {
$data = substr($padded, 0, -2);
return $data;
} else {
// TODO: log the fact that unpadded data was submitted for removal of padding
return false;
}
}
/**
|
|
6d9380f96
|
133 134 |
* Check if a file's contents contains an IV and is symmetrically encrypted * @param string $content |
|
03e52840d
|
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 |
* @return boolean
* @note see also OCA\Encryption\Util->isEncryptedPath()
*/
public static function isCatfileContent($content) {
if (!$content) {
return false;
}
$noPadding = self::removePadding($content);
// Fetch encryption metadata from end of file
$meta = substr($noPadding, -22);
// Fetch IV from end of file
$iv = substr($meta, -16);
// Fetch identifier from start of metadata
$identifier = substr($meta, 0, 6);
if ($identifier === '00iv00') {
return true;
} else {
return false;
}
}
/**
* Check if a file is encrypted according to database file cache
* @param string $path
* @return bool
*/
public static function isEncryptedMeta($path) {
// TODO: Use DI to get \OC\Files\Filesystem out of here
// Fetch all file metadata from DB
$metadata = \OC\Files\Filesystem::getFileInfo($path);
// Return encryption status
return isset($metadata['encrypted']) && ( bool )$metadata['encrypted'];
}
/**
|
|
6d9380f96
|
187 188 |
* Check if a file is encrypted via legacy system * @param boolean $isCatFileContent |
|
03e52840d
|
189 190 191 192 193 194 195 196 197 |
* @param string $relPath The path of the file, relative to user/data;
* e.g. filename or /Docs/filename, NOT admin/files/filename
* @return boolean
*/
public static function isLegacyEncryptedContent($isCatFileContent, $relPath) {
// Fetch all file metadata from DB
$metadata = \OC\Files\Filesystem::getFileInfo($relPath, '');
|
|
31b7f2792
|
198 199 |
// If a file is flagged with encryption in DB, but isn't a // valid content + IV combination, it's probably using the |
|
03e52840d
|
200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 |
// legacy encryption system
if (isset($metadata['encrypted'])
&& $metadata['encrypted'] === true
&& $isCatFileContent === false
) {
return true;
} else {
return false;
}
}
/**
|
|
6d9380f96
|
217 218 219 |
* Symmetrically encrypt a string * @param string $plainContent * @param string $iv |
|
03e52840d
|
220 |
* @param string $passphrase |
|
f7d878ff1
|
221 |
* @param string $cypher used for encryption, currently we support AES-128-CFB and AES-256-CFB |
|
03e52840d
|
222 |
* @return string encrypted file content |
|
f7d878ff1
|
223 |
* @throws \OCA\Encryption\Exceptions\EncryptionException |
|
03e52840d
|
224 |
*/ |
|
f7d878ff1
|
225 |
private static function encrypt($plainContent, $iv, $passphrase = '', $cipher = Crypt::DEFAULT_CIPHER) {
|
|
03e52840d
|
226 |
|
|
f7d878ff1
|
227 |
$encryptedContent = openssl_encrypt($plainContent, $cipher, $passphrase, false, $iv); |
|
03e52840d
|
228 |
|
|
f7d878ff1
|
229 230 231 232 |
if (!$encryptedContent) {
$error = "Encryption (symmetric) of content failed: " . openssl_error_string();
\OCP\Util::writeLog('Encryption library', $error, \OCP\Util::ERROR);
throw new Exceptions\EncryptionException($error, 50);
|
|
03e52840d
|
233 234 |
} |
|
f7d878ff1
|
235 236 |
return $encryptedContent; |
|
03e52840d
|
237 238 239 |
} /** |
|
6d9380f96
|
240 241 242 243 |
* Symmetrically decrypt a string * @param string $encryptedContent * @param string $iv * @param string $passphrase |
|
f7d878ff1
|
244 |
* @param string $cipher cipher user for decryption, currently we support aes128 and aes256 |
|
03e52840d
|
245 246 247 |
* @throws \Exception * @return string decrypted file content */ |
|
f7d878ff1
|
248 |
private static function decrypt($encryptedContent, $iv, $passphrase, $cipher = Crypt::DEFAULT_CIPHER) {
|
|
03e52840d
|
249 |
|
|
f7d878ff1
|
250 |
$plainContent = openssl_decrypt($encryptedContent, $cipher, $passphrase, false, $iv); |
|
03e52840d
|
251 |
|
|
f7d878ff1
|
252 |
if ($plainContent) {
|
|
03e52840d
|
253 |
return $plainContent; |
|
03e52840d
|
254 |
} else {
|
|
03e52840d
|
255 |
throw new \Exception('Encryption library: Decryption (symmetric) of content failed');
|
|
03e52840d
|
256 257 258 259 260 |
} } /** |
|
6d9380f96
|
261 |
* Concatenate encrypted data with its IV and padding |
|
03e52840d
|
262 263 |
* @param string $content content to be concatenated * @param string $iv IV to be concatenated |
|
6d9380f96
|
264 |
* @return string concatenated content |
|
03e52840d
|
265 |
*/ |
|
31b7f2792
|
266 |
private static function concatIv($content, $iv) {
|
|
03e52840d
|
267 268 269 270 271 272 273 274 |
$combined = $content . '00iv00' . $iv; return $combined; } /** |
|
6d9380f96
|
275 |
* Split concatenated data and IV into respective parts |
|
03e52840d
|
276 |
* @param string $catFile concatenated data to be split |
|
6d9380f96
|
277 |
* @return array keys: encrypted, iv |
|
03e52840d
|
278 |
*/ |
|
31b7f2792
|
279 |
private static function splitIv($catFile) {
|
|
03e52840d
|
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 |
// Fetch encryption metadata from end of file $meta = substr($catFile, -22); // Fetch IV from end of file $iv = substr($meta, -16); // Remove IV and IV identifier text to expose encrypted content $encrypted = substr($catFile, 0, -22); $split = array( 'encrypted' => $encrypted, 'iv' => $iv ); return $split; } /** |
|
6d9380f96
|
300 |
* Symmetrically encrypts a string and returns keyfile content |
|
03e52840d
|
301 302 |
* @param string $plainContent content to be encrypted in keyfile * @param string $passphrase |
|
f7d878ff1
|
303 |
* @param string $cypher used for encryption, currently we support AES-128-CFB and AES-256-CFB |
|
6d9380f96
|
304 |
* @return false|string encrypted content combined with IV |
|
03e52840d
|
305 306 307 |
* @note IV need not be specified, as it will be stored in the returned keyfile * and remain accessible therein. */ |
|
f7d878ff1
|
308 |
public static function symmetricEncryptFileContent($plainContent, $passphrase = '', $cipher = Crypt::DEFAULT_CIPHER) {
|
|
03e52840d
|
309 310 311 312 313 314 315 316 |
if (!$plainContent) {
\OCP\Util::writeLog('Encryption library', 'symmetrically encryption failed, no content given.', \OCP\Util::ERROR);
return false;
}
$iv = self::generateIv();
|
|
f7d878ff1
|
317 318 |
try {
$encryptedContent = self::encrypt($plainContent, $iv, $passphrase, $cipher);
|
|
03e52840d
|
319 320 321 322 323 |
// Combine content to encrypt with IV identifier and actual IV $catfile = self::concatIv($encryptedContent, $iv); $padded = self::addPadding($catfile); return $padded; |
|
f7d878ff1
|
324 325 326 |
} catch (OCA\Encryption\Exceptions\EncryptionException $e) {
$message = 'Could not encrypt file content (code: ' . $e->getCode . '): ';
\OCP\Util::writeLog('files_encryption', $message . $e->getMessage, \OCP\Util::ERROR);
|
|
03e52840d
|
327 328 329 330 331 332 333 |
return false; } } /** |
|
6d9380f96
|
334 335 |
* Symmetrically decrypts keyfile content * @param string $keyfileContent |
|
03e52840d
|
336 |
* @param string $passphrase |
|
f7d878ff1
|
337 |
* @param string $cipher cipher used for decryption, currently aes128 and aes256 is supported. |
|
03e52840d
|
338 |
* @throws \Exception |
|
6d9380f96
|
339 |
* @return string|false |
|
03e52840d
|
340 341 342 |
* @internal param string $source * @internal param string $target * @internal param string $key the decryption key |
|
6d9380f96
|
343 |
* @return string decrypted content |
|
03e52840d
|
344 345 346 |
* * This function decrypts a file */ |
|
f7d878ff1
|
347 |
public static function symmetricDecryptFileContent($keyfileContent, $passphrase = '', $cipher = Crypt::DEFAULT_CIPHER) {
|
|
03e52840d
|
348 349 350 351 352 353 354 355 356 357 358 359 360 |
if (!$keyfileContent) {
throw new \Exception('Encryption library: no data provided for decryption');
}
// Remove padding
$noPadding = self::removePadding($keyfileContent);
// Split into enc data and catfile
$catfile = self::splitIv($noPadding);
|
|
f7d878ff1
|
361 |
if ($plainContent = self::decrypt($catfile['encrypted'], $catfile['iv'], $passphrase, $cipher)) {
|
|
03e52840d
|
362 363 364 365 366 367 368 369 370 371 |
return $plainContent;
} else {
return false;
}
}
/**
|
|
6d9380f96
|
372 |
* Decrypt private key and check if the result is a valid keyfile |
|
f7d878ff1
|
373 |
* |
|
03e52840d
|
374 375 |
* @param string $encryptedKey encrypted keyfile * @param string $passphrase to decrypt keyfile |
|
6d9380f96
|
376 |
* @return string|false encrypted private key or false |
|
03e52840d
|
377 378 379 380 381 |
*
* This function decrypts a file
*/
public static function decryptPrivateKey($encryptedKey, $passphrase) {
|
|
f7d878ff1
|
382 383 384 385 386 387 388 389 390 |
$header = self::parseHeader($encryptedKey);
$cipher = self::getCipher($header);
// if we found a header we need to remove it from the key we want to decrypt
if (!empty($header)) {
$encryptedKey = substr($encryptedKey, strpos($encryptedKey, self::HEADEREND) + strlen(self::HEADEREND));
}
$plainKey = self::symmetricDecryptFileContent($encryptedKey, $passphrase, $cipher);
|
|
03e52840d
|
391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 |
// check if this a valid private key
$res = openssl_pkey_get_private($plainKey);
if (is_resource($res)) {
$sslInfo = openssl_pkey_get_details($res);
if (!isset($sslInfo['key'])) {
$plainKey = false;
}
} else {
$plainKey = false;
}
return $plainKey;
}
|
|
03e52840d
|
407 |
/** |
|
6d9380f96
|
408 |
* Create asymmetrically encrypted keyfile content using a generated key |
|
03e52840d
|
409 410 |
* @param string $plainContent content to be encrypted * @param array $publicKeys array keys must be the userId of corresponding user |
|
6d9380f96
|
411 412 |
* @return array keys: keys (array, key = userId), data * @throws \OCA\Encryption\Exceptions\\MultiKeyEncryptException if encryption failed |
|
03e52840d
|
413 414 415 416 |
* @note symmetricDecryptFileContent() can decrypt files created using this method
*/
public static function multiKeyEncrypt($plainContent, array $publicKeys) {
|
|
31b7f2792
|
417 |
// openssl_seal returns false without errors if $plainContent |
|
03e52840d
|
418 419 |
// is empty, so trigger our own error
if (empty($plainContent)) {
|
|
6d9380f96
|
420 |
throw new Exceptions\MultiKeyEncryptException('Cannot mutliKeyEncrypt empty plain content', 10);
|
|
03e52840d
|
421 422 423 424 425 426 427 428 429 430 431 |
}
// Set empty vars to be set by openssl by reference
$sealed = '';
$shareKeys = array();
$mappedShareKeys = array();
if (openssl_seal($plainContent, $sealed, $shareKeys, $publicKeys)) {
$i = 0;
|
|
31b7f2792
|
432 |
// Ensure each shareKey is labelled with its |
|
03e52840d
|
433 434 435 436 437 438 439 440 441 442 443 444 445 446 |
// corresponding userId
foreach ($publicKeys as $userId => $publicKey) {
$mappedShareKeys[$userId] = $shareKeys[$i];
$i++;
}
return array(
'keys' => $mappedShareKeys,
'data' => $sealed
);
} else {
|
|
6d9380f96
|
447 |
throw new Exceptions\MultiKeyEncryptException('multi key encryption failed: ' . openssl_error_string(), 20);
|
|
03e52840d
|
448 449 450 451 452 |
} } /** |
|
6d9380f96
|
453 454 455 456 457 458 459 |
* Asymmetrically encrypt a file using multiple public keys * @param string $encryptedContent * @param string $shareKey * @param mixed $privateKey * @throws \OCA\Encryption\Exceptions\\MultiKeyDecryptException if decryption failed * @internal param string $plainContent contains decrypted content * @return string $plainContent decrypted string |
|
03e52840d
|
460 461 462 463 464 465 466 |
* @note symmetricDecryptFileContent() can be used to decrypt files created using this method
*
* This function decrypts a file
*/
public static function multiKeyDecrypt($encryptedContent, $shareKey, $privateKey) {
if (!$encryptedContent) {
|
|
6d9380f96
|
467 |
throw new Exceptions\MultiKeyDecryptException('Cannot mutliKeyDecrypt empty plain content', 10);
|
|
03e52840d
|
468 469 470 471 472 473 474 |
}
if (openssl_open($encryptedContent, $plainContent, $shareKey, $privateKey)) {
return $plainContent;
} else {
|
|
6d9380f96
|
475 |
throw new Exceptions\MultiKeyDecryptException('multiKeyDecrypt with share-key' . $shareKey . 'failed: ' . openssl_error_string(), 20);
|
|
03e52840d
|
476 477 478 479 480 |
} } /** |
|
6d9380f96
|
481 |
* Generates a pseudo random initialisation vector |
|
03e52840d
|
482 483 |
* @return String $iv generated IV */ |
|
31b7f2792
|
484 |
private static function generateIv() {
|
|
03e52840d
|
485 486 487 488 489 490 491 492 493 494 |
if ($random = openssl_random_pseudo_bytes(12, $strong)) {
if (!$strong) {
// If OpenSSL indicates randomness is insecure, log error
\OCP\Util::writeLog('Encryption library', 'Insecure symmetric key was generated using openssl_random_pseudo_bytes()', \OCP\Util::WARN);
}
|
|
31b7f2792
|
495 |
// We encode the iv purely for string manipulation |
|
03e52840d
|
496 497 498 499 500 501 502 503 504 505 506 507 508 509 |
// purposes - it gets decoded before use
$iv = base64_encode($random);
return $iv;
} else {
throw new \Exception('Generating IV failed');
}
}
/**
|
|
6d9380f96
|
510 511 |
* Generate a pseudo random 256-bit ASCII key, used as file key * @return string|false Generated key |
|
03e52840d
|
512 513 514 515 |
*/
public static function generateKey() {
// Generate key
|
|
6d9380f96
|
516 |
if ($key = base64_encode(openssl_random_pseudo_bytes(32, $strong))) {
|
|
03e52840d
|
517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 |
if (!$strong) {
// If OpenSSL indicates randomness is insecure, log error
throw new \Exception('Encryption library, Insecure symmetric key was generated using openssl_random_pseudo_bytes()');
}
return $key;
} else {
return false;
}
}
/**
|
|
6d9380f96
|
536 537 |
* Get the blowfish encryption handler for a key * @param string $key (optional) |
|
03e52840d
|
538 539 |
* @return \Crypt_Blowfish blowfish object * |
|
31b7f2792
|
540 |
* if the key is left out, the default handler will be used |
|
03e52840d
|
541 |
*/ |
|
31b7f2792
|
542 |
private static function getBlowfish($key = '') {
|
|
03e52840d
|
543 544 545 |
if ($key) {
|
|
6d9380f96
|
546 |
return new \Legacy_Crypt_Blowfish($key); |
|
03e52840d
|
547 548 549 550 551 552 553 554 555 556 |
} else {
return false;
}
}
/**
|
|
6d9380f96
|
557 |
* decrypts content using legacy blowfish system |
|
03e52840d
|
558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 |
* @param string $content the cleartext message you want to decrypt
* @param string $passphrase
* @return string cleartext content
*
* This function decrypts an content
*/
public static function legacyDecrypt($content, $passphrase = '') {
$bf = self::getBlowfish($passphrase);
$decrypted = $bf->decrypt($content);
return $decrypted;
}
/**
|
|
6d9380f96
|
574 |
* @param string $data |
|
03e52840d
|
575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 |
* @param string $key
* @param int $maxLength
* @return string
*/
public static function legacyBlockDecrypt($data, $key = '', $maxLength = 0) {
$result = '';
while (strlen($data)) {
$result .= self::legacyDecrypt(substr($data, 0, 8192), $key);
$data = substr($data, 8192);
}
if ($maxLength > 0) {
return substr($result, 0, $maxLength);
} else {
return rtrim($result, "\0");
}
}
|
|
f7d878ff1
|
593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 |
/**
* read header into array
*
* @param string $data
* @return array
*/
public static function parseHeader($data) {
$result = array();
if (substr($data, 0, strlen(self::HEADERSTART)) === self::HEADERSTART) {
$endAt = strpos($data, self::HEADEREND);
$header = substr($data, 0, $endAt + strlen(self::HEADEREND));
// +1 to not start with an ':' which would result in empty element at the beginning
$exploded = explode(':', substr($header, strlen(self::HEADERSTART)+1));
$element = array_shift($exploded);
while ($element !== self::HEADEREND) {
$result[$element] = array_shift($exploded);
$element = array_shift($exploded);
}
}
return $result;
}
/**
* check if data block is the header
*
* @param string $data
* @return boolean
*/
public static function isHeader($data) {
if (substr($data, 0, strlen(self::HEADERSTART)) === self::HEADERSTART) {
return true;
}
return false;
}
/**
* get chiper from header
*
* @param array $header
* @throws \OCA\Encryption\Exceptions\EncryptionException
*/
public static function getCipher($header) {
$cipher = isset($header['cipher']) ? $header['cipher'] : 'AES-128-CFB';
if ($cipher !== 'AES-256-CFB' && $cipher !== 'AES-128-CFB') {
throw new \OCA\Encryption\Exceptions\EncryptionException('file header broken, no supported cipher defined', 40);
}
return $cipher;
}
/**
* generate header for encrypted file
*/
public static function generateHeader() {
$cipher = Helper::getCipher();
$header = self::HEADERSTART . ':cipher:' . $cipher . ':' . self::HEADEREND;
return $header;
}
|
|
31b7f2792
|
665 |
} |