EncryptionTest.php 13 KB

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