Merge pull request #22579 from owncloud/fix_broken_unencrypted_size

Heal unencrypted file sizes at download time (second approach)
This commit is contained in:
Thomas Müller 2016-02-25 14:34:48 +01:00
commit efc966698f
2 changed files with 286 additions and 4 deletions

View 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

View 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
*