EncryptionTest.php 12 KB

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