EncryptionTest.php 13 KB

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