]> source.dussan.org Git - nextcloud-server.git/commitdiff
recalculate unencrypted size if we assume that the size stored in the db is not correct
authorBjoern Schiessle <schiessle@owncloud.com>
Mon, 22 Feb 2016 16:28:53 +0000 (17:28 +0100)
committerBjoern Schiessle <schiessle@owncloud.com>
Tue, 23 Feb 2016 14:09:46 +0000 (15:09 +0100)
lib/private/files/storage/wrapper/encryption.php
tests/lib/files/storage/wrapper/encryption.php

index 26905dfb3887ed88bb535b2a8930d42b4ad48185..068a688cb74a91dff9d3d2c0b4d4d91b890e5b31 100644 (file)
@@ -61,7 +61,7 @@ class Encryption extends Wrapper {
        private $uid;
 
        /** @var array */
-       private $unencryptedSize;
+       protected $unencryptedSize;
 
        /** @var \OCP\Encryption\IFile */
        private $fileHelper;
@@ -78,6 +78,9 @@ class Encryption extends Wrapper {
        /** @var Manager */
        private $mountManager;
 
+       /** @var array remember for which path we execute the repair step to avoid recursions */
+       private $fixUnencryptedSizeOf = array();
+
        /**
         * @param array $parameters
         * @param IManager $encryptionManager
@@ -147,8 +150,9 @@ class Encryption extends Wrapper {
                }
 
                if (isset($info['fileid']) && $info['encrypted']) {
-                       return $info['size'];
+                       return $this->verifyUnencryptedSize($path, $info['size']);
                }
+
                return $this->storage->filesize($path);
        }
 
@@ -169,8 +173,8 @@ class Encryption extends Wrapper {
                } else {
                        $info = $this->getCache()->get($path);
                        if (isset($info['fileid']) && $info['encrypted']) {
+                               $data['size'] = $this->verifyUnencryptedSize($path, $info['size']);
                                $data['encrypted'] = true;
-                               $data['size'] = $info['size'];
                        }
                }
 
@@ -441,6 +445,128 @@ class Encryption extends Wrapper {
                return $this->storage->fopen($path, $mode);
        }
 
+
+       /**
+        * perform some plausibility checks if the the unencrypted size is correct.
+        * If not, we calculate the correct unencrypted size and return it
+        *
+        * @param string $path internal path relative to the storage root
+        * @param int $unencryptedSize size of the unencrypted file
+        *
+        * @return int unencrypted size
+        */
+       protected function verifyUnencryptedSize($path, $unencryptedSize) {
+
+               $size = $this->storage->filesize($path);
+               $result = $unencryptedSize;
+
+               if ($unencryptedSize < 0 ||
+                       ($size > 0 && $unencryptedSize === $size)
+               ) {
+                       // check if we already calculate the unencrypted size for the
+                       // given path to avoid recursions
+                       if (isset($this->fixUnencryptedSizeOf[$this->getFullPath($path)]) === false) {
+                               $this->fixUnencryptedSizeOf[$this->getFullPath($path)] = true;
+                               try {
+                                       $result = $this->fixUnencryptedSize($path, $size, $unencryptedSize);
+                               } catch (\Exception $e) {
+                                       $this->logger->error('Couldn\'t re-calculate unencrypted size for '. $path);
+                                       $this->logger->logException($e);
+                               }
+                               unset($this->fixUnencryptedSizeOf[$this->getFullPath($path)]);
+                       }
+               }
+
+               return $result;
+       }
+
+       /**
+        * calculate the unencrypted size
+        *
+        * @param string $path internal path relative to the storage root
+        * @param int $size size of the physical file
+        * @param int $unencryptedSize size of the unencrypted file
+        *
+        * @return int calculated unencrypted size
+        */
+       protected function fixUnencryptedSize($path, $size, $unencryptedSize) {
+
+               $headerSize = $this->getHeaderSize($path);
+               $header = $this->getHeader($path);
+               $encryptionModule = $this->getEncryptionModule($path);
+
+               $stream = $this->storage->fopen($path, 'r');
+
+               // if we couldn't open the file we return the old unencrypted size
+               if (!is_resource($stream)) {
+                       $this->logger->error('Could not open ' . $path . '. Recalculation of unencrypted size aborted.');
+                       return $unencryptedSize;
+               }
+
+               $newUnencryptedSize = 0;
+               $size -= $headerSize;
+               $blockSize = $this->util->getBlockSize();
+
+               // if a header exists we skip it
+               if ($headerSize > 0) {
+                       fread($stream, $headerSize);
+               }
+
+               // fast path, else the calculation for $lastChunkNr is bogus
+               if ($size === 0) {
+                       return 0;
+               }
+
+               $signed = (isset($header['signed']) && $header['signed'] === 'true') ? true : false;
+               $unencryptedBlockSize = $encryptionModule->getUnencryptedBlockSize($signed);
+
+               // calculate last chunk nr
+               // next highest is end of chunks, one subtracted is last one
+               // we have to read the last chunk, we can't just calculate it (because of padding etc)
+
+               $lastChunkNr = ceil($size/ $blockSize)-1;
+               // calculate last chunk position
+               $lastChunkPos = ($lastChunkNr * $blockSize);
+               // try to fseek to the last chunk, if it fails we have to read the whole file
+               if (@fseek($stream, $lastChunkPos, SEEK_CUR) === 0) {
+                       $newUnencryptedSize += $lastChunkNr * $unencryptedBlockSize;
+               }
+
+               $lastChunkContentEncrypted='';
+               $count = $blockSize;
+
+               while ($count > 0) {
+                       $data=fread($stream, $blockSize);
+                       $count=strlen($data);
+                       $lastChunkContentEncrypted .= $data;
+                       if(strlen($lastChunkContentEncrypted) > $blockSize) {
+                               $newUnencryptedSize += $unencryptedBlockSize;
+                               $lastChunkContentEncrypted=substr($lastChunkContentEncrypted, $blockSize);
+                       }
+               }
+
+               fclose($stream);
+
+               // we have to decrypt the last chunk to get it actual size
+               $encryptionModule->begin($this->getFullPath($path), $this->uid, 'r', $header, []);
+               $decryptedLastChunk = $encryptionModule->decrypt($lastChunkContentEncrypted, $lastChunkNr . 'end');
+               $decryptedLastChunk .= $encryptionModule->end($this->getFullPath($path), $lastChunkNr . 'end');
+
+               // calc the real file size with the size of the last chunk
+               $newUnencryptedSize += strlen($decryptedLastChunk);
+
+               $this->updateUnencryptedSize($this->getFullPath($path), $newUnencryptedSize);
+
+               // write to cache if applicable
+               $cache = $this->storage->getCache();
+               if ($cache) {
+                       $entry = $cache->get($path);
+                       $cache->update($entry['fileid'], ['size' => $newUnencryptedSize]);
+               }
+
+               return $newUnencryptedSize;
+       }
+
        /**
         * @param Storage $sourceStorage
         * @param string $sourceInternalPath
index 2b93aa86db08ed068c1a7d06836e3950bb2ead49..c18e518fe6dd29961e0c5058f2df82fadbacceb8 100644 (file)
@@ -5,8 +5,9 @@ namespace Test\Files\Storage\Wrapper;
 use OC\Encryption\Util;
 use OC\Files\Storage\Temporary;
 use OC\Files\View;
+use Test\Files\Storage\Storage;
 
-class Encryption extends \Test\Files\Storage\Storage {
+class Encryption extends Storage {
 
        /**
         * block size will always be 8192 for a PHP stream
@@ -210,6 +211,161 @@ class Encryption extends \Test\Files\Storage\Storage {
                return $this->encryptionModule;
        }
 
+       /**
+        * @dataProvider dataTestGetMetaData
+        *
+        * @param string $path
+        * @param array $metaData
+        * @param bool $encrypted
+        * @param bool $unencryptedSizeSet
+        * @param int $storedUnencryptedSize
+        * @param array $expected
+        */
+       public function testGetMetaData($path, $metaData, $encrypted, $unencryptedSizeSet, $storedUnencryptedSize, $expected) {
+
+               $sourceStorage = $this->getMockBuilder('\OC\Files\Storage\Storage')
+                       ->disableOriginalConstructor()->getMock();
+
+               $cache = $this->getMockBuilder('\OC\Files\Cache\Cache')
+                       ->disableOriginalConstructor()->getMock();
+               $cache->expects($this->any())
+                       ->method('get')
+                       ->willReturnCallback(
+                               function($path) use ($encrypted) {
+                                       return ['encrypted' => $encrypted, 'path' => $path, 'size' => 0, 'fileid' => 1];
+                               }
+                       );
+
+               $this->instance = $this->getMockBuilder('\OC\Files\Storage\Wrapper\Encryption')
+                       ->setConstructorArgs(
+                               [
+                                       [
+                                               'storage' => $sourceStorage,
+                                               'root' => 'foo',
+                                               'mountPoint' => '/',
+                                               'mount' => $this->mount
+                                       ],
+                                       $this->encryptionManager, $this->util, $this->logger, $this->file, null, $this->keyStore, $this->update, $this->mountManager
+                               ]
+                       )
+                       ->setMethods(['getCache', 'verifyUnencryptedSize'])
+                       ->getMock();
+
+               if($unencryptedSizeSet) {
+                       $this->invokePrivate($this->instance, 'unencryptedSize', [[$path => $storedUnencryptedSize]]);
+               }
+
+
+               $sourceStorage->expects($this->once())->method('getMetaData')->with($path)
+                       ->willReturn($metaData);
+
+               $this->instance->expects($this->any())->method('getCache')->willReturn($cache);
+               $this->instance->expects($this->any())->method('verifyUnencryptedSize')
+                       ->with($path, 0)->willReturn($expected['size']);
+
+               $result = $this->instance->getMetaData($path);
+               $this->assertSame($expected['encrypted'], $result['encrypted']);
+               $this->assertSame($expected['size'], $result['size']);
+       }
+
+       public function dataTestGetMetaData() {
+               return [
+                       ['/test.txt', ['size' => 42, 'encrypted' => false], true, true, 12, ['size' => 12, 'encrypted' => true]],
+                       ['/test.txt', null, true, true, 12, null],
+                       ['/test.txt', ['size' => 42, 'encrypted' => false], false, false, 12, ['size' => 42, 'encrypted' => false]],
+                       ['/test.txt', ['size' => 42, 'encrypted' => false], true, false, 12, ['size' => 12, 'encrypted' => true]]
+               ];
+       }
+
+       public function testFilesize() {
+               $cache = $this->getMockBuilder('\OC\Files\Cache\Cache')
+                       ->disableOriginalConstructor()->getMock();
+               $cache->expects($this->any())
+                       ->method('get')
+                       ->willReturn(['encrypted' => true, 'path' => '/test.txt', 'size' => 0, 'fileid' => 1]);
+
+               $this->instance = $this->getMockBuilder('\OC\Files\Storage\Wrapper\Encryption')
+                       ->setConstructorArgs(
+                               [
+                                       [
+                                               'storage' => $this->sourceStorage,
+                                               'root' => 'foo',
+                                               'mountPoint' => '/',
+                                               'mount' => $this->mount
+                                       ],
+                                       $this->encryptionManager, $this->util, $this->logger, $this->file, null, $this->keyStore, $this->update, $this->mountManager
+                               ]
+                       )
+                       ->setMethods(['getCache', 'verifyUnencryptedSize'])
+                       ->getMock();
+
+               $this->instance->expects($this->any())->method('getCache')->willReturn($cache);
+               $this->instance->expects($this->any())->method('verifyUnencryptedSize')
+                       ->willReturn(42);
+
+
+               $this->assertSame(42,
+                       $this->instance->filesize('/test.txt')
+               );
+
+       }
+
+       /**
+        * @dataProvider dataTestVerifyUnencryptedSize
+        *
+        * @param int $encryptedSize
+        * @param int $unencryptedSize
+        * @param bool $failure
+        * @param int $expected
+        */
+       public function testVerifyUnencryptedSize($encryptedSize, $unencryptedSize, $failure, $expected) {
+               $sourceStorage = $this->getMockBuilder('\OC\Files\Storage\Storage')
+                       ->disableOriginalConstructor()->getMock();
+
+               $this->instance = $this->getMockBuilder('\OC\Files\Storage\Wrapper\Encryption')
+                       ->setConstructorArgs(
+                               [
+                                       [
+                                               'storage' => $sourceStorage,
+                                               'root' => 'foo',
+                                               'mountPoint' => '/',
+                                               'mount' => $this->mount
+                                       ],
+                                       $this->encryptionManager, $this->util, $this->logger, $this->file, null, $this->keyStore, $this->update, $this->mountManager
+                               ]
+                       )
+                       ->setMethods(['fixUnencryptedSize'])
+                       ->getMock();
+
+               $sourceStorage->expects($this->once())->method('filesize')->willReturn($encryptedSize);
+
+               $this->instance->expects($this->any())->method('fixUnencryptedSize')
+                       ->with('/test.txt', $encryptedSize, $unencryptedSize)
+                       ->willReturnCallback(
+                               function() use ($failure, $expected) {
+                                       if ($failure) {
+                                               throw new \Exception();
+                                       } else {
+                                               return $expected;
+                                       }
+                               }
+                       );
+
+               $this->assertSame(
+                       $expected,
+                       $this->invokePrivate($this->instance, 'verifyUnencryptedSize', ['/test.txt', $unencryptedSize])
+               );
+       }
+
+       public function dataTestVerifyUnencryptedSize() {
+               return [
+                       [120, 80, false, 80],
+                       [120, 120, false, 80],
+                       [120, -1, false, 80],
+                       [120, -1, true, -1]
+               ];
+       }
+
        /**
         * @dataProvider dataTestCopyAndRename
         *