EncryptionTest.php 13 KB

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