diff options
Diffstat (limited to 'apps/encryption/tests/Crypto')
-rw-r--r-- | apps/encryption/tests/Crypto/CryptTest.php | 429 | ||||
-rw-r--r-- | apps/encryption/tests/Crypto/DecryptAllTest.php | 109 | ||||
-rw-r--r-- | apps/encryption/tests/Crypto/EncryptAllTest.php | 385 | ||||
-rw-r--r-- | apps/encryption/tests/Crypto/EncryptionTest.php | 406 |
4 files changed, 1329 insertions, 0 deletions
diff --git a/apps/encryption/tests/Crypto/CryptTest.php b/apps/encryption/tests/Crypto/CryptTest.php new file mode 100644 index 00000000000..1355e2c855d --- /dev/null +++ b/apps/encryption/tests/Crypto/CryptTest.php @@ -0,0 +1,429 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\Encryption\Tests\Crypto; + +use OCA\Encryption\Crypto\Crypt; +use OCP\Encryption\Exceptions\GenericEncryptionException; +use OCP\IConfig; +use OCP\IL10N; +use OCP\IUserSession; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +class CryptTest extends TestCase { + protected LoggerInterface&MockObject $logger; + protected IUserSession&MockObject $userSession; + protected IConfig&MockObject $config; + protected IL10N&MockObject $l; + + protected Crypt $crypt; + + protected function setUp(): void { + parent::setUp(); + + $this->logger = $this->getMockBuilder(LoggerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->logger->expects($this->any()) + ->method('warning'); + $this->userSession = $this->getMockBuilder(IUserSession::class) + ->disableOriginalConstructor() + ->getMock(); + $this->config = $this->getMockBuilder(IConfig::class) + ->disableOriginalConstructor() + ->getMock(); + $this->l = $this->createMock(IL10N::class); + + $this->crypt = new Crypt($this->logger, $this->userSession, $this->config, $this->l); + } + + /** + * test getOpenSSLConfig without any additional parameters + */ + public function testGetOpenSSLConfigBasic(): void { + $this->config->expects($this->once()) + ->method('getSystemValue') + ->with($this->equalTo('openssl'), $this->equalTo([])) + ->willReturn([]); + + $result = self::invokePrivate($this->crypt, 'getOpenSSLConfig'); + $this->assertSame(1, count($result)); + $this->assertArrayHasKey('private_key_bits', $result); + $this->assertSame(4096, $result['private_key_bits']); + } + + /** + * test getOpenSSLConfig with additional parameters defined in config.php + */ + public function testGetOpenSSLConfig(): void { + $this->config->expects($this->once()) + ->method('getSystemValue') + ->with($this->equalTo('openssl'), $this->equalTo([])) + ->willReturn(['foo' => 'bar', 'private_key_bits' => 1028]); + + $result = self::invokePrivate($this->crypt, 'getOpenSSLConfig'); + $this->assertSame(2, count($result)); + $this->assertArrayHasKey('private_key_bits', $result); + $this->assertArrayHasKey('foo', $result); + $this->assertSame(1028, $result['private_key_bits']); + $this->assertSame('bar', $result['foo']); + } + + + /** + * test generateHeader with valid key formats + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestGenerateHeader')] + public function testGenerateHeader($keyFormat, $expected): void { + $this->config->expects($this->once()) + ->method('getSystemValueString') + ->with($this->equalTo('cipher'), $this->equalTo('AES-256-CTR')) + ->willReturn('AES-128-CFB'); + + if ($keyFormat) { + $result = $this->crypt->generateHeader($keyFormat); + } else { + $result = $this->crypt->generateHeader(); + } + + $this->assertSame($expected, $result); + } + + /** + * test generateHeader with invalid key format + * + */ + public function testGenerateHeaderInvalid(): void { + $this->expectException(\InvalidArgumentException::class); + + $this->crypt->generateHeader('unknown'); + } + + public static function dataTestGenerateHeader(): array { + return [ + [null, 'HBEGIN:cipher:AES-128-CFB:keyFormat:hash2:encoding:binary:HEND'], + ['password', 'HBEGIN:cipher:AES-128-CFB:keyFormat:password:encoding:binary:HEND'], + ['hash', 'HBEGIN:cipher:AES-128-CFB:keyFormat:hash:encoding:binary:HEND'] + ]; + } + + public function testGetCipherWithInvalidCipher(): void { + $this->config->expects($this->once()) + ->method('getSystemValueString') + ->with($this->equalTo('cipher'), $this->equalTo('AES-256-CTR')) + ->willReturn('Not-Existing-Cipher'); + $this->logger + ->expects($this->once()) + ->method('warning') + ->with('Unsupported cipher (Not-Existing-Cipher) defined in config.php supported. Falling back to AES-256-CTR'); + + $this->assertSame('AES-256-CTR', $this->crypt->getCipher()); + } + + /** + * @param string $configValue + * @param string $expected + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataProviderGetCipher')] + public function testGetCipher($configValue, $expected): void { + $this->config->expects($this->once()) + ->method('getSystemValueString') + ->with($this->equalTo('cipher'), $this->equalTo('AES-256-CTR')) + ->willReturn($configValue); + + $this->assertSame($expected, + $this->crypt->getCipher() + ); + } + + /** + * data provider for testGetCipher + */ + public static function dataProviderGetCipher(): array { + return [ + ['AES-128-CFB', 'AES-128-CFB'], + ['AES-256-CFB', 'AES-256-CFB'], + ['AES-128-CTR', 'AES-128-CTR'], + ['AES-256-CTR', 'AES-256-CTR'], + + ['unknown', 'AES-256-CTR'] + ]; + } + + /** + * test concatIV() + */ + public function testConcatIV(): void { + $result = self::invokePrivate( + $this->crypt, + 'concatIV', + ['content', 'my_iv']); + + $this->assertSame('content00iv00my_iv', + $result + ); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestSplitMetaData')] + public function testSplitMetaData($data, $expected): void { + $this->config->method('getSystemValueBool') + ->with('encryption_skip_signature_check', false) + ->willReturn(true); + $result = self::invokePrivate($this->crypt, 'splitMetaData', [$data, 'AES-256-CFB']); + $this->assertTrue(is_array($result)); + $this->assertSame(3, count($result)); + $this->assertArrayHasKey('encrypted', $result); + $this->assertArrayHasKey('iv', $result); + $this->assertArrayHasKey('signature', $result); + $this->assertSame($expected['encrypted'], $result['encrypted']); + $this->assertSame($expected['iv'], $result['iv']); + $this->assertSame($expected['signature'], $result['signature']); + } + + public static function dataTestSplitMetaData(): array { + return [ + ['encryptedContent00iv001234567890123456xx', + ['encrypted' => 'encryptedContent', 'iv' => '1234567890123456', 'signature' => false]], + ['encryptedContent00iv00123456789012345600sig00e1992521e437f6915f9173b190a512cfc38a00ac24502db44e0ba10c2bb0cc86xxx', + ['encrypted' => 'encryptedContent', 'iv' => '1234567890123456', 'signature' => 'e1992521e437f6915f9173b190a512cfc38a00ac24502db44e0ba10c2bb0cc86']], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestHasSignature')] + public function testHasSignature($data, $expected): void { + $this->config->method('getSystemValueBool') + ->with('encryption_skip_signature_check', false) + ->willReturn(true); + $this->assertSame($expected, + $this->invokePrivate($this->crypt, 'hasSignature', [$data, 'AES-256-CFB']) + ); + } + + public static function dataTestHasSignature(): array { + return [ + ['encryptedContent00iv001234567890123456xx', false], + ['encryptedContent00iv00123456789012345600sig00e1992521e437f6915f9173b190a512cfc38a00ac24502db44e0ba10c2bb0cc86xxx', true] + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestHasSignatureFail')] + public function testHasSignatureFail($cipher): void { + $this->expectException(GenericEncryptionException::class); + + $data = 'encryptedContent00iv001234567890123456xx'; + $this->invokePrivate($this->crypt, 'hasSignature', [$data, $cipher]); + } + + public static function dataTestHasSignatureFail(): array { + return [ + ['AES-256-CTR'], + ['aes-256-ctr'], + ['AES-128-CTR'], + ['ctr-256-ctr'] + ]; + } + + /** + * test addPadding() + */ + public function testAddPadding(): void { + $result = self::invokePrivate($this->crypt, 'addPadding', ['data']); + $this->assertSame('dataxxx', $result); + } + + /** + * test removePadding() + * + * @param $data + * @param $expected + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataProviderRemovePadding')] + public function testRemovePadding($data, $expected): void { + $result = self::invokePrivate($this->crypt, 'removePadding', [$data]); + $this->assertSame($expected, $result); + } + + /** + * data provider for testRemovePadding + */ + public static function dataProviderRemovePadding(): array { + return [ + ['dataxx', 'data'], + ['data', false] + ]; + } + + /** + * test parseHeader() + */ + public function testParseHeader(): void { + $header = 'HBEGIN:foo:bar:cipher:AES-256-CFB:encoding:binary:HEND'; + $result = self::invokePrivate($this->crypt, 'parseHeader', [$header]); + + $this->assertTrue(is_array($result)); + $this->assertSame(3, count($result)); + $this->assertArrayHasKey('foo', $result); + $this->assertArrayHasKey('cipher', $result); + $this->assertArrayHasKey('encoding', $result); + $this->assertSame('bar', $result['foo']); + $this->assertSame('AES-256-CFB', $result['cipher']); + $this->assertSame('binary', $result['encoding']); + } + + /** + * test encrypt() + * + * @return string + */ + public function testEncrypt() { + $decrypted = 'content'; + $password = 'password'; + $cipher = 'AES-256-CTR'; + $iv = self::invokePrivate($this->crypt, 'generateIv'); + + $this->assertTrue(is_string($iv)); + $this->assertSame(16, strlen($iv)); + + $result = self::invokePrivate($this->crypt, 'encrypt', [$decrypted, $iv, $password, $cipher]); + + $this->assertTrue(is_string($result)); + + return [ + 'password' => $password, + 'iv' => $iv, + 'cipher' => $cipher, + 'encrypted' => $result, + 'decrypted' => $decrypted]; + } + + /** + * test decrypt() + * + * @depends testEncrypt + */ + public function testDecrypt($data): void { + $result = self::invokePrivate( + $this->crypt, + 'decrypt', + [$data['encrypted'], $data['iv'], $data['password'], $data['cipher'], true]); + + $this->assertSame($data['decrypted'], $result); + } + + /** + * test return values of valid ciphers + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestGetKeySize')] + public function testGetKeySize($cipher, $expected): void { + $result = $this->invokePrivate($this->crypt, 'getKeySize', [$cipher]); + $this->assertSame($expected, $result); + } + + /** + * test exception if cipher is unknown + * + */ + public function testGetKeySizeFailure(): void { + $this->expectException(\InvalidArgumentException::class); + + $this->invokePrivate($this->crypt, 'getKeySize', ['foo']); + } + + public static function dataTestGetKeySize(): array { + return [ + ['AES-256-CFB', 32], + ['AES-128-CFB', 16], + ['AES-256-CTR', 32], + ['AES-128-CTR', 16], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestDecryptPrivateKey')] + public function testDecryptPrivateKey($header, $privateKey, $expectedCipher, $isValidKey, $expected): void { + $this->config->method('getSystemValueBool') + ->willReturnMap([ + ['encryption.legacy_format_support', false, true], + ['encryption.use_legacy_base64_encoding', false, false], + ]); + + /** @var Crypt|\PHPUnit\Framework\MockObject\MockObject $crypt */ + $crypt = $this->getMockBuilder(Crypt::class) + ->setConstructorArgs([ + $this->logger, + $this->userSession, + $this->config, + $this->l + ]) + ->onlyMethods([ + 'parseHeader', + 'generatePasswordHash', + 'symmetricDecryptFileContent', + 'isValidPrivateKey' + ]) + ->getMock(); + + $crypt->expects($this->once())->method('parseHeader')->willReturn($header); + if (isset($header['keyFormat']) && $header['keyFormat'] === 'hash') { + $crypt->expects($this->once())->method('generatePasswordHash')->willReturn('hash'); + $password = 'hash'; + } else { + $crypt->expects($this->never())->method('generatePasswordHash'); + $password = 'password'; + } + + $crypt->expects($this->once())->method('symmetricDecryptFileContent') + ->with('privateKey', $password, $expectedCipher)->willReturn('key'); + $crypt->expects($this->once())->method('isValidPrivateKey')->willReturn($isValidKey); + + $result = $crypt->decryptPrivateKey($privateKey, 'password'); + + $this->assertSame($expected, $result); + } + + public static function dataTestDecryptPrivateKey(): array { + return [ + [['cipher' => 'AES-128-CFB', 'keyFormat' => 'password'], 'HBEGIN:HENDprivateKey', 'AES-128-CFB', true, 'key'], + [['cipher' => 'AES-256-CFB', 'keyFormat' => 'password'], 'HBEGIN:HENDprivateKey', 'AES-256-CFB', true, 'key'], + [['cipher' => 'AES-256-CFB', 'keyFormat' => 'password'], 'HBEGIN:HENDprivateKey', 'AES-256-CFB', false, false], + [['cipher' => 'AES-256-CFB', 'keyFormat' => 'hash'], 'HBEGIN:HENDprivateKey', 'AES-256-CFB', true, 'key'], + [['cipher' => 'AES-256-CFB'], 'HBEGIN:HENDprivateKey', 'AES-256-CFB', true, 'key'], + [[], 'privateKey', 'AES-128-CFB', true, 'key'], + ]; + } + + public function testIsValidPrivateKey(): void { + $res = openssl_pkey_new(); + openssl_pkey_export($res, $privateKey); + + // valid private key + $this->assertTrue( + $this->invokePrivate($this->crypt, 'isValidPrivateKey', [$privateKey]) + ); + + // invalid private key + $this->assertFalse( + $this->invokePrivate($this->crypt, 'isValidPrivateKey', ['foo']) + ); + } + + public function testMultiKeyEncrypt(): void { + $res = openssl_pkey_new(); + openssl_pkey_export($res, $privateKey); + $publicKeyPem = openssl_pkey_get_details($res)['key']; + $publicKey = openssl_pkey_get_public($publicKeyPem); + + $shareKeys = $this->crypt->multiKeyEncrypt('content', ['user1' => $publicKey]); + $this->assertEquals( + 'content', + $this->crypt->multiKeyDecrypt($shareKeys['user1'], $privateKey) + ); + } +} diff --git a/apps/encryption/tests/Crypto/DecryptAllTest.php b/apps/encryption/tests/Crypto/DecryptAllTest.php new file mode 100644 index 00000000000..82e6100bce5 --- /dev/null +++ b/apps/encryption/tests/Crypto/DecryptAllTest.php @@ -0,0 +1,109 @@ +<?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 OCA\Encryption\Tests\Crypto; + +use OCA\Encryption\Crypto\Crypt; +use OCA\Encryption\Crypto\DecryptAll; +use OCA\Encryption\KeyManager; +use OCA\Encryption\Session; +use OCA\Encryption\Util; +use PHPUnit\Framework\MockObject\MockObject; +use Symfony\Component\Console\Helper\QuestionHelper; +use Test\TestCase; + +class DecryptAllTest extends TestCase { + + protected DecryptAll $instance; + + protected Util&MockObject $util; + protected KeyManager&MockObject $keyManager; + protected Crypt&MockObject $crypt; + protected Session&MockObject $session; + protected QuestionHelper&MockObject $questionHelper; + + protected function setUp(): void { + parent::setUp(); + + $this->util = $this->getMockBuilder(Util::class) + ->disableOriginalConstructor()->getMock(); + $this->keyManager = $this->getMockBuilder(KeyManager::class) + ->disableOriginalConstructor()->getMock(); + $this->crypt = $this->getMockBuilder(Crypt::class) + ->disableOriginalConstructor()->getMock(); + $this->session = $this->getMockBuilder(Session::class) + ->disableOriginalConstructor()->getMock(); + $this->questionHelper = $this->getMockBuilder(QuestionHelper::class) + ->disableOriginalConstructor()->getMock(); + + $this->instance = new DecryptAll( + $this->util, + $this->keyManager, + $this->crypt, + $this->session, + $this->questionHelper + ); + } + + public function testUpdateSession(): void { + $this->session->expects($this->once())->method('prepareDecryptAll') + ->with('user1', 'key1'); + + $this->invokePrivate($this->instance, 'updateSession', ['user1', 'key1']); + } + + /** + * + * @param string $user + * @param string $recoveryKeyId + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestGetPrivateKey')] + public function testGetPrivateKey($user, $recoveryKeyId, $masterKeyId): void { + $password = 'passwd'; + $recoveryKey = 'recoveryKey'; + $userKey = 'userKey'; + $masterKey = 'userKey'; + $unencryptedKey = 'unencryptedKey'; + + $this->keyManager->expects($this->any())->method('getRecoveryKeyId') + ->willReturn($recoveryKeyId); + + if ($user === $recoveryKeyId) { + $this->keyManager->expects($this->once())->method('getSystemPrivateKey') + ->with($recoveryKeyId)->willReturn($recoveryKey); + $this->keyManager->expects($this->never())->method('getPrivateKey'); + $this->crypt->expects($this->once())->method('decryptPrivateKey') + ->with($recoveryKey, $password)->willReturn($unencryptedKey); + } elseif ($user === $masterKeyId) { + $this->keyManager->expects($this->once())->method('getSystemPrivateKey') + ->with($masterKeyId)->willReturn($masterKey); + $this->keyManager->expects($this->never())->method('getPrivateKey'); + $this->crypt->expects($this->once())->method('decryptPrivateKey') + ->with($masterKey, $password, $masterKeyId)->willReturn($unencryptedKey); + } else { + $this->keyManager->expects($this->never())->method('getSystemPrivateKey'); + $this->keyManager->expects($this->once())->method('getPrivateKey') + ->with($user)->willReturn($userKey); + $this->crypt->expects($this->once())->method('decryptPrivateKey') + ->with($userKey, $password, $user)->willReturn($unencryptedKey); + } + + $this->assertSame($unencryptedKey, + $this->invokePrivate($this->instance, 'getPrivateKey', [$user, $password]) + ); + } + + public static function dataTestGetPrivateKey() { + return [ + ['user1', 'recoveryKey', 'masterKeyId'], + ['recoveryKeyId', 'recoveryKeyId', 'masterKeyId'], + ['masterKeyId', 'masterKeyId', 'masterKeyId'] + ]; + } +} diff --git a/apps/encryption/tests/Crypto/EncryptAllTest.php b/apps/encryption/tests/Crypto/EncryptAllTest.php new file mode 100644 index 00000000000..c56e3375a73 --- /dev/null +++ b/apps/encryption/tests/Crypto/EncryptAllTest.php @@ -0,0 +1,385 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\Encryption\Tests\Crypto; + +use OC\Files\View; +use OCA\Encryption\Crypto\EncryptAll; +use OCA\Encryption\KeyManager; +use OCA\Encryption\Users\Setup; +use OCA\Encryption\Util; +use OCP\Files\FileInfo; +use OCP\IConfig; +use OCP\IL10N; +use OCP\IUserManager; +use OCP\L10N\IFactory; +use OCP\Mail\IMailer; +use OCP\Security\ISecureRandom; +use OCP\UserInterface; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Formatter\OutputFormatterInterface; +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Helper\QuestionHelper; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Test\TestCase; + +class EncryptAllTest extends TestCase { + + protected KeyManager&MockObject $keyManager; + protected Util&MockObject $util; + protected IUserManager&MockObject $userManager; + protected Setup&MockObject $setupUser; + protected View&MockObject $view; + protected IConfig&MockObject $config; + protected IMailer&MockObject $mailer; + protected IL10N&MockObject $l; + protected IFactory&MockObject $l10nFactory; + protected \Symfony\Component\Console\Helper\QuestionHelper&MockObject $questionHelper; + protected \Symfony\Component\Console\Input\InputInterface&MockObject $inputInterface; + protected \Symfony\Component\Console\Output\OutputInterface&MockObject $outputInterface; + protected UserInterface&MockObject $userInterface; + protected ISecureRandom&MockObject $secureRandom; + protected LoggerInterface&MockObject $logger; + + protected EncryptAll $encryptAll; + + protected function setUp(): void { + parent::setUp(); + $this->setupUser = $this->getMockBuilder(Setup::class) + ->disableOriginalConstructor()->getMock(); + $this->keyManager = $this->getMockBuilder(KeyManager::class) + ->disableOriginalConstructor()->getMock(); + $this->util = $this->getMockBuilder(Util::class) + ->disableOriginalConstructor()->getMock(); + $this->userManager = $this->getMockBuilder(IUserManager::class) + ->disableOriginalConstructor()->getMock(); + $this->view = $this->getMockBuilder(View::class) + ->disableOriginalConstructor()->getMock(); + $this->config = $this->getMockBuilder(IConfig::class) + ->disableOriginalConstructor()->getMock(); + $this->mailer = $this->getMockBuilder(IMailer::class) + ->disableOriginalConstructor()->getMock(); + $this->l10nFactory = $this->createMock(IFactory::class); + $this->l = $this->getMockBuilder(IL10N::class) + ->disableOriginalConstructor()->getMock(); + $this->questionHelper = $this->getMockBuilder(QuestionHelper::class) + ->disableOriginalConstructor()->getMock(); + $this->inputInterface = $this->getMockBuilder(InputInterface::class) + ->disableOriginalConstructor()->getMock(); + $this->outputInterface = $this->getMockBuilder(OutputInterface::class) + ->disableOriginalConstructor()->getMock(); + $this->userInterface = $this->getMockBuilder(UserInterface::class) + ->disableOriginalConstructor()->getMock(); + $this->logger = $this->createMock(LoggerInterface::class); + + /** + * We need format method to return a string + * @var OutputFormatterInterface&MockObject + */ + $outputFormatter = $this->createMock(OutputFormatterInterface::class); + $outputFormatter->method('isDecorated')->willReturn(false); + $outputFormatter->method('format')->willReturnArgument(0); + + $this->outputInterface->expects($this->any())->method('getFormatter') + ->willReturn($outputFormatter); + + $this->userManager->expects($this->any())->method('getBackends')->willReturn([$this->userInterface]); + $this->userInterface->expects($this->any())->method('getUsers')->willReturn(['user1', 'user2']); + + $this->secureRandom = $this->getMockBuilder(ISecureRandom::class)->disableOriginalConstructor()->getMock(); + $this->secureRandom->expects($this->any())->method('generate')->willReturn('12345678'); + + + $this->encryptAll = new EncryptAll( + $this->setupUser, + $this->userManager, + $this->view, + $this->keyManager, + $this->util, + $this->config, + $this->mailer, + $this->l, + $this->l10nFactory, + $this->questionHelper, + $this->secureRandom, + $this->logger, + ); + } + + protected function createFileInfoMock($type, string $name): FileInfo&MockObject { + $fileInfo = $this->createMock(FileInfo::class); + $fileInfo->method('getType')->willReturn($type); + $fileInfo->method('getName')->willReturn($name); + return $fileInfo; + } + + public function testEncryptAll(): void { + /** @var EncryptAll&MockObject $encryptAll */ + $encryptAll = $this->getMockBuilder(EncryptAll::class) + ->setConstructorArgs( + [ + $this->setupUser, + $this->userManager, + $this->view, + $this->keyManager, + $this->util, + $this->config, + $this->mailer, + $this->l, + $this->l10nFactory, + $this->questionHelper, + $this->secureRandom, + $this->logger, + ] + ) + ->onlyMethods(['createKeyPairs', 'encryptAllUsersFiles', 'outputPasswords']) + ->getMock(); + + $this->util->expects($this->any())->method('isMasterKeyEnabled')->willReturn(false); + $encryptAll->expects($this->once())->method('createKeyPairs')->with(); + $encryptAll->expects($this->once())->method('outputPasswords')->with(); + $encryptAll->expects($this->once())->method('encryptAllUsersFiles')->with(); + + $encryptAll->encryptAll($this->inputInterface, $this->outputInterface); + } + + public function testEncryptAllWithMasterKey(): void { + /** @var EncryptAll&MockObject $encryptAll */ + $encryptAll = $this->getMockBuilder(EncryptAll::class) + ->setConstructorArgs( + [ + $this->setupUser, + $this->userManager, + $this->view, + $this->keyManager, + $this->util, + $this->config, + $this->mailer, + $this->l, + $this->l10nFactory, + $this->questionHelper, + $this->secureRandom, + $this->logger, + ] + ) + ->onlyMethods(['createKeyPairs', 'encryptAllUsersFiles', 'outputPasswords']) + ->getMock(); + + $this->util->expects($this->any())->method('isMasterKeyEnabled')->willReturn(true); + $encryptAll->expects($this->never())->method('createKeyPairs'); + $this->keyManager->expects($this->once())->method('validateMasterKey'); + $encryptAll->expects($this->once())->method('encryptAllUsersFiles')->with(); + $encryptAll->expects($this->never())->method('outputPasswords'); + + $encryptAll->encryptAll($this->inputInterface, $this->outputInterface); + } + + public function testCreateKeyPairs(): void { + /** @var EncryptAll&MockObject $encryptAll */ + $encryptAll = $this->getMockBuilder(EncryptAll::class) + ->setConstructorArgs( + [ + $this->setupUser, + $this->userManager, + $this->view, + $this->keyManager, + $this->util, + $this->config, + $this->mailer, + $this->l, + $this->l10nFactory, + $this->questionHelper, + $this->secureRandom, + $this->logger, + ] + ) + ->onlyMethods(['setupUserFS', 'generateOneTimePassword']) + ->getMock(); + + + // set protected property $output + $this->invokePrivate($encryptAll, 'output', [$this->outputInterface]); + + $this->keyManager->expects($this->exactly(2))->method('userHasKeys') + ->willReturnCallback( + function ($user) { + if ($user === 'user1') { + return false; + } + return true; + } + ); + + $encryptAll->expects($this->once())->method('setupUserFS')->with('user1'); + $encryptAll->expects($this->once())->method('generateOneTimePassword')->with('user1')->willReturn('password'); + $this->setupUser->expects($this->once())->method('setupUser')->with('user1', 'password'); + + $this->invokePrivate($encryptAll, 'createKeyPairs'); + + $userPasswords = $this->invokePrivate($encryptAll, 'userPasswords'); + + // we only expect the skipped user, because generateOneTimePassword which + // would set the user with the new password was mocked. + // This method will be tested separately + $this->assertSame(1, count($userPasswords)); + $this->assertSame('', $userPasswords['user2']); + } + + public function testEncryptAllUsersFiles(): void { + /** @var EncryptAll&MockObject $encryptAll */ + $encryptAll = $this->getMockBuilder(EncryptAll::class) + ->setConstructorArgs( + [ + $this->setupUser, + $this->userManager, + $this->view, + $this->keyManager, + $this->util, + $this->config, + $this->mailer, + $this->l, + $this->l10nFactory, + $this->questionHelper, + $this->secureRandom, + $this->logger, + ] + ) + ->onlyMethods(['encryptUsersFiles']) + ->getMock(); + + $this->util->expects($this->any())->method('isMasterKeyEnabled')->willReturn(false); + + // set protected property $output + $this->invokePrivate($encryptAll, 'output', [$this->outputInterface]); + $this->invokePrivate($encryptAll, 'userPasswords', [['user1' => 'pwd1', 'user2' => 'pwd2']]); + + $encryptAllCalls = []; + $encryptAll->expects($this->exactly(2)) + ->method('encryptUsersFiles') + ->willReturnCallback(function ($uid) use (&$encryptAllCalls): void { + $encryptAllCalls[] = $uid; + }); + + $this->invokePrivate($encryptAll, 'encryptAllUsersFiles'); + self::assertEquals([ + 'user1', + 'user2', + ], $encryptAllCalls); + } + + public function testEncryptUsersFiles(): void { + /** @var EncryptAll&MockObject $encryptAll */ + $encryptAll = $this->getMockBuilder(EncryptAll::class) + ->setConstructorArgs( + [ + $this->setupUser, + $this->userManager, + $this->view, + $this->keyManager, + $this->util, + $this->config, + $this->mailer, + $this->l, + $this->l10nFactory, + $this->questionHelper, + $this->secureRandom, + $this->logger, + ] + ) + ->onlyMethods(['encryptFile', 'setupUserFS']) + ->getMock(); + + $this->util->expects($this->any())->method('isMasterKeyEnabled')->willReturn(false); + + $this->view->expects($this->exactly(2))->method('getDirectoryContent') + ->willReturnMap([ + [ + '/user1/files', + '', + null, + [ + $this->createFileInfoMock(FileInfo::TYPE_FOLDER, 'foo'), + $this->createFileInfoMock(FileInfo::TYPE_FILE, 'bar'), + ], + ], + [ + '/user1/files/foo', + '', + null, + [ + $this->createFileInfoMock(FileInfo::TYPE_FILE, 'subfile'), + ], + ], + ]); + + $encryptAllCalls = []; + $encryptAll->expects($this->exactly(2)) + ->method('encryptFile') + ->willReturnCallback(function (FileInfo $file, string $path) use (&$encryptAllCalls): bool { + $encryptAllCalls[] = $path; + return true; + }); + + $outputFormatter = $this->createMock(OutputFormatterInterface::class); + $outputFormatter->method('isDecorated')->willReturn(false); + $this->outputInterface->expects($this->any()) + ->method('getFormatter') + ->willReturn($outputFormatter); + $progressBar = new ProgressBar($this->outputInterface); + + $this->invokePrivate($encryptAll, 'encryptUsersFiles', ['user1', $progressBar, '']); + self::assertEquals([ + '/user1/files/bar', + '/user1/files/foo/subfile', + ], $encryptAllCalls); + } + + public function testGenerateOneTimePassword(): void { + $password = $this->invokePrivate($this->encryptAll, 'generateOneTimePassword', ['user1']); + $this->assertTrue(is_string($password)); + $this->assertSame(8, strlen($password)); + + $userPasswords = $this->invokePrivate($this->encryptAll, 'userPasswords'); + $this->assertSame(1, count($userPasswords)); + $this->assertSame($password, $userPasswords['user1']); + } + + /** + * @param $isEncrypted + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestEncryptFile')] + public function testEncryptFile($isEncrypted): void { + $fileInfo = $this->createMock(FileInfo::class); + $fileInfo->expects($this->any())->method('isEncrypted') + ->willReturn($isEncrypted); + $this->view->expects($this->never())->method('getFileInfo'); + + + if ($isEncrypted) { + $this->view->expects($this->never())->method('copy'); + $this->view->expects($this->never())->method('rename'); + } else { + $this->view->expects($this->once())->method('copy'); + $this->view->expects($this->once())->method('rename'); + } + + $this->assertTrue( + $this->invokePrivate($this->encryptAll, 'encryptFile', [$fileInfo, 'foo.txt']) + ); + } + + public static function dataTestEncryptFile(): array { + return [ + [true], + [false], + ]; + } +} diff --git a/apps/encryption/tests/Crypto/EncryptionTest.php b/apps/encryption/tests/Crypto/EncryptionTest.php new file mode 100644 index 00000000000..37e484550ef --- /dev/null +++ b/apps/encryption/tests/Crypto/EncryptionTest.php @@ -0,0 +1,406 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\Encryption\Tests\Crypto; + +use OC\Encryption\Exceptions\DecryptionFailedException; +use OC\Files\View; +use OCA\Encryption\Crypto\Crypt; +use OCA\Encryption\Crypto\DecryptAll; +use OCA\Encryption\Crypto\EncryptAll; +use OCA\Encryption\Crypto\Encryption; +use OCA\Encryption\Exceptions\PublicKeyMissingException; +use OCA\Encryption\KeyManager; +use OCA\Encryption\Session; +use OCA\Encryption\Util; +use OCP\Files\Storage\IStorage; +use OCP\IL10N; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Test\TestCase; + +class EncryptionTest extends TestCase { + + protected Encryption $instance; + + protected KeyManager&MockObject $keyManagerMock; + protected EncryptAll&MockObject $encryptAllMock; + protected DecryptAll&MockObject $decryptAllMock; + protected Session&MockObject $sessionMock; + protected Crypt&MockObject $cryptMock; + protected Util&MockObject $utilMock; + protected LoggerInterface&MockObject $loggerMock; + protected IL10N&MockObject $l10nMock; + protected IStorage&MockObject $storageMock; + + protected function setUp(): void { + parent::setUp(); + + $this->storageMock = $this->getMockBuilder(IStorage::class) + ->disableOriginalConstructor()->getMock(); + $this->cryptMock = $this->getMockBuilder(Crypt::class) + ->disableOriginalConstructor() + ->getMock(); + $this->utilMock = $this->getMockBuilder(Util::class) + ->disableOriginalConstructor() + ->getMock(); + $this->keyManagerMock = $this->getMockBuilder(KeyManager::class) + ->disableOriginalConstructor() + ->getMock(); + $this->sessionMock = $this->getMockBuilder(Session::class) + ->disableOriginalConstructor() + ->getMock(); + $this->encryptAllMock = $this->getMockBuilder(EncryptAll::class) + ->disableOriginalConstructor() + ->getMock(); + $this->decryptAllMock = $this->getMockBuilder(DecryptAll::class) + ->disableOriginalConstructor() + ->getMock(); + $this->loggerMock = $this->getMockBuilder(LoggerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->l10nMock = $this->getMockBuilder(IL10N::class) + ->disableOriginalConstructor() + ->getMock(); + $this->l10nMock->expects($this->any()) + ->method('t') + ->with($this->anything()) + ->willReturnArgument(0); + + $this->instance = new Encryption( + $this->cryptMock, + $this->keyManagerMock, + $this->utilMock, + $this->sessionMock, + $this->encryptAllMock, + $this->decryptAllMock, + $this->loggerMock, + $this->l10nMock + ); + } + + /** + * test if public key from one of the recipients is missing + */ + public function testEndUser1(): void { + $this->sessionMock->expects($this->once()) + ->method('decryptAllModeActivated') + ->willReturn(false); + + $this->instance->begin('/foo/bar', 'user1', 'r', [], ['users' => ['user1', 'user2', 'user3']]); + $this->endTest(); + } + + /** + * test if public key from owner is missing + * + */ + public function testEndUser2(): void { + $this->sessionMock->expects($this->once()) + ->method('decryptAllModeActivated') + ->willReturn(false); + + $this->expectException(PublicKeyMissingException::class); + + $this->instance->begin('/foo/bar', 'user2', 'r', [], ['users' => ['user1', 'user2', 'user3']]); + $this->endTest(); + } + + /** + * common part of testEndUser1 and testEndUser2 + * + * @throws PublicKeyMissingException + */ + public function endTest() { + // prepare internal variables + self::invokePrivate($this->instance, 'isWriteOperation', [true]); + self::invokePrivate($this->instance, 'writeCache', ['']); + + $this->keyManagerMock->expects($this->any()) + ->method('getPublicKey') + ->willReturnCallback([$this, 'getPublicKeyCallback']); + $this->keyManagerMock->expects($this->any()) + ->method('addSystemKeys') + ->willReturnCallback([$this, 'addSystemKeysCallback']); + $this->cryptMock->expects($this->any()) + ->method('multiKeyEncrypt') + ->willReturn([]); + + $this->instance->end('/foo/bar'); + } + + + public function getPublicKeyCallback($uid) { + if ($uid === 'user2') { + throw new PublicKeyMissingException($uid); + } + return $uid; + } + + public function addSystemKeysCallback($accessList, $publicKeys) { + $this->assertSame(2, count($publicKeys)); + $this->assertArrayHasKey('user1', $publicKeys); + $this->assertArrayHasKey('user3', $publicKeys); + return $publicKeys; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataProviderForTestGetPathToRealFile')] + public function testGetPathToRealFile($path, $expected): void { + $this->assertSame($expected, + self::invokePrivate($this->instance, 'getPathToRealFile', [$path]) + ); + } + + public static function dataProviderForTestGetPathToRealFile(): array { + return [ + ['/user/files/foo/bar.txt', '/user/files/foo/bar.txt'], + ['/user/files/foo.txt', '/user/files/foo.txt'], + ['/user/files_versions/foo.txt.v543534', '/user/files/foo.txt'], + ['/user/files_versions/foo/bar.txt.v5454', '/user/files/foo/bar.txt'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestBegin')] + public function testBegin($mode, $header, $legacyCipher, $defaultCipher, $fileKey, $expected): void { + $this->sessionMock->expects($this->once()) + ->method('decryptAllModeActivated') + ->willReturn(false); + + $this->sessionMock->expects($this->never())->method('getDecryptAllUid'); + $this->sessionMock->expects($this->never())->method('getDecryptAllKey'); + $this->keyManagerMock->expects($this->never())->method('getEncryptedFileKey'); + $this->keyManagerMock->expects($this->never())->method('getShareKey'); + $this->cryptMock->expects($this->never())->method('multiKeyDecrypt'); + + $this->cryptMock->expects($this->any()) + ->method('getCipher') + ->willReturn($defaultCipher); + $this->cryptMock->expects($this->any()) + ->method('getLegacyCipher') + ->willReturn($legacyCipher); + if (empty($fileKey)) { + $this->cryptMock->expects($this->once()) + ->method('generateFileKey') + ->willReturn('fileKey'); + } else { + $this->cryptMock->expects($this->never()) + ->method('generateFileKey'); + } + + $this->keyManagerMock->expects($this->once()) + ->method('getFileKey') + ->willReturn($fileKey); + + $result = $this->instance->begin('/user/files/foo.txt', 'user', $mode, $header, []); + + $this->assertArrayHasKey('cipher', $result); + $this->assertSame($expected, $result['cipher']); + if ($mode === 'w') { + $this->assertTrue(self::invokePrivate($this->instance, 'isWriteOperation')); + } else { + $this->assertFalse(self::invokePrivate($this->instance, 'isWriteOperation')); + } + } + + public static function dataTestBegin(): array { + return [ + ['w', ['cipher' => 'myCipher'], 'legacyCipher', 'defaultCipher', 'fileKey', 'defaultCipher'], + ['r', ['cipher' => 'myCipher'], 'legacyCipher', 'defaultCipher', 'fileKey', 'myCipher'], + ['w', [], 'legacyCipher', 'defaultCipher', '', 'defaultCipher'], + ['r', [], 'legacyCipher', 'defaultCipher', 'file_key', 'legacyCipher'], + ]; + } + + + /** + * test begin() if decryptAll mode was activated + */ + public function testBeginDecryptAll(): void { + $path = '/user/files/foo.txt'; + $fileKey = 'fileKey'; + + $this->sessionMock->expects($this->once()) + ->method('decryptAllModeActivated') + ->willReturn(true); + $this->keyManagerMock->expects($this->once()) + ->method('getFileKey') + ->with($path, 'user', null, true) + ->willReturn($fileKey); + + $this->instance->begin($path, 'user', 'r', [], []); + + $this->assertSame($fileKey, + $this->invokePrivate($this->instance, 'fileKey') + ); + } + + /** + * test begin() if encryption is not initialized but the master key is enabled + * in this case we can initialize the encryption without a username/password + * and continue + */ + public function testBeginInitMasterKey(): void { + $this->sessionMock->expects($this->once()) + ->method('decryptAllModeActivated') + ->willReturn(false); + + $this->sessionMock->expects($this->once())->method('isReady')->willReturn(false); + $this->utilMock->expects($this->once())->method('isMasterKeyEnabled') + ->willReturn(true); + $this->keyManagerMock->expects($this->once())->method('init')->with('', ''); + + $this->instance->begin('/user/files/welcome.txt', 'user', 'r', [], []); + } + + /** + * + * @param string $fileKey + * @param boolean $expected + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestUpdate')] + public function testUpdate($fileKey, $expected): void { + $this->keyManagerMock->expects($this->once()) + ->method('getFileKey')->willReturn($fileKey); + + $this->keyManagerMock->expects($this->any()) + ->method('getPublicKey')->willReturn('publicKey'); + + $this->keyManagerMock->expects($this->any()) + ->method('addSystemKeys') + ->willReturnCallback(function ($accessList, $publicKeys) { + return $publicKeys; + }); + + $this->keyManagerMock->expects($this->never())->method('getVersion'); + $this->keyManagerMock->expects($this->never())->method('setVersion'); + + $this->assertSame($expected, + $this->instance->update('path', 'user1', ['users' => ['user1']]) + ); + } + + public static function dataTestUpdate(): array { + return [ + ['', false], + ['fileKey', true] + ]; + } + + public function testUpdateNoUsers(): void { + $this->invokePrivate($this->instance, 'rememberVersion', [['path' => 2]]); + + $this->keyManagerMock->expects($this->never())->method('getFileKey'); + $this->keyManagerMock->expects($this->never())->method('getPublicKey'); + $this->keyManagerMock->expects($this->never())->method('addSystemKeys'); + $this->keyManagerMock->expects($this->once())->method('setVersion') + ->willReturnCallback(function ($path, $version, $view): void { + $this->assertSame('path', $path); + $this->assertSame(2, $version); + $this->assertTrue($view instanceof View); + }); + $this->instance->update('path', 'user1', []); + } + + /** + * Test case if the public key is missing. Nextcloud should still encrypt + * the file for the remaining users + */ + public function testUpdateMissingPublicKey(): void { + $this->keyManagerMock->expects($this->once()) + ->method('getFileKey')->willReturn('fileKey'); + + $this->keyManagerMock->expects($this->any()) + ->method('getPublicKey')->willReturnCallback( + function ($user): void { + throw new PublicKeyMissingException($user); + } + ); + + $this->keyManagerMock->expects($this->any()) + ->method('addSystemKeys') + ->willReturnCallback(function ($accessList, $publicKeys) { + return $publicKeys; + }); + + $this->cryptMock->expects($this->once())->method('multiKeyEncrypt') + ->willReturnCallback( + function ($fileKey, $publicKeys) { + $this->assertEmpty($publicKeys); + $this->assertSame('fileKey', $fileKey); + return []; + } + ); + + $this->keyManagerMock->expects($this->never())->method('getVersion'); + $this->keyManagerMock->expects($this->never())->method('setVersion'); + + $this->assertTrue( + $this->instance->update('path', 'user1', ['users' => ['user1']]) + ); + } + + /** + * by default the encryption module should encrypt regular files, files in + * files_versions and files in files_trashbin + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestShouldEncrypt')] + public function testShouldEncrypt($path, $shouldEncryptHomeStorage, $isHomeStorage, $expected): void { + $this->utilMock->expects($this->once())->method('shouldEncryptHomeStorage') + ->willReturn($shouldEncryptHomeStorage); + + if ($shouldEncryptHomeStorage === false) { + $this->storageMock->expects($this->once())->method('instanceOfStorage') + ->with('\OCP\Files\IHomeStorage')->willReturn($isHomeStorage); + $this->utilMock->expects($this->once())->method('getStorage')->with($path) + ->willReturn($this->storageMock); + } + + $this->assertSame($expected, + $this->instance->shouldEncrypt($path) + ); + } + + public static function dataTestShouldEncrypt(): array { + return [ + ['/user1/files/foo.txt', true, true, true], + ['/user1/files_versions/foo.txt', true, true, true], + ['/user1/files_trashbin/foo.txt', true, true, true], + ['/user1/some_folder/foo.txt', true, true, false], + ['/user1/foo.txt', true, true, false], + ['/user1/files', true, true, false], + ['/user1/files_trashbin', true, true, false], + ['/user1/files_versions', true, true, false], + // test if shouldEncryptHomeStorage is set to false + ['/user1/files/foo.txt', false, true, false], + ['/user1/files_versions/foo.txt', false, false, true], + ]; + } + + + public function testDecrypt(): void { + $this->expectException(DecryptionFailedException::class); + $this->expectExceptionMessage('Cannot decrypt this file, probably this is a shared file. Please ask the file owner to reshare the file with you.'); + + $this->instance->decrypt('abc'); + } + + public function testPrepareDecryptAll(): void { + /** @var \Symfony\Component\Console\Input\InputInterface $input */ + $input = $this->createMock(InputInterface::class); + /** @var \Symfony\Component\Console\Output\OutputInterface $output */ + $output = $this->createMock(OutputInterface::class); + + $this->decryptAllMock->expects($this->once())->method('prepare') + ->with($input, $output, 'user'); + + $this->instance->prepareDecryptAll($input, $output, 'user'); + } +} |