diff options
-rw-r--r-- | apps/encryption/appinfo/application.php | 3 | ||||
-rw-r--r-- | apps/encryption/appinfo/register_command.php | 3 | ||||
-rw-r--r-- | apps/encryption/lib/crypto/crypt.php | 247 | ||||
-rw-r--r-- | apps/encryption/lib/crypto/encryption.php | 84 | ||||
-rw-r--r-- | apps/encryption/lib/keymanager.php | 34 | ||||
-rw-r--r-- | apps/encryption/settings/settings-admin.php | 3 | ||||
-rw-r--r-- | apps/encryption/settings/settings-personal.php | 3 | ||||
-rw-r--r-- | apps/encryption/tests/lib/KeyManagerTest.php | 67 | ||||
-rw-r--r-- | apps/encryption/tests/lib/crypto/cryptTest.php | 104 | ||||
-rw-r--r-- | apps/encryption/tests/lib/crypto/encryptionTest.php | 2 | ||||
-rw-r--r-- | apps/files_versions/lib/storage.php | 10 | ||||
-rw-r--r-- | lib/private/files/cache/cache.php | 9 | ||||
-rw-r--r-- | lib/private/files/fileinfo.php | 9 | ||||
-rw-r--r-- | lib/private/files/storage/wrapper/encryption.php | 16 | ||||
-rw-r--r-- | lib/private/files/stream/encryption.php | 31 | ||||
-rw-r--r-- | lib/public/encryption/iencryptionmodule.php | 7 | ||||
-rw-r--r-- | settings/changepassword/controller.php | 3 | ||||
-rw-r--r-- | tests/lib/files/stream/encryption.php | 1 |
18 files changed, 499 insertions, 137 deletions
diff --git a/apps/encryption/appinfo/application.php b/apps/encryption/appinfo/application.php index 433e9e86284..6d01d3e8353 100644 --- a/apps/encryption/appinfo/application.php +++ b/apps/encryption/appinfo/application.php @@ -131,7 +131,8 @@ class Application extends \OCP\AppFramework\App { $server = $c->getServer(); return new Crypt($server->getLogger(), $server->getUserSession(), - $server->getConfig()); + $server->getConfig(), + $server->getL10N($c->getAppName())); }); $container->registerService('Session', diff --git a/apps/encryption/appinfo/register_command.php b/apps/encryption/appinfo/register_command.php index 2bb49d55c2e..5f32718cdf0 100644 --- a/apps/encryption/appinfo/register_command.php +++ b/apps/encryption/appinfo/register_command.php @@ -25,11 +25,12 @@ use Symfony\Component\Console\Helper\QuestionHelper; $userManager = OC::$server->getUserManager(); $view = new \OC\Files\View(); $config = \OC::$server->getConfig(); +$l = \OC::$server->getL10N('encryption'); $userSession = \OC::$server->getUserSession(); $connection = \OC::$server->getDatabaseConnection(); $logger = \OC::$server->getLogger(); $questionHelper = new QuestionHelper(); -$crypt = new \OCA\Encryption\Crypto\Crypt($logger, $userSession, $config); +$crypt = new \OCA\Encryption\Crypto\Crypt($logger, $userSession, $config, $l); $util = new \OCA\Encryption\Util($view, $crypt, $logger, $userSession, $config, $userManager); $application->add(new MigrateKeys($userManager, $view, $connection, $config, $logger)); diff --git a/apps/encryption/lib/crypto/crypt.php b/apps/encryption/lib/crypto/crypt.php index e387380cd95..b4c10f42790 100644 --- a/apps/encryption/lib/crypto/crypt.php +++ b/apps/encryption/lib/crypto/crypt.php @@ -29,16 +29,32 @@ namespace OCA\Encryption\Crypto; use OC\Encryption\Exceptions\DecryptionFailedException; use OC\Encryption\Exceptions\EncryptionFailedException; +use OC\HintException; use OCA\Encryption\Exceptions\MultiKeyDecryptException; use OCA\Encryption\Exceptions\MultiKeyEncryptException; use OCP\Encryption\Exceptions\GenericEncryptionException; use OCP\IConfig; +use OCP\IL10N; use OCP\ILogger; use OCP\IUserSession; +/** + * Class Crypt provides the encryption implementation of the default ownCloud + * encryption module. As default AES-256-CTR is used, it does however offer support + * for the following modes: + * + * - AES-256-CTR + * - AES-128-CTR + * - AES-256-CFB + * - AES-128-CFB + * + * For integrity protection Encrypt-Then-MAC using HMAC-SHA256 is used. + * + * @package OCA\Encryption\Crypto + */ class Crypt { - const DEFAULT_CIPHER = 'AES-256-CFB'; + const DEFAULT_CIPHER = 'AES-256-CTR'; // default cipher from old ownCloud versions const LEGACY_CIPHER = 'AES-128-CFB'; @@ -48,33 +64,41 @@ class Crypt { const HEADER_START = 'HBEGIN'; const HEADER_END = 'HEND'; - /** - * @var ILogger - */ + + /** @var ILogger */ private $logger; - /** - * @var string - */ + + /** @var string */ private $user; - /** - * @var IConfig - */ + + /** @var IConfig */ private $config; - /** - * @var array - */ + /** @var array */ private $supportedKeyFormats; + /** @var IL10N */ + private $l; + + /** @var array */ + private $supportedCiphersAndKeySize = [ + 'AES-256-CTR' => 32, + 'AES-128-CTR' => 16, + 'AES-256-CFB' => 32, + 'AES-128-CFB' => 16, + ]; + /** * @param ILogger $logger * @param IUserSession $userSession * @param IConfig $config + * @param IL10N $l */ - public function __construct(ILogger $logger, IUserSession $userSession, IConfig $config) { + public function __construct(ILogger $logger, IUserSession $userSession, IConfig $config, IL10N $l) { $this->logger = $logger; $this->user = $userSession && $userSession->isLoggedIn() ? $userSession->getUser()->getUID() : '"no user given"'; $this->config = $config; + $this->l = $l; $this->supportedKeyFormats = ['hash', 'password']; } @@ -145,10 +169,12 @@ class Crypt { /** * @param string $plainContent * @param string $passPhrase + * @param int $version + * @param int $position * @return false|string - * @throws GenericEncryptionException + * @throws EncryptionFailedException */ - public function symmetricEncryptFileContent($plainContent, $passPhrase) { + public function symmetricEncryptFileContent($plainContent, $passPhrase, $version, $position) { if (!$plainContent) { $this->logger->error('Encryption Library, symmetrical encryption failed no content given', @@ -162,8 +188,13 @@ class Crypt { $iv, $passPhrase, $this->getCipher()); + + // Create a signature based on the key as well as the current version + $sig = $this->createSignature($encryptedContent, $passPhrase.$version.$position); + // combine content to encrypt the IV identifier and actual IV $catFile = $this->concatIV($encryptedContent, $iv); + $catFile = $this->concatSig($catFile, $sig); $padded = $this->addPadding($catFile); return $padded; @@ -225,8 +256,13 @@ class Crypt { */ public function getCipher() { $cipher = $this->config->getSystemValue('cipher', self::DEFAULT_CIPHER); - if ($cipher !== 'AES-256-CFB' && $cipher !== 'AES-128-CFB') { - $this->logger->warning('Wrong cipher defined in config.php only AES-128-CFB and AES-256-CFB are supported. Fall back' . self::DEFAULT_CIPHER, + if (!isset($this->supportedCiphersAndKeySize[$cipher])) { + $this->logger->warning( + sprintf( + 'Unsupported cipher (%s) defined in config.php supported. Falling back to %s', + $cipher, + self::DEFAULT_CIPHER + ), ['app' => 'encryption']); $cipher = self::DEFAULT_CIPHER; } @@ -237,19 +273,20 @@ class Crypt { /** * get key size depending on the cipher * - * @param string $cipher supported ('AES-256-CFB' and 'AES-128-CFB') + * @param string $cipher * @return int * @throws \InvalidArgumentException */ protected function getKeySize($cipher) { - if ($cipher === 'AES-256-CFB') { - return 32; - } else if ($cipher === 'AES-128-CFB') { - return 16; + if(isset($this->supportedCiphersAndKeySize[$cipher])) { + return $this->supportedCiphersAndKeySize[$cipher]; } throw new \InvalidArgumentException( - 'Wrong cipher defined only AES-128-CFB and AES-256-CFB are supported.' + sprintf( + 'Unsupported cipher (%s) defined.', + $cipher + ) ); } @@ -272,11 +309,24 @@ class Crypt { } /** + * @param string $encryptedContent + * @param string $signature + * @return string + */ + private function concatSig($encryptedContent, $signature) { + return $encryptedContent . '00sig00' . $signature; + } + + /** + * Note: This is _NOT_ a padding used for encryption purposes. It is solely + * used to achieve the PHP stream size. It has _NOTHING_ to do with the + * encrypted content and is not used in any crypto primitive. + * * @param string $data * @return string */ private function addPadding($data) { - return $data . 'xx'; + return $data . 'xxx'; } /** @@ -318,7 +368,9 @@ class Crypt { $hash = $this->generatePasswordHash($password, $cipher, $uid); $encryptedKey = $this->symmetricEncryptFileContent( $privateKey, - $hash + $hash, + 0, + 0 ); return $encryptedKey; @@ -357,9 +409,12 @@ class Crypt { self::HEADER_END) + strlen(self::HEADER_END)); } - $plainKey = $this->symmetricDecryptFileContent($privateKey, + $plainKey = $this->symmetricDecryptFileContent( + $privateKey, $password, - $cipher); + $cipher, + 0 + ); if ($this->isValidPrivateKey($plainKey) === false) { return false; @@ -390,14 +445,17 @@ class Crypt { * @param string $keyFileContents * @param string $passPhrase * @param string $cipher + * @param int $version + * @param int $position * @return string * @throws DecryptionFailedException */ - public function symmetricDecryptFileContent($keyFileContents, $passPhrase, $cipher = self::DEFAULT_CIPHER) { - // Remove Padding - $noPadding = $this->removePadding($keyFileContents); + public function symmetricDecryptFileContent($keyFileContents, $passPhrase, $cipher = self::DEFAULT_CIPHER, $version = 0, $position = 0) { + $catFile = $this->splitMetaData($keyFileContents, $cipher); - $catFile = $this->splitIv($noPadding); + if ($catFile['signature'] !== false) { + $this->checkSignature($catFile['encrypted'], $passPhrase.$version.$position, $catFile['signature']); + } return $this->decrypt($catFile['encrypted'], $catFile['iv'], @@ -406,42 +464,103 @@ class Crypt { } /** - * remove padding + * check for valid signature * - * @param $padded - * @return string|false + * @param string $data + * @param string $passPhrase + * @param string $expectedSignature + * @throws HintException */ - private function removePadding($padded) { - if (substr($padded, -2) === 'xx') { - return substr($padded, 0, -2); + private function checkSignature($data, $passPhrase, $expectedSignature) { + $signature = $this->createSignature($data, $passPhrase); + if (!hash_equals($expectedSignature, $signature)) { + throw new HintException('Bad Signature', $this->l->t('Bad Signature')); } - return false; } /** - * split iv from encrypted content + * create signature * - * @param string|false $catFile + * @param string $data + * @param string $passPhrase * @return string */ - private function splitIv($catFile) { - // Fetch encryption metadata from end of file - $meta = substr($catFile, -22); + private function createSignature($data, $passPhrase) { + $passPhrase = hash('sha512', $passPhrase . 'a', true); + $signature = hash_hmac('sha256', $data, $passPhrase); + return $signature; + } - // Fetch IV from end of file - $iv = substr($meta, -16); - // Remove IV and IV Identifier text to expose encrypted content + /** + * remove padding + * + * @param string $padded + * @param bool $hasSignature did the block contain a signature, in this case we use a different padding + * @return string|false + */ + private function removePadding($padded, $hasSignature = false) { + if ($hasSignature === false && substr($padded, -2) === 'xx') { + return substr($padded, 0, -2); + } elseif ($hasSignature === true && substr($padded, -3) === 'xxx') { + return substr($padded, 0, -3); + } + return false; + } - $encrypted = substr($catFile, 0, -22); + /** + * split meta data from encrypted file + * Note: for now, we assume that the meta data always start with the iv + * followed by the signature, if available + * + * @param string $catFile + * @param string $cipher + * @return array + */ + private function splitMetaData($catFile, $cipher) { + if ($this->hasSignature($catFile, $cipher)) { + $catFile = $this->removePadding($catFile, true); + $meta = substr($catFile, -93); + $iv = substr($meta, strlen('00iv00'), 16); + $sig = substr($meta, 22 + strlen('00sig00')); + $encrypted = substr($catFile, 0, -93); + } else { + $catFile = $this->removePadding($catFile); + $meta = substr($catFile, -22); + $iv = substr($meta, -16); + $sig = false; + $encrypted = substr($catFile, 0, -22); + } return [ 'encrypted' => $encrypted, - 'iv' => $iv + 'iv' => $iv, + 'signature' => $sig ]; } /** + * check if encrypted block is signed + * + * @param string $catFile + * @param string $cipher + * @return bool + * @throws HintException + */ + private function hasSignature($catFile, $cipher) { + $meta = substr($catFile, -93); + $signaturePosition = strpos($meta, '00sig00'); + + // enforce signature for the new 'CTR' ciphers + if ($signaturePosition === false && strpos(strtolower($cipher), 'ctr') !== false) { + throw new HintException('Missing Signature', $this->l->t('Missing Signature')); + } + + return ($signaturePosition !== false); + } + + + /** * @param string $encryptedContent * @param string $iv * @param string $passPhrase @@ -496,40 +615,18 @@ class Crypt { * @throws GenericEncryptionException */ private function generateIv() { - $random = openssl_random_pseudo_bytes(12, $strong); - if ($random) { - if (!$strong) { - // If OpenSSL indicates randomness is insecure log error - $this->logger->error('Encryption Library: Insecure symmetric key was generated using openssl_random_psudo_bytes()', - ['app' => 'encryption']); - } - - /* - * We encode the iv purely for string manipulation - * purposes -it gets decoded before use - */ - return base64_encode($random); - } - // If we ever get here we've failed anyway no need for an else - throw new GenericEncryptionException('Generating IV Failed'); + return random_bytes(16); } /** - * Generate a cryptographically secure pseudo-random base64 encoded 256-bit - * ASCII key, used as file key + * Generate a cryptographically secure pseudo-random 256-bit ASCII key, used + * as file key * * @return string * @throws \Exception */ public function generateFileKey() { - // Generate key - $key = base64_encode(openssl_random_pseudo_bytes(32, $strong)); - if (!$key || !$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; + return random_bytes(32); } /** diff --git a/apps/encryption/lib/crypto/encryption.php b/apps/encryption/lib/crypto/encryption.php index 3b66684a7f4..a637f52a869 100644 --- a/apps/encryption/lib/crypto/encryption.php +++ b/apps/encryption/lib/crypto/encryption.php @@ -29,6 +29,7 @@ namespace OCA\Encryption\Crypto; use OC\Encryption\Exceptions\DecryptionFailedException; +use OC\Files\View; use OCA\Encryption\Exceptions\PublicKeyMissingException; use OCA\Encryption\Session; use OCA\Encryption\Util; @@ -56,6 +57,9 @@ class Encryption implements IEncryptionModule { private $path; /** @var string */ + private $realPath; + + /** @var string */ private $user; /** @var string */ @@ -94,6 +98,16 @@ class Encryption implements IEncryptionModule { /** @var DecryptAll */ private $decryptAll; + /** @var int unencrypted block size if block contains signature */ + private $unencryptedBlockSizeSigned = 6072; + + /** @var int unencrypted block size */ + private $unencryptedBlockSize = 6126; + + /** @var int Current version of the file */ + private $version = 0; + + /** * * @param Crypt $crypt @@ -156,8 +170,8 @@ class Encryption implements IEncryptionModule { * 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); + $this->realPath = $path; $this->accessList = $accessList; $this->user = $user; $this->isWriteOperation = false; @@ -173,6 +187,8 @@ class Encryption implements IEncryptionModule { $this->fileKey = $this->keyManager->getFileKey($this->path, $this->user); } + $this->version = (int)$this->keyManager->getVersion($this->realPath, new View()); + if ( $mode === 'w' || $mode === 'w+' @@ -185,17 +201,17 @@ class Encryption implements IEncryptionModule { } } - if (isset($header['cipher'])) { - $this->cipher = $header['cipher']; - } elseif ($this->isWriteOperation) { + if ($this->isWriteOperation) { $this->cipher = $this->crypt->getCipher(); + } elseif (isset($header['cipher'])) { + $this->cipher = $header['cipher']; } else { // if we read a file without a header we fall-back to the legacy cipher // which was used in <=oC6 $this->cipher = $this->crypt->getLegacyCipher(); } - return array('cipher' => $this->cipher); + return array('cipher' => $this->cipher, 'signed' => 'true'); } /** @@ -204,17 +220,25 @@ class Encryption implements IEncryptionModule { * buffer. * * @param string $path to the file + * @param int $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 */ - public function end($path) { + public function end($path, $position = 0) { $result = ''; if ($this->isWriteOperation) { + // Partial files do not increase the version + if(\OC\Files\Cache\Scanner::isPartialFile($path)) { + $version = $this->version; + } else { + $version = $this->version + 1; + } + $this->keyManager->setVersion($this->path, $this->version+1, new View()); if (!empty($this->writeCache)) { - $result = $this->crypt->symmetricEncryptFileContent($this->writeCache, $this->fileKey); + $result = $this->crypt->symmetricEncryptFileContent($this->writeCache, $this->fileKey, $version, $position); $this->writeCache = ''; } $publicKeys = array(); @@ -248,12 +272,12 @@ class Encryption implements IEncryptionModule { * encrypt data * * @param string $data you want to encrypt + * @param int $position * @return string encrypted data */ - public function encrypt($data) { - + public function encrypt($data, $position = 0) { // If extra data is left over from the last round, make sure it - // is integrated into the next 6126 / 8192 block + // is integrated into the next block if ($this->writeCache) { // Concat writeCache to start of $data @@ -275,7 +299,7 @@ class Encryption implements IEncryptionModule { // If data remaining to be written is less than the // size of 1 6126 byte block - if ($remainingLength < 6126) { + if ($remainingLength < $this->unencryptedBlockSizeSigned) { // Set writeCache to contents of $data // The writeCache will be carried over to the @@ -293,14 +317,20 @@ class Encryption implements IEncryptionModule { } else { // Read the chunk from the start of $data - $chunk = substr($data, 0, 6126); + $chunk = substr($data, 0, $this->unencryptedBlockSizeSigned); - $encrypted .= $this->crypt->symmetricEncryptFileContent($chunk, $this->fileKey); + // Partial files do not increase the version + if(\OC\Files\Cache\Scanner::isPartialFile($this->path)) { + $version = $this->version; + } else { + $version = $this->version + 1; + } + $encrypted .= $this->crypt->symmetricEncryptFileContent($chunk, $this->fileKey, $version, $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, 6126); + $data = substr($data, $this->unencryptedBlockSizeSigned); } @@ -313,10 +343,11 @@ class Encryption implements IEncryptionModule { * decrypt data * * @param string $data you want to decrypt + * @param int $position * @return string decrypted data * @throws DecryptionFailedException */ - public function decrypt($data) { + public function decrypt($data, $position = 0) { if (empty($this->fileKey)) { $msg = 'Can not decrypt this file, probably this is a shared file. Please ask the file owner to reshare the file with you.'; $hint = $this->l->t('Can not decrypt this file, probably this is a shared file. Please ask the file owner to reshare the file with you.'); @@ -325,11 +356,7 @@ class Encryption implements IEncryptionModule { throw new DecryptionFailedException($msg, $hint); } - $result = ''; - if (!empty($data)) { - $result = $this->crypt->symmetricDecryptFileContent($data, $this->fileKey, $this->cipher); - } - return $result; + return $this->crypt->symmetricDecryptFileContent($data, $this->fileKey, $this->cipher, $this->version, $position); } /** @@ -342,6 +369,10 @@ class Encryption implements IEncryptionModule { */ public function update($path, $uid, array $accessList) { $fileKey = $this->keyManager->getFileKey($path, $uid); + if(empty($this->realPath)) { + $this->realPath = $path; + } + $version = $this->keyManager->getVersion($this->realPath, new View()); if (!empty($fileKey)) { @@ -362,6 +393,8 @@ class Encryption implements IEncryptionModule { $this->keyManager->setAllFileKeys($path, $encryptedFileKey); + $this->keyManager->setVersion($path, $version, new View()); + } else { $this->logger->debug('no file key found, we assume that the file "{file}" is not encrypted', array('file' => $path, 'app' => 'encryption')); @@ -407,10 +440,15 @@ class Encryption implements IEncryptionModule { * get size of the unencrypted payload per block. * ownCloud read/write files with a block size of 8192 byte * - * @return integer + * @param bool $signed + * @return int */ - public function getUnencryptedBlockSize() { - return 6126; + public function getUnencryptedBlockSize($signed = false) { + if ($signed === false) { + return $this->unencryptedBlockSize; + } + + return $this->unencryptedBlockSizeSigned; } /** diff --git a/apps/encryption/lib/keymanager.php b/apps/encryption/lib/keymanager.php index b6365cf2cce..0c957e12012 100644 --- a/apps/encryption/lib/keymanager.php +++ b/apps/encryption/lib/keymanager.php @@ -25,12 +25,14 @@ namespace OCA\Encryption; use OC\Encryption\Exceptions\DecryptionFailedException; +use OC\Files\View; use OCA\Encryption\Crypto\Encryption; use OCA\Encryption\Exceptions\PrivateKeyMissingException; use OCA\Encryption\Exceptions\PublicKeyMissingException; use OCA\Encryption\Crypto\Crypt; use OCP\Encryption\Keys\IStorage; use OCP\IConfig; +use OCP\IDBConnection; use OCP\ILogger; use OCP\IUserSession; @@ -413,6 +415,37 @@ class KeyManager { } /** + * Get the current version of a file + * + * @param string $path + * @param View $view + * @return int + */ + public function getVersion($path, View $view) { + $fileInfo = $view->getFileInfo($path); + if($fileInfo === false) { + return 0; + } + return $fileInfo->getEncryptedVersion(); + } + + /** + * Set the current version of a file + * + * @param string $path + * @param int $version + * @param View $view + */ + public function setVersion($path, $version, View $view) { + $fileInfo= $view->getFileInfo($path); + + if($fileInfo !== false) { + $cache = $fileInfo->getStorage()->getCache(); + $cache->update($fileInfo->getId(), ['encrypted' => $version, 'encryptedVersion' => $version]); + } + } + + /** * get the encrypted file key * * @param string $path @@ -546,6 +579,7 @@ class KeyManager { /** * @param string $path + * @return bool */ public function deleteAllFileKeys($path) { return $this->keyStorage->deleteAllFileKeys($path); diff --git a/apps/encryption/settings/settings-admin.php b/apps/encryption/settings/settings-admin.php index c3d523f27da..6c7c0987fd7 100644 --- a/apps/encryption/settings/settings-admin.php +++ b/apps/encryption/settings/settings-admin.php @@ -29,7 +29,8 @@ $tmpl = new OCP\Template('encryption', 'settings-admin'); $crypt = new \OCA\Encryption\Crypto\Crypt( \OC::$server->getLogger(), \OC::$server->getUserSession(), - \OC::$server->getConfig()); + \OC::$server->getConfig(), + \OC::$server->getL10N('encryption')); $util = new \OCA\Encryption\Util( new \OC\Files\View(), diff --git a/apps/encryption/settings/settings-personal.php b/apps/encryption/settings/settings-personal.php index 2dff5904850..0f6e9353707 100644 --- a/apps/encryption/settings/settings-personal.php +++ b/apps/encryption/settings/settings-personal.php @@ -28,7 +28,8 @@ $template = new OCP\Template('encryption', 'settings-personal'); $crypt = new \OCA\Encryption\Crypto\Crypt( \OC::$server->getLogger(), $userSession, - \OC::$server->getConfig()); + \OC::$server->getConfig(), + \OC::$server->getL10N('encryption')); $util = new \OCA\Encryption\Util( new \OC\Files\View(), diff --git a/apps/encryption/tests/lib/KeyManagerTest.php b/apps/encryption/tests/lib/KeyManagerTest.php index c69610fb541..ea1830db4d5 100644 --- a/apps/encryption/tests/lib/KeyManagerTest.php +++ b/apps/encryption/tests/lib/KeyManagerTest.php @@ -579,4 +579,71 @@ class KeyManagerTest extends TestCase { ]; } + public function testGetVersionWithoutFileInfo() { + $view = $this->getMockBuilder('\\OC\\Files\\View') + ->disableOriginalConstructor()->getMock(); + $view->expects($this->once()) + ->method('getFileInfo') + ->with('/admin/files/myfile.txt') + ->willReturn(false); + + $this->assertSame(0, $this->instance->getVersion('/admin/files/myfile.txt', $view)); + } + + public function testGetVersionWithFileInfo() { + $view = $this->getMockBuilder('\\OC\\Files\\View') + ->disableOriginalConstructor()->getMock(); + $fileInfo = $this->getMockBuilder('\\OC\\Files\\FileInfo') + ->disableOriginalConstructor()->getMock(); + $fileInfo->expects($this->once()) + ->method('getEncryptedVersion') + ->willReturn(1337); + $view->expects($this->once()) + ->method('getFileInfo') + ->with('/admin/files/myfile.txt') + ->willReturn($fileInfo); + + $this->assertSame(1337, $this->instance->getVersion('/admin/files/myfile.txt', $view)); + } + + public function testSetVersionWithFileInfo() { + $view = $this->getMockBuilder('\\OC\\Files\\View') + ->disableOriginalConstructor()->getMock(); + $cache = $this->getMockBuilder('\\OCP\\Files\\Cache\\ICache') + ->disableOriginalConstructor()->getMock(); + $cache->expects($this->once()) + ->method('update') + ->with(123, ['encrypted' => 5, 'encryptedVersion' => 5]); + $storage = $this->getMockBuilder('\\OCP\\Files\\Storage') + ->disableOriginalConstructor()->getMock(); + $storage->expects($this->once()) + ->method('getCache') + ->willReturn($cache); + $fileInfo = $this->getMockBuilder('\\OC\\Files\\FileInfo') + ->disableOriginalConstructor()->getMock(); + $fileInfo->expects($this->once()) + ->method('getStorage') + ->willReturn($storage); + $fileInfo->expects($this->once()) + ->method('getId') + ->willReturn(123); + $view->expects($this->once()) + ->method('getFileInfo') + ->with('/admin/files/myfile.txt') + ->willReturn($fileInfo); + + $this->instance->setVersion('/admin/files/myfile.txt', 5, $view); + } + + public function testSetVersionWithoutFileInfo() { + $view = $this->getMockBuilder('\\OC\\Files\\View') + ->disableOriginalConstructor()->getMock(); + $view->expects($this->once()) + ->method('getFileInfo') + ->with('/admin/files/myfile.txt') + ->willReturn(false); + + $this->instance->setVersion('/admin/files/myfile.txt', 5, $view); + } + } diff --git a/apps/encryption/tests/lib/crypto/cryptTest.php b/apps/encryption/tests/lib/crypto/cryptTest.php index e599cc28963..d94aea463cf 100644 --- a/apps/encryption/tests/lib/crypto/cryptTest.php +++ b/apps/encryption/tests/lib/crypto/cryptTest.php @@ -39,6 +39,10 @@ class cryptTest extends TestCase { /** @var \PHPUnit_Framework_MockObject_MockObject */ private $config; + + /** @var \PHPUnit_Framework_MockObject_MockObject */ + private $l; + /** @var Crypt */ private $crypt; @@ -57,8 +61,9 @@ class cryptTest extends TestCase { $this->config = $this->getMockBuilder('OCP\IConfig') ->disableOriginalConstructor() ->getMock(); + $this->l = $this->getMock('OCP\IL10N'); - $this->crypt = new Crypt($this->logger, $this->userSession, $this->config); + $this->crypt = new Crypt($this->logger, $this->userSession, $this->config, $this->l); } /** @@ -105,7 +110,7 @@ class cryptTest extends TestCase { $this->config->expects($this->once()) ->method('getSystemValue') - ->with($this->equalTo('cipher'), $this->equalTo('AES-256-CFB')) + ->with($this->equalTo('cipher'), $this->equalTo('AES-256-CTR')) ->willReturn('AES-128-CFB'); if ($keyFormat) { @@ -126,6 +131,9 @@ class cryptTest extends TestCase { $this->crypt->generateHeader('unknown'); } + /** + * @return array + */ public function dataTestGenerateHeader() { return [ [null, 'HBEGIN:cipher:AES-128-CFB:keyFormat:hash:HEND'], @@ -134,16 +142,28 @@ class cryptTest extends TestCase { ]; } + public function testGetCipherWithInvalidCipher() { + $this->config->expects($this->once()) + ->method('getSystemValue') + ->with($this->equalTo('cipher'), $this->equalTo('AES-256-CTR')) + ->willReturn('Not-Existing-Cipher'); + $this->logger + ->expects($this->once()) + ->method('warning') + ->with('Unsupported cipher (Not-Existing-Cipher) defined in config.php supported. Falling back to AES-256-CTR'); + + $this->assertSame('AES-256-CTR', $this->crypt->getCipher()); + } + /** * @dataProvider dataProviderGetCipher * @param string $configValue * @param string $expected */ public function testGetCipher($configValue, $expected) { - $this->config->expects($this->once()) ->method('getSystemValue') - ->with($this->equalTo('cipher'), $this->equalTo('AES-256-CFB')) + ->with($this->equalTo('cipher'), $this->equalTo('AES-256-CTR')) ->willReturn($configValue); $this->assertSame($expected, @@ -161,7 +181,10 @@ class cryptTest extends TestCase { return array( array('AES-128-CFB', 'AES-128-CFB'), array('AES-256-CFB', 'AES-256-CFB'), - array('unknown', 'AES-256-CFB') + array('AES-128-CTR', 'AES-128-CTR'), + array('AES-256-CTR', 'AES-256-CTR'), + + array('unknown', 'AES-256-CTR') ); } @@ -181,17 +204,61 @@ class cryptTest extends TestCase { } /** - * test splitIV() + * @dataProvider dataTestSplitMetaData */ - public function testSplitIV() { - $data = 'encryptedContent00iv001234567890123456'; - $result = self::invokePrivate($this->crypt, 'splitIV', array($data)); + public function testSplitMetaData($data, $expected) { + $result = self::invokePrivate($this->crypt, 'splitMetaData', array($data, 'AES-256-CFB')); $this->assertTrue(is_array($result)); - $this->assertSame(2, count($result)); + $this->assertSame(3, count($result)); $this->assertArrayHasKey('encrypted', $result); $this->assertArrayHasKey('iv', $result); - $this->assertSame('encryptedContent', $result['encrypted']); - $this->assertSame('1234567890123456', $result['iv']); + $this->assertArrayHasKey('signature', $result); + $this->assertSame($expected['encrypted'], $result['encrypted']); + $this->assertSame($expected['iv'], $result['iv']); + $this->assertSame($expected['signature'], $result['signature']); + } + + public function dataTestSplitMetaData() { + return [ + ['encryptedContent00iv001234567890123456xx', + ['encrypted' => 'encryptedContent', 'iv' => '1234567890123456', 'signature' => false]], + ['encryptedContent00iv00123456789012345600sig00e1992521e437f6915f9173b190a512cfc38a00ac24502db44e0ba10c2bb0cc86xxx', + ['encrypted' => 'encryptedContent', 'iv' => '1234567890123456', 'signature' => 'e1992521e437f6915f9173b190a512cfc38a00ac24502db44e0ba10c2bb0cc86']], + ]; + } + + /** + * @dataProvider dataTestHasSignature + */ + public function testHasSignature($data, $expected) { + $this->assertSame($expected, + $this->invokePrivate($this->crypt, 'hasSignature', array($data, 'AES-256-CFB')) + ); + } + + public function dataTestHasSignature() { + return [ + ['encryptedContent00iv001234567890123456xx', false], + ['encryptedContent00iv00123456789012345600sig00e1992521e437f6915f9173b190a512cfc38a00ac24502db44e0ba10c2bb0cc86xxx', true] + ]; + } + + /** + * @dataProvider dataTestHasSignatureFail + * @expectedException \OC\HintException + */ + public function testHasSignatureFail($cipher) { + $data = 'encryptedContent00iv001234567890123456xx'; + $this->invokePrivate($this->crypt, 'hasSignature', array($data, $cipher)); + } + + public function dataTestHasSignatureFail() { + return [ + ['AES-256-CTR'], + ['aes-256-ctr'], + ['AES-128-CTR'], + ['ctr-256-ctr'] + ]; } /** @@ -199,7 +266,7 @@ class cryptTest extends TestCase { */ public function testAddPadding() { $result = self::invokePrivate($this->crypt, 'addPadding', array('data')); - $this->assertSame('dataxx', $result); + $this->assertSame('dataxxx', $result); } /** @@ -303,10 +370,15 @@ class cryptTest extends TestCase { $this->invokePrivate($this->crypt, 'getKeySize', ['foo']); } + /** + * @return array + */ public function dataTestGetKeySize() { return [ ['AES-256-CFB', 32], ['AES-128-CFB', 16], + ['AES-256-CTR', 32], + ['AES-128-CTR', 16], ]; } @@ -320,7 +392,8 @@ class cryptTest extends TestCase { [ $this->logger, $this->userSession, - $this->config + $this->config, + $this->l ] ) ->setMethods( @@ -351,6 +424,9 @@ class cryptTest extends TestCase { $this->assertSame($expected, $result); } + /** + * @return array + */ public function dataTestDecryptPrivateKey() { return [ [['cipher' => 'AES-128-CFB', 'keyFormat' => 'password'], 'HBEGIN:HENDprivateKey', 'AES-128-CFB', true, 'key'], diff --git a/apps/encryption/tests/lib/crypto/encryptionTest.php b/apps/encryption/tests/lib/crypto/encryptionTest.php index 62e77c742d8..ad943ab6e49 100644 --- a/apps/encryption/tests/lib/crypto/encryptionTest.php +++ b/apps/encryption/tests/lib/crypto/encryptionTest.php @@ -229,7 +229,7 @@ class EncryptionTest extends TestCase { public function dataTestBegin() { return array( - array('w', ['cipher' => 'myCipher'], 'legacyCipher', 'defaultCipher', 'fileKey', 'myCipher'), + array('w', ['cipher' => 'myCipher'], 'legacyCipher', 'defaultCipher', 'fileKey', 'defaultCipher'), array('r', ['cipher' => 'myCipher'], 'legacyCipher', 'defaultCipher', 'fileKey', 'myCipher'), array('w', [], 'legacyCipher', 'defaultCipher', '', 'defaultCipher'), array('r', [], 'legacyCipher', 'defaultCipher', 'file_key', 'legacyCipher'), diff --git a/apps/files_versions/lib/storage.php b/apps/files_versions/lib/storage.php index 47acec1d763..0b121c344f9 100644 --- a/apps/files_versions/lib/storage.php +++ b/apps/files_versions/lib/storage.php @@ -165,7 +165,15 @@ class Storage { $mtime = $users_view->filemtime('files/' . $filename); $users_view->copy('files/' . $filename, 'files_versions/' . $filename . '.v' . $mtime); // call getFileInfo to enforce a file cache entry for the new version - $users_view->getFileInfo('files_versions/' . $filename . '.v' . $mtime); + $newFileInfo = $users_view->getFileInfo('files_versions/' . $filename . '.v' . $mtime); + + // Keep the "encrypted" value of the original file + $oldVersion = $files_view->getFileInfo($filename)->getEncryptedVersion(); + $qb = \OC::$server->getDatabaseConnection()->getQueryBuilder(); + $qb->update('filecache') + ->set('encrypted', $qb->createNamedParameter($oldVersion)) + ->where($qb->expr()->eq('fileid', $qb->createNamedParameter($newFileInfo->getId()))) + ->execute(); } } diff --git a/lib/private/files/cache/cache.php b/lib/private/files/cache/cache.php index 22b9f49e528..b30666d48d2 100644 --- a/lib/private/files/cache/cache.php +++ b/lib/private/files/cache/cache.php @@ -145,6 +145,7 @@ class Cache implements ICache { $data['size'] = 0 + $data['size']; $data['mtime'] = (int)$data['mtime']; $data['storage_mtime'] = (int)$data['storage_mtime']; + $data['encryptedVersion'] = (int)$data['encrypted']; $data['encrypted'] = (bool)$data['encrypted']; $data['storage'] = $this->storageId; $data['mimetype'] = $this->mimetypeLoader->getMimetypeById($data['mimetype']); @@ -345,8 +346,12 @@ class Cache implements ICache { $queryParts[] = '`mtime`'; } } elseif ($name === 'encrypted') { - // Boolean to integer conversion - $value = $value ? 1 : 0; + if(isset($data['encryptedVersion'])) { + $value = $data['encryptedVersion']; + } else { + // Boolean to integer conversion + $value = $value ? 1 : 0; + } } $params[] = $value; $queryParts[] = '`' . $name . '`'; diff --git a/lib/private/files/fileinfo.php b/lib/private/files/fileinfo.php index f22e1099e26..1d722a46735 100644 --- a/lib/private/files/fileinfo.php +++ b/lib/private/files/fileinfo.php @@ -194,6 +194,15 @@ class FileInfo implements \OCP\Files\FileInfo, \ArrayAccess { } /** + * Return the currently version used for the HMAC in the encryption app + * + * @return int + */ + public function getEncryptedVersion() { + return isset($this->data['encryptedVersion']) ? (int) $this->data['encryptedVersion'] : 1; + } + + /** * @return int */ public function getPermissions() { diff --git a/lib/private/files/storage/wrapper/encryption.php b/lib/private/files/storage/wrapper/encryption.php index f358bd59239..14d3b15bbae 100644 --- a/lib/private/files/storage/wrapper/encryption.php +++ b/lib/private/files/storage/wrapper/encryption.php @@ -39,6 +39,7 @@ use OCP\Encryption\Keys\IStorage; use OCP\Files\Mount\IMountPoint; use OCP\Files\Storage; use OCP\ILogger; +use OCP\Files\Cache\ICacheEntry; class Encryption extends Wrapper { @@ -129,13 +130,16 @@ class Encryption extends Wrapper { if (isset($this->unencryptedSize[$fullPath])) { $size = $this->unencryptedSize[$fullPath]; // update file cache - if ($info) { + if ($info instanceof ICacheEntry) { $info = $info->getData(); + $info['encrypted'] = $info['encryptedVersion']; } else { - $info = []; + if (!is_array($info)) { + $info = []; + } + $info['encrypted'] = true; } - $info['encrypted'] = true; $info['size'] = $size; $this->getCache()->put($path, $info); @@ -343,6 +347,7 @@ class Encryption extends Wrapper { $shouldEncrypt = false; $encryptionModule = null; $header = $this->getHeader($path); + $signed = (isset($header['signed']) && $header['signed'] === 'true') ? true : false; $fullPath = $this->getFullPath($path); $encryptionModuleId = $this->util->getEncryptionModuleId($header); @@ -377,7 +382,7 @@ class Encryption extends Wrapper { || $mode === 'wb' || $mode === 'wb+' ) { - // don't overwrite encrypted files if encyption is not enabled + // don't overwrite encrypted files if encryption is not enabled if ($targetIsEncrypted && $encryptionEnabled === false) { throw new GenericEncryptionException('Tried to access encrypted file but encryption is not enabled'); } @@ -385,6 +390,7 @@ class Encryption extends Wrapper { // if $encryptionModuleId is empty, the default module will be used $encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId); $shouldEncrypt = $encryptionModule->shouldEncrypt($fullPath); + $signed = true; } } else { $info = $this->getCache()->get($path); @@ -422,7 +428,7 @@ class Encryption extends Wrapper { } $handle = \OC\Files\Stream\Encryption::wrap($source, $path, $fullPath, $header, $this->uid, $encryptionModule, $this->storage, $this, $this->util, $this->fileHelper, $mode, - $size, $unencryptedSize, $headerSize); + $size, $unencryptedSize, $headerSize, $signed); return $handle; } diff --git a/lib/private/files/stream/encryption.php b/lib/private/files/stream/encryption.php index c884cd8fa07..63949035b5a 100644 --- a/lib/private/files/stream/encryption.php +++ b/lib/private/files/stream/encryption.php @@ -72,6 +72,9 @@ class Encryption extends Wrapper { /** @var string */ protected $fullPath; + /** @var bool */ + protected $signed; + /** * header data returned by the encryption module, will be written to the file * in case of a write operation @@ -110,7 +113,8 @@ class Encryption extends Wrapper { 'size', 'unencryptedSize', 'encryptionStorage', - 'headerSize' + 'headerSize', + 'signed' ); } @@ -132,6 +136,7 @@ class Encryption extends Wrapper { * @param int $size * @param int $unencryptedSize * @param int $headerSize + * @param bool $signed * @param string $wrapper stream wrapper class * @return resource * @@ -148,6 +153,7 @@ class Encryption extends Wrapper { $size, $unencryptedSize, $headerSize, + $signed, $wrapper = 'OC\Files\Stream\Encryption') { $context = stream_context_create(array( @@ -164,7 +170,8 @@ class Encryption extends Wrapper { 'size' => $size, 'unencryptedSize' => $unencryptedSize, 'encryptionStorage' => $encStorage, - 'headerSize' => $headerSize + 'headerSize' => $headerSize, + 'signed' => $signed ) )); @@ -225,7 +232,7 @@ class Encryption extends Wrapper { $this->position = 0; $this->cache = ''; $this->writeFlag = false; - $this->unencryptedBlockSize = $this->encryptionModule->getUnencryptedBlockSize(); + $this->unencryptedBlockSize = $this->encryptionModule->getUnencryptedBlockSize($this->signed); if ( $mode === 'w' @@ -392,8 +399,9 @@ class Encryption extends Wrapper { } public function stream_close() { - $this->flush(); - $remainingData = $this->encryptionModule->end($this->fullPath); + $this->flush('end'); + $position = (int)floor($this->position/$this->unencryptedBlockSize); + $remainingData = $this->encryptionModule->end($this->fullPath, $position . 'end'); if ($this->readOnly === false) { if(!empty($remainingData)) { parent::stream_write($remainingData); @@ -405,15 +413,17 @@ class Encryption extends Wrapper { /** * write block to file + * @param string $positionPrefix */ - protected function flush() { + protected function flush($positionPrefix = '') { // write to disk only when writeFlag was set to 1 if ($this->writeFlag) { // Disable the file proxies so that encryption is not // automatically attempted when the file is written to disk - // we are handling that separately here and we don't want to // get into an infinite loop - $encrypted = $this->encryptionModule->encrypt($this->cache); + $position = (int)floor($this->position/$this->unencryptedBlockSize); + $encrypted = $this->encryptionModule->encrypt($this->cache, $position . $positionPrefix); $bytesWritten = parent::stream_write($encrypted); $this->writeFlag = false; // Check whether the write concerns the last block @@ -440,7 +450,12 @@ class Encryption extends Wrapper { if ($this->cache === '' && !($this->position === $this->unencryptedSize && ($this->position % $this->unencryptedBlockSize) === 0)) { // Get the data from the file handle $data = parent::stream_read($this->util->getBlockSize()); - $this->cache = $this->encryptionModule->decrypt($data); + $position = (int)floor($this->position/$this->unencryptedBlockSize); + $numberOfChunks = (int)($this->unencryptedSize / $this->unencryptedBlockSize); + if($numberOfChunks === $position) { + $position .= 'end'; + } + $this->cache = $this->encryptionModule->decrypt($data, $position); } } diff --git a/lib/public/encryption/iencryptionmodule.php b/lib/public/encryption/iencryptionmodule.php index 426e4ddecce..45e0b79c2a9 100644 --- a/lib/public/encryption/iencryptionmodule.php +++ b/lib/public/encryption/iencryptionmodule.php @@ -119,10 +119,11 @@ interface IEncryptionModule { * get size of the unencrypted payload per block. * ownCloud read/write files with a block size of 8192 byte * - * @return integer - * @since 8.1.0 + * @param bool $signed + * @return int + * @since 8.1.0 optional parameter $signed was added in 9.0.0 */ - public function getUnencryptedBlockSize(); + public function getUnencryptedBlockSize($signed = false); /** * check if the encryption module is able to read the file, diff --git a/settings/changepassword/controller.php b/settings/changepassword/controller.php index bfa599c1d04..8469ec1423a 100644 --- a/settings/changepassword/controller.php +++ b/settings/changepassword/controller.php @@ -89,7 +89,8 @@ class Controller { $crypt = new \OCA\Encryption\Crypto\Crypt( \OC::$server->getLogger(), \OC::$server->getUserSession(), - \OC::$server->getConfig()); + \OC::$server->getConfig(), + \OC::$server->getL10N('encryption')); $keyStorage = \OC::$server->getEncryptionKeyStorage(); $util = new \OCA\Encryption\Util( new \OC\Files\View(), diff --git a/tests/lib/files/stream/encryption.php b/tests/lib/files/stream/encryption.php index f9d8f076b63..f67dd09bc4d 100644 --- a/tests/lib/files/stream/encryption.php +++ b/tests/lib/files/stream/encryption.php @@ -117,6 +117,7 @@ class Encryption extends \Test\TestCase { $header->setAccessible(true); $header->setValue($streamWrapper, array()); $header->setAccessible(false); + $this->invokePrivate($streamWrapper, 'signed', [true]); // call stream_open, that's the method we want to test $dummyVar = 'foo'; |