|
8 | 8 |
|
9 | 9 | namespace OCA\encryption\tests; |
10 | 10 |
|
| 11 | +use OC\Files\ObjectStore\ObjectStoreStorage; |
| 12 | +use OC\Files\ObjectStore\StorageObjectStore; |
11 | 13 | use OC\Files\Storage\Temporary; |
12 | 14 | use OC\Files\Storage\Wrapper\Encryption; |
13 | 15 | use OC\Files\View; |
14 | 16 | use OCA\Encryption\KeyManager; |
15 | 17 | use OCP\Files\Mount\IMountManager; |
| 18 | +use OCP\Files\ObjectStore\IObjectStore; |
16 | 19 | use OCP\Files\Storage\IDisableEncryptionStorage; |
17 | 20 | use OCP\Server; |
18 | 21 | use Test\TestCase; |
@@ -71,4 +74,149 @@ public function testMoveFromEncrypted(): void { |
71 | 74 | $this->assertEquals('bar', $unencryptedStorage->file_get_contents('foo.txt')); |
72 | 75 | $this->assertFalse($unencryptedCache->get('foo.txt')->isEncrypted()); |
73 | 76 | } |
| 77 | + |
| 78 | + /** |
| 79 | + * The metadata only move between storages sharing an object store must not be taken |
| 80 | + * for an encrypted source: the ciphertext would stay in the object store while the |
| 81 | + * cache entry loses its `encrypted` mark. |
| 82 | + */ |
| 83 | + public function testMoveFromEncryptedObjectStore(): void { |
| 84 | + [ |
| 85 | + 'view' => $view, |
| 86 | + 'objectStore' => $objectStore, |
| 87 | + 'unencryptedStorage' => $unencryptedStorage, |
| 88 | + ] = $this->setUpSharedObjectStoreMounts(); |
| 89 | + |
| 90 | + $view->file_put_contents('enc/foo.txt', 'bar'); |
| 91 | + $this->assertEquals('bar', $view->file_get_contents('enc/foo.txt')); |
| 92 | + |
| 93 | + $view->rename('enc/foo.txt', 'unenc/foo.txt'); |
| 94 | + |
| 95 | + $this->assertEquals('bar', $view->file_get_contents('unenc/foo.txt')); |
| 96 | + $this->assertFalse($unencryptedStorage->getCache()->get('foo.txt')->isEncrypted()); |
| 97 | + $this->assertStringStartsNotWith( |
| 98 | + 'HBEGIN:', |
| 99 | + $this->readRawObject($objectStore, $unencryptedStorage, 'foo.txt'), |
| 100 | + 'the object was moved verbatim and is still encrypted at rest' |
| 101 | + ); |
| 102 | + // a move must not leave the source behind, neither on disk nor in the cache |
| 103 | + $this->assertFalse($view->file_exists('enc/foo.txt'), 'the source file still exists after the move'); |
| 104 | + } |
| 105 | + |
| 106 | + /** |
| 107 | + * Same as above for the copy shortcut, which hands the ciphertext to the object |
| 108 | + * store's server side copy. |
| 109 | + */ |
| 110 | + public function testCopyFromEncryptedObjectStore(): void { |
| 111 | + [ |
| 112 | + 'view' => $view, |
| 113 | + 'objectStore' => $objectStore, |
| 114 | + 'unencryptedStorage' => $unencryptedStorage, |
| 115 | + ] = $this->setUpSharedObjectStoreMounts(); |
| 116 | + |
| 117 | + $view->file_put_contents('enc/foo.txt', 'bar'); |
| 118 | + |
| 119 | + $view->copy('enc/foo.txt', 'unenc/foo.txt'); |
| 120 | + |
| 121 | + $this->assertEquals('bar', $view->file_get_contents('enc/foo.txt')); |
| 122 | + $this->assertEquals('bar', $view->file_get_contents('unenc/foo.txt')); |
| 123 | + $this->assertFalse($unencryptedStorage->getCache()->get('foo.txt')->isEncrypted()); |
| 124 | + $this->assertStringStartsNotWith( |
| 125 | + 'HBEGIN:', |
| 126 | + $this->readRawObject($objectStore, $unencryptedStorage, 'foo.txt'), |
| 127 | + 'the object was copied verbatim and is still encrypted at rest' |
| 128 | + ); |
| 129 | + } |
| 130 | + |
| 131 | + /** |
| 132 | + * A file without the `encrypted` mark holds plain content even on a wrapped storage |
| 133 | + * (only some paths encrypt, e.g. not uploads/) and must keep the metadata only move. |
| 134 | + */ |
| 135 | + public function testMoveUnencryptedFileFromEncryptionWrappedObjectStore(): void { |
| 136 | + [ |
| 137 | + 'view' => $view, |
| 138 | + 'unencryptedStorage' => $unencryptedStorage, |
| 139 | + 'encryptedBackingStorage' => $encryptedBackingStorage, |
| 140 | + ] = $this->setUpSharedObjectStoreMounts(); |
| 141 | + |
| 142 | + // bypasses the encryption wrapper: plain content, no `encrypted` mark |
| 143 | + $encryptedBackingStorage->file_put_contents('plain.txt', 'plain content'); |
| 144 | + $sourceEntry = $encryptedBackingStorage->getCache()->get('plain.txt'); |
| 145 | + $this->assertFalse($sourceEntry->isEncrypted()); |
| 146 | + |
| 147 | + $view->rename('enc/plain.txt', 'unenc/plain.txt'); |
| 148 | + |
| 149 | + $this->assertEquals('plain content', $view->file_get_contents('unenc/plain.txt')); |
| 150 | + $this->assertSame( |
| 151 | + $sourceEntry->getId(), |
| 152 | + $unencryptedStorage->getCache()->get('plain.txt')->getId(), |
| 153 | + 'a plain file must keep the metadata only move that preserves the file id' |
| 154 | + ); |
| 155 | + $this->assertFalse($view->file_exists('enc/plain.txt'), 'the source file still exists after the move'); |
| 156 | + } |
| 157 | + |
| 158 | + /** |
| 159 | + * A folder carries no `encrypted` mark of its own while any of its children may be |
| 160 | + * encrypted, so a folder move must always take the encryption aware path. |
| 161 | + */ |
| 162 | + public function testMoveFolderFromEncryptedObjectStore(): void { |
| 163 | + [ |
| 164 | + 'view' => $view, |
| 165 | + 'objectStore' => $objectStore, |
| 166 | + 'unencryptedStorage' => $unencryptedStorage, |
| 167 | + ] = $this->setUpSharedObjectStoreMounts(); |
| 168 | + |
| 169 | + $view->mkdir('enc/dir'); |
| 170 | + $view->file_put_contents('enc/dir/foo.txt', 'bar'); |
| 171 | + |
| 172 | + $view->rename('enc/dir', 'unenc/dir'); |
| 173 | + |
| 174 | + $this->assertEquals('bar', $view->file_get_contents('unenc/dir/foo.txt')); |
| 175 | + $this->assertFalse($unencryptedStorage->getCache()->get('dir/foo.txt')->isEncrypted()); |
| 176 | + $this->assertStringStartsNotWith( |
| 177 | + 'HBEGIN:', |
| 178 | + $this->readRawObject($objectStore, $unencryptedStorage, 'dir/foo.txt'), |
| 179 | + 'the folder took the metadata only move and left the child encrypted at rest' |
| 180 | + ); |
| 181 | + $this->assertFalse($view->file_exists('enc/dir'), 'the source folder still exists after the move'); |
| 182 | + } |
| 183 | + |
| 184 | + /** |
| 185 | + * Two object store storages backed by the same object store, one mounted with and one |
| 186 | + * without the encryption wrapper. |
| 187 | + * |
| 188 | + * @return array{view: View, objectStore: IObjectStore, unencryptedStorage: ObjectStoreStorage, encryptedBackingStorage: ObjectStoreStorage} |
| 189 | + */ |
| 190 | + private function setUpSharedObjectStoreMounts(): array { |
| 191 | + Server::get(KeyManager::class)->validateMasterKey(); |
| 192 | + Server::get(KeyManager::class)->validateShareKey(); |
| 193 | + $this->createUser('test1', 'test2'); |
| 194 | + $this->setupForUser('test1', 'test2'); |
| 195 | + |
| 196 | + // a shared object store instance makes the storage ids match, enabling the shortcuts |
| 197 | + $objectStore = new StorageObjectStore(new Temporary()); |
| 198 | + $encrypted = new ObjectStoreStorage(['objectstore' => $objectStore, 'storageid' => 'test-enc']); |
| 199 | + $unencrypted = new ObjectStoreNoEncrypted(['objectstore' => $objectStore, 'storageid' => 'test-unenc']); |
| 200 | + |
| 201 | + $this->registerMount('test1', $encrypted, '/test1/files/enc'); |
| 202 | + $this->registerMount('test1', $unencrypted, '/test1/files/unenc'); |
| 203 | + |
| 204 | + $this->loginWithEncryption('test1'); |
| 205 | + |
| 206 | + return [ |
| 207 | + 'view' => new View('/test1/files'), |
| 208 | + 'objectStore' => $objectStore, |
| 209 | + 'unencryptedStorage' => $unencrypted, |
| 210 | + 'encryptedBackingStorage' => $encrypted, |
| 211 | + ]; |
| 212 | + } |
| 213 | + |
| 214 | + private function readRawObject(IObjectStore $objectStore, ObjectStoreStorage $storage, string $path): string { |
| 215 | + $fileId = $storage->getCache()->get($path)->getId(); |
| 216 | + $handle = $objectStore->readObject($storage->getURN($fileId)); |
| 217 | + $content = stream_get_contents($handle); |
| 218 | + fclose($handle); |
| 219 | + |
| 220 | + return $content; |
| 221 | + } |
74 | 222 | } |
0 commit comments