@@ -87,9 +87,31 @@ private function getCommand(): TestableFixKeyLocation {
8787 );
8888 }
8989
90- private function markAsEncrypted (TemporaryUnwrapped $ storage , string $ path ): void {
90+ private function markAsEncrypted (TemporaryUnwrapped $ storage , string $ path, int $ unencryptedSize ): void {
9191 $ cache = $ storage ->getCache ();
92- $ cache ->update ($ cache ->get ($ path )->getId (), ['encrypted ' => 1 ]);
92+ $ cache ->update ($ cache ->get ($ path )->getId (), [
93+ 'encrypted ' => 1 ,
94+ 'unencrypted_size ' => $ unencryptedSize ,
95+ ]);
96+ }
97+
98+ /**
99+ * A second user whose key tree can hold misplaced keys. The key tree is created
100+ * directly, no login needed, the encryption session of the first user stays
101+ * untouched.
102+ */
103+ private function setUpSecondUser (): void {
104+ $ this ->createUser ('test2 ' , 'test2 ' );
105+ }
106+
107+ private function moveKeyToSecondUserTree (string $ keyPath , string $ name ): void {
108+ $ rootView = new View ();
109+ foreach (['/test2 ' , '/test2/files_encryption ' , '/test2/files_encryption/keys ' ] as $ dir ) {
110+ if (!$ rootView ->file_exists ($ dir )) {
111+ $ rootView ->mkdir ($ dir );
112+ }
113+ }
114+ $ rootView ->rename (rtrim ($ keyPath , '/ ' ), '/test2/files_encryption/keys/ ' . $ name );
93115 }
94116
95117 /**
@@ -107,7 +129,7 @@ public function testKeyValidationReadsThroughEncryption(): void {
107129 // ciphertext under a path that has no key
108130 $ view ->file_put_contents ('stray/stray.txt ' , $ encryptedBackingStorage ->file_get_contents ('original.txt ' ));
109131 $ strayStorage = $ view ->getFileInfo ('stray/stray.txt ' )->getStorage ();
110- $ this ->markAsEncrypted ($ strayStorage , 'stray.txt ' );
132+ $ this ->markAsEncrypted ($ strayStorage , 'stray.txt ' , strlen ( ' secret content ' ) );
111133
112134 $ userFolder = Server::get (IRootFolder::class)->getUserFolder ('test1 ' );
113135 $ command = $ this ->getCommand ();
@@ -137,7 +159,7 @@ public function testFailedDecryptionRollsBack(): void {
137159 $ cipher = $ encryptedBackingStorage ->file_get_contents ('original.txt ' );
138160 $ view ->file_put_contents ('stray/broken.txt ' , $ cipher );
139161 $ strayStorage = $ view ->getFileInfo ('stray/broken.txt ' )->getStorage ();
140- $ this ->markAsEncrypted ($ strayStorage , 'broken.txt ' );
162+ $ this ->markAsEncrypted ($ strayStorage , 'broken.txt ' , strlen ( ' secret content ' ) );
141163
142164 $ userFolder = Server::get (IRootFolder::class)->getUserFolder ('test1 ' );
143165 $ strayNode = $ userFolder ->get ('stray/broken.txt ' );
@@ -153,11 +175,13 @@ public function testFailedDecryptionRollsBack(): void {
153175 $ brokenKeyPath = self ::invokePrivate ($ command , 'getUserKeyPath ' , [$ user , $ strayNode ]);
154176 $ rootView ->copy ($ wrongKey , str_replace ('broken.txt ' , 'broken.txt.bak ' , $ brokenKeyPath ));
155177
178+ $ threw = false ;
156179 try {
157180 self ::invokePrivate ($ command , 'decryptWithSystemKey ' , [$ strayNode , $ wrongKey ]);
158- $ this -> fail ( ' decrypting with the wrong key must fail ' );
159- } catch ( \ Exception $ e ) {
181+ } catch ( \ Exception ) {
182+ $ threw = true ;
160183 }
184+ $ this ->assertTrue ($ threw , 'decrypting with the wrong key must fail ' );
161185
162186 $ this ->assertTrue ($ view ->file_exists ('stray/broken.txt ' ), 'the original file has to be restored ' );
163187 $ this ->assertSame ($ cipher , $ view ->file_get_contents ('stray/broken.txt ' ), 'the original content has to be intact ' );
@@ -169,6 +193,70 @@ public function testFailedDecryptionRollsBack(): void {
169193 $ this ->assertFalse ($ rootView ->file_exists ($ systemKeyPathBak ), 'no temporary system key must be left behind ' );
170194 }
171195
196+ /**
197+ * A key that only exists in the tree of another user must be found and validated.
198+ * The harness mounts are not system wide, the encryption wrapper resolves the keys
199+ * of the stray file through the user tree, so the search is exercised with that
200+ * staging path, the system wide flow only stages at a different location.
201+ */
202+ public function testKeyFoundInAnotherUsersTree (): void {
203+ [
204+ 'view ' => $ view ,
205+ 'encryptedBackingStorage ' => $ encryptedBackingStorage ,
206+ ] = $ this ->setUpMounts ();
207+ $ this ->setUpSecondUser ();
208+
209+ $ view ->file_put_contents ('enc/original.txt ' , 'secret content ' );
210+ $ view ->file_put_contents ('stray/orphan.txt ' , $ encryptedBackingStorage ->file_get_contents ('original.txt ' ));
211+ $ strayStorage = $ view ->getFileInfo ('stray/orphan.txt ' )->getStorage ();
212+ $ this ->markAsEncrypted ($ strayStorage , 'orphan.txt ' , strlen ('secret content ' ));
213+
214+ $ command = $ this ->getCommand ();
215+ $ user = Server::get (IUserManager::class)->get ('test1 ' );
216+ $ userFolder = Server::get (IRootFolder::class)->getUserFolder ('test1 ' );
217+ $ originalKey = self ::invokePrivate ($ command , 'getUserKeyPath ' , [$ user , $ userFolder ->get ('enc/original.txt ' )]);
218+ // the key sits in ANOTHER user's tree, under the name of the stray file
219+ $ this ->moveKeyToSecondUserTree ($ originalKey , 'orphan.txt ' );
220+
221+ $ strayNode = $ userFolder ->get ('stray/orphan.txt ' );
222+ $ stagePath = self ::invokePrivate ($ command , 'getUserKeyPath ' , [$ user , $ strayNode ]);
223+ $ foundKey = self ::invokePrivate ($ command , 'findKeyInUserTrees ' , [$ user , $ strayNode , $ stagePath ]);
224+
225+ $ this ->assertNotNull ($ foundKey , 'the key in the other tree has to be found ' );
226+ $ this ->assertStringContainsString ('/test2/ ' , $ foundKey );
227+ }
228+
229+ /**
230+ * With --personal an encrypted file in the personal space whose key was lost is
231+ * restored from another user's tree, healthy files stay untouched.
232+ */
233+ public function testPersonalFileKeyFoundInAnotherUsersTree (): void {
234+ $ this ->setUpMounts ();
235+ $ this ->setUpSecondUser ();
236+
237+ $ view = new View ('/test1/files ' );
238+ $ view ->file_put_contents ('personal.txt ' , 'personal content ' );
239+ $ view ->file_put_contents ('healthy.txt ' , 'healthy content ' );
240+
241+ $ command = $ this ->getCommand ();
242+ $ user = Server::get (IUserManager::class)->get ('test1 ' );
243+ $ userFolder = Server::get (IRootFolder::class)->getUserFolder ('test1 ' );
244+ $ personalKey = self ::invokePrivate ($ command , 'getUserKeyPath ' , [$ user , $ userFolder ->get ('personal.txt ' )]);
245+ $ this ->moveKeyToSecondUserTree ($ personalKey , 'personal.txt ' );
246+
247+ $ command ->systemMounts = [];
248+ $ tester = new CommandTester ($ command );
249+ $ exitCode = $ tester ->execute (['user ' => 'test1 ' , '--personal ' => true ]);
250+ $ display = $ tester ->getDisplay ();
251+
252+ $ this ->assertSame (Command::SUCCESS , $ exitCode , $ display );
253+ $ this ->assertStringContainsString ('Migrated key from ' , $ display );
254+ $ this ->assertEquals ('personal content ' , $ view ->file_get_contents ('personal.txt ' ));
255+ $ rootView = new View ();
256+ $ this ->assertTrue ($ rootView ->file_exists ($ personalKey ), 'the key has to be back at the path of the file ' );
257+ $ this ->assertStringNotContainsString ('healthy.txt ' , $ display , 'healthy files must not be touched ' );
258+ }
259+
172260 /**
173261 * One broken file must not abort the whole run, the remaining files still get
174262 * processed and the failure is reported.
@@ -180,17 +268,17 @@ public function testExecuteContinuesAfterFailure(): void {
180268
181269 $ view ->file_put_contents ('stray/a-ghost.txt ' , 'gone ' );
182270 $ strayStorage = $ view ->getFileInfo ('stray/a-ghost.txt ' )->getStorage ();
183- $ this ->markAsEncrypted ($ strayStorage , 'a-ghost.txt ' );
271+ $ this ->markAsEncrypted ($ strayStorage , 'a-ghost.txt ' , strlen ( ' gone ' ) );
184272 // cache row without a backing file, reading it fails like an object store 404
185273 $ strayStorage ->unlink ('a-ghost.txt ' );
186274
187275 $ view ->file_put_contents ('stray/b-plain.txt ' , 'plain data ' );
188- $ this ->markAsEncrypted ($ strayStorage , 'b-plain.txt ' );
276+ $ this ->markAsEncrypted ($ strayStorage , 'b-plain.txt ' , strlen ( ' plain data ' ) );
189277
190278 // ciphertext without any key while the user has no key directory at all,
191279 // the key search has to come up empty instead of erroring out
192280 $ view ->file_put_contents ('stray/c-cipher.txt ' , 'HBEGIN:oc_encryption_module:OC_DEFAULT_MODULE:HEND ' );
193- $ this ->markAsEncrypted ($ strayStorage , 'c-cipher.txt ' );
281+ $ this ->markAsEncrypted ($ strayStorage , 'c-cipher.txt ' , 8 );
194282
195283 $ command = $ this ->getCommand ();
196284 $ mount = $ this ->createMock (ICachedMountInfo::class);
0 commit comments