Browse Source

recalculate unencrypted size if we assume that the size stored in the db is not correct

tags/v9.0.0RC1
Bjoern Schiessle 8 years ago
parent
commit
834b51b83b

+ 129
- 3
lib/private/files/storage/wrapper/encryption.php 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

+ 157
- 1
tests/lib/files/storage/wrapper/encryption.php 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
*

Loading…
Cancel
Save