EncryptionTest.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
  6. * SPDX-License-Identifier: AGPL-3.0-only
  7. */
  8. namespace Test\Files\Stream;
  9. use OC\Files\Cache\CacheEntry;
  10. use OC\Files\Storage\Wrapper\Wrapper;
  11. use OC\Files\View;
  12. use OC\Memcache\ArrayCache;
  13. use OCP\Encryption\IEncryptionModule;
  14. use OCP\EventDispatcher\IEventDispatcher;
  15. use OCP\Files\Cache\ICache;
  16. use OCP\ICacheFactory;
  17. use OCP\IConfig;
  18. use PHPUnit\Framework\MockObject\MockObject;
  19. use Psr\Log\LoggerInterface;
  20. class EncryptionTest extends \Test\TestCase {
  21. public const DEFAULT_WRAPPER = '\OC\Files\Stream\Encryption';
  22. private IEncryptionModule&MockObject $encryptionModule;
  23. /**
  24. * @param class-string<Wrapper> $wrapper
  25. * @return resource
  26. */
  27. protected function getStream(string $fileName, string $mode, int $unencryptedSize, string $wrapper = self::DEFAULT_WRAPPER, int $unencryptedSizeOnClose = 0) {
  28. clearstatcache();
  29. $size = filesize($fileName);
  30. $source = fopen($fileName, $mode);
  31. $internalPath = $fileName;
  32. $fullPath = $fileName;
  33. $header = [];
  34. $uid = '';
  35. $this->encryptionModule = $this->buildMockModule();
  36. $cache = $this->createMock(ICache::class);
  37. $storage = $this->getMockBuilder('\OC\Files\Storage\Storage')
  38. ->disableOriginalConstructor()->getMock();
  39. $encStorage = $this->getMockBuilder('\OC\Files\Storage\Wrapper\Encryption')
  40. ->disableOriginalConstructor()->getMock();
  41. $config = $this->getMockBuilder(IConfig::class)
  42. ->disableOriginalConstructor()
  43. ->getMock();
  44. $arrayCache = $this->createMock(ArrayCache::class);
  45. $groupManager = $this->getMockBuilder('\OC\Group\Manager')
  46. ->disableOriginalConstructor()
  47. ->getMock();
  48. $file = $this->getMockBuilder('\OC\Encryption\File')
  49. ->disableOriginalConstructor()
  50. ->setMethods(['getAccessList'])
  51. ->getMock();
  52. $file->expects($this->any())->method('getAccessList')->willReturn([]);
  53. $util = $this->getMockBuilder('\OC\Encryption\Util')
  54. ->setMethods(['getUidAndFilename'])
  55. ->setConstructorArgs([new View(), new \OC\User\Manager(
  56. $config,
  57. $this->createMock(ICacheFactory::class),
  58. $this->createMock(IEventDispatcher::class),
  59. $this->createMock(LoggerInterface::class),
  60. ), $groupManager, $config, $arrayCache])
  61. ->getMock();
  62. $util->expects($this->any())
  63. ->method('getUidAndFilename')
  64. ->willReturn(['user1', $internalPath]);
  65. $storage->expects($this->any())->method('getCache')->willReturn($cache);
  66. $entry = new CacheEntry([
  67. 'fileid' => 5,
  68. 'encryptedVersion' => 2,
  69. 'unencrypted_size' => $unencryptedSizeOnClose,
  70. ]);
  71. $cache->expects($this->any())->method('get')->willReturn($entry);
  72. $cache->expects($this->any())->method('update')->with(5, ['encrypted' => 3, 'encryptedVersion' => 3, 'unencrypted_size' => $unencryptedSizeOnClose]);
  73. return $wrapper::wrap(
  74. $source,
  75. $internalPath,
  76. $fullPath,
  77. $header,
  78. $uid,
  79. $this->encryptionModule,
  80. $storage,
  81. $encStorage,
  82. $util,
  83. $file,
  84. $mode,
  85. $size,
  86. $unencryptedSize,
  87. 8192,
  88. true,
  89. $wrapper,
  90. );
  91. }
  92. /**
  93. * @dataProvider dataProviderStreamOpen()
  94. */
  95. public function testStreamOpen(
  96. $isMasterKeyUsed,
  97. $mode,
  98. $fullPath,
  99. $fileExists,
  100. $expectedSharePath,
  101. $expectedSize,
  102. $expectedUnencryptedSize,
  103. $expectedReadOnly,
  104. ): void {
  105. // build mocks
  106. $encryptionModuleMock = $this->getMockBuilder('\OCP\Encryption\IEncryptionModule')
  107. ->disableOriginalConstructor()->getMock();
  108. $encryptionModuleMock->expects($this->any())->method('needDetailedAccessList')->willReturn(!$isMasterKeyUsed);
  109. $encryptionModuleMock->expects($this->once())
  110. ->method('getUnencryptedBlockSize')->willReturn(99);
  111. $encryptionModuleMock->expects($this->once())
  112. ->method('begin')->willReturn([]);
  113. $storageMock = $this->getMockBuilder('\OC\Files\Storage\Storage')
  114. ->disableOriginalConstructor()->getMock();
  115. $storageMock->expects($this->once())->method('file_exists')->willReturn($fileExists);
  116. $fileMock = $this->getMockBuilder('\OC\Encryption\File')
  117. ->disableOriginalConstructor()->getMock();
  118. if ($isMasterKeyUsed) {
  119. $fileMock->expects($this->never())->method('getAccessList');
  120. } else {
  121. $fileMock->expects($this->once())->method('getAccessList')
  122. ->willReturnCallback(function ($sharePath) use ($expectedSharePath) {
  123. $this->assertSame($expectedSharePath, $sharePath);
  124. return [];
  125. });
  126. }
  127. $utilMock = $this->getMockBuilder('\OC\Encryption\Util')
  128. ->disableOriginalConstructor()->getMock();
  129. $utilMock->expects($this->any())
  130. ->method('getHeaderSize')
  131. ->willReturn(8192);
  132. // get a instance of the stream wrapper
  133. $streamWrapper = $this->getMockBuilder('\OC\Files\Stream\Encryption')
  134. ->setMethods(['loadContext', 'writeHeader', 'skipHeader'])->disableOriginalConstructor()->getMock();
  135. // set internal properties of the stream wrapper
  136. $stream = new \ReflectionClass('\OC\Files\Stream\Encryption');
  137. $encryptionModule = $stream->getProperty('encryptionModule');
  138. $encryptionModule->setAccessible(true);
  139. $encryptionModule->setValue($streamWrapper, $encryptionModuleMock);
  140. $encryptionModule->setAccessible(false);
  141. $storage = $stream->getProperty('storage');
  142. $storage->setAccessible(true);
  143. $storage->setValue($streamWrapper, $storageMock);
  144. $storage->setAccessible(false);
  145. $file = $stream->getProperty('file');
  146. $file->setAccessible(true);
  147. $file->setValue($streamWrapper, $fileMock);
  148. $file->setAccessible(false);
  149. $util = $stream->getProperty('util');
  150. $util->setAccessible(true);
  151. $util->setValue($streamWrapper, $utilMock);
  152. $util->setAccessible(false);
  153. $fullPathP = $stream->getProperty('fullPath');
  154. $fullPathP->setAccessible(true);
  155. $fullPathP->setValue($streamWrapper, $fullPath);
  156. $fullPathP->setAccessible(false);
  157. $header = $stream->getProperty('header');
  158. $header->setAccessible(true);
  159. $header->setValue($streamWrapper, []);
  160. $header->setAccessible(false);
  161. $this->invokePrivate($streamWrapper, 'signed', [true]);
  162. $this->invokePrivate($streamWrapper, 'internalPath', [$fullPath]);
  163. $this->invokePrivate($streamWrapper, 'uid', ['test']);
  164. // call stream_open, that's the method we want to test
  165. $dummyVar = 'foo';
  166. $streamWrapper->stream_open('', $mode, '', $dummyVar);
  167. // check internal properties
  168. $size = $stream->getProperty('size');
  169. $size->setAccessible(true);
  170. $this->assertSame($expectedSize, $size->getValue($streamWrapper));
  171. $size->setAccessible(false);
  172. $unencryptedSize = $stream->getProperty('unencryptedSize');
  173. $unencryptedSize->setAccessible(true);
  174. $this->assertSame($expectedUnencryptedSize, $unencryptedSize->getValue($streamWrapper));
  175. $unencryptedSize->setAccessible(false);
  176. $readOnly = $stream->getProperty('readOnly');
  177. $readOnly->setAccessible(true);
  178. $this->assertSame($expectedReadOnly, $readOnly->getValue($streamWrapper));
  179. $readOnly->setAccessible(false);
  180. }
  181. public function dataProviderStreamOpen() {
  182. return [
  183. [false, 'r', '/foo/bar/test.txt', true, '/foo/bar/test.txt', null, null, true],
  184. [false, 'r', '/foo/bar/test.txt', false, '/foo/bar', null, null, true],
  185. [false, 'w', '/foo/bar/test.txt', true, '/foo/bar/test.txt', 8192, 0, false],
  186. [true, 'r', '/foo/bar/test.txt', true, '/foo/bar/test.txt', null, null, true],
  187. [true, 'r', '/foo/bar/test.txt', false, '/foo/bar', null, null, true],
  188. [true, 'w', '/foo/bar/test.txt', true, '/foo/bar/test.txt', 8192, 0, false],
  189. ];
  190. }
  191. public function testWriteRead(): void {
  192. $fileName = tempnam('/tmp', 'FOO');
  193. $stream = $this->getStream($fileName, 'w+', 0, self::DEFAULT_WRAPPER, 6);
  194. $this->assertEquals(6, fwrite($stream, 'foobar'));
  195. fclose($stream);
  196. $stream = $this->getStream($fileName, 'r', 6);
  197. $this->assertEquals('foobar', fread($stream, 100));
  198. fclose($stream);
  199. $stream = $this->getStream($fileName, 'r+', 6, self::DEFAULT_WRAPPER, 6);
  200. $this->assertEquals(3, fwrite($stream, 'bar'));
  201. fclose($stream);
  202. $stream = $this->getStream($fileName, 'r', 6);
  203. $this->assertEquals('barbar', fread($stream, 100));
  204. fclose($stream);
  205. unlink($fileName);
  206. }
  207. public function testRewind(): void {
  208. $fileName = tempnam('/tmp', 'FOO');
  209. $stream = $this->getStream($fileName, 'w+', 0, self::DEFAULT_WRAPPER, 6);
  210. $this->assertEquals(6, fwrite($stream, 'foobar'));
  211. $this->assertEquals(true, rewind($stream));
  212. $this->assertEquals('foobar', fread($stream, 100));
  213. $this->assertEquals(true, rewind($stream));
  214. $this->assertEquals(3, fwrite($stream, 'bar'));
  215. fclose($stream);
  216. $stream = $this->getStream($fileName, 'r', 6);
  217. $this->assertEquals('barbar', fread($stream, 100));
  218. fclose($stream);
  219. unlink($fileName);
  220. }
  221. public function testSeek(): void {
  222. $fileName = tempnam('/tmp', 'FOO');
  223. $stream = $this->getStream($fileName, 'w+', 0, self::DEFAULT_WRAPPER, 9);
  224. $this->assertEquals(6, fwrite($stream, 'foobar'));
  225. $this->assertEquals(0, fseek($stream, 3));
  226. $this->assertEquals(6, fwrite($stream, 'foobar'));
  227. fclose($stream);
  228. $stream = $this->getStream($fileName, 'r', 9);
  229. $this->assertEquals('foofoobar', fread($stream, 100));
  230. $this->assertEquals(-1, fseek($stream, 10));
  231. $this->assertEquals(0, fseek($stream, 9));
  232. $this->assertEquals(-1, fseek($stream, -10, SEEK_CUR));
  233. $this->assertEquals(0, fseek($stream, -9, SEEK_CUR));
  234. $this->assertEquals(-1, fseek($stream, -10, SEEK_END));
  235. $this->assertEquals(0, fseek($stream, -9, SEEK_END));
  236. fclose($stream);
  237. unlink($fileName);
  238. }
  239. public function dataFilesProvider() {
  240. return [
  241. ['lorem-big.txt'],
  242. ['block-aligned.txt'],
  243. ['block-aligned-plus-one.txt'],
  244. ];
  245. }
  246. /**
  247. * @dataProvider dataFilesProvider
  248. */
  249. public function testWriteReadBigFile($testFile): void {
  250. $expectedData = file_get_contents(\OC::$SERVERROOT . '/tests/data/' . $testFile);
  251. // write it
  252. $fileName = tempnam('/tmp', 'FOO');
  253. $stream = $this->getStream($fileName, 'w+', 0, self::DEFAULT_WRAPPER, strlen($expectedData));
  254. // while writing the file from the beginning to the end we should never try
  255. // to read parts of the file. This should only happen for write operations
  256. // in the middle of a file
  257. $this->encryptionModule->expects($this->never())->method('decrypt');
  258. fwrite($stream, $expectedData);
  259. fclose($stream);
  260. // read it all
  261. $stream = $this->getStream($fileName, 'r', strlen($expectedData));
  262. $data = stream_get_contents($stream);
  263. fclose($stream);
  264. $this->assertEquals($expectedData, $data);
  265. // another read test with a loop like we do in several places:
  266. $stream = $this->getStream($fileName, 'r', strlen($expectedData));
  267. $data = '';
  268. while (!feof($stream)) {
  269. $data .= fread($stream, 8192);
  270. }
  271. fclose($stream);
  272. $this->assertEquals($expectedData, $data);
  273. unlink($fileName);
  274. }
  275. /**
  276. * simulate a non-seekable storage
  277. *
  278. * @dataProvider dataFilesProvider
  279. */
  280. public function testWriteToNonSeekableStorage($testFile): void {
  281. $wrapper = $this->getMockBuilder('\OC\Files\Stream\Encryption')
  282. ->setMethods(['parentSeekStream'])->getMock();
  283. $wrapper->expects($this->any())->method('parentSeekStream')->willReturn(false);
  284. $expectedData = file_get_contents(\OC::$SERVERROOT . '/tests/data/' . $testFile);
  285. // write it
  286. $fileName = tempnam('/tmp', 'FOO');
  287. $stream = $this->getStream($fileName, 'w+', 0, '\Test\Files\Stream\DummyEncryptionWrapper', strlen($expectedData));
  288. // while writing the file from the beginning to the end we should never try
  289. // to read parts of the file. This should only happen for write operations
  290. // in the middle of a file
  291. $this->encryptionModule->expects($this->never())->method('decrypt');
  292. fwrite($stream, $expectedData);
  293. fclose($stream);
  294. // read it all
  295. $stream = $this->getStream($fileName, 'r', strlen($expectedData), '\Test\Files\Stream\DummyEncryptionWrapper', strlen($expectedData));
  296. $data = stream_get_contents($stream);
  297. fclose($stream);
  298. $this->assertEquals($expectedData, $data);
  299. // another read test with a loop like we do in several places:
  300. $stream = $this->getStream($fileName, 'r', strlen($expectedData));
  301. $data = '';
  302. while (!feof($stream)) {
  303. $data .= fread($stream, 8192);
  304. }
  305. fclose($stream);
  306. $this->assertEquals($expectedData, $data);
  307. unlink($fileName);
  308. }
  309. protected function buildMockModule(): IEncryptionModule&MockObject {
  310. $encryptionModule = $this->getMockBuilder('\OCP\Encryption\IEncryptionModule')
  311. ->disableOriginalConstructor()
  312. ->setMethods(['getId', 'getDisplayName', 'begin', 'end', 'encrypt', 'decrypt', 'update', 'shouldEncrypt', 'getUnencryptedBlockSize', 'isReadable', 'encryptAll', 'prepareDecryptAll', 'isReadyForUser', 'needDetailedAccessList'])
  313. ->getMock();
  314. $encryptionModule->expects($this->any())->method('getId')->willReturn('UNIT_TEST_MODULE');
  315. $encryptionModule->expects($this->any())->method('getDisplayName')->willReturn('Unit test module');
  316. $encryptionModule->expects($this->any())->method('begin')->willReturn([]);
  317. $encryptionModule->expects($this->any())->method('end')->willReturn('');
  318. $encryptionModule->expects($this->any())->method('isReadable')->willReturn(true);
  319. $encryptionModule->expects($this->any())->method('needDetailedAccessList')->willReturn(false);
  320. $encryptionModule->expects($this->any())->method('encrypt')->willReturnCallback(function ($data) {
  321. // simulate different block size by adding some padding to the data
  322. if (isset($data[6125])) {
  323. return str_pad($data, 8192, 'X');
  324. }
  325. // last block
  326. return $data;
  327. });
  328. $encryptionModule->expects($this->any())->method('decrypt')->willReturnCallback(function ($data) {
  329. if (isset($data[8191])) {
  330. return substr($data, 0, 6126);
  331. }
  332. // last block
  333. return $data;
  334. });
  335. $encryptionModule->expects($this->any())->method('update')->willReturn(true);
  336. $encryptionModule->expects($this->any())->method('shouldEncrypt')->willReturn(true);
  337. $encryptionModule->expects($this->any())->method('getUnencryptedBlockSize')->willReturn(6126);
  338. return $encryptionModule;
  339. }
  340. }