diff options
Diffstat (limited to 'apps/encryption/lib/Crypto/Encryption.php')
-rw-r--r-- | apps/encryption/lib/Crypto/Encryption.php | 229 |
1 files changed, 92 insertions, 137 deletions
diff --git a/apps/encryption/lib/Crypto/Encryption.php b/apps/encryption/lib/Crypto/Encryption.php index 58cefcbe087..6d388624e48 100644 --- a/apps/encryption/lib/Crypto/Encryption.php +++ b/apps/encryption/lib/Crypto/Encryption.php @@ -1,48 +1,23 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Björn Schießle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Clark Tomlinson <fallen013@gmail.com> - * @author Jan-Christoph Borchardt <hey@jancborchardt.net> - * @author Joas Schilling <coding@schilljs.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Valdnet <47037905+Valdnet@users.noreply.github.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Encryption\Crypto; use OC\Encryption\Exceptions\DecryptionFailedException; use OC\Files\Cache\Scanner; use OC\Files\View; +use OCA\Encryption\Exceptions\MultiKeyEncryptException; use OCA\Encryption\Exceptions\PublicKeyMissingException; use OCA\Encryption\KeyManager; use OCA\Encryption\Session; use OCA\Encryption\Util; use OCP\Encryption\IEncryptionModule; use OCP\IL10N; -use OCP\ILogger; +use Psr\Log\LoggerInterface; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -50,11 +25,6 @@ class Encryption implements IEncryptionModule { public const ID = 'OC_DEFAULT_MODULE'; public const DISPLAY_NAME = 'Default encryption module'; - /** - * @var Crypt - */ - private $crypt; - /** @var string */ private $cipher; @@ -64,8 +34,7 @@ class Encryption implements IEncryptionModule { /** @var string */ private $user; - /** @var array */ - private $owner; + private array $owner; /** @var string */ private $fileKey; @@ -73,78 +42,34 @@ class Encryption implements IEncryptionModule { /** @var string */ private $writeCache; - /** @var KeyManager */ - private $keyManager; - /** @var array */ private $accessList; /** @var boolean */ private $isWriteOperation; - /** @var Util */ - private $util; - - /** @var Session */ - private $session; - - /** @var ILogger */ - private $logger; - - /** @var IL10N */ - private $l; - - /** @var EncryptAll */ - private $encryptAll; - - /** @var bool */ - private $useMasterPassword; - - /** @var DecryptAll */ - private $decryptAll; + private bool $useMasterPassword; - /** @var int unencrypted block size if block contains signature */ - private $unencryptedBlockSizeSigned = 6072; - - /** @var int unencrypted block size */ - private $unencryptedBlockSize = 6126; + private bool $useLegacyBase64Encoding = false; /** @var int Current version of the file */ - private $version = 0; + private int $version = 0; /** @var array remember encryption signature version */ private static $rememberVersion = []; - - /** - * - * @param Crypt $crypt - * @param KeyManager $keyManager - * @param Util $util - * @param Session $session - * @param EncryptAll $encryptAll - * @param DecryptAll $decryptAll - * @param ILogger $logger - * @param IL10N $il10n - */ - public function __construct(Crypt $crypt, - KeyManager $keyManager, - Util $util, - Session $session, - EncryptAll $encryptAll, - DecryptAll $decryptAll, - ILogger $logger, - IL10N $il10n) { - $this->crypt = $crypt; - $this->keyManager = $keyManager; - $this->util = $util; - $this->session = $session; - $this->encryptAll = $encryptAll; - $this->decryptAll = $decryptAll; - $this->logger = $logger; - $this->l = $il10n; + public function __construct( + private Crypt $crypt, + private KeyManager $keyManager, + private Util $util, + private Session $session, + private EncryptAll $encryptAll, + private DecryptAll $decryptAll, + private LoggerInterface $logger, + private IL10N $l, + ) { $this->owner = []; - $this->useMasterPassword = $util->isMasterKeyEnabled(); + $this->useMasterPassword = $this->util->isMasterKeyEnabled(); } /** @@ -175,8 +100,8 @@ class Encryption implements IEncryptionModule { * @param array $accessList who has access to the file contains the key 'users' and 'public' * * @return array $header contain data as key-value pairs which should be - * written to the header, in case of a write operation - * or if no additional data is needed return a empty array + * written to the header, in case of a write operation + * or if no additional data is needed return a empty array */ public function begin($path, $user, $mode, array $header, array $accessList) { $this->path = $this->getPathToRealFile($path); @@ -184,6 +109,12 @@ class Encryption implements IEncryptionModule { $this->user = $user; $this->isWriteOperation = false; $this->writeCache = ''; + $this->useLegacyBase64Encoding = true; + + + if (isset($header['encoding'])) { + $this->useLegacyBase64Encoding = $header['encoding'] !== Crypt::BINARY_ENCODING_FORMAT; + } if ($this->session->isReady() === false) { // if the master key is enabled we can initialize encryption @@ -193,15 +124,10 @@ class Encryption implements IEncryptionModule { } } - if ($this->session->decryptAllModeActivated()) { - $encryptedFileKey = $this->keyManager->getEncryptedFileKey($this->path); - $shareKey = $this->keyManager->getShareKey($this->path, $this->session->getDecryptAllUid()); - $this->fileKey = $this->crypt->multiKeyDecrypt($encryptedFileKey, - $shareKey, - $this->session->getDecryptAllKey()); - } else { - $this->fileKey = $this->keyManager->getFileKey($this->path, $this->user); - } + /* If useLegacyFileKey is not specified in header, auto-detect, to be safe */ + $useLegacyFileKey = (($header['useLegacyFileKey'] ?? '') == 'false' ? false : null); + + $this->fileKey = $this->keyManager->getFileKey($this->path, $this->user, $useLegacyFileKey, $this->session->decryptAllModeActivated()); // always use the version from the original file, also part files // need to have a correct version number if they get moved over to the @@ -229,6 +155,7 @@ class Encryption implements IEncryptionModule { if ($this->isWriteOperation) { $this->cipher = $this->crypt->getCipher(); + $this->useLegacyBase64Encoding = $this->crypt->useLegacyBase64Encoding(); } elseif (isset($header['cipher'])) { $this->cipher = $header['cipher']; } else { @@ -237,7 +164,17 @@ class Encryption implements IEncryptionModule { $this->cipher = $this->crypt->getLegacyCipher(); } - return ['cipher' => $this->cipher, 'signed' => 'true']; + $result = [ + 'cipher' => $this->cipher, + 'signed' => 'true', + 'useLegacyFileKey' => 'false', + ]; + + if ($this->useLegacyBase64Encoding !== true) { + $result['encoding'] = Crypt::BINARY_ENCODING_FORMAT; + } + + return $result; } /** @@ -246,14 +183,14 @@ class Encryption implements IEncryptionModule { * buffer. * * @param string $path to the file - * @param int $position + * @param string $position * @return string remained data which should be written to the file in case * of a write operation * @throws PublicKeyMissingException * @throws \Exception - * @throws \OCA\Encryption\Exceptions\MultiKeyEncryptException + * @throws MultiKeyEncryptException */ - public function end($path, $position = 0) { + public function end($path, $position = '0') { $result = ''; if ($this->isWriteOperation) { // in case of a part file we remember the new signature versions @@ -288,10 +225,18 @@ class Encryption implements IEncryptionModule { } $publicKeys = $this->keyManager->addSystemKeys($this->accessList, $publicKeys, $this->getOwner($path)); - $encryptedKeyfiles = $this->crypt->multiKeyEncrypt($this->fileKey, $publicKeys); - $this->keyManager->setAllFileKeys($this->path, $encryptedKeyfiles); + $shareKeys = $this->crypt->multiKeyEncrypt($this->fileKey, $publicKeys); + if (!$this->keyManager->deleteLegacyFileKey($this->path)) { + $this->logger->warning( + 'Failed to delete legacy filekey for {path}', + ['app' => 'encryption', 'path' => $path] + ); + } + foreach ($shareKeys as $uid => $keyFile) { + $this->keyManager->setShareKey($this->path, $uid, $keyFile); + } } - return $result; + return $result ?: ''; } @@ -307,7 +252,6 @@ class Encryption implements IEncryptionModule { // If extra data is left over from the last round, make sure it // is integrated into the next block if ($this->writeCache) { - // Concat writeCache to start of $data $data = $this->writeCache . $data; @@ -319,15 +263,13 @@ class Encryption implements IEncryptionModule { $encrypted = ''; // While there still remains some data to be processed & written while (strlen($data) > 0) { - // Remaining length for this iteration, not of the // entire file (may be greater than 8192 bytes) $remainingLength = strlen($data); // If data remaining to be written is less than the - // size of 1 6126 byte block - if ($remainingLength < $this->unencryptedBlockSizeSigned) { - + // size of 1 unencrypted block + if ($remainingLength < $this->getUnencryptedBlockSize(true)) { // Set writeCache to contents of $data // The writeCache will be carried over to the // next write round, and added to the start of @@ -341,16 +283,15 @@ class Encryption implements IEncryptionModule { // Clear $data ready for next round $data = ''; } else { - // Read the chunk from the start of $data - $chunk = substr($data, 0, $this->unencryptedBlockSizeSigned); + $chunk = substr($data, 0, $this->getUnencryptedBlockSize(true)); - $encrypted .= $this->crypt->symmetricEncryptFileContent($chunk, $this->fileKey, $this->version + 1, $position); + $encrypted .= $this->crypt->symmetricEncryptFileContent($chunk, $this->fileKey, $this->version + 1, (string)$position); // Remove the chunk we just processed from // $data, leaving only unprocessed data in $data // var, for handling on the next round - $data = substr($data, $this->unencryptedBlockSizeSigned); + $data = substr($data, $this->getUnencryptedBlockSize(true)); } } @@ -374,7 +315,7 @@ class Encryption implements IEncryptionModule { throw new DecryptionFailedException($msg, $hint); } - return $this->crypt->symmetricDecryptFileContent($data, $this->fileKey, $this->cipher, $this->version, $position); + return $this->crypt->symmetricDecryptFileContent($data, $this->fileKey, $this->cipher, $this->version, $position, !$this->useLegacyBase64Encoding); } /** @@ -383,7 +324,7 @@ class Encryption implements IEncryptionModule { * @param string $path path to the file which should be updated * @param string $uid of the user who performs the operation * @param array $accessList who has access to the file contains the key 'users' and 'public' - * @return boolean + * @return bool */ public function update($path, $uid, array $accessList) { if (empty($accessList)) { @@ -391,10 +332,10 @@ class Encryption implements IEncryptionModule { $this->keyManager->setVersion($path, self::$rememberVersion[$path], new View()); unset(self::$rememberVersion[$path]); } - return; + return false; } - $fileKey = $this->keyManager->getFileKey($path, $uid); + $fileKey = $this->keyManager->getFileKey($path, $uid, null); if (!empty($fileKey)) { $publicKeys = []; @@ -412,11 +353,13 @@ class Encryption implements IEncryptionModule { $publicKeys = $this->keyManager->addSystemKeys($accessList, $publicKeys, $this->getOwner($path)); - $encryptedFileKey = $this->crypt->multiKeyEncrypt($fileKey, $publicKeys); + $shareKeys = $this->crypt->multiKeyEncrypt($fileKey, $publicKeys); $this->keyManager->deleteAllFileKeys($path); - $this->keyManager->setAllFileKeys($path, $encryptedFileKey); + foreach ($shareKeys as $uid => $keyFile) { + $this->keyManager->setShareKey($path, $uid, $keyFile); + } } else { $this->logger->debug('no file key found, we assume that the file "{file}" is not encrypted', ['file' => $path, 'app' => 'encryption']); @@ -462,15 +405,27 @@ class Encryption implements IEncryptionModule { * get size of the unencrypted payload per block. * Nextcloud read/write files with a block size of 8192 byte * + * Encrypted blocks have a 22-byte IV and 2 bytes of padding, encrypted and + * signed blocks have also a 71-byte signature and 1 more byte of padding, + * resulting respectively in: + * + * 8192 - 22 - 2 = 8168 bytes in each unsigned unencrypted block + * 8192 - 22 - 2 - 71 - 1 = 8096 bytes in each signed unencrypted block + * + * Legacy base64 encoding then reduces the available size by a 3/4 factor: + * + * 8168 * (3/4) = 6126 bytes in each base64-encoded unsigned unencrypted block + * 8096 * (3/4) = 6072 bytes in each base64-encoded signed unencrypted block + * * @param bool $signed * @return int */ public function getUnencryptedBlockSize($signed = false) { - if ($signed === false) { - return $this->unencryptedBlockSize; + if ($this->useLegacyBase64Encoding) { + return $signed ? 6072 : 6126; + } else { + return $signed ? 8096 : 8168; } - - return $this->unencryptedBlockSizeSigned; } /** @@ -478,12 +433,12 @@ class Encryption implements IEncryptionModule { * e.g. if all encryption keys exists * * @param string $path - * @param string $uid user for whom we want to check if he can read the file + * @param string $uid user for whom we want to check if they can read the file * @return bool * @throws DecryptionFailedException */ public function isReadable($path, $uid) { - $fileKey = $this->keyManager->getFileKey($path, $uid); + $fileKey = $this->keyManager->getFileKey($path, $uid, null); if (empty($fileKey)) { $owner = $this->util->getOwner($path); if ($owner !== $uid) { @@ -491,8 +446,8 @@ class Encryption implements IEncryptionModule { // error message because in this case it means that the file was // shared with the user at a point where the user didn't had a // valid private/public key - $msg = 'Encryption module "' . $this->getDisplayName() . - '" is not able to read ' . $path; + $msg = 'Encryption module "' . $this->getDisplayName() + . '" is not able to read ' . $path; $hint = $this->l->t('Cannot read this file, probably this is a shared file. Please ask the file owner to reshare the file with you.'); $this->logger->warning($msg); throw new DecryptionFailedException($msg, $hint); |