diff options
Diffstat (limited to 'tests/lib/Encryption')
-rw-r--r-- | tests/lib/Encryption/DecryptAllTest.php | 404 | ||||
-rw-r--r-- | tests/lib/Encryption/EncryptionWrapperTest.php | 83 | ||||
-rw-r--r-- | tests/lib/Encryption/Keys/StorageTest.php | 611 | ||||
-rw-r--r-- | tests/lib/Encryption/ManagerTest.php | 274 | ||||
-rw-r--r-- | tests/lib/Encryption/UpdateTest.php | 190 | ||||
-rw-r--r-- | tests/lib/Encryption/UtilTest.php | 248 |
6 files changed, 1810 insertions, 0 deletions
diff --git a/tests/lib/Encryption/DecryptAllTest.php b/tests/lib/Encryption/DecryptAllTest.php new file mode 100644 index 00000000000..979e12e03b3 --- /dev/null +++ b/tests/lib/Encryption/DecryptAllTest.php @@ -0,0 +1,404 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ + +namespace Test\Encryption; + +use OC\Encryption\DecryptAll; +use OC\Encryption\Exceptions\DecryptionFailedException; +use OC\Encryption\Manager; +use OC\Files\FileInfo; +use OC\Files\View; +use OCP\Files\Storage\IStorage; +use OCP\IUserManager; +use OCP\UserInterface; +use Symfony\Component\Console\Formatter\OutputFormatterInterface; +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Test\TestCase; + +/** + * Class DecryptAllTest + * + * @group DB + * + * @package Test\Encryption + */ +class DecryptAllTest extends TestCase { + /** @var \PHPUnit\Framework\MockObject\MockObject | IUserManager */ + protected $userManager; + + /** @var \PHPUnit\Framework\MockObject\MockObject | Manager */ + protected $encryptionManager; + + /** @var \PHPUnit\Framework\MockObject\MockObject | View */ + protected $view; + + /** @var \PHPUnit\Framework\MockObject\MockObject | \Symfony\Component\Console\Input\InputInterface */ + protected $inputInterface; + + /** @var \PHPUnit\Framework\MockObject\MockObject | \Symfony\Component\Console\Output\OutputInterface */ + protected $outputInterface; + + /** @var \PHPUnit\Framework\MockObject\MockObject|UserInterface */ + protected $userInterface; + + /** @var DecryptAll */ + protected $instance; + + protected function setUp(): void { + parent::setUp(); + + $this->userManager = $this->getMockBuilder(IUserManager::class) + ->disableOriginalConstructor()->getMock(); + $this->encryptionManager = $this->getMockBuilder('OC\Encryption\Manager') + ->disableOriginalConstructor()->getMock(); + $this->view = $this->getMockBuilder(View::class) + ->disableOriginalConstructor()->getMock(); + $this->inputInterface = $this->getMockBuilder(InputInterface::class) + ->disableOriginalConstructor()->getMock(); + $this->outputInterface = $this->getMockBuilder(OutputInterface::class) + ->disableOriginalConstructor()->getMock(); + $this->outputInterface->expects($this->any())->method('isDecorated') + ->willReturn(false); + $this->userInterface = $this->getMockBuilder(UserInterface::class) + ->disableOriginalConstructor()->getMock(); + + /* We need format method to return a string */ + $outputFormatter = $this->createMock(OutputFormatterInterface::class); + $outputFormatter->method('format')->willReturn('foo'); + $outputFormatter->method('isDecorated')->willReturn(false); + + $this->outputInterface->expects($this->any())->method('getFormatter') + ->willReturn($outputFormatter); + + $this->instance = new DecryptAll($this->encryptionManager, $this->userManager, $this->view); + + $this->invokePrivate($this->instance, 'input', [$this->inputInterface]); + $this->invokePrivate($this->instance, 'output', [$this->outputInterface]); + } + + public static function dataDecryptAll(): array { + return [ + [true, 'user1', true], + [false, 'user1', true], + [true, '0', true], + [false, '0', true], + [true, '', false], + ]; + } + + /** + * @param bool $prepareResult + * @param string $user + * @param bool $userExistsChecked + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataDecryptAll')] + public function testDecryptAll($prepareResult, $user, $userExistsChecked): void { + if ($userExistsChecked) { + $this->userManager->expects($this->once())->method('userExists')->willReturn(true); + } else { + $this->userManager->expects($this->never())->method('userExists'); + } + /** @var DecryptAll | \PHPUnit\Framework\MockObject\MockObject | $instance */ + $instance = $this->getMockBuilder('OC\Encryption\DecryptAll') + ->setConstructorArgs( + [ + $this->encryptionManager, + $this->userManager, + $this->view + ] + ) + ->onlyMethods(['prepareEncryptionModules', 'decryptAllUsersFiles']) + ->getMock(); + + $instance->expects($this->once()) + ->method('prepareEncryptionModules') + ->with($user) + ->willReturn($prepareResult); + + if ($prepareResult) { + $instance->expects($this->once()) + ->method('decryptAllUsersFiles') + ->with($user); + } else { + $instance->expects($this->never())->method('decryptAllUsersFiles'); + } + + $instance->decryptAll($this->inputInterface, $this->outputInterface, $user); + } + + /** + * test decrypt all call with a user who doesn't exists + */ + public function testDecryptAllWrongUser(): void { + $this->userManager->expects($this->once())->method('userExists')->willReturn(false); + $this->outputInterface->expects($this->once())->method('writeln') + ->with('User "user1" does not exist. Please check the username and try again'); + + $this->assertFalse( + $this->instance->decryptAll($this->inputInterface, $this->outputInterface, 'user1') + ); + } + + public static function dataTrueFalse(): array { + return [ + [true], + [false], + ]; + } + + /** + * @param bool $success + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataTrueFalse')] + public function testPrepareEncryptionModules($success): void { + $user = 'user1'; + + $dummyEncryptionModule = $this->getMockBuilder('OCP\Encryption\IEncryptionModule') + ->disableOriginalConstructor()->getMock(); + + $dummyEncryptionModule->expects($this->once()) + ->method('prepareDecryptAll') + ->with($this->inputInterface, $this->outputInterface, $user) + ->willReturn($success); + + $callback = function () use ($dummyEncryptionModule) { + return $dummyEncryptionModule; + }; + $moduleDescription = [ + 'id' => 'id', + 'displayName' => 'displayName', + 'callback' => $callback + ]; + + $this->encryptionManager->expects($this->once()) + ->method('getEncryptionModules') + ->willReturn([$moduleDescription]); + + $this->assertSame($success, + $this->invokePrivate($this->instance, 'prepareEncryptionModules', [$user]) + ); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestDecryptAllUsersFiles')] + public function testDecryptAllUsersFiles($user): void { + /** @var DecryptAll | \PHPUnit\Framework\MockObject\MockObject | $instance */ + $instance = $this->getMockBuilder('OC\Encryption\DecryptAll') + ->setConstructorArgs( + [ + $this->encryptionManager, + $this->userManager, + $this->view + ] + ) + ->onlyMethods(['decryptUsersFiles']) + ->getMock(); + + $this->invokePrivate($instance, 'input', [$this->inputInterface]); + $this->invokePrivate($instance, 'output', [$this->outputInterface]); + + if (empty($user)) { + $this->userManager->expects($this->once()) + ->method('getBackends') + ->willReturn([$this->userInterface]); + $this->userInterface->expects($this->any()) + ->method('getUsers') + ->willReturn(['user1', 'user2']); + $calls = [ + 'user1', + 'user2', + ]; + $instance->expects($this->exactly(2)) + ->method('decryptUsersFiles') + ->willReturnCallback(function ($user) use (&$calls): void { + $expected = array_shift($calls); + $this->assertEquals($expected, $user); + }); + } else { + $instance->expects($this->once()) + ->method('decryptUsersFiles') + ->with($user); + } + + $this->invokePrivate($instance, 'decryptAllUsersFiles', [$user]); + } + + public static function dataTestDecryptAllUsersFiles(): array { + return [ + ['user1'], + [''] + ]; + } + + public function testDecryptUsersFiles(): void { + /** @var DecryptAll | \PHPUnit\Framework\MockObject\MockObject $instance */ + $instance = $this->getMockBuilder('OC\Encryption\DecryptAll') + ->setConstructorArgs( + [ + $this->encryptionManager, + $this->userManager, + $this->view + ] + ) + ->onlyMethods(['decryptFile']) + ->getMock(); + + $storage = $this->getMockBuilder(IStorage::class) + ->disableOriginalConstructor()->getMock(); + + + $sharedStorage = $this->getMockBuilder(IStorage::class) + ->disableOriginalConstructor()->getMock(); + + $sharedStorage->expects($this->once()) + ->method('instanceOfStorage') + ->with('OCA\Files_Sharing\SharedStorage') + ->willReturn(true); + + $this->view->expects($this->exactly(2)) + ->method('getDirectoryContent') + ->willReturnMap([ + [ + '/user1/files', '', null, + [ + new FileInfo('path', $storage, 'intPath', ['name' => 'foo', 'type' => 'dir'], null), + new FileInfo('path', $storage, 'intPath', ['name' => 'bar', 'type' => 'file', 'encrypted' => true], null), + new FileInfo('path', $sharedStorage, 'intPath', ['name' => 'shared', 'type' => 'file', 'encrypted' => true], null), + ], + ], + [ + '/user1/files/foo', '', null, + [ + new FileInfo('path', $storage, 'intPath', ['name' => 'subfile', 'type' => 'file', 'encrypted' => true], null) + ], + ], + ]); + + $this->view->expects($this->any())->method('is_dir') + ->willReturnCallback( + function ($path) { + if ($path === '/user1/files/foo') { + return true; + } + return false; + } + ); + + $calls = [ + '/user1/files/bar', + '/user1/files/foo/subfile', + ]; + $instance->expects($this->exactly(2)) + ->method('decryptFile') + ->willReturnCallback(function ($path) use (&$calls): void { + $expected = array_shift($calls); + $this->assertEquals($expected, $path); + }); + + + /* We need format method to return a string */ + $outputFormatter = $this->createMock(OutputFormatterInterface::class); + $outputFormatter->method('isDecorated')->willReturn(false); + $outputFormatter->method('format')->willReturn('foo'); + + $output = $this->createMock(OutputInterface::class); + $output->expects($this->any()) + ->method('getFormatter') + ->willReturn($outputFormatter); + $progressBar = new ProgressBar($output); + + $this->invokePrivate($instance, 'decryptUsersFiles', ['user1', $progressBar, '']); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataTrueFalse')] + public function testDecryptFile($isEncrypted): void { + $path = 'test.txt'; + + /** @var DecryptAll | \PHPUnit\Framework\MockObject\MockObject $instance */ + $instance = $this->getMockBuilder('OC\Encryption\DecryptAll') + ->setConstructorArgs( + [ + $this->encryptionManager, + $this->userManager, + $this->view + ] + ) + ->onlyMethods(['getTimestamp']) + ->getMock(); + + $fileInfo = $this->createMock(FileInfo::class); + $fileInfo->expects($this->any())->method('isEncrypted') + ->willReturn($isEncrypted); + $this->view->expects($this->any())->method('getFileInfo') + ->willReturn($fileInfo); + + if ($isEncrypted) { + $instance->expects($this->any())->method('getTimestamp')->willReturn(42); + + $this->view->expects($this->once()) + ->method('copy') + ->with($path, $path . '.decrypted.42'); + $this->view->expects($this->once()) + ->method('rename') + ->with($path . '.decrypted.42', $path); + } else { + $instance->expects($this->never())->method('getTimestamp'); + $this->view->expects($this->never())->method('copy'); + $this->view->expects($this->never())->method('rename'); + } + $this->assertTrue( + $this->invokePrivate($instance, 'decryptFile', [$path]) + ); + } + + public function testDecryptFileFailure(): void { + $path = 'test.txt'; + + /** @var DecryptAll | \PHPUnit\Framework\MockObject\MockObject $instance */ + $instance = $this->getMockBuilder('OC\Encryption\DecryptAll') + ->setConstructorArgs( + [ + $this->encryptionManager, + $this->userManager, + $this->view + ] + ) + ->onlyMethods(['getTimestamp']) + ->getMock(); + + + $fileInfo = $this->createMock(FileInfo::class); + $fileInfo->expects($this->any())->method('isEncrypted') + ->willReturn(true); + $this->view->expects($this->any())->method('getFileInfo') + ->willReturn($fileInfo); + + $instance->expects($this->any())->method('getTimestamp')->willReturn(42); + + $this->view->expects($this->once()) + ->method('copy') + ->with($path, $path . '.decrypted.42') + ->willReturnCallback(function (): void { + throw new DecryptionFailedException(); + }); + + $this->view->expects($this->never())->method('rename'); + $this->view->expects($this->once()) + ->method('file_exists') + ->with($path . '.decrypted.42') + ->willReturn(true); + $this->view->expects($this->once()) + ->method('unlink') + ->with($path . '.decrypted.42'); + + $this->assertFalse( + $this->invokePrivate($instance, 'decryptFile', [$path]) + ); + } +} diff --git a/tests/lib/Encryption/EncryptionWrapperTest.php b/tests/lib/Encryption/EncryptionWrapperTest.php new file mode 100644 index 00000000000..58bf5aff005 --- /dev/null +++ b/tests/lib/Encryption/EncryptionWrapperTest.php @@ -0,0 +1,83 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ + +namespace Test\Encryption; + +use OC\Encryption\EncryptionWrapper; +use OC\Encryption\Manager; +use OC\Files\Storage\Wrapper\Encryption; +use OC\Memcache\ArrayCache; +use OCA\Files_Trashbin\Storage; +use OCP\Files\Mount\IMountPoint; +use OCP\Files\Storage\IDisableEncryptionStorage; +use OCP\Files\Storage\IStorage; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +class EncryptionWrapperTest extends TestCase { + /** @var EncryptionWrapper */ + private $instance; + + /** @var \PHPUnit\Framework\MockObject\MockObject | LoggerInterface */ + private $logger; + + /** @var \PHPUnit\Framework\MockObject\MockObject | \OC\Encryption\Manager */ + private $manager; + + /** @var \PHPUnit\Framework\MockObject\MockObject|ArrayCache */ + private $arrayCache; + + protected function setUp(): void { + parent::setUp(); + + $this->arrayCache = $this->createMock(ArrayCache::class); + $this->manager = $this->createMock(Manager::class); + $this->logger = $this->createMock(LoggerInterface::class); + + $this->instance = new EncryptionWrapper($this->arrayCache, $this->manager, $this->logger); + } + + + #[\PHPUnit\Framework\Attributes\DataProvider('provideWrapStorage')] + public function testWrapStorage($expectedWrapped, $wrappedStorages): void { + $storage = $this->getMockBuilder(IStorage::class) + ->disableOriginalConstructor() + ->getMock(); + + foreach ($wrappedStorages as $wrapper) { + $storage->expects($this->any()) + ->method('instanceOfStorage') + ->willReturnMap([ + [$wrapper, true], + ]); + } + + $mount = $this->getMockBuilder(IMountPoint::class) + ->disableOriginalConstructor() + ->getMock(); + + $returnedStorage = $this->instance->wrapStorage('mountPoint', $storage, $mount); + + $this->assertEquals( + $expectedWrapped, + $returnedStorage->instanceOfStorage(Encryption::class), + 'Asserted that the storage is (not) wrapped with encryption' + ); + } + + public static function provideWrapStorage(): array { + return [ + // Wrap when not wrapped or not wrapped with storage + [true, []], + [true, [Storage::class]], + + // Do not wrap shared storages + [false, [IDisableEncryptionStorage::class]], + ]; + } +} diff --git a/tests/lib/Encryption/Keys/StorageTest.php b/tests/lib/Encryption/Keys/StorageTest.php new file mode 100644 index 00000000000..333d8d8ce21 --- /dev/null +++ b/tests/lib/Encryption/Keys/StorageTest.php @@ -0,0 +1,611 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\Encryption\Keys; + +use OC\Encryption\Keys\Storage; +use OC\Encryption\Util; +use OC\Files\View; +use OCP\IConfig; +use OCP\Security\ICrypto; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class StorageTest extends TestCase { + /** @var Storage */ + protected $storage; + + /** @var MockObject|\OC\Encryption\Util */ + protected $util; + + /** @var MockObject|View */ + protected $view; + + /** @var MockObject|IConfig */ + protected $config; + + /** @var MockObject|ICrypto */ + protected $crypto; + + private array $mkdirStack = []; + + protected function setUp(): void { + parent::setUp(); + + $this->util = $this->getMockBuilder(Util::class) + ->disableOriginalConstructor() + ->onlyMethods(array_diff(get_class_methods(Util::class), ['getFileKeyDir'])) + ->getMock(); + + $this->view = $this->getMockBuilder(View::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->crypto = $this->createMock(ICrypto::class); + $this->crypto->method('encrypt') + ->willReturnCallback(function ($data, $pass) { + return $data; + }); + $this->crypto->method('decrypt') + ->willReturnCallback(function ($data, $pass) { + return $data; + }); + + $this->config = $this->getMockBuilder(IConfig::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->storage = new Storage($this->view, $this->util, $this->crypto, $this->config); + } + + public function testSetFileKey(): void { + $this->config->method('getSystemValueString') + ->with('version') + ->willReturn('20.0.0.2'); + $this->util->expects($this->any()) + ->method('getUidAndFilename') + ->willReturn(['user1', '/files/foo.txt']); + $this->util->expects($this->any()) + ->method('stripPartialFileExtension') + ->willReturnArgument(0); + $this->util->expects($this->any()) + ->method('isSystemWideMountPoint') + ->willReturn(false); + + $data = json_encode(['key' => base64_encode('key')]); + $this->view->expects($this->once()) + ->method('file_put_contents') + ->with($this->equalTo('/user1/files_encryption/keys/files/foo.txt/encModule/fileKey'), + $this->equalTo($data)) + ->willReturn(strlen($data)); + + $this->assertTrue( + $this->storage->setFileKey('user1/files/foo.txt', 'fileKey', 'key', 'encModule') + ); + } + + public function testSetFileOld(): void { + $this->config->method('getSystemValueString') + ->with('version') + ->willReturn('20.0.0.0'); + $this->util->expects($this->any()) + ->method('getUidAndFilename') + ->willReturn(['user1', '/files/foo.txt']); + $this->util->expects($this->any()) + ->method('stripPartialFileExtension') + ->willReturnArgument(0); + $this->util->expects($this->any()) + ->method('isSystemWideMountPoint') + ->willReturn(false); + $this->crypto->expects($this->never()) + ->method('encrypt'); + $this->view->expects($this->once()) + ->method('file_put_contents') + ->with($this->equalTo('/user1/files_encryption/keys/files/foo.txt/encModule/fileKey'), + $this->equalTo('key')) + ->willReturn(strlen('key')); + + $this->assertTrue( + $this->storage->setFileKey('user1/files/foo.txt', 'fileKey', 'key', 'encModule') + ); + } + + public static function dataTestGetFileKey() { + return [ + ['/files/foo.txt', '/files/foo.txt', true, 'key'], + ['/files/foo.txt.ocTransferId2111130212.part', '/files/foo.txt', true, 'key'], + ['/files/foo.txt.ocTransferId2111130212.part', '/files/foo.txt', false, 'key2'], + ]; + } + + /** + * + * @param string $path + * @param string $strippedPartialName + * @param bool $originalKeyExists + * @param string $expectedKeyContent + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestGetFileKey')] + public function testGetFileKey($path, $strippedPartialName, $originalKeyExists, $expectedKeyContent): void { + $this->config->method('getSystemValueString') + ->with('version') + ->willReturn('20.0.0.2'); + $this->util->expects($this->any()) + ->method('getUidAndFilename') + ->willReturnMap([ + ['user1/files/foo.txt', ['user1', '/files/foo.txt']], + ['user1/files/foo.txt.ocTransferId2111130212.part', ['user1', '/files/foo.txt.ocTransferId2111130212.part']], + ]); + // we need to strip away the part file extension in order to reuse a + // existing key if it exists, otherwise versions will break + $this->util->expects($this->once()) + ->method('stripPartialFileExtension') + ->willReturn('user1' . $strippedPartialName); + $this->util->expects($this->any()) + ->method('isSystemWideMountPoint') + ->willReturn(false); + + $this->crypto->method('decrypt') + ->willReturnCallback(function ($data, $pass) { + return $data; + }); + + if (!$originalKeyExists) { + $this->view->expects($this->exactly(2)) + ->method('file_exists') + ->willReturnMap([ + ['/user1/files_encryption/keys' . $strippedPartialName . '/encModule/fileKey', $originalKeyExists], + ['/user1/files_encryption/keys' . $path . '/encModule/fileKey', true], + ]); + + $this->view->expects($this->once()) + ->method('file_get_contents') + ->with($this->equalTo('/user1/files_encryption/keys' . $path . '/encModule/fileKey')) + ->willReturn(json_encode(['key' => base64_encode('key2')])); + } else { + $this->view->expects($this->once()) + ->method('file_exists') + ->with($this->equalTo('/user1/files_encryption/keys' . $strippedPartialName . '/encModule/fileKey')) + ->willReturn($originalKeyExists); + + $this->view->expects($this->once()) + ->method('file_get_contents') + ->with($this->equalTo('/user1/files_encryption/keys' . $strippedPartialName . '/encModule/fileKey')) + ->willReturn(json_encode(['key' => base64_encode('key')])); + } + + $this->assertSame($expectedKeyContent, + $this->storage->getFileKey('user1' . $path, 'fileKey', 'encModule') + ); + } + + public function testSetFileKeySystemWide(): void { + $this->config->method('getSystemValueString') + ->with('version') + ->willReturn('20.0.0.2'); + + $this->util->expects($this->any()) + ->method('getUidAndFilename') + ->willReturn(['user1', '/files/foo.txt']); + $this->util->expects($this->any()) + ->method('isSystemWideMountPoint') + ->willReturn(true); + $this->util->expects($this->any()) + ->method('stripPartialFileExtension') + ->willReturnArgument(0); + + $this->crypto->method('encrypt') + ->willReturnCallback(function ($data, $pass) { + return $data; + }); + + $data = json_encode(['key' => base64_encode('key')]); + $this->view->expects($this->once()) + ->method('file_put_contents') + ->with($this->equalTo('/files_encryption/keys/files/foo.txt/encModule/fileKey'), + $this->equalTo($data)) + ->willReturn(strlen($data)); + + $this->assertTrue( + $this->storage->setFileKey('user1/files/foo.txt', 'fileKey', 'key', 'encModule') + ); + } + + public function testGetFileKeySystemWide(): void { + $this->config->method('getSystemValueString') + ->with('version') + ->willReturn('20.0.0.2'); + + $this->util->expects($this->any()) + ->method('getUidAndFilename') + ->willReturn(['user1', '/files/foo.txt']); + $this->util->expects($this->any()) + ->method('stripPartialFileExtension') + ->willReturnArgument(0); + $this->util->expects($this->any()) + ->method('isSystemWideMountPoint') + ->willReturn(true); + $this->view->expects($this->once()) + ->method('file_get_contents') + ->with($this->equalTo('/files_encryption/keys/files/foo.txt/encModule/fileKey')) + ->willReturn(json_encode(['key' => base64_encode('key')])); + $this->view->expects($this->once()) + ->method('file_exists') + ->with($this->equalTo('/files_encryption/keys/files/foo.txt/encModule/fileKey')) + ->willReturn(true); + + $this->assertSame('key', + $this->storage->getFileKey('user1/files/foo.txt', 'fileKey', 'encModule') + ); + } + + public function testSetSystemUserKey(): void { + $this->config->method('getSystemValueString') + ->with('version') + ->willReturn('20.0.0.2'); + + $data = json_encode([ + 'key' => base64_encode('key'), + 'uid' => null] + ); + $this->view->expects($this->once()) + ->method('file_put_contents') + ->with($this->equalTo('/files_encryption/encModule/shareKey_56884'), + $this->equalTo($data)) + ->willReturn(strlen($data)); + + $this->assertTrue( + $this->storage->setSystemUserKey('shareKey_56884', 'key', 'encModule') + ); + } + + public function testSetUserKey(): void { + $this->config->method('getSystemValueString') + ->with('version') + ->willReturn('20.0.0.2'); + + $data = json_encode([ + 'key' => base64_encode('key'), + 'uid' => 'user1'] + ); + $this->view->expects($this->once()) + ->method('file_put_contents') + ->with($this->equalTo('/user1/files_encryption/encModule/user1.publicKey'), + $this->equalTo($data)) + ->willReturn(strlen($data)); + + $this->assertTrue( + $this->storage->setUserKey('user1', 'publicKey', 'key', 'encModule') + ); + } + + public function testGetSystemUserKey(): void { + $this->config->method('getSystemValueString') + ->with('version') + ->willReturn('20.0.0.2'); + + $data = json_encode([ + 'key' => base64_encode('key'), + 'uid' => null] + ); + $this->view->expects($this->once()) + ->method('file_get_contents') + ->with($this->equalTo('/files_encryption/encModule/shareKey_56884')) + ->willReturn($data); + $this->view->expects($this->once()) + ->method('file_exists') + ->with($this->equalTo('/files_encryption/encModule/shareKey_56884')) + ->willReturn(true); + + $this->assertSame('key', + $this->storage->getSystemUserKey('shareKey_56884', 'encModule') + ); + } + + public function testGetUserKey(): void { + $this->config->method('getSystemValueString') + ->with('version') + ->willReturn('20.0.0.2'); + + $data = json_encode([ + 'key' => base64_encode('key'), + 'uid' => 'user1'] + ); + $this->view->expects($this->once()) + ->method('file_get_contents') + ->with($this->equalTo('/user1/files_encryption/encModule/user1.publicKey')) + ->willReturn($data); + $this->view->expects($this->once()) + ->method('file_exists') + ->with($this->equalTo('/user1/files_encryption/encModule/user1.publicKey')) + ->willReturn(true); + + $this->assertSame('key', + $this->storage->getUserKey('user1', 'publicKey', 'encModule') + ); + } + + public function testDeleteUserKey(): void { + $this->view->expects($this->once()) + ->method('file_exists') + ->with($this->equalTo('/user1/files_encryption/encModule/user1.publicKey')) + ->willReturn(true); + $this->view->expects($this->once()) + ->method('unlink') + ->with($this->equalTo('/user1/files_encryption/encModule/user1.publicKey')) + ->willReturn(true); + + $this->assertTrue( + $this->storage->deleteUserKey('user1', 'publicKey', 'encModule') + ); + } + + public function testDeleteSystemUserKey(): void { + $this->view->expects($this->once()) + ->method('file_exists') + ->with($this->equalTo('/files_encryption/encModule/shareKey_56884')) + ->willReturn(true); + $this->view->expects($this->once()) + ->method('unlink') + ->with($this->equalTo('/files_encryption/encModule/shareKey_56884')) + ->willReturn(true); + + $this->assertTrue( + $this->storage->deleteSystemUserKey('shareKey_56884', 'encModule') + ); + } + + public function testDeleteFileKeySystemWide(): void { + $this->util->expects($this->any()) + ->method('getUidAndFilename') + ->willReturn(['user1', '/files/foo.txt']); + $this->util->expects($this->any()) + ->method('stripPartialFileExtension') + ->willReturnArgument(0); + $this->util->expects($this->any()) + ->method('isSystemWideMountPoint') + ->willReturn(true); + $this->view->expects($this->once()) + ->method('file_exists') + ->with($this->equalTo('/files_encryption/keys/files/foo.txt/encModule/fileKey')) + ->willReturn(true); + $this->view->expects($this->once()) + ->method('unlink') + ->with($this->equalTo('/files_encryption/keys/files/foo.txt/encModule/fileKey')) + ->willReturn(true); + + $this->assertTrue( + $this->storage->deleteFileKey('user1/files/foo.txt', 'fileKey', 'encModule') + ); + } + + public function testDeleteFileKey(): void { + $this->util->expects($this->any()) + ->method('getUidAndFilename') + ->willReturn(['user1', '/files/foo.txt']); + $this->util->expects($this->any()) + ->method('stripPartialFileExtension') + ->willReturnArgument(0); + $this->util->expects($this->any()) + ->method('isSystemWideMountPoint') + ->willReturn(false); + $this->view->expects($this->once()) + ->method('file_exists') + ->with($this->equalTo('/user1/files_encryption/keys/files/foo.txt/encModule/fileKey')) + ->willReturn(true); + $this->view->expects($this->once()) + ->method('unlink') + ->with($this->equalTo('/user1/files_encryption/keys/files/foo.txt/encModule/fileKey')) + ->willReturn(true); + + $this->assertTrue( + $this->storage->deleteFileKey('user1/files/foo.txt', 'fileKey', 'encModule') + ); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataProviderCopyRename')] + public function testRenameKeys($source, $target, $systemWideMountSource, $systemWideMountTarget, $expectedSource, $expectedTarget): void { + $this->view->expects($this->any()) + ->method('file_exists') + ->willReturn(true); + $this->view->expects($this->any()) + ->method('is_dir') + ->willReturn(true); + $this->view->expects($this->once()) + ->method('rename') + ->with( + $this->equalTo($expectedSource), + $this->equalTo($expectedTarget)) + ->willReturn(true); + $this->util->expects($this->any()) + ->method('getUidAndFilename') + ->willReturnCallback([$this, 'getUidAndFilenameCallback']); + $this->util->expects($this->any()) + ->method('isSystemWideMountPoint') + ->willReturnCallback(function ($path, $owner) use ($systemWideMountSource, $systemWideMountTarget) { + if (strpos($path, 'source.txt') !== false) { + return $systemWideMountSource; + } + return $systemWideMountTarget; + }); + + $this->storage->renameKeys($source, $target); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataProviderCopyRename')] + public function testCopyKeys($source, $target, $systemWideMountSource, $systemWideMountTarget, $expectedSource, $expectedTarget): void { + $this->view->expects($this->any()) + ->method('file_exists') + ->willReturn(true); + $this->view->expects($this->any()) + ->method('is_dir') + ->willReturn(true); + $this->view->expects($this->once()) + ->method('copy') + ->with( + $this->equalTo($expectedSource), + $this->equalTo($expectedTarget)) + ->willReturn(true); + $this->util->expects($this->any()) + ->method('getUidAndFilename') + ->willReturnCallback([$this, 'getUidAndFilenameCallback']); + $this->util->expects($this->any()) + ->method('isSystemWideMountPoint') + ->willReturnCallback(function ($path, $owner) use ($systemWideMountSource, $systemWideMountTarget) { + if (strpos($path, 'source.txt') !== false) { + return $systemWideMountSource; + } + return $systemWideMountTarget; + }); + + $this->storage->copyKeys($source, $target); + } + + public function getUidAndFilenameCallback() { + $args = func_get_args(); + + $path = $args[0]; + $parts = explode('/', $path); + + return [$parts[1], '/' . implode('/', array_slice($parts, 2))]; + } + + public static function dataProviderCopyRename() { + return [ + ['/user1/files/source.txt', '/user1/files/target.txt', false, false, + '/user1/files_encryption/keys/files/source.txt/', '/user1/files_encryption/keys/files/target.txt/'], + ['/user1/files/foo/source.txt', '/user1/files/target.txt', false, false, + '/user1/files_encryption/keys/files/foo/source.txt/', '/user1/files_encryption/keys/files/target.txt/'], + ['/user1/files/source.txt', '/user1/files/foo/target.txt', false, false, + '/user1/files_encryption/keys/files/source.txt/', '/user1/files_encryption/keys/files/foo/target.txt/'], + ['/user1/files/source.txt', '/user1/files/foo/target.txt', true, true, + '/files_encryption/keys/files/source.txt/', '/files_encryption/keys/files/foo/target.txt/'], + ['/user1/files/source.txt', '/user1/files/target.txt', false, true, + '/user1/files_encryption/keys/files/source.txt/', '/files_encryption/keys/files/target.txt/'], + ['/user1/files/source.txt', '/user1/files/target.txt', true, false, + '/files_encryption/keys/files/source.txt/', '/user1/files_encryption/keys/files/target.txt/'], + + ['/user2/files/source.txt', '/user1/files/target.txt', false, false, + '/user2/files_encryption/keys/files/source.txt/', '/user1/files_encryption/keys/files/target.txt/'], + ['/user2/files/foo/source.txt', '/user1/files/target.txt', false, false, + '/user2/files_encryption/keys/files/foo/source.txt/', '/user1/files_encryption/keys/files/target.txt/'], + ['/user2/files/source.txt', '/user1/files/foo/target.txt', false, false, + '/user2/files_encryption/keys/files/source.txt/', '/user1/files_encryption/keys/files/foo/target.txt/'], + ['/user2/files/source.txt', '/user1/files/foo/target.txt', true, true, + '/files_encryption/keys/files/source.txt/', '/files_encryption/keys/files/foo/target.txt/'], + ['/user2/files/source.txt', '/user1/files/target.txt', false, true, + '/user2/files_encryption/keys/files/source.txt/', '/files_encryption/keys/files/target.txt/'], + ['/user2/files/source.txt', '/user1/files/target.txt', true, false, + '/files_encryption/keys/files/source.txt/', '/user1/files_encryption/keys/files/target.txt/'], + ]; + } + + /** + * + * @param string $path + * @param boolean $systemWideMountPoint + * @param string $storageRoot + * @param string $expected + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestGetPathToKeys')] + public function testGetPathToKeys($path, $systemWideMountPoint, $storageRoot, $expected): void { + $this->invokePrivate($this->storage, 'root_dir', [$storageRoot]); + + $this->util->expects($this->any()) + ->method('getUidAndFilename') + ->willReturnCallback([$this, 'getUidAndFilenameCallback']); + $this->util->expects($this->any()) + ->method('isSystemWideMountPoint') + ->willReturn($systemWideMountPoint); + + $this->assertSame($expected, + self::invokePrivate($this->storage, 'getPathToKeys', [$path]) + ); + } + + public static function dataTestGetPathToKeys() { + return [ + ['/user1/files/source.txt', false, '', '/user1/files_encryption/keys/files/source.txt/'], + ['/user1/files/source.txt', true, '', '/files_encryption/keys/files/source.txt/'], + ['/user1/files/source.txt', false, 'storageRoot', '/storageRoot/user1/files_encryption/keys/files/source.txt/'], + ['/user1/files/source.txt', true, 'storageRoot', '/storageRoot/files_encryption/keys/files/source.txt/'], + ]; + } + + public function testKeySetPreparation(): void { + $this->view->expects($this->any()) + ->method('file_exists') + ->willReturn(false); + $this->view->expects($this->any()) + ->method('is_dir') + ->willReturn(false); + $this->view->expects($this->any()) + ->method('mkdir') + ->willReturnCallback([$this, 'mkdirCallback']); + + $this->mkdirStack = [ + '/user1/files_encryption/keys/foo', + '/user1/files_encryption/keys', + '/user1/files_encryption', + '/user1']; + + self::invokePrivate($this->storage, 'keySetPreparation', ['/user1/files_encryption/keys/foo']); + } + + public function mkdirCallback() { + $args = func_get_args(); + $expected = array_pop($this->mkdirStack); + $this->assertSame($expected, $args[0]); + } + + + /** + * @param bool $createBackupDir + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestBackupUserKeys')] + public function testBackupUserKeys($createBackupDir): void { + $storage = $this->getMockBuilder('OC\Encryption\Keys\Storage') + ->setConstructorArgs([$this->view, $this->util, $this->crypto, $this->config]) + ->onlyMethods(['getTimestamp']) + ->getMock(); + + $storage->expects($this->any())->method('getTimestamp')->willReturn('1234567'); + + $this->view->expects($this->once())->method('file_exists') + ->with('user1/files_encryption/backup')->willReturn(!$createBackupDir); + + if ($createBackupDir) { + $calls = [ + 'user1/files_encryption/backup', + 'user1/files_encryption/backup/test.encryptionModule.1234567', + ]; + $this->view->expects($this->exactly(2))->method('mkdir') + ->willReturnCallback(function ($path) use (&$calls): void { + $expected = array_shift($calls); + $this->assertEquals($expected, $path); + }); + } else { + $this->view->expects($this->once())->method('mkdir') + ->with('user1/files_encryption/backup/test.encryptionModule.1234567'); + } + + $this->view->expects($this->once())->method('copy') + ->with( + 'user1/files_encryption/encryptionModule', + 'user1/files_encryption/backup/test.encryptionModule.1234567' + )->willReturn(true); + + $this->assertTrue($storage->backupUserKeys('encryptionModule', 'test', 'user1')); + } + + public static function dataTestBackupUserKeys() { + return [ + [true], [false] + ]; + } +} diff --git a/tests/lib/Encryption/ManagerTest.php b/tests/lib/Encryption/ManagerTest.php new file mode 100644 index 00000000000..e9b6ddae8a0 --- /dev/null +++ b/tests/lib/Encryption/ManagerTest.php @@ -0,0 +1,274 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace Test\Encryption; + +use OC\Encryption\Exceptions\ModuleAlreadyExistsException; +use OC\Encryption\Exceptions\ModuleDoesNotExistsException; +use OC\Encryption\Manager; +use OC\Encryption\Util; +use OC\Files\View; +use OC\Memcache\ArrayCache; +use OCP\Encryption\IEncryptionModule; +use OCP\IConfig; +use OCP\IL10N; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +class ManagerTest extends TestCase { + /** @var Manager */ + private $manager; + + /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */ + private $config; + + /** @var LoggerInterface|\PHPUnit\Framework\MockObject\MockObject */ + private $logger; + + /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */ + private $l10n; + + /** @var View|\PHPUnit\Framework\MockObject\MockObject */ + private $view; + + /** @var Util|\PHPUnit\Framework\MockObject\MockObject */ + private $util; + + /** @var ArrayCache|\PHPUnit\Framework\MockObject\MockObject */ + private $arrayCache; + + protected function setUp(): void { + parent::setUp(); + $this->config = $this->createMock(IConfig::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->l10n = $this->createMock(IL10N::class); + $this->view = $this->createMock(View::class); + $this->util = $this->createMock(Util::class); + $this->arrayCache = $this->createMock(ArrayCache::class); + $this->manager = new Manager($this->config, $this->logger, $this->l10n, $this->view, $this->util, $this->arrayCache); + } + + public function testManagerIsDisabled(): void { + $this->assertFalse($this->manager->isEnabled()); + } + + public function testManagerIsDisabledIfEnabledButNoModules(): void { + $this->config->expects($this->any())->method('getAppValue')->willReturn(true); + $this->assertFalse($this->manager->isEnabled()); + } + + public function testManagerIsDisabledIfDisabledButModules(): void { + $this->config->expects($this->any())->method('getAppValue')->willReturn(false); + $em = $this->createMock(IEncryptionModule::class); + $em->expects($this->any())->method('getId')->willReturn('id'); + $em->expects($this->any())->method('getDisplayName')->willReturn('TestDummyModule0'); + $this->manager->registerEncryptionModule('id', 'TestDummyModule0', function () use ($em) { + return $em; + }); + $this->assertFalse($this->manager->isEnabled()); + } + + public function testManagerIsEnabled(): void { + $this->config->expects($this->any())->method('getSystemValueBool')->willReturn(true); + $this->config->expects($this->any())->method('getAppValue')->willReturn('yes'); + $this->assertTrue($this->manager->isEnabled()); + } + + public function testModuleRegistration() { + $this->config->expects($this->any())->method('getAppValue')->willReturn('yes'); + + $this->addNewEncryptionModule($this->manager, 0); + $this->assertCount(1, $this->manager->getEncryptionModules()); + + return $this->manager; + } + + /** + * @depends testModuleRegistration + */ + public function testModuleReRegistration($manager): void { + $this->expectException(ModuleAlreadyExistsException::class); + $this->expectExceptionMessage('Id "ID0" already used by encryption module "TestDummyModule0"'); + + $this->addNewEncryptionModule($manager, 0); + } + + public function testModuleUnRegistration(): void { + $this->config->expects($this->any())->method('getAppValue')->willReturn(true); + $this->addNewEncryptionModule($this->manager, 0); + $this->assertCount(1, $this->manager->getEncryptionModules()); + + $this->manager->unregisterEncryptionModule('ID0'); + $this->assertEmpty($this->manager->getEncryptionModules()); + } + + + public function testGetEncryptionModuleUnknown(): void { + $this->expectException(ModuleDoesNotExistsException::class); + $this->expectExceptionMessage('Module with ID: unknown does not exist.'); + + $this->config->expects($this->any())->method('getAppValue')->willReturn(true); + $this->addNewEncryptionModule($this->manager, 0); + $this->assertCount(1, $this->manager->getEncryptionModules()); + $this->manager->getEncryptionModule('unknown'); + } + + public function testGetEncryptionModuleEmpty(): void { + global $defaultId; + $defaultId = null; + + $this->config->expects($this->any()) + ->method('getAppValue') + ->with('core', 'default_encryption_module') + ->willReturnCallback(function () { + global $defaultId; + return $defaultId; + }); + + $this->addNewEncryptionModule($this->manager, 0); + $this->assertCount(1, $this->manager->getEncryptionModules()); + $this->addNewEncryptionModule($this->manager, 1); + $this->assertCount(2, $this->manager->getEncryptionModules()); + + // Should return the default module + $defaultId = 'ID0'; + $this->assertEquals('ID0', $this->manager->getEncryptionModule()->getId()); + $defaultId = 'ID1'; + $this->assertEquals('ID1', $this->manager->getEncryptionModule()->getId()); + } + + public function testGetEncryptionModule(): void { + global $defaultId; + $defaultId = null; + + $this->config->expects($this->any()) + ->method('getAppValue') + ->with('core', 'default_encryption_module') + ->willReturnCallback(function () { + global $defaultId; + return $defaultId; + }); + + $this->addNewEncryptionModule($this->manager, 0); + $defaultId = 'ID0'; + $this->assertCount(1, $this->manager->getEncryptionModules()); + + $en0 = $this->manager->getEncryptionModule('ID0'); + $this->assertEquals('ID0', $en0->getId()); + + $en0 = self::invokePrivate($this->manager, 'getDefaultEncryptionModule'); + $this->assertEquals('ID0', $en0->getId()); + + $this->assertEquals('ID0', $this->manager->getDefaultEncryptionModuleId()); + } + + public function testSetDefaultEncryptionModule(): void { + global $defaultId; + $defaultId = null; + + $this->config->expects($this->any()) + ->method('getAppValue') + ->with('core', 'default_encryption_module') + ->willReturnCallback(function () { + global $defaultId; + return $defaultId; + }); + + $this->addNewEncryptionModule($this->manager, 0); + $this->assertCount(1, $this->manager->getEncryptionModules()); + $this->addNewEncryptionModule($this->manager, 1); + $this->assertCount(2, $this->manager->getEncryptionModules()); + + // Default module is the first we set + $defaultId = 'ID0'; + $this->assertEquals('ID0', $this->manager->getDefaultEncryptionModuleId()); + + // Set to an existing module + $this->config->expects($this->once()) + ->method('setAppValue') + ->with('core', 'default_encryption_module', 'ID1'); + $this->assertTrue($this->manager->setDefaultEncryptionModule('ID1')); + $defaultId = 'ID1'; + $this->assertEquals('ID1', $this->manager->getDefaultEncryptionModuleId()); + + // Set to an unexisting module + $this->assertFalse($this->manager->setDefaultEncryptionModule('ID2')); + $this->assertEquals('ID1', $this->manager->getDefaultEncryptionModuleId()); + } + + // /** + // * @expectedException \OC\Encryption\Exceptions\ModuleAlreadyExistsException + // * @expectedExceptionMessage Id "0" already used by encryption module "TestDummyModule0" + // */ + // public function testModuleRegistration() { + // $config = $this->createMock(IConfig::class); + // $config->expects($this->any())->method('getSystemValueBool')->willReturn(true); + // $em = $this->createMock(IEncryptionModule::class); + // $em->expects($this->any())->method('getId')->willReturn(0); + // $em->expects($this->any())->method('getDisplayName')->willReturn('TestDummyModule0'); + // $m = new Manager($config); + // $m->registerEncryptionModule($em); + // $this->assertTrue($m->isEnabled()); + // $m->registerEncryptionModule($em); + // } + // + // public function testModuleUnRegistration() { + // $config = $this->createMock(IConfig::class); + // $config->expects($this->any())->method('getSystemValueBool')->willReturn(true); + // $em = $this->createMock(IEncryptionModule::class); + // $em->expects($this->any())->method('getId')->willReturn(0); + // $em->expects($this->any())->method('getDisplayName')->willReturn('TestDummyModule0'); + // $m = new Manager($config); + // $m->registerEncryptionModule($em); + // $this->assertTrue($m->isEnabled()); + // $m->unregisterEncryptionModule($em); + // $this->assertFalse($m->isEnabled()); + // } + // + // /** + // * @expectedException \OC\Encryption\Exceptions\ModuleDoesNotExistsException + // * @expectedExceptionMessage Module with ID: unknown does not exist. + // */ + // public function testGetEncryptionModuleUnknown() { + // $config = $this->createMock(IConfig::class); + // $config->expects($this->any())->method('getSystemValueBool')->willReturn(true); + // $em = $this->createMock(IEncryptionModule::class); + // $em->expects($this->any())->method('getId')->willReturn(0); + // $em->expects($this->any())->method('getDisplayName')->willReturn('TestDummyModule0'); + // $m = new Manager($config); + // $m->registerEncryptionModule($em); + // $this->assertTrue($m->isEnabled()); + // $m->getEncryptionModule('unknown'); + // } + // + // public function testGetEncryptionModule() { + // $config = $this->createMock(IConfig::class); + // $config->expects($this->any())->method('getSystemValueBool')->willReturn(true); + // $em = $this->createMock(IEncryptionModule::class); + // $em->expects($this->any())->method('getId')->willReturn(0); + // $em->expects($this->any())->method('getDisplayName')->willReturn('TestDummyModule0'); + // $m = new Manager($config); + // $m->registerEncryptionModule($em); + // $this->assertTrue($m->isEnabled()); + // $en0 = $m->getEncryptionModule(0); + // $this->assertEquals(0, $en0->getId()); + // } + + protected function addNewEncryptionModule(Manager $manager, $id) { + $encryptionModule = $this->createMock(IEncryptionModule::class); + $encryptionModule->expects($this->any()) + ->method('getId') + ->willReturn('ID' . $id); + $encryptionModule->expects($this->any()) + ->method('getDisplayName') + ->willReturn('TestDummyModule' . $id); + /** @var IEncryptionModule $encryptionModule */ + $manager->registerEncryptionModule('ID' . $id, 'TestDummyModule' . $id, function () use ($encryptionModule) { + return $encryptionModule; + }); + } +} diff --git a/tests/lib/Encryption/UpdateTest.php b/tests/lib/Encryption/UpdateTest.php new file mode 100644 index 00000000000..04ca224c0a1 --- /dev/null +++ b/tests/lib/Encryption/UpdateTest.php @@ -0,0 +1,190 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ + +namespace Test\Encryption; + +use OC\Encryption\File; +use OC\Encryption\Update; +use OC\Encryption\Util; +use OC\Files\View; +use OCP\Encryption\IEncryptionModule; +use OCP\Files\File as OCPFile; +use OCP\Files\Folder; +use OCP\IUser; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +class UpdateTest extends TestCase { + private string $uid; + private View&MockObject $view; + private Util&MockObject $util; + private \OC\Encryption\Manager&MockObject $encryptionManager; + private IEncryptionModule&MockObject $encryptionModule; + private File&MockObject $fileHelper; + private LoggerInterface&MockObject $logger; + + protected function setUp(): void { + parent::setUp(); + + $this->view = $this->createMock(View::class); + $this->util = $this->createMock(Util::class); + $this->encryptionManager = $this->createMock(\OC\Encryption\Manager::class); + $this->fileHelper = $this->createMock(File::class); + $this->encryptionModule = $this->createMock(IEncryptionModule::class); + $this->logger = $this->createMock(LoggerInterface::class); + + $this->uid = 'testUser1'; + } + + private function getUserMock(string $uid): IUser&MockObject { + $user = $this->createMock(IUser::class); + $user->expects(self::any()) + ->method('getUID') + ->willReturn($uid); + return $user; + } + + private function getFileMock(string $path, string $owner): OCPFile&MockObject { + $node = $this->createMock(OCPFile::class); + $node->expects(self::atLeastOnce()) + ->method('getPath') + ->willReturn($path); + $node->expects(self::any()) + ->method('getOwner') + ->willReturn($this->getUserMock($owner)); + + return $node; + } + + private function getFolderMock(string $path, string $owner): Folder&MockObject { + $node = $this->createMock(Folder::class); + $node->expects(self::atLeastOnce()) + ->method('getPath') + ->willReturn($path); + $node->expects(self::any()) + ->method('getOwner') + ->willReturn($this->getUserMock($owner)); + + return $node; + } + + /** + * + * @param string $path + * @param boolean $isDir + * @param array $allFiles + * @param integer $numberOfFiles + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestUpdate')] + public function testUpdate($path, $isDir, $allFiles, $numberOfFiles): void { + $updateMock = $this->getUpdateMock(['getOwnerPath']); + $updateMock->expects($this->once())->method('getOwnerPath') + ->willReturnCallback(fn (OCPFile|Folder $node) => '/user/' . $node->getPath()); + + $this->encryptionManager->expects($this->once()) + ->method('getEncryptionModule') + ->willReturn($this->encryptionModule); + + if ($isDir) { + $this->util->expects($this->once()) + ->method('getAllFiles') + ->willReturn($allFiles); + $node = $this->getFolderMock($path, 'user'); + } else { + $node = $this->getFileMock($path, 'user'); + } + + $this->fileHelper->expects($this->exactly($numberOfFiles)) + ->method('getAccessList') + ->willReturn(['users' => [], 'public' => false]); + + $this->encryptionModule->expects($this->exactly($numberOfFiles)) + ->method('update') + ->willReturn(true); + + $updateMock->update($node); + } + + public static function dataTestUpdate(): array { + return [ + ['/user/files/foo', true, ['/user/files/foo/file1.txt', '/user/files/foo/file1.txt'], 2], + ['/user/files/test.txt', false, [], 1], + ]; + } + + /** + * + * @param string $source + * @param string $target + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestPostRename')] + public function testPostRename($source, $target): void { + $updateMock = $this->getUpdateMock(['update','getOwnerPath']); + + $sourceNode = $this->getFileMock($source, 'user'); + $targetNode = $this->getFileMock($target, 'user'); + + if (dirname($source) === dirname($target)) { + $updateMock->expects($this->never())->method('getOwnerPath'); + $updateMock->expects($this->never())->method('update'); + } else { + $updateMock->expects($this->once())->method('update') + ->willReturnCallback(fn (OCPFile|Folder $node) => $this->assertSame( + $target, + $node->getPath(), + 'update needs to be executed for the target destination' + )); + } + + $updateMock->postRename($sourceNode, $targetNode); + } + + public static function dataTestPostRename(): array { + return [ + ['/test.txt', '/testNew.txt'], + ['/folder/test.txt', '/testNew.txt'], + ['/test.txt', '/folder/testNew.txt'], + ]; + } + + public function testPostRestore(): void { + $updateMock = $this->getUpdateMock(['update']); + + $updateMock->expects($this->once())->method('update') + ->willReturnCallback(fn (OCPFile|Folder $node) => $this->assertSame( + '/folder/test.txt', + $node->getPath(), + 'update needs to be executed for the target destination' + )); + + $updateMock->postRestore($this->getFileMock('/folder/test.txt', 'user')); + } + + /** + * create mock of the update method + * + * @param array $methods methods which should be set + */ + protected function getUpdateMock(array $methods): Update&MockObject { + return $this->getMockBuilder(Update::class) + ->setConstructorArgs( + [ + $this->util, + $this->encryptionManager, + $this->fileHelper, + $this->logger, + $this->uid + ] + ) + ->onlyMethods($methods) + ->getMock(); + } +} diff --git a/tests/lib/Encryption/UtilTest.php b/tests/lib/Encryption/UtilTest.php new file mode 100644 index 00000000000..d1fefee872a --- /dev/null +++ b/tests/lib/Encryption/UtilTest.php @@ -0,0 +1,248 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace Test\Encryption; + +use OC\Encryption\Exceptions\EncryptionHeaderKeyExistsException; +use OC\Encryption\Util; +use OC\Files\View; +use OCP\Encryption\IEncryptionModule; +use OCP\IConfig; +use OCP\IGroupManager; +use OCP\IUserManager; +use Test\TestCase; + +class UtilTest extends TestCase { + /** + * block size will always be 8192 for a PHP stream + * + * @see https://bugs.php.net/bug.php?id=21641 + */ + protected static int $headerSize = 8192; + + /** @var \PHPUnit\Framework\MockObject\MockObject */ + protected $view; + + /** @var \PHPUnit\Framework\MockObject\MockObject|IUserManager */ + protected $userManager; + + /** @var \PHPUnit\Framework\MockObject\MockObject|IGroupManager */ + protected $groupManager; + + /** @var \PHPUnit\Framework\MockObject\MockObject|IConfig */ + private $config; + private Util $util; + + protected function setUp(): void { + parent::setUp(); + $this->view = $this->getMockBuilder(View::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->userManager = $this->createMock(IUserManager::class); + $this->groupManager = $this->createMock(IGroupManager::class); + $this->config = $this->createMock(IConfig::class); + + $this->util = new Util( + $this->view, + $this->userManager, + $this->groupManager, + $this->config + ); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('providesHeadersForEncryptionModule')] + public function testGetEncryptionModuleId($expected, $header): void { + $id = $this->util->getEncryptionModuleId($header); + $this->assertEquals($expected, $id); + } + + public static function providesHeadersForEncryptionModule(): array { + return [ + ['', []], + ['', ['1']], + [2, ['oc_encryption_module' => 2]], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('providesHeaders')] + public function testCreateHeader($expected, $header, $moduleId): void { + $em = $this->createMock(IEncryptionModule::class); + $em->expects($this->any())->method('getId')->willReturn($moduleId); + + $result = $this->util->createHeader($header, $em); + $this->assertEquals($expected, $result); + } + + public static function providesHeaders(): array { + return [ + [str_pad('HBEGIN:oc_encryption_module:0:HEND', self::$headerSize, '-', STR_PAD_RIGHT) + , [], '0'], + [str_pad('HBEGIN:oc_encryption_module:0:custom_header:foo:HEND', self::$headerSize, '-', STR_PAD_RIGHT) + , ['custom_header' => 'foo'], '0'], + ]; + } + + + public function testCreateHeaderFailed(): void { + $this->expectException(EncryptionHeaderKeyExistsException::class); + + + $header = ['header1' => 1, 'header2' => 2, 'oc_encryption_module' => 'foo']; + + $em = $this->createMock(IEncryptionModule::class); + $em->expects($this->any())->method('getId')->willReturn('moduleId'); + + $this->util->createHeader($header, $em); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('providePathsForTestIsExcluded')] + public function testIsExcluded($path, $keyStorageRoot, $expected): void { + $this->config->expects($this->once()) + ->method('getAppValue') + ->with('core', 'encryption_key_storage_root', '') + ->willReturn($keyStorageRoot); + $this->userManager + ->expects($this->any()) + ->method('userExists') + ->willReturnCallback([$this, 'isExcludedCallback']); + + $this->assertSame($expected, + $this->util->isExcluded($path) + ); + } + + public static function providePathsForTestIsExcluded(): array { + return [ + ['/files_encryption', '', true], + ['files_encryption/foo.txt', '', true], + ['test/foo.txt', '', false], + ['/user1/files_encryption/foo.txt', '', true], + ['/user1/files/foo.txt', '', false], + ['/keyStorage/user1/files/foo.txt', 'keyStorage', true], + ['/keyStorage/files_encryption', '/keyStorage', true], + ['keyStorage/user1/files_encryption', '/keyStorage/', true], + + ]; + } + + public function isExcludedCallback() { + $args = func_get_args(); + if ($args[0] === 'user1') { + return true; + } + + return false; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestIsFile')] + public function testIsFile($path, $expected): void { + $this->assertSame($expected, + $this->util->isFile($path) + ); + } + + public static function dataTestIsFile(): array { + return [ + ['/user/files/test.txt', true], + ['/user/files', true], + ['/user/files_versions/test.txt', false], + ['/user/foo/files/test.txt', false], + ['/files/foo/files/test.txt', false], + ['/user', false], + ['/user/test.txt', false], + ]; + } + + /** + * + * @param string $path + * @param string $expected + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestStripPartialFileExtension')] + public function testStripPartialFileExtension($path, $expected): void { + $this->assertSame($expected, + $this->util->stripPartialFileExtension($path)); + } + + public static function dataTestStripPartialFileExtension(): array { + return [ + ['/foo/test.txt', '/foo/test.txt'], + ['/foo/test.txt.part', '/foo/test.txt'], + ['/foo/test.txt.ocTransferId7567846853.part', '/foo/test.txt'], + ['/foo/test.txt.ocTransferId7567.part', '/foo/test.txt'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestParseRawHeader')] + public function testParseRawHeader($rawHeader, $expected): void { + $result = $this->util->parseRawHeader($rawHeader); + $this->assertSameSize($expected, $result); + foreach ($result as $key => $value) { + $this->assertArrayHasKey($key, $expected); + $this->assertSame($expected[$key], $value); + } + } + + public static function dataTestParseRawHeader(): array { + return [ + [str_pad('HBEGIN:oc_encryption_module:0:HEND', self::$headerSize, '-', STR_PAD_RIGHT) + , [Util::HEADER_ENCRYPTION_MODULE_KEY => '0']], + [str_pad('HBEGIN:oc_encryption_module:0:custom_header:foo:HEND', self::$headerSize, '-', STR_PAD_RIGHT) + , ['custom_header' => 'foo', Util::HEADER_ENCRYPTION_MODULE_KEY => '0']], + [str_pad('HelloWorld', self::$headerSize, '-', STR_PAD_RIGHT), []], + ['', []], + [str_pad('HBEGIN:oc_encryption_module:0', self::$headerSize, '-', STR_PAD_RIGHT) + , []], + [str_pad('oc_encryption_module:0:HEND', self::$headerSize, '-', STR_PAD_RIGHT) + , []], + ]; + } + + /** + * + * @param bool $isSystemWideMountPoint + * @param string $storageRoot + * @param string $expected + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestGetFileKeyDir')] + public function testGetFileKeyDir($isSystemWideMountPoint, $storageRoot, $expected): void { + $path = '/user1/files/foo/bar.txt'; + $owner = 'user1'; + $relativePath = '/foo/bar.txt'; + + $util = $this->getMockBuilder(Util::class) + ->onlyMethods(['isSystemWideMountPoint', 'getUidAndFilename', 'getKeyStorageRoot']) + ->setConstructorArgs([ + $this->view, + $this->userManager, + $this->groupManager, + $this->config + ]) + ->getMock(); + + $util->expects($this->once())->method('getKeyStorageRoot') + ->willReturn($storageRoot); + $util->expects($this->once())->method('isSystemWideMountPoint') + ->willReturn($isSystemWideMountPoint); + $util->expects($this->once())->method('getUidAndFilename') + ->with($path)->willReturn([$owner, $relativePath]); + + $this->assertSame($expected, + $util->getFileKeyDir('OC_DEFAULT_MODULE', $path) + ); + } + + public static function dataTestGetFileKeyDir(): array { + return [ + [false, '', '/user1/files_encryption/keys/foo/bar.txt/OC_DEFAULT_MODULE/'], + [true, '', '/files_encryption/keys/foo/bar.txt/OC_DEFAULT_MODULE/'], + [false, 'newStorageRoot', '/newStorageRoot/user1/files_encryption/keys/foo/bar.txt/OC_DEFAULT_MODULE/'], + [true, 'newStorageRoot', '/newStorageRoot/files_encryption/keys/foo/bar.txt/OC_DEFAULT_MODULE/'], + ]; + } +} |