EncryptionTest.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. *
  5. * @author Bjoern Schiessle <bjoern@schiessle.org>
  6. * @author Björn Schießle <bjoern@schiessle.org>
  7. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  8. * @author Joas Schilling <coding@schilljs.com>
  9. * @author Morris Jobke <hey@morrisjobke.de>
  10. * @author Roeland Jago Douma <roeland@famdouma.nl>
  11. *
  12. * @license AGPL-3.0
  13. *
  14. * This code is free software: you can redistribute it and/or modify
  15. * it under the terms of the GNU Affero General Public License, version 3,
  16. * as published by the Free Software Foundation.
  17. *
  18. * This program is distributed in the hope that it will be useful,
  19. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  20. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  21. * GNU Affero General Public License for more details.
  22. *
  23. * You should have received a copy of the GNU Affero General Public License, version 3,
  24. * along with this program. If not, see <http://www.gnu.org/licenses/>
  25. *
  26. */
  27. namespace OCA\Encryption\Tests\Crypto;
  28. use OCA\Encryption\Crypto\Crypt;
  29. use OCA\Encryption\Crypto\DecryptAll;
  30. use OCA\Encryption\Crypto\EncryptAll;
  31. use OCA\Encryption\Crypto\Encryption;
  32. use OCA\Encryption\Exceptions\PublicKeyMissingException;
  33. use OCA\Encryption\KeyManager;
  34. use OCA\Encryption\Session;
  35. use OCA\Encryption\Util;
  36. use OCP\Files\Storage;
  37. use OCP\IL10N;
  38. use OCP\ILogger;
  39. use Symfony\Component\Console\Input\InputInterface;
  40. use Symfony\Component\Console\Output\OutputInterface;
  41. use Test\TestCase;
  42. class EncryptionTest extends TestCase {
  43. /** @var Encryption */
  44. private $instance;
  45. /** @var \OCA\Encryption\KeyManager|\PHPUnit\Framework\MockObject\MockObject */
  46. private $keyManagerMock;
  47. /** @var \OCA\Encryption\Crypto\EncryptAll|\PHPUnit\Framework\MockObject\MockObject */
  48. private $encryptAllMock;
  49. /** @var \OCA\Encryption\Crypto\DecryptAll|\PHPUnit\Framework\MockObject\MockObject */
  50. private $decryptAllMock;
  51. /** @var \OCA\Encryption\Session|\PHPUnit\Framework\MockObject\MockObject */
  52. private $sessionMock;
  53. /** @var \OCA\Encryption\Crypto\Crypt|\PHPUnit\Framework\MockObject\MockObject */
  54. private $cryptMock;
  55. /** @var \OCA\Encryption\Util|\PHPUnit\Framework\MockObject\MockObject */
  56. private $utilMock;
  57. /** @var \OCP\ILogger|\PHPUnit\Framework\MockObject\MockObject */
  58. private $loggerMock;
  59. /** @var \OCP\IL10N|\PHPUnit\Framework\MockObject\MockObject */
  60. private $l10nMock;
  61. /** @var \OCP\Files\Storage|\PHPUnit\Framework\MockObject\MockObject */
  62. private $storageMock;
  63. protected function setUp(): void {
  64. parent::setUp();
  65. $this->storageMock = $this->getMockBuilder(Storage::class)
  66. ->disableOriginalConstructor()->getMock();
  67. $this->cryptMock = $this->getMockBuilder(Crypt::class)
  68. ->disableOriginalConstructor()
  69. ->getMock();
  70. $this->utilMock = $this->getMockBuilder(Util::class)
  71. ->disableOriginalConstructor()
  72. ->getMock();
  73. $this->keyManagerMock = $this->getMockBuilder(KeyManager::class)
  74. ->disableOriginalConstructor()
  75. ->getMock();
  76. $this->sessionMock = $this->getMockBuilder(Session::class)
  77. ->disableOriginalConstructor()
  78. ->getMock();
  79. $this->encryptAllMock = $this->getMockBuilder(EncryptAll::class)
  80. ->disableOriginalConstructor()
  81. ->getMock();
  82. $this->decryptAllMock = $this->getMockBuilder(DecryptAll::class)
  83. ->disableOriginalConstructor()
  84. ->getMock();
  85. $this->loggerMock = $this->getMockBuilder(ILogger::class)
  86. ->disableOriginalConstructor()
  87. ->getMock();
  88. $this->l10nMock = $this->getMockBuilder(IL10N::class)
  89. ->disableOriginalConstructor()
  90. ->getMock();
  91. $this->l10nMock->expects($this->any())
  92. ->method('t')
  93. ->with($this->anything())
  94. ->willReturnArgument(0);
  95. $this->instance = new Encryption(
  96. $this->cryptMock,
  97. $this->keyManagerMock,
  98. $this->utilMock,
  99. $this->sessionMock,
  100. $this->encryptAllMock,
  101. $this->decryptAllMock,
  102. $this->loggerMock,
  103. $this->l10nMock
  104. );
  105. }
  106. /**
  107. * test if public key from one of the recipients is missing
  108. */
  109. public function testEndUser1() {
  110. $this->instance->begin('/foo/bar', 'user1', 'r', [], ['users' => ['user1', 'user2', 'user3']]);
  111. $this->endTest();
  112. }
  113. /**
  114. * test if public key from owner is missing
  115. *
  116. */
  117. public function testEndUser2() {
  118. $this->expectException(\OCA\Encryption\Exceptions\PublicKeyMissingException::class);
  119. $this->instance->begin('/foo/bar', 'user2', 'r', [], ['users' => ['user1', 'user2', 'user3']]);
  120. $this->endTest();
  121. }
  122. /**
  123. * common part of testEndUser1 and testEndUser2
  124. *
  125. * @throws PublicKeyMissingException
  126. */
  127. public function endTest() {
  128. // prepare internal variables
  129. self::invokePrivate($this->instance, 'isWriteOperation', [true]);
  130. self::invokePrivate($this->instance, 'writeCache', ['']);
  131. $this->keyManagerMock->expects($this->any())
  132. ->method('getPublicKey')
  133. ->willReturnCallback([$this, 'getPublicKeyCallback']);
  134. $this->keyManagerMock->expects($this->any())
  135. ->method('addSystemKeys')
  136. ->willReturnCallback([$this, 'addSystemKeysCallback']);
  137. $this->cryptMock->expects($this->any())
  138. ->method('multiKeyEncrypt')
  139. ->willReturn(true);
  140. $this->instance->end('/foo/bar');
  141. }
  142. public function getPublicKeyCallback($uid) {
  143. if ($uid === 'user2') {
  144. throw new PublicKeyMissingException($uid);
  145. }
  146. return $uid;
  147. }
  148. public function addSystemKeysCallback($accessList, $publicKeys) {
  149. $this->assertSame(2, count($publicKeys));
  150. $this->assertArrayHasKey('user1', $publicKeys);
  151. $this->assertArrayHasKey('user3', $publicKeys);
  152. return $publicKeys;
  153. }
  154. /**
  155. * @dataProvider dataProviderForTestGetPathToRealFile
  156. */
  157. public function testGetPathToRealFile($path, $expected) {
  158. $this->assertSame($expected,
  159. self::invokePrivate($this->instance, 'getPathToRealFile', [$path])
  160. );
  161. }
  162. public function dataProviderForTestGetPathToRealFile() {
  163. return [
  164. ['/user/files/foo/bar.txt', '/user/files/foo/bar.txt'],
  165. ['/user/files/foo.txt', '/user/files/foo.txt'],
  166. ['/user/files_versions/foo.txt.v543534', '/user/files/foo.txt'],
  167. ['/user/files_versions/foo/bar.txt.v5454', '/user/files/foo/bar.txt'],
  168. ];
  169. }
  170. /**
  171. * @dataProvider dataTestBegin
  172. */
  173. public function testBegin($mode, $header, $legacyCipher, $defaultCipher, $fileKey, $expected) {
  174. $this->sessionMock->expects($this->once())
  175. ->method('decryptAllModeActivated')
  176. ->willReturn(false);
  177. $this->sessionMock->expects($this->never())->method('getDecryptAllUid');
  178. $this->sessionMock->expects($this->never())->method('getDecryptAllKey');
  179. $this->keyManagerMock->expects($this->never())->method('getEncryptedFileKey');
  180. $this->keyManagerMock->expects($this->never())->method('getShareKey');
  181. $this->cryptMock->expects($this->never())->method('multiKeyDecrypt');
  182. $this->cryptMock->expects($this->any())
  183. ->method('getCipher')
  184. ->willReturn($defaultCipher);
  185. $this->cryptMock->expects($this->any())
  186. ->method('getLegacyCipher')
  187. ->willReturn($legacyCipher);
  188. if (empty($fileKey)) {
  189. $this->cryptMock->expects($this->once())
  190. ->method('generateFileKey')
  191. ->willReturn('fileKey');
  192. } else {
  193. $this->cryptMock->expects($this->never())
  194. ->method('generateFileKey');
  195. }
  196. $this->keyManagerMock->expects($this->once())
  197. ->method('getFileKey')
  198. ->willReturn($fileKey);
  199. $result = $this->instance->begin('/user/files/foo.txt', 'user', $mode, $header, []);
  200. $this->assertArrayHasKey('cipher', $result);
  201. $this->assertSame($expected, $result['cipher']);
  202. if ($mode === 'w') {
  203. $this->assertTrue(self::invokePrivate($this->instance, 'isWriteOperation'));
  204. } else {
  205. $this->assertFalse(self::invokePrivate($this->instance, 'isWriteOperation'));
  206. }
  207. }
  208. public function dataTestBegin() {
  209. return [
  210. ['w', ['cipher' => 'myCipher'], 'legacyCipher', 'defaultCipher', 'fileKey', 'defaultCipher'],
  211. ['r', ['cipher' => 'myCipher'], 'legacyCipher', 'defaultCipher', 'fileKey', 'myCipher'],
  212. ['w', [], 'legacyCipher', 'defaultCipher', '', 'defaultCipher'],
  213. ['r', [], 'legacyCipher', 'defaultCipher', 'file_key', 'legacyCipher'],
  214. ];
  215. }
  216. /**
  217. * test begin() if decryptAll mode was activated
  218. */
  219. public function testBeginDecryptAll() {
  220. $path = '/user/files/foo.txt';
  221. $recoveryKeyId = 'recoveryKeyId';
  222. $recoveryShareKey = 'recoveryShareKey';
  223. $decryptAllKey = 'decryptAllKey';
  224. $fileKey = 'fileKey';
  225. $this->sessionMock->expects($this->once())
  226. ->method('decryptAllModeActivated')
  227. ->willReturn(true);
  228. $this->sessionMock->expects($this->once())
  229. ->method('getDecryptAllUid')
  230. ->willReturn($recoveryKeyId);
  231. $this->sessionMock->expects($this->once())
  232. ->method('getDecryptAllKey')
  233. ->willReturn($decryptAllKey);
  234. $this->keyManagerMock->expects($this->once())
  235. ->method('getEncryptedFileKey')
  236. ->willReturn('encryptedFileKey');
  237. $this->keyManagerMock->expects($this->once())
  238. ->method('getShareKey')
  239. ->with($path, $recoveryKeyId)
  240. ->willReturn($recoveryShareKey);
  241. $this->cryptMock->expects($this->once())
  242. ->method('multiKeyDecrypt')
  243. ->with('encryptedFileKey', $recoveryShareKey, $decryptAllKey)
  244. ->willReturn($fileKey);
  245. $this->keyManagerMock->expects($this->never())->method('getFileKey');
  246. $this->instance->begin($path, 'user', 'r', [], []);
  247. $this->assertSame($fileKey,
  248. $this->invokePrivate($this->instance, 'fileKey')
  249. );
  250. }
  251. /**
  252. * test begin() if encryption is not initialized but the master key is enabled
  253. * in this case we can initialize the encryption without a username/password
  254. * and continue
  255. */
  256. public function testBeginInitMasterKey() {
  257. $this->sessionMock->expects($this->once())->method('isReady')->willReturn(false);
  258. $this->utilMock->expects($this->once())->method('isMasterKeyEnabled')
  259. ->willReturn(true);
  260. $this->keyManagerMock->expects($this->once())->method('init')->with('', '');
  261. $this->instance->begin('/user/files/welcome.txt', 'user', 'r', [], []);
  262. }
  263. /**
  264. * @dataProvider dataTestUpdate
  265. *
  266. * @param string $fileKey
  267. * @param boolean $expected
  268. */
  269. public function testUpdate($fileKey, $expected) {
  270. $this->keyManagerMock->expects($this->once())
  271. ->method('getFileKey')->willReturn($fileKey);
  272. $this->keyManagerMock->expects($this->any())
  273. ->method('getPublicKey')->willReturn('publicKey');
  274. $this->keyManagerMock->expects($this->any())
  275. ->method('addSystemKeys')
  276. ->willReturnCallback(function ($accessList, $publicKeys) {
  277. return $publicKeys;
  278. });
  279. $this->keyManagerMock->expects($this->never())->method('getVersion');
  280. $this->keyManagerMock->expects($this->never())->method('setVersion');
  281. $this->assertSame($expected,
  282. $this->instance->update('path', 'user1', ['users' => ['user1']])
  283. );
  284. }
  285. public function dataTestUpdate() {
  286. return [
  287. ['', false],
  288. ['fileKey', true]
  289. ];
  290. }
  291. public function testUpdateNoUsers() {
  292. $this->invokePrivate($this->instance, 'rememberVersion', [['path' => 2]]);
  293. $this->keyManagerMock->expects($this->never())->method('getFileKey');
  294. $this->keyManagerMock->expects($this->never())->method('getPublicKey');
  295. $this->keyManagerMock->expects($this->never())->method('addSystemKeys');
  296. $this->keyManagerMock->expects($this->once())->method('setVersion')
  297. ->willReturnCallback(function ($path, $version, $view) {
  298. $this->assertSame('path', $path);
  299. $this->assertSame(2, $version);
  300. $this->assertTrue($view instanceof \OC\Files\View);
  301. });
  302. $this->instance->update('path', 'user1', []);
  303. }
  304. /**
  305. * Test case if the public key is missing. Nextcloud should still encrypt
  306. * the file for the remaining users
  307. */
  308. public function testUpdateMissingPublicKey() {
  309. $this->keyManagerMock->expects($this->once())
  310. ->method('getFileKey')->willReturn('fileKey');
  311. $this->keyManagerMock->expects($this->any())
  312. ->method('getPublicKey')->willReturnCallback(
  313. function ($user) {
  314. throw new PublicKeyMissingException($user);
  315. }
  316. );
  317. $this->keyManagerMock->expects($this->any())
  318. ->method('addSystemKeys')
  319. ->willReturnCallback(function ($accessList, $publicKeys) {
  320. return $publicKeys;
  321. });
  322. $this->cryptMock->expects($this->once())->method('multiKeyEncrypt')
  323. ->willReturnCallback(
  324. function ($fileKey, $publicKeys) {
  325. $this->assertEmpty($publicKeys);
  326. $this->assertSame('fileKey', $fileKey);
  327. }
  328. );
  329. $this->keyManagerMock->expects($this->never())->method('getVersion');
  330. $this->keyManagerMock->expects($this->never())->method('setVersion');
  331. $this->assertTrue(
  332. $this->instance->update('path', 'user1', ['users' => ['user1']])
  333. );
  334. }
  335. /**
  336. * by default the encryption module should encrypt regular files, files in
  337. * files_versions and files in files_trashbin
  338. *
  339. * @dataProvider dataTestShouldEncrypt
  340. */
  341. public function testShouldEncrypt($path, $shouldEncryptHomeStorage, $isHomeStorage, $expected) {
  342. $this->utilMock->expects($this->once())->method('shouldEncryptHomeStorage')
  343. ->willReturn($shouldEncryptHomeStorage);
  344. if ($shouldEncryptHomeStorage === false) {
  345. $this->storageMock->expects($this->once())->method('instanceOfStorage')
  346. ->with('\OCP\Files\IHomeStorage')->willReturn($isHomeStorage);
  347. $this->utilMock->expects($this->once())->method('getStorage')->with($path)
  348. ->willReturn($this->storageMock);
  349. }
  350. $this->assertSame($expected,
  351. $this->instance->shouldEncrypt($path)
  352. );
  353. }
  354. public function dataTestShouldEncrypt() {
  355. return [
  356. ['/user1/files/foo.txt', true, true, true],
  357. ['/user1/files_versions/foo.txt', true, true, true],
  358. ['/user1/files_trashbin/foo.txt', true, true, true],
  359. ['/user1/some_folder/foo.txt', true, true, false],
  360. ['/user1/foo.txt', true, true, false],
  361. ['/user1/files', true, true, false],
  362. ['/user1/files_trashbin', true, true, false],
  363. ['/user1/files_versions', true, true, false],
  364. // test if shouldEncryptHomeStorage is set to false
  365. ['/user1/files/foo.txt', false, true, false],
  366. ['/user1/files_versions/foo.txt', false, false, true],
  367. ];
  368. }
  369. public function testDecrypt() {
  370. $this->expectException(\OC\Encryption\Exceptions\DecryptionFailedException::class);
  371. $this->expectExceptionMessage('Cannot decrypt this file, probably this is a shared file. Please ask the file owner to reshare the file with you.');
  372. $this->instance->decrypt('abc');
  373. }
  374. public function testPrepareDecryptAll() {
  375. /** @var \Symfony\Component\Console\Input\InputInterface $input */
  376. $input = $this->createMock(InputInterface::class);
  377. /** @var \Symfony\Component\Console\Output\OutputInterface $output */
  378. $output = $this->createMock(OutputInterface::class);
  379. $this->decryptAllMock->expects($this->once())->method('prepare')
  380. ->with($input, $output, 'user');
  381. $this->instance->prepareDecryptAll($input, $output, 'user');
  382. }
  383. }