Completed
Push — master ( 36f793...6cdf09 )
by
unknown
42:22 queued 15:32
created
lib/private/Encryption/DecryptAll.php 2 patches
Indentation   +211 added lines, -211 removed lines patch added patch discarded remove patch
@@ -20,215 +20,215 @@
 block discarded – undo
20 20
 use Symfony\Component\Console\Output\OutputInterface;
21 21
 
22 22
 class DecryptAll {
23
-	/** @var array<string,list<string>> files which couldn't be decrypted */
24
-	protected array $failed = [];
25
-
26
-	public function __construct(
27
-		protected IManager $encryptionManager,
28
-		protected IUserManager $userManager,
29
-		protected View $rootView,
30
-	) {
31
-	}
32
-
33
-	/**
34
-	 * start to decrypt all files
35
-	 *
36
-	 * @param string $user which users data folder should be decrypted, default = all users
37
-	 * @throws \Exception
38
-	 */
39
-	public function decryptAll(InputInterface $input, OutputInterface $output, string $user = ''): bool {
40
-		if ($user !== '' && $this->userManager->userExists($user) === false) {
41
-			$output->writeln('User "' . $user . '" does not exist. Please check the username and try again');
42
-			return false;
43
-		}
44
-
45
-		$output->writeln('prepare encryption modules...');
46
-		if ($this->prepareEncryptionModules($input, $output, $user) === false) {
47
-			return false;
48
-		}
49
-		$output->writeln(' done.');
50
-
51
-		$this->failed = [];
52
-		$this->decryptAllUsersFiles($output, $user);
53
-
54
-		/** @psalm-suppress RedundantCondition $this->failed is modified by decryptAllUsersFiles, not clear why psalm fails to see it */
55
-		if (empty($this->failed)) {
56
-			$output->writeln('all files could be decrypted successfully!');
57
-		} else {
58
-			$output->writeln('Files for following users couldn\'t be decrypted, ');
59
-			$output->writeln('maybe the user is not set up in a way that supports this operation: ');
60
-			foreach ($this->failed as $uid => $paths) {
61
-				$output->writeln('    ' . $uid);
62
-				foreach ($paths as $path) {
63
-					$output->writeln('        ' . $path);
64
-				}
65
-			}
66
-			$output->writeln('');
67
-		}
68
-
69
-		return true;
70
-	}
71
-
72
-	/**
73
-	 * prepare encryption modules to perform the decrypt all function
74
-	 */
75
-	protected function prepareEncryptionModules(InputInterface $input, OutputInterface $output, string $user): bool {
76
-		// prepare all encryption modules for decrypt all
77
-		$encryptionModules = $this->encryptionManager->getEncryptionModules();
78
-		foreach ($encryptionModules as $moduleDesc) {
79
-			/** @var IEncryptionModule $module */
80
-			$module = call_user_func($moduleDesc['callback']);
81
-			$output->writeln('');
82
-			$output->writeln('Prepare "' . $module->getDisplayName() . '"');
83
-			$output->writeln('');
84
-			if ($module->prepareDecryptAll($input, $output, $user) === false) {
85
-				$output->writeln('Module "' . $moduleDesc['displayName'] . '" does not support the functionality to decrypt all files again or the initialization of the module failed!');
86
-				return false;
87
-			}
88
-		}
89
-
90
-		return true;
91
-	}
92
-
93
-	/**
94
-	 * iterate over all user and encrypt their files
95
-	 *
96
-	 * @param string $user which users files should be decrypted, default = all users
97
-	 */
98
-	protected function decryptAllUsersFiles(OutputInterface $output, string $user = ''): void {
99
-		$output->writeln("\n");
100
-
101
-		$userList = [];
102
-		if ($user === '') {
103
-			$fetchUsersProgress = new ProgressBar($output);
104
-			$fetchUsersProgress->setFormat(" %message% \n [%bar%]");
105
-			$fetchUsersProgress->start();
106
-			$fetchUsersProgress->setMessage('Fetch list of users...');
107
-			$fetchUsersProgress->advance();
108
-
109
-			foreach ($this->userManager->getBackends() as $backend) {
110
-				$limit = 500;
111
-				$offset = 0;
112
-				do {
113
-					$users = $backend->getUsers('', $limit, $offset);
114
-					foreach ($users as $user) {
115
-						$userList[] = $user;
116
-					}
117
-					$offset += $limit;
118
-					$fetchUsersProgress->advance();
119
-				} while (count($users) >= $limit);
120
-				$fetchUsersProgress->setMessage('Fetch list of users... finished');
121
-				$fetchUsersProgress->finish();
122
-			}
123
-		} else {
124
-			$userList[] = $user;
125
-		}
126
-
127
-		$output->writeln("\n\n");
128
-
129
-		$progress = new ProgressBar($output);
130
-		$progress->setFormat(" %message% \n [%bar%]");
131
-		$progress->start();
132
-		$progress->setMessage('starting to decrypt files...');
133
-		$progress->advance();
134
-
135
-		$numberOfUsers = count($userList);
136
-		$userNo = 1;
137
-		foreach ($userList as $uid) {
138
-			$userCount = "$uid ($userNo of $numberOfUsers)";
139
-			$this->decryptUsersFiles($uid, $progress, $userCount);
140
-			$userNo++;
141
-		}
142
-
143
-		$progress->setMessage('starting to decrypt files... finished');
144
-		$progress->finish();
145
-
146
-		$output->writeln("\n\n");
147
-	}
148
-
149
-	/**
150
-	 * encrypt files from the given user
151
-	 */
152
-	protected function decryptUsersFiles(string $uid, ProgressBar $progress, string $userCount): void {
153
-		$this->setupUserFS($uid);
154
-		$directories = [];
155
-		$directories[] = '/' . $uid . '/files';
156
-
157
-		while ($root = array_pop($directories)) {
158
-			$content = $this->rootView->getDirectoryContent($root);
159
-			foreach ($content as $file) {
160
-				// only decrypt files owned by the user
161
-				if ($file->getStorage()->instanceOfStorage('OCA\Files_Sharing\SharedStorage')) {
162
-					continue;
163
-				}
164
-				$path = $root . '/' . $file['name'];
165
-				if ($this->rootView->is_dir($path)) {
166
-					$directories[] = $path;
167
-					continue;
168
-				} else {
169
-					try {
170
-						$progress->setMessage("decrypt files for user $userCount: $path");
171
-						$progress->advance();
172
-						if ($file->isEncrypted() === false) {
173
-							$progress->setMessage("decrypt files for user $userCount: $path (already decrypted)");
174
-							$progress->advance();
175
-						} else {
176
-							if ($this->decryptFile($path) === false) {
177
-								$progress->setMessage("decrypt files for user $userCount: $path (already decrypted)");
178
-								$progress->advance();
179
-							}
180
-						}
181
-					} catch (\Exception $e) {
182
-						if (isset($this->failed[$uid])) {
183
-							$this->failed[$uid][] = $path;
184
-						} else {
185
-							$this->failed[$uid] = [$path];
186
-						}
187
-					}
188
-				}
189
-			}
190
-		}
191
-	}
192
-
193
-	/**
194
-	 * encrypt file
195
-	 */
196
-	protected function decryptFile(string $path): bool {
197
-		// skip already decrypted files
198
-		$fileInfo = $this->rootView->getFileInfo($path);
199
-		if ($fileInfo !== false && !$fileInfo->isEncrypted()) {
200
-			return true;
201
-		}
202
-
203
-		$source = $path;
204
-		$target = $path . '.decrypted.' . $this->getTimestamp();
205
-
206
-		try {
207
-			$this->rootView->copy($source, $target);
208
-			$this->rootView->touch($target, $fileInfo->getMTime());
209
-			$this->rootView->rename($target, $source);
210
-		} catch (DecryptionFailedException $e) {
211
-			if ($this->rootView->file_exists($target)) {
212
-				$this->rootView->unlink($target);
213
-			}
214
-			return false;
215
-		}
216
-
217
-		return true;
218
-	}
219
-
220
-	/**
221
-	 * get current timestamp
222
-	 */
223
-	protected function getTimestamp(): int {
224
-		return time();
225
-	}
226
-
227
-	/**
228
-	 * setup user file system
229
-	 */
230
-	protected function setupUserFS(string $uid): void {
231
-		\OC_Util::tearDownFS();
232
-		\OC_Util::setupFS($uid);
233
-	}
23
+    /** @var array<string,list<string>> files which couldn't be decrypted */
24
+    protected array $failed = [];
25
+
26
+    public function __construct(
27
+        protected IManager $encryptionManager,
28
+        protected IUserManager $userManager,
29
+        protected View $rootView,
30
+    ) {
31
+    }
32
+
33
+    /**
34
+     * start to decrypt all files
35
+     *
36
+     * @param string $user which users data folder should be decrypted, default = all users
37
+     * @throws \Exception
38
+     */
39
+    public function decryptAll(InputInterface $input, OutputInterface $output, string $user = ''): bool {
40
+        if ($user !== '' && $this->userManager->userExists($user) === false) {
41
+            $output->writeln('User "' . $user . '" does not exist. Please check the username and try again');
42
+            return false;
43
+        }
44
+
45
+        $output->writeln('prepare encryption modules...');
46
+        if ($this->prepareEncryptionModules($input, $output, $user) === false) {
47
+            return false;
48
+        }
49
+        $output->writeln(' done.');
50
+
51
+        $this->failed = [];
52
+        $this->decryptAllUsersFiles($output, $user);
53
+
54
+        /** @psalm-suppress RedundantCondition $this->failed is modified by decryptAllUsersFiles, not clear why psalm fails to see it */
55
+        if (empty($this->failed)) {
56
+            $output->writeln('all files could be decrypted successfully!');
57
+        } else {
58
+            $output->writeln('Files for following users couldn\'t be decrypted, ');
59
+            $output->writeln('maybe the user is not set up in a way that supports this operation: ');
60
+            foreach ($this->failed as $uid => $paths) {
61
+                $output->writeln('    ' . $uid);
62
+                foreach ($paths as $path) {
63
+                    $output->writeln('        ' . $path);
64
+                }
65
+            }
66
+            $output->writeln('');
67
+        }
68
+
69
+        return true;
70
+    }
71
+
72
+    /**
73
+     * prepare encryption modules to perform the decrypt all function
74
+     */
75
+    protected function prepareEncryptionModules(InputInterface $input, OutputInterface $output, string $user): bool {
76
+        // prepare all encryption modules for decrypt all
77
+        $encryptionModules = $this->encryptionManager->getEncryptionModules();
78
+        foreach ($encryptionModules as $moduleDesc) {
79
+            /** @var IEncryptionModule $module */
80
+            $module = call_user_func($moduleDesc['callback']);
81
+            $output->writeln('');
82
+            $output->writeln('Prepare "' . $module->getDisplayName() . '"');
83
+            $output->writeln('');
84
+            if ($module->prepareDecryptAll($input, $output, $user) === false) {
85
+                $output->writeln('Module "' . $moduleDesc['displayName'] . '" does not support the functionality to decrypt all files again or the initialization of the module failed!');
86
+                return false;
87
+            }
88
+        }
89
+
90
+        return true;
91
+    }
92
+
93
+    /**
94
+     * iterate over all user and encrypt their files
95
+     *
96
+     * @param string $user which users files should be decrypted, default = all users
97
+     */
98
+    protected function decryptAllUsersFiles(OutputInterface $output, string $user = ''): void {
99
+        $output->writeln("\n");
100
+
101
+        $userList = [];
102
+        if ($user === '') {
103
+            $fetchUsersProgress = new ProgressBar($output);
104
+            $fetchUsersProgress->setFormat(" %message% \n [%bar%]");
105
+            $fetchUsersProgress->start();
106
+            $fetchUsersProgress->setMessage('Fetch list of users...');
107
+            $fetchUsersProgress->advance();
108
+
109
+            foreach ($this->userManager->getBackends() as $backend) {
110
+                $limit = 500;
111
+                $offset = 0;
112
+                do {
113
+                    $users = $backend->getUsers('', $limit, $offset);
114
+                    foreach ($users as $user) {
115
+                        $userList[] = $user;
116
+                    }
117
+                    $offset += $limit;
118
+                    $fetchUsersProgress->advance();
119
+                } while (count($users) >= $limit);
120
+                $fetchUsersProgress->setMessage('Fetch list of users... finished');
121
+                $fetchUsersProgress->finish();
122
+            }
123
+        } else {
124
+            $userList[] = $user;
125
+        }
126
+
127
+        $output->writeln("\n\n");
128
+
129
+        $progress = new ProgressBar($output);
130
+        $progress->setFormat(" %message% \n [%bar%]");
131
+        $progress->start();
132
+        $progress->setMessage('starting to decrypt files...');
133
+        $progress->advance();
134
+
135
+        $numberOfUsers = count($userList);
136
+        $userNo = 1;
137
+        foreach ($userList as $uid) {
138
+            $userCount = "$uid ($userNo of $numberOfUsers)";
139
+            $this->decryptUsersFiles($uid, $progress, $userCount);
140
+            $userNo++;
141
+        }
142
+
143
+        $progress->setMessage('starting to decrypt files... finished');
144
+        $progress->finish();
145
+
146
+        $output->writeln("\n\n");
147
+    }
148
+
149
+    /**
150
+     * encrypt files from the given user
151
+     */
152
+    protected function decryptUsersFiles(string $uid, ProgressBar $progress, string $userCount): void {
153
+        $this->setupUserFS($uid);
154
+        $directories = [];
155
+        $directories[] = '/' . $uid . '/files';
156
+
157
+        while ($root = array_pop($directories)) {
158
+            $content = $this->rootView->getDirectoryContent($root);
159
+            foreach ($content as $file) {
160
+                // only decrypt files owned by the user
161
+                if ($file->getStorage()->instanceOfStorage('OCA\Files_Sharing\SharedStorage')) {
162
+                    continue;
163
+                }
164
+                $path = $root . '/' . $file['name'];
165
+                if ($this->rootView->is_dir($path)) {
166
+                    $directories[] = $path;
167
+                    continue;
168
+                } else {
169
+                    try {
170
+                        $progress->setMessage("decrypt files for user $userCount: $path");
171
+                        $progress->advance();
172
+                        if ($file->isEncrypted() === false) {
173
+                            $progress->setMessage("decrypt files for user $userCount: $path (already decrypted)");
174
+                            $progress->advance();
175
+                        } else {
176
+                            if ($this->decryptFile($path) === false) {
177
+                                $progress->setMessage("decrypt files for user $userCount: $path (already decrypted)");
178
+                                $progress->advance();
179
+                            }
180
+                        }
181
+                    } catch (\Exception $e) {
182
+                        if (isset($this->failed[$uid])) {
183
+                            $this->failed[$uid][] = $path;
184
+                        } else {
185
+                            $this->failed[$uid] = [$path];
186
+                        }
187
+                    }
188
+                }
189
+            }
190
+        }
191
+    }
192
+
193
+    /**
194
+     * encrypt file
195
+     */
196
+    protected function decryptFile(string $path): bool {
197
+        // skip already decrypted files
198
+        $fileInfo = $this->rootView->getFileInfo($path);
199
+        if ($fileInfo !== false && !$fileInfo->isEncrypted()) {
200
+            return true;
201
+        }
202
+
203
+        $source = $path;
204
+        $target = $path . '.decrypted.' . $this->getTimestamp();
205
+
206
+        try {
207
+            $this->rootView->copy($source, $target);
208
+            $this->rootView->touch($target, $fileInfo->getMTime());
209
+            $this->rootView->rename($target, $source);
210
+        } catch (DecryptionFailedException $e) {
211
+            if ($this->rootView->file_exists($target)) {
212
+                $this->rootView->unlink($target);
213
+            }
214
+            return false;
215
+        }
216
+
217
+        return true;
218
+    }
219
+
220
+    /**
221
+     * get current timestamp
222
+     */
223
+    protected function getTimestamp(): int {
224
+        return time();
225
+    }
226
+
227
+    /**
228
+     * setup user file system
229
+     */
230
+    protected function setupUserFS(string $uid): void {
231
+        \OC_Util::tearDownFS();
232
+        \OC_Util::setupFS($uid);
233
+    }
234 234
 }
Please login to merge, or discard this patch.
Spacing   +8 added lines, -8 removed lines patch added patch discarded remove patch
@@ -38,7 +38,7 @@  discard block
 block discarded – undo
38 38
 	 */
39 39
 	public function decryptAll(InputInterface $input, OutputInterface $output, string $user = ''): bool {
40 40
 		if ($user !== '' && $this->userManager->userExists($user) === false) {
41
-			$output->writeln('User "' . $user . '" does not exist. Please check the username and try again');
41
+			$output->writeln('User "'.$user.'" does not exist. Please check the username and try again');
42 42
 			return false;
43 43
 		}
44 44
 
@@ -58,9 +58,9 @@  discard block
 block discarded – undo
58 58
 			$output->writeln('Files for following users couldn\'t be decrypted, ');
59 59
 			$output->writeln('maybe the user is not set up in a way that supports this operation: ');
60 60
 			foreach ($this->failed as $uid => $paths) {
61
-				$output->writeln('    ' . $uid);
61
+				$output->writeln('    '.$uid);
62 62
 				foreach ($paths as $path) {
63
-					$output->writeln('        ' . $path);
63
+					$output->writeln('        '.$path);
64 64
 				}
65 65
 			}
66 66
 			$output->writeln('');
@@ -79,10 +79,10 @@  discard block
 block discarded – undo
79 79
 			/** @var IEncryptionModule $module */
80 80
 			$module = call_user_func($moduleDesc['callback']);
81 81
 			$output->writeln('');
82
-			$output->writeln('Prepare "' . $module->getDisplayName() . '"');
82
+			$output->writeln('Prepare "'.$module->getDisplayName().'"');
83 83
 			$output->writeln('');
84 84
 			if ($module->prepareDecryptAll($input, $output, $user) === false) {
85
-				$output->writeln('Module "' . $moduleDesc['displayName'] . '" does not support the functionality to decrypt all files again or the initialization of the module failed!');
85
+				$output->writeln('Module "'.$moduleDesc['displayName'].'" does not support the functionality to decrypt all files again or the initialization of the module failed!');
86 86
 				return false;
87 87
 			}
88 88
 		}
@@ -152,7 +152,7 @@  discard block
 block discarded – undo
152 152
 	protected function decryptUsersFiles(string $uid, ProgressBar $progress, string $userCount): void {
153 153
 		$this->setupUserFS($uid);
154 154
 		$directories = [];
155
-		$directories[] = '/' . $uid . '/files';
155
+		$directories[] = '/'.$uid.'/files';
156 156
 
157 157
 		while ($root = array_pop($directories)) {
158 158
 			$content = $this->rootView->getDirectoryContent($root);
@@ -161,7 +161,7 @@  discard block
 block discarded – undo
161 161
 				if ($file->getStorage()->instanceOfStorage('OCA\Files_Sharing\SharedStorage')) {
162 162
 					continue;
163 163
 				}
164
-				$path = $root . '/' . $file['name'];
164
+				$path = $root.'/'.$file['name'];
165 165
 				if ($this->rootView->is_dir($path)) {
166 166
 					$directories[] = $path;
167 167
 					continue;
@@ -201,7 +201,7 @@  discard block
 block discarded – undo
201 201
 		}
202 202
 
203 203
 		$source = $path;
204
-		$target = $path . '.decrypted.' . $this->getTimestamp();
204
+		$target = $path.'.decrypted.'.$this->getTimestamp();
205 205
 
206 206
 		try {
207 207
 			$this->rootView->copy($source, $target);
Please login to merge, or discard this patch.
lib/private/Encryption/Update.php 2 patches
Indentation   +83 added lines, -83 removed lines patch added patch discarded remove patch
@@ -22,96 +22,96 @@
 block discarded – undo
22 22
  * update encrypted files, e.g. because a file was shared
23 23
  */
24 24
 class Update {
25
-	public function __construct(
26
-		protected Util $util,
27
-		protected Manager $encryptionManager,
28
-		protected File $file,
29
-		protected LoggerInterface $logger,
30
-	) {
31
-	}
25
+    public function __construct(
26
+        protected Util $util,
27
+        protected Manager $encryptionManager,
28
+        protected File $file,
29
+        protected LoggerInterface $logger,
30
+    ) {
31
+    }
32 32
 
33
-	/**
34
-	 * hook after file was shared
35
-	 */
36
-	public function postShared(OCPFile|Folder $node): void {
37
-		$this->update($node);
38
-	}
33
+    /**
34
+     * hook after file was shared
35
+     */
36
+    public function postShared(OCPFile|Folder $node): void {
37
+        $this->update($node);
38
+    }
39 39
 
40
-	/**
41
-	 * hook after file was unshared
42
-	 */
43
-	public function postUnshared(OCPFile|Folder $node): void {
44
-		$this->update($node);
45
-	}
40
+    /**
41
+     * hook after file was unshared
42
+     */
43
+    public function postUnshared(OCPFile|Folder $node): void {
44
+        $this->update($node);
45
+    }
46 46
 
47
-	/**
48
-	 * inform encryption module that a file was restored from the trash bin,
49
-	 * e.g. to update the encryption keys
50
-	 */
51
-	public function postRestore(OCPFile|Folder $node): void {
52
-		$this->update($node);
53
-	}
47
+    /**
48
+     * inform encryption module that a file was restored from the trash bin,
49
+     * e.g. to update the encryption keys
50
+     */
51
+    public function postRestore(OCPFile|Folder $node): void {
52
+        $this->update($node);
53
+    }
54 54
 
55
-	/**
56
-	 * inform encryption module that a file was renamed,
57
-	 * e.g. to update the encryption keys
58
-	 */
59
-	public function postRename(OCPFile|Folder $source, OCPFile|Folder $target): void {
60
-		if (dirname($source->getPath()) !== dirname($target->getPath())) {
61
-			$this->update($target);
62
-		}
63
-	}
55
+    /**
56
+     * inform encryption module that a file was renamed,
57
+     * e.g. to update the encryption keys
58
+     */
59
+    public function postRename(OCPFile|Folder $source, OCPFile|Folder $target): void {
60
+        if (dirname($source->getPath()) !== dirname($target->getPath())) {
61
+            $this->update($target);
62
+        }
63
+    }
64 64
 
65
-	/**
66
-	 * get owner and path relative to data/
67
-	 *
68
-	 * @throws \InvalidArgumentException
69
-	 */
70
-	protected function getOwnerPath(OCPFile|Folder $node): string {
71
-		$owner = $node->getOwner()?->getUID();
72
-		if ($owner === null) {
73
-			throw new InvalidArgumentException('No owner found for ' . $node->getId());
74
-		}
75
-		$view = new View('/' . $owner . '/files');
76
-		try {
77
-			$path = $view->getPath($node->getId());
78
-		} catch (NotFoundException $e) {
79
-			throw new InvalidArgumentException('No file found for ' . $node->getId(), previous:$e);
80
-		}
81
-		return '/' . $owner . '/files/' . $path;
82
-	}
65
+    /**
66
+     * get owner and path relative to data/
67
+     *
68
+     * @throws \InvalidArgumentException
69
+     */
70
+    protected function getOwnerPath(OCPFile|Folder $node): string {
71
+        $owner = $node->getOwner()?->getUID();
72
+        if ($owner === null) {
73
+            throw new InvalidArgumentException('No owner found for ' . $node->getId());
74
+        }
75
+        $view = new View('/' . $owner . '/files');
76
+        try {
77
+            $path = $view->getPath($node->getId());
78
+        } catch (NotFoundException $e) {
79
+            throw new InvalidArgumentException('No file found for ' . $node->getId(), previous:$e);
80
+        }
81
+        return '/' . $owner . '/files/' . $path;
82
+    }
83 83
 
84
-	/**
85
-	 * notify encryption module about added/removed users from a file/folder
86
-	 *
87
-	 * @param string $path relative to data/
88
-	 * @throws Exceptions\ModuleDoesNotExistsException
89
-	 */
90
-	public function update(OCPFile|Folder $node): void {
91
-		$encryptionModule = $this->encryptionManager->getEncryptionModule();
84
+    /**
85
+     * notify encryption module about added/removed users from a file/folder
86
+     *
87
+     * @param string $path relative to data/
88
+     * @throws Exceptions\ModuleDoesNotExistsException
89
+     */
90
+    public function update(OCPFile|Folder $node): void {
91
+        $encryptionModule = $this->encryptionManager->getEncryptionModule();
92 92
 
93
-		// if the encryption module doesn't encrypt the files on a per-user basis
94
-		// we have nothing to do here.
95
-		if ($encryptionModule->needDetailedAccessList() === false) {
96
-			return;
97
-		}
93
+        // if the encryption module doesn't encrypt the files on a per-user basis
94
+        // we have nothing to do here.
95
+        if ($encryptionModule->needDetailedAccessList() === false) {
96
+            return;
97
+        }
98 98
 
99
-		$path = $this->getOwnerPath($node);
100
-		// if a folder was shared, get a list of all (sub-)folders
101
-		if ($node instanceof Folder) {
102
-			$allFiles = $this->util->getAllFiles($path);
103
-		} else {
104
-			$allFiles = [$path];
105
-		}
99
+        $path = $this->getOwnerPath($node);
100
+        // if a folder was shared, get a list of all (sub-)folders
101
+        if ($node instanceof Folder) {
102
+            $allFiles = $this->util->getAllFiles($path);
103
+        } else {
104
+            $allFiles = [$path];
105
+        }
106 106
 
107
-		foreach ($allFiles as $file) {
108
-			$usersSharing = $this->file->getAccessList($file);
109
-			try {
110
-				$encryptionModule->update($file, '', $usersSharing);
111
-			} catch (GenericEncryptionException $e) {
112
-				// If the update of an individual file fails e.g. due to a corrupt key we should continue the operation and just log the failure
113
-				$this->logger->error('Failed to update encryption module for ' . $file, [ 'exception' => $e ]);
114
-			}
115
-		}
116
-	}
107
+        foreach ($allFiles as $file) {
108
+            $usersSharing = $this->file->getAccessList($file);
109
+            try {
110
+                $encryptionModule->update($file, '', $usersSharing);
111
+            } catch (GenericEncryptionException $e) {
112
+                // If the update of an individual file fails e.g. due to a corrupt key we should continue the operation and just log the failure
113
+                $this->logger->error('Failed to update encryption module for ' . $file, [ 'exception' => $e ]);
114
+            }
115
+        }
116
+    }
117 117
 }
Please login to merge, or discard this patch.
Spacing   +11 added lines, -11 removed lines patch added patch discarded remove patch
@@ -33,14 +33,14 @@  discard block
 block discarded – undo
33 33
 	/**
34 34
 	 * hook after file was shared
35 35
 	 */
36
-	public function postShared(OCPFile|Folder $node): void {
36
+	public function postShared(OCPFile | Folder $node): void {
37 37
 		$this->update($node);
38 38
 	}
39 39
 
40 40
 	/**
41 41
 	 * hook after file was unshared
42 42
 	 */
43
-	public function postUnshared(OCPFile|Folder $node): void {
43
+	public function postUnshared(OCPFile | Folder $node): void {
44 44
 		$this->update($node);
45 45
 	}
46 46
 
@@ -48,7 +48,7 @@  discard block
 block discarded – undo
48 48
 	 * inform encryption module that a file was restored from the trash bin,
49 49
 	 * e.g. to update the encryption keys
50 50
 	 */
51
-	public function postRestore(OCPFile|Folder $node): void {
51
+	public function postRestore(OCPFile | Folder $node): void {
52 52
 		$this->update($node);
53 53
 	}
54 54
 
@@ -56,7 +56,7 @@  discard block
 block discarded – undo
56 56
 	 * inform encryption module that a file was renamed,
57 57
 	 * e.g. to update the encryption keys
58 58
 	 */
59
-	public function postRename(OCPFile|Folder $source, OCPFile|Folder $target): void {
59
+	public function postRename(OCPFile | Folder $source, OCPFile | Folder $target): void {
60 60
 		if (dirname($source->getPath()) !== dirname($target->getPath())) {
61 61
 			$this->update($target);
62 62
 		}
@@ -67,18 +67,18 @@  discard block
 block discarded – undo
67 67
 	 *
68 68
 	 * @throws \InvalidArgumentException
69 69
 	 */
70
-	protected function getOwnerPath(OCPFile|Folder $node): string {
70
+	protected function getOwnerPath(OCPFile | Folder $node): string {
71 71
 		$owner = $node->getOwner()?->getUID();
72 72
 		if ($owner === null) {
73
-			throw new InvalidArgumentException('No owner found for ' . $node->getId());
73
+			throw new InvalidArgumentException('No owner found for '.$node->getId());
74 74
 		}
75
-		$view = new View('/' . $owner . '/files');
75
+		$view = new View('/'.$owner.'/files');
76 76
 		try {
77 77
 			$path = $view->getPath($node->getId());
78 78
 		} catch (NotFoundException $e) {
79
-			throw new InvalidArgumentException('No file found for ' . $node->getId(), previous:$e);
79
+			throw new InvalidArgumentException('No file found for '.$node->getId(), previous:$e);
80 80
 		}
81
-		return '/' . $owner . '/files/' . $path;
81
+		return '/'.$owner.'/files/'.$path;
82 82
 	}
83 83
 
84 84
 	/**
@@ -87,7 +87,7 @@  discard block
 block discarded – undo
87 87
 	 * @param string $path relative to data/
88 88
 	 * @throws Exceptions\ModuleDoesNotExistsException
89 89
 	 */
90
-	public function update(OCPFile|Folder $node): void {
90
+	public function update(OCPFile | Folder $node): void {
91 91
 		$encryptionModule = $this->encryptionManager->getEncryptionModule();
92 92
 
93 93
 		// if the encryption module doesn't encrypt the files on a per-user basis
@@ -110,7 +110,7 @@  discard block
 block discarded – undo
110 110
 				$encryptionModule->update($file, '', $usersSharing);
111 111
 			} catch (GenericEncryptionException $e) {
112 112
 				// If the update of an individual file fails e.g. due to a corrupt key we should continue the operation and just log the failure
113
-				$this->logger->error('Failed to update encryption module for ' . $file, [ 'exception' => $e ]);
113
+				$this->logger->error('Failed to update encryption module for '.$file, ['exception' => $e]);
114 114
 			}
115 115
 		}
116 116
 	}
Please login to merge, or discard this patch.
lib/private/Encryption/EncryptionEventListener.php 1 patch
Indentation   +59 added lines, -59 removed lines patch added patch discarded remove patch
@@ -27,68 +27,68 @@
 block discarded – undo
27 27
 
28 28
 /** @template-implements IEventListener<NodeRenamedEvent|ShareCreatedEvent|ShareDeletedEvent|NodeRestoredEvent> */
29 29
 class EncryptionEventListener implements IEventListener {
30
-	private ?Update $updater = null;
30
+    private ?Update $updater = null;
31 31
 
32
-	public function __construct(
33
-		private IUserSession $userSession,
34
-		private SetupManager $setupManager,
35
-		private Manager $encryptionManager,
36
-		private IUserManager $userManager,
37
-	) {
38
-	}
32
+    public function __construct(
33
+        private IUserSession $userSession,
34
+        private SetupManager $setupManager,
35
+        private Manager $encryptionManager,
36
+        private IUserManager $userManager,
37
+    ) {
38
+    }
39 39
 
40
-	public static function register(IEventDispatcher $dispatcher): void {
41
-		$dispatcher->addServiceListener(NodeRenamedEvent::class, static::class);
42
-		$dispatcher->addServiceListener(ShareCreatedEvent::class, static::class);
43
-		$dispatcher->addServiceListener(ShareDeletedEvent::class, static::class);
44
-		$dispatcher->addServiceListener(NodeRestoredEvent::class, static::class);
45
-	}
40
+    public static function register(IEventDispatcher $dispatcher): void {
41
+        $dispatcher->addServiceListener(NodeRenamedEvent::class, static::class);
42
+        $dispatcher->addServiceListener(ShareCreatedEvent::class, static::class);
43
+        $dispatcher->addServiceListener(ShareDeletedEvent::class, static::class);
44
+        $dispatcher->addServiceListener(NodeRestoredEvent::class, static::class);
45
+    }
46 46
 
47
-	public function handle(Event $event): void {
48
-		if (!$this->encryptionManager->isEnabled()) {
49
-			return;
50
-		}
51
-		if ($event instanceof NodeRenamedEvent) {
52
-			$this->getUpdate()->postRename($event->getSource(), $event->getTarget());
53
-		} elseif ($event instanceof ShareCreatedEvent) {
54
-			$this->getUpdate()->postShared($event->getShare()->getNode());
55
-		} elseif ($event instanceof ShareDeletedEvent) {
56
-			try {
57
-				// In case the unsharing happens in a background job, we don't have
58
-				// a session and we load instead the user from the UserManager
59
-				$owner = $this->userManager->get($event->getShare()->getShareOwner());
60
-				$this->getUpdate($owner)->postUnshared($event->getShare()->getNode());
61
-			} catch (NotFoundException $e) {
62
-				/* The node was deleted already, nothing to update */
63
-			}
64
-		} elseif ($event instanceof NodeRestoredEvent) {
65
-			$this->getUpdate()->postRestore($event->getTarget());
66
-		}
67
-	}
47
+    public function handle(Event $event): void {
48
+        if (!$this->encryptionManager->isEnabled()) {
49
+            return;
50
+        }
51
+        if ($event instanceof NodeRenamedEvent) {
52
+            $this->getUpdate()->postRename($event->getSource(), $event->getTarget());
53
+        } elseif ($event instanceof ShareCreatedEvent) {
54
+            $this->getUpdate()->postShared($event->getShare()->getNode());
55
+        } elseif ($event instanceof ShareDeletedEvent) {
56
+            try {
57
+                // In case the unsharing happens in a background job, we don't have
58
+                // a session and we load instead the user from the UserManager
59
+                $owner = $this->userManager->get($event->getShare()->getShareOwner());
60
+                $this->getUpdate($owner)->postUnshared($event->getShare()->getNode());
61
+            } catch (NotFoundException $e) {
62
+                /* The node was deleted already, nothing to update */
63
+            }
64
+        } elseif ($event instanceof NodeRestoredEvent) {
65
+            $this->getUpdate()->postRestore($event->getTarget());
66
+        }
67
+    }
68 68
 
69
-	private function getUpdate(?IUser $owner = null): Update {
70
-		$user = $this->userSession->getUser();
71
-		if (!$user && ($owner !== null)) {
72
-			$user = $owner;
73
-		}
74
-		if ($user) {
75
-			if (!$this->setupManager->isSetupComplete($user)) {
76
-				$this->setupManager->setupForUser($user);
77
-			}
78
-		}
79
-		if (is_null($this->updater)) {
80
-			$this->updater = new Update(
81
-				new Util(
82
-					new View(),
83
-					$this->userManager,
84
-					\OC::$server->getGroupManager(),
85
-					\OC::$server->getConfig()),
86
-				\OC::$server->getEncryptionManager(),
87
-				\OC::$server->get(IFile::class),
88
-				\OC::$server->get(LoggerInterface::class),
89
-			);
90
-		}
69
+    private function getUpdate(?IUser $owner = null): Update {
70
+        $user = $this->userSession->getUser();
71
+        if (!$user && ($owner !== null)) {
72
+            $user = $owner;
73
+        }
74
+        if ($user) {
75
+            if (!$this->setupManager->isSetupComplete($user)) {
76
+                $this->setupManager->setupForUser($user);
77
+            }
78
+        }
79
+        if (is_null($this->updater)) {
80
+            $this->updater = new Update(
81
+                new Util(
82
+                    new View(),
83
+                    $this->userManager,
84
+                    \OC::$server->getGroupManager(),
85
+                    \OC::$server->getConfig()),
86
+                \OC::$server->getEncryptionManager(),
87
+                \OC::$server->get(IFile::class),
88
+                \OC::$server->get(LoggerInterface::class),
89
+            );
90
+        }
91 91
 
92
-		return $this->updater;
93
-	}
92
+        return $this->updater;
93
+    }
94 94
 }
Please login to merge, or discard this patch.
lib/private/Files/Storage/Wrapper/Encryption.php 1 patch
Indentation   +919 added lines, -919 removed lines patch added patch discarded remove patch
@@ -29,923 +29,923 @@
 block discarded – undo
29 29
 use Psr\Log\LoggerInterface;
30 30
 
31 31
 class Encryption extends Wrapper {
32
-	use LocalTempFileTrait;
33
-
34
-	private string $mountPoint;
35
-	protected array $unencryptedSize = [];
36
-	private IMountPoint $mount;
37
-	/** for which path we execute the repair step to avoid recursions */
38
-	private array $fixUnencryptedSizeOf = [];
39
-	/** @var CappedMemoryCache<bool> */
40
-	private CappedMemoryCache $encryptedPaths;
41
-	private bool $enabled = true;
42
-
43
-	/**
44
-	 * @param array $parameters
45
-	 */
46
-	public function __construct(
47
-		array $parameters,
48
-		private IManager $encryptionManager,
49
-		private Util $util,
50
-		private LoggerInterface $logger,
51
-		private IFile $fileHelper,
52
-		private ?string $uid,
53
-		private IStorage $keyStorage,
54
-		private Manager $mountManager,
55
-		private ArrayCache $arrayCache,
56
-	) {
57
-		$this->mountPoint = $parameters['mountPoint'];
58
-		$this->mount = $parameters['mount'];
59
-		$this->encryptedPaths = new CappedMemoryCache();
60
-		parent::__construct($parameters);
61
-	}
62
-
63
-	public function filesize(string $path): int|float|false {
64
-		$fullPath = $this->getFullPath($path);
65
-
66
-		$info = $this->getCache()->get($path);
67
-		if ($info === false) {
68
-			/* Pass call to wrapped storage, it may be a special file like a part file */
69
-			return $this->storage->filesize($path);
70
-		}
71
-		if (isset($this->unencryptedSize[$fullPath])) {
72
-			$size = $this->unencryptedSize[$fullPath];
73
-
74
-			// Update file cache (only if file is already cached).
75
-			// Certain files are not cached (e.g. *.part).
76
-			if (isset($info['fileid'])) {
77
-				if ($info instanceof ICacheEntry) {
78
-					$info['encrypted'] = $info['encryptedVersion'];
79
-				} else {
80
-					/**
81
-					 * @psalm-suppress RedundantCondition
82
-					 */
83
-					if (!is_array($info)) {
84
-						$info = [];
85
-					}
86
-					$info['encrypted'] = true;
87
-					$info = new CacheEntry($info);
88
-				}
89
-
90
-				if ($size !== $info->getUnencryptedSize()) {
91
-					$this->getCache()->update($info->getId(), [
92
-						'unencrypted_size' => $size
93
-					]);
94
-				}
95
-			}
96
-
97
-			return $size;
98
-		}
99
-
100
-		if (isset($info['fileid']) && $info['encrypted']) {
101
-			return $this->verifyUnencryptedSize($path, $info->getUnencryptedSize());
102
-		}
103
-
104
-		return $this->storage->filesize($path);
105
-	}
106
-
107
-	private function modifyMetaData(string $path, array $data): array {
108
-		$fullPath = $this->getFullPath($path);
109
-		$info = $this->getCache()->get($path);
110
-
111
-		if (isset($this->unencryptedSize[$fullPath])) {
112
-			$data['encrypted'] = true;
113
-			$data['size'] = $this->unencryptedSize[$fullPath];
114
-			$data['unencrypted_size'] = $data['size'];
115
-		} else {
116
-			if (isset($info['fileid']) && $info['encrypted']) {
117
-				$data['size'] = $this->verifyUnencryptedSize($path, $info->getUnencryptedSize());
118
-				$data['encrypted'] = true;
119
-				$data['unencrypted_size'] = $data['size'];
120
-			}
121
-		}
122
-
123
-		if (isset($info['encryptedVersion']) && $info['encryptedVersion'] > 1) {
124
-			$data['encryptedVersion'] = $info['encryptedVersion'];
125
-		}
126
-
127
-		return $data;
128
-	}
129
-
130
-	public function getMetaData(string $path): ?array {
131
-		$data = $this->storage->getMetaData($path);
132
-		if (is_null($data)) {
133
-			return null;
134
-		}
135
-		return $this->modifyMetaData($path, $data);
136
-	}
137
-
138
-	public function getDirectoryContent(string $directory): \Traversable {
139
-		$parent = rtrim($directory, '/');
140
-		foreach ($this->getWrapperStorage()->getDirectoryContent($directory) as $data) {
141
-			yield $this->modifyMetaData($parent . '/' . $data['name'], $data);
142
-		}
143
-	}
144
-
145
-	public function file_get_contents(string $path): string|false {
146
-		$encryptionModule = $this->getEncryptionModule($path);
147
-
148
-		if ($encryptionModule) {
149
-			$handle = $this->fopen($path, 'r');
150
-			if (!$handle) {
151
-				return false;
152
-			}
153
-			$data = stream_get_contents($handle);
154
-			fclose($handle);
155
-			return $data;
156
-		}
157
-		return $this->storage->file_get_contents($path);
158
-	}
159
-
160
-	public function file_put_contents(string $path, mixed $data): int|float|false {
161
-		// file put content will always be translated to a stream write
162
-		$handle = $this->fopen($path, 'w');
163
-		if (is_resource($handle)) {
164
-			$written = fwrite($handle, $data);
165
-			fclose($handle);
166
-			return $written;
167
-		}
168
-
169
-		return false;
170
-	}
171
-
172
-	public function unlink(string $path): bool {
173
-		$fullPath = $this->getFullPath($path);
174
-		if ($this->util->isExcluded($fullPath)) {
175
-			return $this->storage->unlink($path);
176
-		}
177
-
178
-		$encryptionModule = $this->getEncryptionModule($path);
179
-		if ($encryptionModule) {
180
-			$this->keyStorage->deleteAllFileKeys($fullPath);
181
-		}
182
-
183
-		return $this->storage->unlink($path);
184
-	}
185
-
186
-	public function rename(string $source, string $target): bool {
187
-		$result = $this->storage->rename($source, $target);
188
-
189
-		if ($result
190
-			// versions always use the keys from the original file, so we can skip
191
-			// this step for versions
192
-			&& $this->isVersion($target) === false
193
-			&& $this->encryptionManager->isEnabled()) {
194
-			$sourcePath = $this->getFullPath($source);
195
-			if (!$this->util->isExcluded($sourcePath)) {
196
-				$targetPath = $this->getFullPath($target);
197
-				if (isset($this->unencryptedSize[$sourcePath])) {
198
-					$this->unencryptedSize[$targetPath] = $this->unencryptedSize[$sourcePath];
199
-				}
200
-				$this->keyStorage->renameKeys($sourcePath, $targetPath);
201
-				$module = $this->getEncryptionModule($target);
202
-				if ($module) {
203
-					$module->update($targetPath, $this->uid, []);
204
-				}
205
-			}
206
-		}
207
-
208
-		return $result;
209
-	}
210
-
211
-	public function rmdir(string $path): bool {
212
-		$result = $this->storage->rmdir($path);
213
-		$fullPath = $this->getFullPath($path);
214
-		if ($result
215
-			&& $this->util->isExcluded($fullPath) === false
216
-			&& $this->encryptionManager->isEnabled()
217
-		) {
218
-			$this->keyStorage->deleteAllFileKeys($fullPath);
219
-		}
220
-
221
-		return $result;
222
-	}
223
-
224
-	public function isReadable(string $path): bool {
225
-		$isReadable = true;
226
-
227
-		$metaData = $this->getMetaData($path);
228
-		if (
229
-			!$this->is_dir($path)
230
-			&& isset($metaData['encrypted'])
231
-			&& $metaData['encrypted'] === true
232
-		) {
233
-			$fullPath = $this->getFullPath($path);
234
-			$module = $this->getEncryptionModule($path);
235
-			$isReadable = $module->isReadable($fullPath, $this->uid);
236
-		}
237
-
238
-		return $this->storage->isReadable($path) && $isReadable;
239
-	}
240
-
241
-	public function copy(string $source, string $target): bool {
242
-		$sourcePath = $this->getFullPath($source);
243
-
244
-		if ($this->util->isExcluded($sourcePath)) {
245
-			return $this->storage->copy($source, $target);
246
-		}
247
-
248
-		// need to stream copy file by file in case we copy between a encrypted
249
-		// and a unencrypted storage
250
-		$this->unlink($target);
251
-		return $this->copyFromStorage($this, $source, $target);
252
-	}
253
-
254
-	public function fopen(string $path, string $mode) {
255
-		// check if the file is stored in the array cache, this means that we
256
-		// copy a file over to the versions folder, in this case we don't want to
257
-		// decrypt it
258
-		if ($this->arrayCache->hasKey('encryption_copy_version_' . $path)) {
259
-			$this->arrayCache->remove('encryption_copy_version_' . $path);
260
-			return $this->storage->fopen($path, $mode);
261
-		}
262
-
263
-		if (!$this->enabled) {
264
-			return $this->storage->fopen($path, $mode);
265
-		}
266
-
267
-		$encryptionEnabled = $this->encryptionManager->isEnabled();
268
-		$shouldEncrypt = false;
269
-		$encryptionModule = null;
270
-		$header = $this->getHeader($path);
271
-		$signed = isset($header['signed']) && $header['signed'] === 'true';
272
-		$fullPath = $this->getFullPath($path);
273
-		$encryptionModuleId = $this->util->getEncryptionModuleId($header);
274
-
275
-		if ($this->util->isExcluded($fullPath) === false) {
276
-			$size = $unencryptedSize = 0;
277
-			$realFile = $this->util->stripPartialFileExtension($path);
278
-			$targetExists = $this->is_file($realFile) || $this->file_exists($path);
279
-			$targetIsEncrypted = false;
280
-			if ($targetExists) {
281
-				// in case the file exists we require the explicit module as
282
-				// specified in the file header - otherwise we need to fail hard to
283
-				// prevent data loss on client side
284
-				if (!empty($encryptionModuleId)) {
285
-					$targetIsEncrypted = true;
286
-					$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
287
-				}
288
-
289
-				if ($this->file_exists($path)) {
290
-					$size = $this->storage->filesize($path);
291
-					$unencryptedSize = $this->filesize($path);
292
-				} else {
293
-					$size = $unencryptedSize = 0;
294
-				}
295
-			}
296
-
297
-			try {
298
-				if (
299
-					$mode === 'w'
300
-					|| $mode === 'w+'
301
-					|| $mode === 'wb'
302
-					|| $mode === 'wb+'
303
-				) {
304
-					// if we update a encrypted file with a un-encrypted one we change the db flag
305
-					if ($targetIsEncrypted && $encryptionEnabled === false) {
306
-						$cache = $this->storage->getCache();
307
-						$entry = $cache->get($path);
308
-						$cache->update($entry->getId(), ['encrypted' => 0]);
309
-					}
310
-					if ($encryptionEnabled) {
311
-						// if $encryptionModuleId is empty, the default module will be used
312
-						$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
313
-						$shouldEncrypt = $encryptionModule->shouldEncrypt($fullPath);
314
-						$signed = true;
315
-					}
316
-				} else {
317
-					$info = $this->getCache()->get($path);
318
-					// only get encryption module if we found one in the header
319
-					// or if file should be encrypted according to the file cache
320
-					if (!empty($encryptionModuleId)) {
321
-						$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
322
-						$shouldEncrypt = true;
323
-					} elseif ($info !== false && $info['encrypted'] === true) {
324
-						// we come from a old installation. No header and/or no module defined
325
-						// but the file is encrypted. In this case we need to use the
326
-						// OC_DEFAULT_MODULE to read the file
327
-						$encryptionModule = $this->encryptionManager->getEncryptionModule('OC_DEFAULT_MODULE');
328
-						$shouldEncrypt = true;
329
-						$targetIsEncrypted = true;
330
-					}
331
-				}
332
-			} catch (ModuleDoesNotExistsException $e) {
333
-				$this->logger->warning('Encryption module "' . $encryptionModuleId . '" not found, file will be stored unencrypted', [
334
-					'exception' => $e,
335
-					'app' => 'core',
336
-				]);
337
-			}
338
-
339
-			// encryption disabled on write of new file and write to existing unencrypted file -> don't encrypt
340
-			if (!$this->shouldEncrypt($path)) {
341
-				if (!$targetExists || !$targetIsEncrypted) {
342
-					$shouldEncrypt = false;
343
-				}
344
-			}
345
-
346
-			if ($shouldEncrypt === true && $encryptionModule !== null) {
347
-				$this->encryptedPaths->set($this->util->stripPartialFileExtension($path), true);
348
-				$headerSize = $this->getHeaderSize($path);
349
-				if ($mode === 'r' && $headerSize === 0) {
350
-					$firstBlock = $this->readFirstBlock($path);
351
-					if (!$firstBlock) {
352
-						throw new InvalidHeaderException("Unable to get header block for $path");
353
-					} elseif (!str_starts_with($firstBlock, Util::HEADER_START)) {
354
-						throw new InvalidHeaderException("Unable to get header size for $path, file doesn't start with encryption header");
355
-					} else {
356
-						throw new InvalidHeaderException("Unable to get header size for $path, even though file does start with encryption header");
357
-					}
358
-				}
359
-				$source = $this->storage->fopen($path, $mode);
360
-				if (!is_resource($source)) {
361
-					return false;
362
-				}
363
-				$handle = \OC\Files\Stream\Encryption::wrap($source, $path, $fullPath, $header,
364
-					$this->uid, $encryptionModule, $this->storage, $this, $this->util, $this->fileHelper, $mode,
365
-					$size, $unencryptedSize, $headerSize, $signed);
366
-
367
-				return $handle;
368
-			}
369
-		}
370
-
371
-		return $this->storage->fopen($path, $mode);
372
-	}
373
-
374
-
375
-	/**
376
-	 * perform some plausibility checks if the unencrypted size is correct.
377
-	 * If not, we calculate the correct unencrypted size and return it
378
-	 *
379
-	 * @param string $path internal path relative to the storage root
380
-	 * @param int $unencryptedSize size of the unencrypted file
381
-	 *
382
-	 * @return int unencrypted size
383
-	 */
384
-	protected function verifyUnencryptedSize(string $path, int $unencryptedSize): int {
385
-		$size = $this->storage->filesize($path);
386
-		$result = $unencryptedSize;
387
-
388
-		if ($unencryptedSize < 0
389
-			|| ($size > 0 && $unencryptedSize === $size)
390
-			|| $unencryptedSize > $size
391
-		) {
392
-			// check if we already calculate the unencrypted size for the
393
-			// given path to avoid recursions
394
-			if (isset($this->fixUnencryptedSizeOf[$this->getFullPath($path)]) === false) {
395
-				$this->fixUnencryptedSizeOf[$this->getFullPath($path)] = true;
396
-				try {
397
-					$result = $this->fixUnencryptedSize($path, $size, $unencryptedSize);
398
-				} catch (\Exception $e) {
399
-					$this->logger->error('Couldn\'t re-calculate unencrypted size for ' . $path, ['exception' => $e]);
400
-				}
401
-				unset($this->fixUnencryptedSizeOf[$this->getFullPath($path)]);
402
-			}
403
-		}
404
-
405
-		return $result;
406
-	}
407
-
408
-	/**
409
-	 * calculate the unencrypted size
410
-	 *
411
-	 * @param string $path internal path relative to the storage root
412
-	 * @param int $size size of the physical file
413
-	 * @param int $unencryptedSize size of the unencrypted file
414
-	 */
415
-	protected function fixUnencryptedSize(string $path, int $size, int $unencryptedSize): int|float {
416
-		$headerSize = $this->getHeaderSize($path);
417
-		$header = $this->getHeader($path);
418
-		$encryptionModule = $this->getEncryptionModule($path);
419
-
420
-		$stream = $this->storage->fopen($path, 'r');
421
-
422
-		// if we couldn't open the file we return the old unencrypted size
423
-		if (!is_resource($stream)) {
424
-			$this->logger->error('Could not open ' . $path . '. Recalculation of unencrypted size aborted.');
425
-			return $unencryptedSize;
426
-		}
427
-
428
-		$newUnencryptedSize = 0;
429
-		$size -= $headerSize;
430
-		$blockSize = $this->util->getBlockSize();
431
-
432
-		// if a header exists we skip it
433
-		if ($headerSize > 0) {
434
-			$this->fread_block($stream, $headerSize);
435
-		}
436
-
437
-		// fast path, else the calculation for $lastChunkNr is bogus
438
-		if ($size === 0) {
439
-			return 0;
440
-		}
441
-
442
-		$signed = isset($header['signed']) && $header['signed'] === 'true';
443
-		$unencryptedBlockSize = $encryptionModule->getUnencryptedBlockSize($signed);
444
-
445
-		// calculate last chunk nr
446
-		// next highest is end of chunks, one subtracted is last one
447
-		// we have to read the last chunk, we can't just calculate it (because of padding etc)
448
-
449
-		$lastChunkNr = ceil($size / $blockSize) - 1;
450
-		// calculate last chunk position
451
-		$lastChunkPos = ($lastChunkNr * $blockSize);
452
-		// try to fseek to the last chunk, if it fails we have to read the whole file
453
-		if (@fseek($stream, $lastChunkPos, SEEK_CUR) === 0) {
454
-			$newUnencryptedSize += $lastChunkNr * $unencryptedBlockSize;
455
-		}
456
-
457
-		$lastChunkContentEncrypted = '';
458
-		$count = $blockSize;
459
-
460
-		while ($count > 0) {
461
-			$data = $this->fread_block($stream, $blockSize);
462
-			$count = strlen($data);
463
-			$lastChunkContentEncrypted .= $data;
464
-			if (strlen($lastChunkContentEncrypted) > $blockSize) {
465
-				$newUnencryptedSize += $unencryptedBlockSize;
466
-				$lastChunkContentEncrypted = substr($lastChunkContentEncrypted, $blockSize);
467
-			}
468
-		}
469
-
470
-		fclose($stream);
471
-
472
-		// we have to decrypt the last chunk to get it actual size
473
-		$encryptionModule->begin($this->getFullPath($path), $this->uid, 'r', $header, []);
474
-		$decryptedLastChunk = $encryptionModule->decrypt($lastChunkContentEncrypted, $lastChunkNr . 'end');
475
-		$decryptedLastChunk .= $encryptionModule->end($this->getFullPath($path), $lastChunkNr . 'end');
476
-
477
-		// calc the real file size with the size of the last chunk
478
-		$newUnencryptedSize += strlen($decryptedLastChunk);
479
-
480
-		$this->updateUnencryptedSize($this->getFullPath($path), $newUnencryptedSize);
481
-
482
-		// write to cache if applicable
483
-		$cache = $this->storage->getCache();
484
-		$entry = $cache->get($path);
485
-		$cache->update($entry['fileid'], [
486
-			'unencrypted_size' => $newUnencryptedSize
487
-		]);
488
-
489
-		return $newUnencryptedSize;
490
-	}
491
-
492
-	/**
493
-	 * fread_block
494
-	 *
495
-	 * This function is a wrapper around the fread function.  It is based on the
496
-	 * stream_read_block function from lib/private/Files/Streams/Encryption.php
497
-	 * It calls stream read until the requested $blockSize was received or no remaining data is present.
498
-	 * This is required as stream_read only returns smaller chunks of data when the stream fetches from a
499
-	 * remote storage over the internet and it does not care about the given $blockSize.
500
-	 *
501
-	 * @param resource $handle the stream to read from
502
-	 * @param int $blockSize Length of requested data block in bytes
503
-	 * @return string Data fetched from stream.
504
-	 */
505
-	private function fread_block($handle, int $blockSize): string {
506
-		$remaining = $blockSize;
507
-		$data = '';
508
-
509
-		do {
510
-			$chunk = fread($handle, $remaining);
511
-			$chunk_len = strlen($chunk);
512
-			$data .= $chunk;
513
-			$remaining -= $chunk_len;
514
-		} while (($remaining > 0) && ($chunk_len > 0));
515
-
516
-		return $data;
517
-	}
518
-
519
-	public function moveFromStorage(
520
-		Storage\IStorage $sourceStorage,
521
-		string $sourceInternalPath,
522
-		string $targetInternalPath,
523
-		$preserveMtime = true,
524
-	): bool {
525
-		if ($sourceStorage === $this) {
526
-			return $this->rename($sourceInternalPath, $targetInternalPath);
527
-		}
528
-
529
-		// TODO clean this up once the underlying moveFromStorage in OC\Files\Storage\Wrapper\Common is fixed:
530
-		// - call $this->storage->moveFromStorage() instead of $this->copyBetweenStorage
531
-		// - copy the file cache update from  $this->copyBetweenStorage to this method
532
-		// - copy the copyKeys() call from  $this->copyBetweenStorage to this method
533
-		// - remove $this->copyBetweenStorage
534
-
535
-		if (!$sourceStorage->isDeletable($sourceInternalPath)) {
536
-			return false;
537
-		}
538
-
539
-		$result = $this->copyBetweenStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, true);
540
-		if ($result) {
541
-			$setPreserveCacheOnDelete = $sourceStorage->instanceOfStorage(ObjectStoreStorage::class) && !$this->instanceOfStorage(ObjectStoreStorage::class);
542
-			if ($setPreserveCacheOnDelete) {
543
-				/** @var ObjectStoreStorage $sourceStorage */
544
-				$sourceStorage->setPreserveCacheOnDelete(true);
545
-			}
546
-			try {
547
-				if ($sourceStorage->is_dir($sourceInternalPath)) {
548
-					$result = $sourceStorage->rmdir($sourceInternalPath);
549
-				} else {
550
-					$result = $sourceStorage->unlink($sourceInternalPath);
551
-				}
552
-			} finally {
553
-				if ($setPreserveCacheOnDelete) {
554
-					/** @var ObjectStoreStorage $sourceStorage */
555
-					$sourceStorage->setPreserveCacheOnDelete(false);
556
-				}
557
-			}
558
-		}
559
-		return $result;
560
-	}
561
-
562
-	public function copyFromStorage(
563
-		Storage\IStorage $sourceStorage,
564
-		string $sourceInternalPath,
565
-		string $targetInternalPath,
566
-		$preserveMtime = false,
567
-		$isRename = false,
568
-	): bool {
569
-		// TODO clean this up once the underlying moveFromStorage in OC\Files\Storage\Wrapper\Common is fixed:
570
-		// - call $this->storage->copyFromStorage() instead of $this->copyBetweenStorage
571
-		// - copy the file cache update from  $this->copyBetweenStorage to this method
572
-		// - copy the copyKeys() call from  $this->copyBetweenStorage to this method
573
-		// - remove $this->copyBetweenStorage
574
-
575
-		return $this->copyBetweenStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, $isRename);
576
-	}
577
-
578
-	/**
579
-	 * Update the encrypted cache version in the database
580
-	 */
581
-	private function updateEncryptedVersion(
582
-		Storage\IStorage $sourceStorage,
583
-		string $sourceInternalPath,
584
-		string $targetInternalPath,
585
-		bool $isRename,
586
-		bool $keepEncryptionVersion,
587
-	): void {
588
-		$isEncrypted = $this->shouldEncrypt($targetInternalPath);
589
-		$cacheInformation = [
590
-			'encrypted' => $isEncrypted,
591
-		];
592
-		if ($isEncrypted) {
593
-			$sourceCacheEntry = $sourceStorage->getCache()->get($sourceInternalPath);
594
-			$targetCacheEntry = $this->getCache()->get($targetInternalPath);
595
-
596
-			// Rename of the cache already happened, so we do the cleanup on the target
597
-			if ($sourceCacheEntry === false && $targetCacheEntry !== false) {
598
-				$encryptedVersion = $targetCacheEntry['encryptedVersion'];
599
-				$isRename = false;
600
-			} else {
601
-				$encryptedVersion = $sourceCacheEntry['encryptedVersion'];
602
-			}
603
-
604
-			// In case of a move operation from an unencrypted to an encrypted
605
-			// storage the old encrypted version would stay with "0" while the
606
-			// correct value would be "1". Thus we manually set the value to "1"
607
-			// for those cases.
608
-			// See also https://github.com/owncloud/core/issues/23078
609
-			if ($encryptedVersion === 0 || !$keepEncryptionVersion) {
610
-				$encryptedVersion = 1;
611
-			}
612
-
613
-			$cacheInformation['encryptedVersion'] = $encryptedVersion;
614
-		}
615
-
616
-		// in case of a rename we need to manipulate the source cache because
617
-		// this information will be kept for the new target
618
-		if ($isRename) {
619
-			$sourceStorage->getCache()->put($sourceInternalPath, $cacheInformation);
620
-		} else {
621
-			$this->getCache()->put($targetInternalPath, $cacheInformation);
622
-		}
623
-	}
624
-
625
-	/**
626
-	 * copy file between two storages
627
-	 * @throws \Exception
628
-	 */
629
-	private function copyBetweenStorage(
630
-		Storage\IStorage $sourceStorage,
631
-		string $sourceInternalPath,
632
-		string $targetInternalPath,
633
-		bool $preserveMtime,
634
-		bool $isRename,
635
-	): bool {
636
-		// for versions we have nothing to do, because versions should always use the
637
-		// key from the original file. Just create a 1:1 copy and done
638
-		if ($this->isVersion($targetInternalPath)
639
-			|| $this->isVersion($sourceInternalPath)) {
640
-			// remember that we try to create a version so that we can detect it during
641
-			// fopen($sourceInternalPath) and by-pass the encryption in order to
642
-			// create a 1:1 copy of the file
643
-			$this->arrayCache->set('encryption_copy_version_' . $sourceInternalPath, true);
644
-			$result = $this->storage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
645
-			$this->arrayCache->remove('encryption_copy_version_' . $sourceInternalPath);
646
-			if ($result) {
647
-				$info = $this->getCache('', $sourceStorage)->get($sourceInternalPath);
648
-				// make sure that we update the unencrypted size for the version
649
-				if (isset($info['encrypted']) && $info['encrypted'] === true) {
650
-					$this->updateUnencryptedSize(
651
-						$this->getFullPath($targetInternalPath),
652
-						$info->getUnencryptedSize()
653
-					);
654
-				}
655
-				$this->updateEncryptedVersion($sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename, true);
656
-			}
657
-			return $result;
658
-		}
659
-
660
-		// first copy the keys that we reuse the existing file key on the target location
661
-		// and don't create a new one which would break versions for example.
662
-		if ($sourceStorage->instanceOfStorage(Common::class) && $sourceStorage->getMountOption('mount_point')) {
663
-			$mountPoint = $sourceStorage->getMountOption('mount_point');
664
-			$source = $mountPoint . '/' . $sourceInternalPath;
665
-			$target = $this->getFullPath($targetInternalPath);
666
-			$this->copyKeys($source, $target);
667
-		} else {
668
-			$this->logger->error('Could not find mount point, can\'t keep encryption keys');
669
-		}
670
-
671
-		if ($sourceStorage->is_dir($sourceInternalPath)) {
672
-			$dh = $sourceStorage->opendir($sourceInternalPath);
673
-			if (!$this->is_dir($targetInternalPath)) {
674
-				$result = $this->mkdir($targetInternalPath);
675
-			} else {
676
-				$result = true;
677
-			}
678
-			if (is_resource($dh)) {
679
-				while ($result && ($file = readdir($dh)) !== false) {
680
-					if (!Filesystem::isIgnoredDir($file)) {
681
-						$result = $this->copyFromStorage($sourceStorage, $sourceInternalPath . '/' . $file, $targetInternalPath . '/' . $file, $preserveMtime, $isRename);
682
-					}
683
-				}
684
-			}
685
-		} else {
686
-			$source = false;
687
-			$target = false;
688
-			try {
689
-				$source = $sourceStorage->fopen($sourceInternalPath, 'r');
690
-				$target = $this->fopen($targetInternalPath, 'w');
691
-				if ($source === false || $target === false) {
692
-					$result = false;
693
-				} else {
694
-					[, $result] = Files::streamCopy($source, $target, true);
695
-				}
696
-			} finally {
697
-				if ($source !== false) {
698
-					fclose($source);
699
-				}
700
-				if ($target !== false) {
701
-					fclose($target);
702
-				}
703
-			}
704
-			if ($result) {
705
-				if ($preserveMtime) {
706
-					$this->touch($targetInternalPath, $sourceStorage->filemtime($sourceInternalPath));
707
-				}
708
-				$this->updateEncryptedVersion($sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename, false);
709
-			} else {
710
-				// delete partially written target file
711
-				$this->unlink($targetInternalPath);
712
-				// delete cache entry that was created by fopen
713
-				$this->getCache()->remove($targetInternalPath);
714
-			}
715
-		}
716
-		return (bool)$result;
717
-	}
718
-
719
-	public function getLocalFile(string $path): string|false {
720
-		if ($this->encryptionManager->isEnabled()) {
721
-			$cachedFile = $this->getCachedFile($path);
722
-			if (is_string($cachedFile)) {
723
-				return $cachedFile;
724
-			}
725
-		}
726
-		return $this->storage->getLocalFile($path);
727
-	}
728
-
729
-	public function isLocal(): bool {
730
-		if ($this->encryptionManager->isEnabled()) {
731
-			return false;
732
-		}
733
-		return $this->storage->isLocal();
734
-	}
735
-
736
-	public function stat(string $path): array|false {
737
-		$stat = $this->storage->stat($path);
738
-		if (!$stat) {
739
-			return false;
740
-		}
741
-		$fileSize = $this->filesize($path);
742
-		$stat['size'] = $fileSize;
743
-		$stat[7] = $fileSize;
744
-		$stat['hasHeader'] = $this->getHeaderSize($path) > 0;
745
-		return $stat;
746
-	}
747
-
748
-	public function hash(string $type, string $path, bool $raw = false): string|false {
749
-		$fh = $this->fopen($path, 'rb');
750
-		if ($fh === false) {
751
-			return false;
752
-		}
753
-		$ctx = hash_init($type);
754
-		hash_update_stream($ctx, $fh);
755
-		fclose($fh);
756
-		return hash_final($ctx, $raw);
757
-	}
758
-
759
-	/**
760
-	 * return full path, including mount point
761
-	 *
762
-	 * @param string $path relative to mount point
763
-	 * @return string full path including mount point
764
-	 */
765
-	protected function getFullPath(string $path): string {
766
-		return Filesystem::normalizePath($this->mountPoint . '/' . $path);
767
-	}
768
-
769
-	/**
770
-	 * read first block of encrypted file, typically this will contain the
771
-	 * encryption header
772
-	 */
773
-	protected function readFirstBlock(string $path): string {
774
-		$firstBlock = '';
775
-		if ($this->storage->is_file($path)) {
776
-			$handle = $this->storage->fopen($path, 'r');
777
-			if ($handle === false) {
778
-				return '';
779
-			}
780
-			$firstBlock = fread($handle, $this->util->getHeaderSize());
781
-			fclose($handle);
782
-		}
783
-		return $firstBlock;
784
-	}
785
-
786
-	/**
787
-	 * return header size of given file
788
-	 */
789
-	protected function getHeaderSize(string $path): int {
790
-		$headerSize = 0;
791
-		$realFile = $this->util->stripPartialFileExtension($path);
792
-		if ($this->storage->is_file($realFile)) {
793
-			$path = $realFile;
794
-		}
795
-		$firstBlock = $this->readFirstBlock($path);
796
-
797
-		if (str_starts_with($firstBlock, Util::HEADER_START)) {
798
-			$headerSize = $this->util->getHeaderSize();
799
-		}
800
-
801
-		return $headerSize;
802
-	}
803
-
804
-	/**
805
-	 * read header from file
806
-	 */
807
-	protected function getHeader(string $path): array {
808
-		$realFile = $this->util->stripPartialFileExtension($path);
809
-		$exists = $this->storage->is_file($realFile);
810
-		if ($exists) {
811
-			$path = $realFile;
812
-		}
813
-
814
-		$result = [];
815
-
816
-		$isEncrypted = $this->encryptedPaths->get($realFile);
817
-		if (is_null($isEncrypted)) {
818
-			$info = $this->getCache()->get($path);
819
-			$isEncrypted = isset($info['encrypted']) && $info['encrypted'] === true;
820
-		}
821
-
822
-		if ($isEncrypted) {
823
-			$firstBlock = $this->readFirstBlock($path);
824
-			$result = $this->util->parseRawHeader($firstBlock);
825
-
826
-			// if the header doesn't contain a encryption module we check if it is a
827
-			// legacy file. If true, we add the default encryption module
828
-			if (!isset($result[Util::HEADER_ENCRYPTION_MODULE_KEY]) && (!empty($result) || $exists)) {
829
-				$result[Util::HEADER_ENCRYPTION_MODULE_KEY] = 'OC_DEFAULT_MODULE';
830
-			}
831
-		}
832
-
833
-		return $result;
834
-	}
835
-
836
-	/**
837
-	 * read encryption module needed to read/write the file located at $path
838
-	 *
839
-	 * @throws ModuleDoesNotExistsException
840
-	 * @throws \Exception
841
-	 */
842
-	protected function getEncryptionModule(string $path): ?\OCP\Encryption\IEncryptionModule {
843
-		$encryptionModule = null;
844
-		$header = $this->getHeader($path);
845
-		$encryptionModuleId = $this->util->getEncryptionModuleId($header);
846
-		if (!empty($encryptionModuleId)) {
847
-			try {
848
-				$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
849
-			} catch (ModuleDoesNotExistsException $e) {
850
-				$this->logger->critical('Encryption module defined in "' . $path . '" not loaded!');
851
-				throw $e;
852
-			}
853
-		}
854
-
855
-		return $encryptionModule;
856
-	}
857
-
858
-	public function updateUnencryptedSize(string $path, int|float $unencryptedSize): void {
859
-		$this->unencryptedSize[$path] = $unencryptedSize;
860
-	}
861
-
862
-	/**
863
-	 * copy keys to new location
864
-	 *
865
-	 * @param string $source path relative to data/
866
-	 * @param string $target path relative to data/
867
-	 */
868
-	protected function copyKeys(string $source, string $target): bool {
869
-		if (!$this->util->isExcluded($source)) {
870
-			return $this->keyStorage->copyKeys($source, $target);
871
-		}
872
-
873
-		return false;
874
-	}
875
-
876
-	/**
877
-	 * check if path points to a files version
878
-	 */
879
-	protected function isVersion(string $path): bool {
880
-		$normalized = Filesystem::normalizePath($path);
881
-		return substr($normalized, 0, strlen('/files_versions/')) === '/files_versions/';
882
-	}
883
-
884
-	/**
885
-	 * check if the given storage should be encrypted or not
886
-	 */
887
-	public function shouldEncrypt(string $path): bool {
888
-		if (!$this->encryptionManager->isEnabled()) {
889
-			return false;
890
-		}
891
-		$fullPath = $this->getFullPath($path);
892
-		$mountPointConfig = $this->mount->getOption('encrypt', true);
893
-		if ($mountPointConfig === false) {
894
-			return false;
895
-		}
896
-
897
-		try {
898
-			$encryptionModule = $this->getEncryptionModule($fullPath);
899
-		} catch (ModuleDoesNotExistsException $e) {
900
-			return false;
901
-		}
902
-
903
-		if ($encryptionModule === null) {
904
-			$encryptionModule = $this->encryptionManager->getEncryptionModule();
905
-		}
906
-
907
-		return $encryptionModule->shouldEncrypt($fullPath);
908
-	}
909
-
910
-	public function writeStream(string $path, $stream, ?int $size = null): int {
911
-		// always fall back to fopen
912
-		$target = $this->fopen($path, 'w');
913
-		if ($target === false) {
914
-			throw new GenericFileException("Failed to open $path for writing");
915
-		}
916
-		[$count, $result] = Files::streamCopy($stream, $target, true);
917
-		fclose($stream);
918
-		fclose($target);
919
-
920
-		// object store, stores the size after write and doesn't update this during scan
921
-		// manually store the unencrypted size
922
-		if ($result && $this->getWrapperStorage()->instanceOfStorage(ObjectStoreStorage::class) && $this->shouldEncrypt($path)) {
923
-			$this->getCache()->put($path, ['unencrypted_size' => $count]);
924
-		}
925
-
926
-		return $count;
927
-	}
928
-
929
-	public function clearIsEncryptedCache(): void {
930
-		$this->encryptedPaths->clear();
931
-	}
932
-
933
-	/**
934
-	 * Allow temporarily disabling the wrapper
935
-	 */
936
-	public function setEnabled(bool $enabled): void {
937
-		$this->enabled = $enabled;
938
-	}
939
-
940
-	/**
941
-	 * Check if the on-disk data for a file has a valid encrypted header
942
-	 *
943
-	 * @param string $path
944
-	 * @return bool
945
-	 */
946
-	public function hasValidHeader(string $path): bool {
947
-		$firstBlock = $this->readFirstBlock($path);
948
-		$header = $this->util->parseRawHeader($firstBlock);
949
-		return (count($header) > 0);
950
-	}
32
+    use LocalTempFileTrait;
33
+
34
+    private string $mountPoint;
35
+    protected array $unencryptedSize = [];
36
+    private IMountPoint $mount;
37
+    /** for which path we execute the repair step to avoid recursions */
38
+    private array $fixUnencryptedSizeOf = [];
39
+    /** @var CappedMemoryCache<bool> */
40
+    private CappedMemoryCache $encryptedPaths;
41
+    private bool $enabled = true;
42
+
43
+    /**
44
+     * @param array $parameters
45
+     */
46
+    public function __construct(
47
+        array $parameters,
48
+        private IManager $encryptionManager,
49
+        private Util $util,
50
+        private LoggerInterface $logger,
51
+        private IFile $fileHelper,
52
+        private ?string $uid,
53
+        private IStorage $keyStorage,
54
+        private Manager $mountManager,
55
+        private ArrayCache $arrayCache,
56
+    ) {
57
+        $this->mountPoint = $parameters['mountPoint'];
58
+        $this->mount = $parameters['mount'];
59
+        $this->encryptedPaths = new CappedMemoryCache();
60
+        parent::__construct($parameters);
61
+    }
62
+
63
+    public function filesize(string $path): int|float|false {
64
+        $fullPath = $this->getFullPath($path);
65
+
66
+        $info = $this->getCache()->get($path);
67
+        if ($info === false) {
68
+            /* Pass call to wrapped storage, it may be a special file like a part file */
69
+            return $this->storage->filesize($path);
70
+        }
71
+        if (isset($this->unencryptedSize[$fullPath])) {
72
+            $size = $this->unencryptedSize[$fullPath];
73
+
74
+            // Update file cache (only if file is already cached).
75
+            // Certain files are not cached (e.g. *.part).
76
+            if (isset($info['fileid'])) {
77
+                if ($info instanceof ICacheEntry) {
78
+                    $info['encrypted'] = $info['encryptedVersion'];
79
+                } else {
80
+                    /**
81
+                     * @psalm-suppress RedundantCondition
82
+                     */
83
+                    if (!is_array($info)) {
84
+                        $info = [];
85
+                    }
86
+                    $info['encrypted'] = true;
87
+                    $info = new CacheEntry($info);
88
+                }
89
+
90
+                if ($size !== $info->getUnencryptedSize()) {
91
+                    $this->getCache()->update($info->getId(), [
92
+                        'unencrypted_size' => $size
93
+                    ]);
94
+                }
95
+            }
96
+
97
+            return $size;
98
+        }
99
+
100
+        if (isset($info['fileid']) && $info['encrypted']) {
101
+            return $this->verifyUnencryptedSize($path, $info->getUnencryptedSize());
102
+        }
103
+
104
+        return $this->storage->filesize($path);
105
+    }
106
+
107
+    private function modifyMetaData(string $path, array $data): array {
108
+        $fullPath = $this->getFullPath($path);
109
+        $info = $this->getCache()->get($path);
110
+
111
+        if (isset($this->unencryptedSize[$fullPath])) {
112
+            $data['encrypted'] = true;
113
+            $data['size'] = $this->unencryptedSize[$fullPath];
114
+            $data['unencrypted_size'] = $data['size'];
115
+        } else {
116
+            if (isset($info['fileid']) && $info['encrypted']) {
117
+                $data['size'] = $this->verifyUnencryptedSize($path, $info->getUnencryptedSize());
118
+                $data['encrypted'] = true;
119
+                $data['unencrypted_size'] = $data['size'];
120
+            }
121
+        }
122
+
123
+        if (isset($info['encryptedVersion']) && $info['encryptedVersion'] > 1) {
124
+            $data['encryptedVersion'] = $info['encryptedVersion'];
125
+        }
126
+
127
+        return $data;
128
+    }
129
+
130
+    public function getMetaData(string $path): ?array {
131
+        $data = $this->storage->getMetaData($path);
132
+        if (is_null($data)) {
133
+            return null;
134
+        }
135
+        return $this->modifyMetaData($path, $data);
136
+    }
137
+
138
+    public function getDirectoryContent(string $directory): \Traversable {
139
+        $parent = rtrim($directory, '/');
140
+        foreach ($this->getWrapperStorage()->getDirectoryContent($directory) as $data) {
141
+            yield $this->modifyMetaData($parent . '/' . $data['name'], $data);
142
+        }
143
+    }
144
+
145
+    public function file_get_contents(string $path): string|false {
146
+        $encryptionModule = $this->getEncryptionModule($path);
147
+
148
+        if ($encryptionModule) {
149
+            $handle = $this->fopen($path, 'r');
150
+            if (!$handle) {
151
+                return false;
152
+            }
153
+            $data = stream_get_contents($handle);
154
+            fclose($handle);
155
+            return $data;
156
+        }
157
+        return $this->storage->file_get_contents($path);
158
+    }
159
+
160
+    public function file_put_contents(string $path, mixed $data): int|float|false {
161
+        // file put content will always be translated to a stream write
162
+        $handle = $this->fopen($path, 'w');
163
+        if (is_resource($handle)) {
164
+            $written = fwrite($handle, $data);
165
+            fclose($handle);
166
+            return $written;
167
+        }
168
+
169
+        return false;
170
+    }
171
+
172
+    public function unlink(string $path): bool {
173
+        $fullPath = $this->getFullPath($path);
174
+        if ($this->util->isExcluded($fullPath)) {
175
+            return $this->storage->unlink($path);
176
+        }
177
+
178
+        $encryptionModule = $this->getEncryptionModule($path);
179
+        if ($encryptionModule) {
180
+            $this->keyStorage->deleteAllFileKeys($fullPath);
181
+        }
182
+
183
+        return $this->storage->unlink($path);
184
+    }
185
+
186
+    public function rename(string $source, string $target): bool {
187
+        $result = $this->storage->rename($source, $target);
188
+
189
+        if ($result
190
+            // versions always use the keys from the original file, so we can skip
191
+            // this step for versions
192
+            && $this->isVersion($target) === false
193
+            && $this->encryptionManager->isEnabled()) {
194
+            $sourcePath = $this->getFullPath($source);
195
+            if (!$this->util->isExcluded($sourcePath)) {
196
+                $targetPath = $this->getFullPath($target);
197
+                if (isset($this->unencryptedSize[$sourcePath])) {
198
+                    $this->unencryptedSize[$targetPath] = $this->unencryptedSize[$sourcePath];
199
+                }
200
+                $this->keyStorage->renameKeys($sourcePath, $targetPath);
201
+                $module = $this->getEncryptionModule($target);
202
+                if ($module) {
203
+                    $module->update($targetPath, $this->uid, []);
204
+                }
205
+            }
206
+        }
207
+
208
+        return $result;
209
+    }
210
+
211
+    public function rmdir(string $path): bool {
212
+        $result = $this->storage->rmdir($path);
213
+        $fullPath = $this->getFullPath($path);
214
+        if ($result
215
+            && $this->util->isExcluded($fullPath) === false
216
+            && $this->encryptionManager->isEnabled()
217
+        ) {
218
+            $this->keyStorage->deleteAllFileKeys($fullPath);
219
+        }
220
+
221
+        return $result;
222
+    }
223
+
224
+    public function isReadable(string $path): bool {
225
+        $isReadable = true;
226
+
227
+        $metaData = $this->getMetaData($path);
228
+        if (
229
+            !$this->is_dir($path)
230
+            && isset($metaData['encrypted'])
231
+            && $metaData['encrypted'] === true
232
+        ) {
233
+            $fullPath = $this->getFullPath($path);
234
+            $module = $this->getEncryptionModule($path);
235
+            $isReadable = $module->isReadable($fullPath, $this->uid);
236
+        }
237
+
238
+        return $this->storage->isReadable($path) && $isReadable;
239
+    }
240
+
241
+    public function copy(string $source, string $target): bool {
242
+        $sourcePath = $this->getFullPath($source);
243
+
244
+        if ($this->util->isExcluded($sourcePath)) {
245
+            return $this->storage->copy($source, $target);
246
+        }
247
+
248
+        // need to stream copy file by file in case we copy between a encrypted
249
+        // and a unencrypted storage
250
+        $this->unlink($target);
251
+        return $this->copyFromStorage($this, $source, $target);
252
+    }
253
+
254
+    public function fopen(string $path, string $mode) {
255
+        // check if the file is stored in the array cache, this means that we
256
+        // copy a file over to the versions folder, in this case we don't want to
257
+        // decrypt it
258
+        if ($this->arrayCache->hasKey('encryption_copy_version_' . $path)) {
259
+            $this->arrayCache->remove('encryption_copy_version_' . $path);
260
+            return $this->storage->fopen($path, $mode);
261
+        }
262
+
263
+        if (!$this->enabled) {
264
+            return $this->storage->fopen($path, $mode);
265
+        }
266
+
267
+        $encryptionEnabled = $this->encryptionManager->isEnabled();
268
+        $shouldEncrypt = false;
269
+        $encryptionModule = null;
270
+        $header = $this->getHeader($path);
271
+        $signed = isset($header['signed']) && $header['signed'] === 'true';
272
+        $fullPath = $this->getFullPath($path);
273
+        $encryptionModuleId = $this->util->getEncryptionModuleId($header);
274
+
275
+        if ($this->util->isExcluded($fullPath) === false) {
276
+            $size = $unencryptedSize = 0;
277
+            $realFile = $this->util->stripPartialFileExtension($path);
278
+            $targetExists = $this->is_file($realFile) || $this->file_exists($path);
279
+            $targetIsEncrypted = false;
280
+            if ($targetExists) {
281
+                // in case the file exists we require the explicit module as
282
+                // specified in the file header - otherwise we need to fail hard to
283
+                // prevent data loss on client side
284
+                if (!empty($encryptionModuleId)) {
285
+                    $targetIsEncrypted = true;
286
+                    $encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
287
+                }
288
+
289
+                if ($this->file_exists($path)) {
290
+                    $size = $this->storage->filesize($path);
291
+                    $unencryptedSize = $this->filesize($path);
292
+                } else {
293
+                    $size = $unencryptedSize = 0;
294
+                }
295
+            }
296
+
297
+            try {
298
+                if (
299
+                    $mode === 'w'
300
+                    || $mode === 'w+'
301
+                    || $mode === 'wb'
302
+                    || $mode === 'wb+'
303
+                ) {
304
+                    // if we update a encrypted file with a un-encrypted one we change the db flag
305
+                    if ($targetIsEncrypted && $encryptionEnabled === false) {
306
+                        $cache = $this->storage->getCache();
307
+                        $entry = $cache->get($path);
308
+                        $cache->update($entry->getId(), ['encrypted' => 0]);
309
+                    }
310
+                    if ($encryptionEnabled) {
311
+                        // if $encryptionModuleId is empty, the default module will be used
312
+                        $encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
313
+                        $shouldEncrypt = $encryptionModule->shouldEncrypt($fullPath);
314
+                        $signed = true;
315
+                    }
316
+                } else {
317
+                    $info = $this->getCache()->get($path);
318
+                    // only get encryption module if we found one in the header
319
+                    // or if file should be encrypted according to the file cache
320
+                    if (!empty($encryptionModuleId)) {
321
+                        $encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
322
+                        $shouldEncrypt = true;
323
+                    } elseif ($info !== false && $info['encrypted'] === true) {
324
+                        // we come from a old installation. No header and/or no module defined
325
+                        // but the file is encrypted. In this case we need to use the
326
+                        // OC_DEFAULT_MODULE to read the file
327
+                        $encryptionModule = $this->encryptionManager->getEncryptionModule('OC_DEFAULT_MODULE');
328
+                        $shouldEncrypt = true;
329
+                        $targetIsEncrypted = true;
330
+                    }
331
+                }
332
+            } catch (ModuleDoesNotExistsException $e) {
333
+                $this->logger->warning('Encryption module "' . $encryptionModuleId . '" not found, file will be stored unencrypted', [
334
+                    'exception' => $e,
335
+                    'app' => 'core',
336
+                ]);
337
+            }
338
+
339
+            // encryption disabled on write of new file and write to existing unencrypted file -> don't encrypt
340
+            if (!$this->shouldEncrypt($path)) {
341
+                if (!$targetExists || !$targetIsEncrypted) {
342
+                    $shouldEncrypt = false;
343
+                }
344
+            }
345
+
346
+            if ($shouldEncrypt === true && $encryptionModule !== null) {
347
+                $this->encryptedPaths->set($this->util->stripPartialFileExtension($path), true);
348
+                $headerSize = $this->getHeaderSize($path);
349
+                if ($mode === 'r' && $headerSize === 0) {
350
+                    $firstBlock = $this->readFirstBlock($path);
351
+                    if (!$firstBlock) {
352
+                        throw new InvalidHeaderException("Unable to get header block for $path");
353
+                    } elseif (!str_starts_with($firstBlock, Util::HEADER_START)) {
354
+                        throw new InvalidHeaderException("Unable to get header size for $path, file doesn't start with encryption header");
355
+                    } else {
356
+                        throw new InvalidHeaderException("Unable to get header size for $path, even though file does start with encryption header");
357
+                    }
358
+                }
359
+                $source = $this->storage->fopen($path, $mode);
360
+                if (!is_resource($source)) {
361
+                    return false;
362
+                }
363
+                $handle = \OC\Files\Stream\Encryption::wrap($source, $path, $fullPath, $header,
364
+                    $this->uid, $encryptionModule, $this->storage, $this, $this->util, $this->fileHelper, $mode,
365
+                    $size, $unencryptedSize, $headerSize, $signed);
366
+
367
+                return $handle;
368
+            }
369
+        }
370
+
371
+        return $this->storage->fopen($path, $mode);
372
+    }
373
+
374
+
375
+    /**
376
+     * perform some plausibility checks if the unencrypted size is correct.
377
+     * If not, we calculate the correct unencrypted size and return it
378
+     *
379
+     * @param string $path internal path relative to the storage root
380
+     * @param int $unencryptedSize size of the unencrypted file
381
+     *
382
+     * @return int unencrypted size
383
+     */
384
+    protected function verifyUnencryptedSize(string $path, int $unencryptedSize): int {
385
+        $size = $this->storage->filesize($path);
386
+        $result = $unencryptedSize;
387
+
388
+        if ($unencryptedSize < 0
389
+            || ($size > 0 && $unencryptedSize === $size)
390
+            || $unencryptedSize > $size
391
+        ) {
392
+            // check if we already calculate the unencrypted size for the
393
+            // given path to avoid recursions
394
+            if (isset($this->fixUnencryptedSizeOf[$this->getFullPath($path)]) === false) {
395
+                $this->fixUnencryptedSizeOf[$this->getFullPath($path)] = true;
396
+                try {
397
+                    $result = $this->fixUnencryptedSize($path, $size, $unencryptedSize);
398
+                } catch (\Exception $e) {
399
+                    $this->logger->error('Couldn\'t re-calculate unencrypted size for ' . $path, ['exception' => $e]);
400
+                }
401
+                unset($this->fixUnencryptedSizeOf[$this->getFullPath($path)]);
402
+            }
403
+        }
404
+
405
+        return $result;
406
+    }
407
+
408
+    /**
409
+     * calculate the unencrypted size
410
+     *
411
+     * @param string $path internal path relative to the storage root
412
+     * @param int $size size of the physical file
413
+     * @param int $unencryptedSize size of the unencrypted file
414
+     */
415
+    protected function fixUnencryptedSize(string $path, int $size, int $unencryptedSize): int|float {
416
+        $headerSize = $this->getHeaderSize($path);
417
+        $header = $this->getHeader($path);
418
+        $encryptionModule = $this->getEncryptionModule($path);
419
+
420
+        $stream = $this->storage->fopen($path, 'r');
421
+
422
+        // if we couldn't open the file we return the old unencrypted size
423
+        if (!is_resource($stream)) {
424
+            $this->logger->error('Could not open ' . $path . '. Recalculation of unencrypted size aborted.');
425
+            return $unencryptedSize;
426
+        }
427
+
428
+        $newUnencryptedSize = 0;
429
+        $size -= $headerSize;
430
+        $blockSize = $this->util->getBlockSize();
431
+
432
+        // if a header exists we skip it
433
+        if ($headerSize > 0) {
434
+            $this->fread_block($stream, $headerSize);
435
+        }
436
+
437
+        // fast path, else the calculation for $lastChunkNr is bogus
438
+        if ($size === 0) {
439
+            return 0;
440
+        }
441
+
442
+        $signed = isset($header['signed']) && $header['signed'] === 'true';
443
+        $unencryptedBlockSize = $encryptionModule->getUnencryptedBlockSize($signed);
444
+
445
+        // calculate last chunk nr
446
+        // next highest is end of chunks, one subtracted is last one
447
+        // we have to read the last chunk, we can't just calculate it (because of padding etc)
448
+
449
+        $lastChunkNr = ceil($size / $blockSize) - 1;
450
+        // calculate last chunk position
451
+        $lastChunkPos = ($lastChunkNr * $blockSize);
452
+        // try to fseek to the last chunk, if it fails we have to read the whole file
453
+        if (@fseek($stream, $lastChunkPos, SEEK_CUR) === 0) {
454
+            $newUnencryptedSize += $lastChunkNr * $unencryptedBlockSize;
455
+        }
456
+
457
+        $lastChunkContentEncrypted = '';
458
+        $count = $blockSize;
459
+
460
+        while ($count > 0) {
461
+            $data = $this->fread_block($stream, $blockSize);
462
+            $count = strlen($data);
463
+            $lastChunkContentEncrypted .= $data;
464
+            if (strlen($lastChunkContentEncrypted) > $blockSize) {
465
+                $newUnencryptedSize += $unencryptedBlockSize;
466
+                $lastChunkContentEncrypted = substr($lastChunkContentEncrypted, $blockSize);
467
+            }
468
+        }
469
+
470
+        fclose($stream);
471
+
472
+        // we have to decrypt the last chunk to get it actual size
473
+        $encryptionModule->begin($this->getFullPath($path), $this->uid, 'r', $header, []);
474
+        $decryptedLastChunk = $encryptionModule->decrypt($lastChunkContentEncrypted, $lastChunkNr . 'end');
475
+        $decryptedLastChunk .= $encryptionModule->end($this->getFullPath($path), $lastChunkNr . 'end');
476
+
477
+        // calc the real file size with the size of the last chunk
478
+        $newUnencryptedSize += strlen($decryptedLastChunk);
479
+
480
+        $this->updateUnencryptedSize($this->getFullPath($path), $newUnencryptedSize);
481
+
482
+        // write to cache if applicable
483
+        $cache = $this->storage->getCache();
484
+        $entry = $cache->get($path);
485
+        $cache->update($entry['fileid'], [
486
+            'unencrypted_size' => $newUnencryptedSize
487
+        ]);
488
+
489
+        return $newUnencryptedSize;
490
+    }
491
+
492
+    /**
493
+     * fread_block
494
+     *
495
+     * This function is a wrapper around the fread function.  It is based on the
496
+     * stream_read_block function from lib/private/Files/Streams/Encryption.php
497
+     * It calls stream read until the requested $blockSize was received or no remaining data is present.
498
+     * This is required as stream_read only returns smaller chunks of data when the stream fetches from a
499
+     * remote storage over the internet and it does not care about the given $blockSize.
500
+     *
501
+     * @param resource $handle the stream to read from
502
+     * @param int $blockSize Length of requested data block in bytes
503
+     * @return string Data fetched from stream.
504
+     */
505
+    private function fread_block($handle, int $blockSize): string {
506
+        $remaining = $blockSize;
507
+        $data = '';
508
+
509
+        do {
510
+            $chunk = fread($handle, $remaining);
511
+            $chunk_len = strlen($chunk);
512
+            $data .= $chunk;
513
+            $remaining -= $chunk_len;
514
+        } while (($remaining > 0) && ($chunk_len > 0));
515
+
516
+        return $data;
517
+    }
518
+
519
+    public function moveFromStorage(
520
+        Storage\IStorage $sourceStorage,
521
+        string $sourceInternalPath,
522
+        string $targetInternalPath,
523
+        $preserveMtime = true,
524
+    ): bool {
525
+        if ($sourceStorage === $this) {
526
+            return $this->rename($sourceInternalPath, $targetInternalPath);
527
+        }
528
+
529
+        // TODO clean this up once the underlying moveFromStorage in OC\Files\Storage\Wrapper\Common is fixed:
530
+        // - call $this->storage->moveFromStorage() instead of $this->copyBetweenStorage
531
+        // - copy the file cache update from  $this->copyBetweenStorage to this method
532
+        // - copy the copyKeys() call from  $this->copyBetweenStorage to this method
533
+        // - remove $this->copyBetweenStorage
534
+
535
+        if (!$sourceStorage->isDeletable($sourceInternalPath)) {
536
+            return false;
537
+        }
538
+
539
+        $result = $this->copyBetweenStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, true);
540
+        if ($result) {
541
+            $setPreserveCacheOnDelete = $sourceStorage->instanceOfStorage(ObjectStoreStorage::class) && !$this->instanceOfStorage(ObjectStoreStorage::class);
542
+            if ($setPreserveCacheOnDelete) {
543
+                /** @var ObjectStoreStorage $sourceStorage */
544
+                $sourceStorage->setPreserveCacheOnDelete(true);
545
+            }
546
+            try {
547
+                if ($sourceStorage->is_dir($sourceInternalPath)) {
548
+                    $result = $sourceStorage->rmdir($sourceInternalPath);
549
+                } else {
550
+                    $result = $sourceStorage->unlink($sourceInternalPath);
551
+                }
552
+            } finally {
553
+                if ($setPreserveCacheOnDelete) {
554
+                    /** @var ObjectStoreStorage $sourceStorage */
555
+                    $sourceStorage->setPreserveCacheOnDelete(false);
556
+                }
557
+            }
558
+        }
559
+        return $result;
560
+    }
561
+
562
+    public function copyFromStorage(
563
+        Storage\IStorage $sourceStorage,
564
+        string $sourceInternalPath,
565
+        string $targetInternalPath,
566
+        $preserveMtime = false,
567
+        $isRename = false,
568
+    ): bool {
569
+        // TODO clean this up once the underlying moveFromStorage in OC\Files\Storage\Wrapper\Common is fixed:
570
+        // - call $this->storage->copyFromStorage() instead of $this->copyBetweenStorage
571
+        // - copy the file cache update from  $this->copyBetweenStorage to this method
572
+        // - copy the copyKeys() call from  $this->copyBetweenStorage to this method
573
+        // - remove $this->copyBetweenStorage
574
+
575
+        return $this->copyBetweenStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, $isRename);
576
+    }
577
+
578
+    /**
579
+     * Update the encrypted cache version in the database
580
+     */
581
+    private function updateEncryptedVersion(
582
+        Storage\IStorage $sourceStorage,
583
+        string $sourceInternalPath,
584
+        string $targetInternalPath,
585
+        bool $isRename,
586
+        bool $keepEncryptionVersion,
587
+    ): void {
588
+        $isEncrypted = $this->shouldEncrypt($targetInternalPath);
589
+        $cacheInformation = [
590
+            'encrypted' => $isEncrypted,
591
+        ];
592
+        if ($isEncrypted) {
593
+            $sourceCacheEntry = $sourceStorage->getCache()->get($sourceInternalPath);
594
+            $targetCacheEntry = $this->getCache()->get($targetInternalPath);
595
+
596
+            // Rename of the cache already happened, so we do the cleanup on the target
597
+            if ($sourceCacheEntry === false && $targetCacheEntry !== false) {
598
+                $encryptedVersion = $targetCacheEntry['encryptedVersion'];
599
+                $isRename = false;
600
+            } else {
601
+                $encryptedVersion = $sourceCacheEntry['encryptedVersion'];
602
+            }
603
+
604
+            // In case of a move operation from an unencrypted to an encrypted
605
+            // storage the old encrypted version would stay with "0" while the
606
+            // correct value would be "1". Thus we manually set the value to "1"
607
+            // for those cases.
608
+            // See also https://github.com/owncloud/core/issues/23078
609
+            if ($encryptedVersion === 0 || !$keepEncryptionVersion) {
610
+                $encryptedVersion = 1;
611
+            }
612
+
613
+            $cacheInformation['encryptedVersion'] = $encryptedVersion;
614
+        }
615
+
616
+        // in case of a rename we need to manipulate the source cache because
617
+        // this information will be kept for the new target
618
+        if ($isRename) {
619
+            $sourceStorage->getCache()->put($sourceInternalPath, $cacheInformation);
620
+        } else {
621
+            $this->getCache()->put($targetInternalPath, $cacheInformation);
622
+        }
623
+    }
624
+
625
+    /**
626
+     * copy file between two storages
627
+     * @throws \Exception
628
+     */
629
+    private function copyBetweenStorage(
630
+        Storage\IStorage $sourceStorage,
631
+        string $sourceInternalPath,
632
+        string $targetInternalPath,
633
+        bool $preserveMtime,
634
+        bool $isRename,
635
+    ): bool {
636
+        // for versions we have nothing to do, because versions should always use the
637
+        // key from the original file. Just create a 1:1 copy and done
638
+        if ($this->isVersion($targetInternalPath)
639
+            || $this->isVersion($sourceInternalPath)) {
640
+            // remember that we try to create a version so that we can detect it during
641
+            // fopen($sourceInternalPath) and by-pass the encryption in order to
642
+            // create a 1:1 copy of the file
643
+            $this->arrayCache->set('encryption_copy_version_' . $sourceInternalPath, true);
644
+            $result = $this->storage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
645
+            $this->arrayCache->remove('encryption_copy_version_' . $sourceInternalPath);
646
+            if ($result) {
647
+                $info = $this->getCache('', $sourceStorage)->get($sourceInternalPath);
648
+                // make sure that we update the unencrypted size for the version
649
+                if (isset($info['encrypted']) && $info['encrypted'] === true) {
650
+                    $this->updateUnencryptedSize(
651
+                        $this->getFullPath($targetInternalPath),
652
+                        $info->getUnencryptedSize()
653
+                    );
654
+                }
655
+                $this->updateEncryptedVersion($sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename, true);
656
+            }
657
+            return $result;
658
+        }
659
+
660
+        // first copy the keys that we reuse the existing file key on the target location
661
+        // and don't create a new one which would break versions for example.
662
+        if ($sourceStorage->instanceOfStorage(Common::class) && $sourceStorage->getMountOption('mount_point')) {
663
+            $mountPoint = $sourceStorage->getMountOption('mount_point');
664
+            $source = $mountPoint . '/' . $sourceInternalPath;
665
+            $target = $this->getFullPath($targetInternalPath);
666
+            $this->copyKeys($source, $target);
667
+        } else {
668
+            $this->logger->error('Could not find mount point, can\'t keep encryption keys');
669
+        }
670
+
671
+        if ($sourceStorage->is_dir($sourceInternalPath)) {
672
+            $dh = $sourceStorage->opendir($sourceInternalPath);
673
+            if (!$this->is_dir($targetInternalPath)) {
674
+                $result = $this->mkdir($targetInternalPath);
675
+            } else {
676
+                $result = true;
677
+            }
678
+            if (is_resource($dh)) {
679
+                while ($result && ($file = readdir($dh)) !== false) {
680
+                    if (!Filesystem::isIgnoredDir($file)) {
681
+                        $result = $this->copyFromStorage($sourceStorage, $sourceInternalPath . '/' . $file, $targetInternalPath . '/' . $file, $preserveMtime, $isRename);
682
+                    }
683
+                }
684
+            }
685
+        } else {
686
+            $source = false;
687
+            $target = false;
688
+            try {
689
+                $source = $sourceStorage->fopen($sourceInternalPath, 'r');
690
+                $target = $this->fopen($targetInternalPath, 'w');
691
+                if ($source === false || $target === false) {
692
+                    $result = false;
693
+                } else {
694
+                    [, $result] = Files::streamCopy($source, $target, true);
695
+                }
696
+            } finally {
697
+                if ($source !== false) {
698
+                    fclose($source);
699
+                }
700
+                if ($target !== false) {
701
+                    fclose($target);
702
+                }
703
+            }
704
+            if ($result) {
705
+                if ($preserveMtime) {
706
+                    $this->touch($targetInternalPath, $sourceStorage->filemtime($sourceInternalPath));
707
+                }
708
+                $this->updateEncryptedVersion($sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename, false);
709
+            } else {
710
+                // delete partially written target file
711
+                $this->unlink($targetInternalPath);
712
+                // delete cache entry that was created by fopen
713
+                $this->getCache()->remove($targetInternalPath);
714
+            }
715
+        }
716
+        return (bool)$result;
717
+    }
718
+
719
+    public function getLocalFile(string $path): string|false {
720
+        if ($this->encryptionManager->isEnabled()) {
721
+            $cachedFile = $this->getCachedFile($path);
722
+            if (is_string($cachedFile)) {
723
+                return $cachedFile;
724
+            }
725
+        }
726
+        return $this->storage->getLocalFile($path);
727
+    }
728
+
729
+    public function isLocal(): bool {
730
+        if ($this->encryptionManager->isEnabled()) {
731
+            return false;
732
+        }
733
+        return $this->storage->isLocal();
734
+    }
735
+
736
+    public function stat(string $path): array|false {
737
+        $stat = $this->storage->stat($path);
738
+        if (!$stat) {
739
+            return false;
740
+        }
741
+        $fileSize = $this->filesize($path);
742
+        $stat['size'] = $fileSize;
743
+        $stat[7] = $fileSize;
744
+        $stat['hasHeader'] = $this->getHeaderSize($path) > 0;
745
+        return $stat;
746
+    }
747
+
748
+    public function hash(string $type, string $path, bool $raw = false): string|false {
749
+        $fh = $this->fopen($path, 'rb');
750
+        if ($fh === false) {
751
+            return false;
752
+        }
753
+        $ctx = hash_init($type);
754
+        hash_update_stream($ctx, $fh);
755
+        fclose($fh);
756
+        return hash_final($ctx, $raw);
757
+    }
758
+
759
+    /**
760
+     * return full path, including mount point
761
+     *
762
+     * @param string $path relative to mount point
763
+     * @return string full path including mount point
764
+     */
765
+    protected function getFullPath(string $path): string {
766
+        return Filesystem::normalizePath($this->mountPoint . '/' . $path);
767
+    }
768
+
769
+    /**
770
+     * read first block of encrypted file, typically this will contain the
771
+     * encryption header
772
+     */
773
+    protected function readFirstBlock(string $path): string {
774
+        $firstBlock = '';
775
+        if ($this->storage->is_file($path)) {
776
+            $handle = $this->storage->fopen($path, 'r');
777
+            if ($handle === false) {
778
+                return '';
779
+            }
780
+            $firstBlock = fread($handle, $this->util->getHeaderSize());
781
+            fclose($handle);
782
+        }
783
+        return $firstBlock;
784
+    }
785
+
786
+    /**
787
+     * return header size of given file
788
+     */
789
+    protected function getHeaderSize(string $path): int {
790
+        $headerSize = 0;
791
+        $realFile = $this->util->stripPartialFileExtension($path);
792
+        if ($this->storage->is_file($realFile)) {
793
+            $path = $realFile;
794
+        }
795
+        $firstBlock = $this->readFirstBlock($path);
796
+
797
+        if (str_starts_with($firstBlock, Util::HEADER_START)) {
798
+            $headerSize = $this->util->getHeaderSize();
799
+        }
800
+
801
+        return $headerSize;
802
+    }
803
+
804
+    /**
805
+     * read header from file
806
+     */
807
+    protected function getHeader(string $path): array {
808
+        $realFile = $this->util->stripPartialFileExtension($path);
809
+        $exists = $this->storage->is_file($realFile);
810
+        if ($exists) {
811
+            $path = $realFile;
812
+        }
813
+
814
+        $result = [];
815
+
816
+        $isEncrypted = $this->encryptedPaths->get($realFile);
817
+        if (is_null($isEncrypted)) {
818
+            $info = $this->getCache()->get($path);
819
+            $isEncrypted = isset($info['encrypted']) && $info['encrypted'] === true;
820
+        }
821
+
822
+        if ($isEncrypted) {
823
+            $firstBlock = $this->readFirstBlock($path);
824
+            $result = $this->util->parseRawHeader($firstBlock);
825
+
826
+            // if the header doesn't contain a encryption module we check if it is a
827
+            // legacy file. If true, we add the default encryption module
828
+            if (!isset($result[Util::HEADER_ENCRYPTION_MODULE_KEY]) && (!empty($result) || $exists)) {
829
+                $result[Util::HEADER_ENCRYPTION_MODULE_KEY] = 'OC_DEFAULT_MODULE';
830
+            }
831
+        }
832
+
833
+        return $result;
834
+    }
835
+
836
+    /**
837
+     * read encryption module needed to read/write the file located at $path
838
+     *
839
+     * @throws ModuleDoesNotExistsException
840
+     * @throws \Exception
841
+     */
842
+    protected function getEncryptionModule(string $path): ?\OCP\Encryption\IEncryptionModule {
843
+        $encryptionModule = null;
844
+        $header = $this->getHeader($path);
845
+        $encryptionModuleId = $this->util->getEncryptionModuleId($header);
846
+        if (!empty($encryptionModuleId)) {
847
+            try {
848
+                $encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
849
+            } catch (ModuleDoesNotExistsException $e) {
850
+                $this->logger->critical('Encryption module defined in "' . $path . '" not loaded!');
851
+                throw $e;
852
+            }
853
+        }
854
+
855
+        return $encryptionModule;
856
+    }
857
+
858
+    public function updateUnencryptedSize(string $path, int|float $unencryptedSize): void {
859
+        $this->unencryptedSize[$path] = $unencryptedSize;
860
+    }
861
+
862
+    /**
863
+     * copy keys to new location
864
+     *
865
+     * @param string $source path relative to data/
866
+     * @param string $target path relative to data/
867
+     */
868
+    protected function copyKeys(string $source, string $target): bool {
869
+        if (!$this->util->isExcluded($source)) {
870
+            return $this->keyStorage->copyKeys($source, $target);
871
+        }
872
+
873
+        return false;
874
+    }
875
+
876
+    /**
877
+     * check if path points to a files version
878
+     */
879
+    protected function isVersion(string $path): bool {
880
+        $normalized = Filesystem::normalizePath($path);
881
+        return substr($normalized, 0, strlen('/files_versions/')) === '/files_versions/';
882
+    }
883
+
884
+    /**
885
+     * check if the given storage should be encrypted or not
886
+     */
887
+    public function shouldEncrypt(string $path): bool {
888
+        if (!$this->encryptionManager->isEnabled()) {
889
+            return false;
890
+        }
891
+        $fullPath = $this->getFullPath($path);
892
+        $mountPointConfig = $this->mount->getOption('encrypt', true);
893
+        if ($mountPointConfig === false) {
894
+            return false;
895
+        }
896
+
897
+        try {
898
+            $encryptionModule = $this->getEncryptionModule($fullPath);
899
+        } catch (ModuleDoesNotExistsException $e) {
900
+            return false;
901
+        }
902
+
903
+        if ($encryptionModule === null) {
904
+            $encryptionModule = $this->encryptionManager->getEncryptionModule();
905
+        }
906
+
907
+        return $encryptionModule->shouldEncrypt($fullPath);
908
+    }
909
+
910
+    public function writeStream(string $path, $stream, ?int $size = null): int {
911
+        // always fall back to fopen
912
+        $target = $this->fopen($path, 'w');
913
+        if ($target === false) {
914
+            throw new GenericFileException("Failed to open $path for writing");
915
+        }
916
+        [$count, $result] = Files::streamCopy($stream, $target, true);
917
+        fclose($stream);
918
+        fclose($target);
919
+
920
+        // object store, stores the size after write and doesn't update this during scan
921
+        // manually store the unencrypted size
922
+        if ($result && $this->getWrapperStorage()->instanceOfStorage(ObjectStoreStorage::class) && $this->shouldEncrypt($path)) {
923
+            $this->getCache()->put($path, ['unencrypted_size' => $count]);
924
+        }
925
+
926
+        return $count;
927
+    }
928
+
929
+    public function clearIsEncryptedCache(): void {
930
+        $this->encryptedPaths->clear();
931
+    }
932
+
933
+    /**
934
+     * Allow temporarily disabling the wrapper
935
+     */
936
+    public function setEnabled(bool $enabled): void {
937
+        $this->enabled = $enabled;
938
+    }
939
+
940
+    /**
941
+     * Check if the on-disk data for a file has a valid encrypted header
942
+     *
943
+     * @param string $path
944
+     * @return bool
945
+     */
946
+    public function hasValidHeader(string $path): bool {
947
+        $firstBlock = $this->readFirstBlock($path);
948
+        $header = $this->util->parseRawHeader($firstBlock);
949
+        return (count($header) > 0);
950
+    }
951 951
 }
Please login to merge, or discard this patch.
lib/private/Files/Cache/Cache.php 1 patch
Indentation   +1260 added lines, -1260 removed lines patch added patch discarded remove patch
@@ -47,1264 +47,1264 @@
 block discarded – undo
47 47
  * - ChangePropagator: updates the mtime and etags of parent folders whenever a change to the cache is made to the cache by the updater
48 48
  */
49 49
 class Cache implements ICache {
50
-	use MoveFromCacheTrait {
51
-		MoveFromCacheTrait::moveFromCache as moveFromCacheFallback;
52
-	}
53
-
54
-	/**
55
-	 * @var array partial data for the cache
56
-	 */
57
-	protected array $partial = [];
58
-	protected string $storageId;
59
-	protected Storage $storageCache;
60
-	protected IMimeTypeLoader $mimetypeLoader;
61
-	protected IDBConnection $connection;
62
-	protected SystemConfig $systemConfig;
63
-	protected LoggerInterface $logger;
64
-	protected QuerySearchHelper $querySearchHelper;
65
-	protected IEventDispatcher $eventDispatcher;
66
-	protected IFilesMetadataManager $metadataManager;
67
-
68
-	public function __construct(
69
-		private IStorage $storage,
70
-		// this constructor is used in to many pleases to easily do proper di
71
-		// so instead we group it all together
72
-		?CacheDependencies $dependencies = null,
73
-	) {
74
-		$this->storageId = $storage->getId();
75
-		if (strlen($this->storageId) > 64) {
76
-			$this->storageId = md5($this->storageId);
77
-		}
78
-		if (!$dependencies) {
79
-			$dependencies = \OCP\Server::get(CacheDependencies::class);
80
-		}
81
-		$this->storageCache = new Storage($this->storage, true, $dependencies->getConnection());
82
-		$this->mimetypeLoader = $dependencies->getMimeTypeLoader();
83
-		$this->connection = $dependencies->getConnection();
84
-		$this->systemConfig = $dependencies->getSystemConfig();
85
-		$this->logger = $dependencies->getLogger();
86
-		$this->querySearchHelper = $dependencies->getQuerySearchHelper();
87
-		$this->eventDispatcher = $dependencies->getEventDispatcher();
88
-		$this->metadataManager = $dependencies->getMetadataManager();
89
-	}
90
-
91
-	protected function getQueryBuilder() {
92
-		return new CacheQueryBuilder(
93
-			$this->connection->getQueryBuilder(),
94
-			$this->metadataManager,
95
-		);
96
-	}
97
-
98
-	public function getStorageCache(): Storage {
99
-		return $this->storageCache;
100
-	}
101
-
102
-	/**
103
-	 * Get the numeric storage id for this cache's storage
104
-	 *
105
-	 * @return int
106
-	 */
107
-	public function getNumericStorageId() {
108
-		return $this->storageCache->getNumericId();
109
-	}
110
-
111
-	/**
112
-	 * get the stored metadata of a file or folder
113
-	 *
114
-	 * @param string|int $file either the path of a file or folder or the file id for a file or folder
115
-	 * @return ICacheEntry|false the cache entry as array or false if the file is not found in the cache
116
-	 */
117
-	public function get($file) {
118
-		$query = $this->getQueryBuilder();
119
-		$query->selectFileCache();
120
-		$metadataQuery = $query->selectMetadata();
121
-
122
-		if (is_string($file) || $file == '') {
123
-			// normalize file
124
-			$file = $this->normalize($file);
125
-
126
-			$query->wherePath($file);
127
-		} else { //file id
128
-			$query->whereFileId($file);
129
-		}
130
-		$query->whereStorageId($this->getNumericStorageId());
131
-
132
-		$result = $query->executeQuery();
133
-		$data = $result->fetch();
134
-		$result->closeCursor();
135
-
136
-		if ($data !== false) {
137
-			$data['metadata'] = $metadataQuery->extractMetadata($data)->asArray();
138
-			return self::cacheEntryFromData($data, $this->mimetypeLoader);
139
-		} else {
140
-			//merge partial data
141
-			if (is_string($file) && isset($this->partial[$file])) {
142
-				return $this->partial[$file];
143
-			}
144
-		}
145
-
146
-		return false;
147
-	}
148
-
149
-	/**
150
-	 * Create a CacheEntry from database row
151
-	 *
152
-	 * @param array $data
153
-	 * @param IMimeTypeLoader $mimetypeLoader
154
-	 * @return CacheEntry
155
-	 */
156
-	public static function cacheEntryFromData($data, IMimeTypeLoader $mimetypeLoader) {
157
-		//fix types
158
-		$data['name'] = (string)$data['name'];
159
-		$data['path'] = (string)$data['path'];
160
-		$data['fileid'] = (int)$data['fileid'];
161
-		$data['parent'] = (int)$data['parent'];
162
-		$data['size'] = Util::numericToNumber($data['size']);
163
-		$data['unencrypted_size'] = Util::numericToNumber($data['unencrypted_size'] ?? 0);
164
-		$data['mtime'] = (int)$data['mtime'];
165
-		$data['storage_mtime'] = (int)$data['storage_mtime'];
166
-		$data['encryptedVersion'] = (int)$data['encrypted'];
167
-		$data['encrypted'] = (bool)$data['encrypted'];
168
-		$data['storage_id'] = $data['storage'];
169
-		$data['storage'] = (int)$data['storage'];
170
-		$data['mimetype'] = $mimetypeLoader->getMimetypeById($data['mimetype']);
171
-		$data['mimepart'] = $mimetypeLoader->getMimetypeById($data['mimepart']);
172
-		if ($data['storage_mtime'] == 0) {
173
-			$data['storage_mtime'] = $data['mtime'];
174
-		}
175
-		if (isset($data['f_permissions'])) {
176
-			$data['scan_permissions'] = $data['f_permissions'];
177
-		}
178
-		$data['permissions'] = (int)$data['permissions'];
179
-		if (isset($data['creation_time'])) {
180
-			$data['creation_time'] = (int)$data['creation_time'];
181
-		}
182
-		if (isset($data['upload_time'])) {
183
-			$data['upload_time'] = (int)$data['upload_time'];
184
-		}
185
-		return new CacheEntry($data);
186
-	}
187
-
188
-	/**
189
-	 * get the metadata of all files stored in $folder
190
-	 *
191
-	 * @param string $folder
192
-	 * @return ICacheEntry[]
193
-	 */
194
-	public function getFolderContents($folder) {
195
-		$fileId = $this->getId($folder);
196
-		return $this->getFolderContentsById($fileId);
197
-	}
198
-
199
-	/**
200
-	 * get the metadata of all files stored in $folder
201
-	 *
202
-	 * @param int $fileId the file id of the folder
203
-	 * @return ICacheEntry[]
204
-	 */
205
-	public function getFolderContentsById($fileId) {
206
-		if ($fileId > -1) {
207
-			$query = $this->getQueryBuilder();
208
-			$query->selectFileCache()
209
-				->whereParent($fileId)
210
-				->whereStorageId($this->getNumericStorageId())
211
-				->orderBy('name', 'ASC');
212
-
213
-			$metadataQuery = $query->selectMetadata();
214
-
215
-			$result = $query->executeQuery();
216
-			$files = $result->fetchAll();
217
-			$result->closeCursor();
218
-
219
-			return array_map(function (array $data) use ($metadataQuery) {
220
-				$data['metadata'] = $metadataQuery->extractMetadata($data)->asArray();
221
-				return self::cacheEntryFromData($data, $this->mimetypeLoader);
222
-			}, $files);
223
-		}
224
-		return [];
225
-	}
226
-
227
-	/**
228
-	 * insert or update meta data for a file or folder
229
-	 *
230
-	 * @param string $file
231
-	 * @param array $data
232
-	 *
233
-	 * @return int file id
234
-	 * @throws \RuntimeException
235
-	 */
236
-	public function put($file, array $data) {
237
-		if (($id = $this->getId($file)) > -1) {
238
-			$this->update($id, $data);
239
-			return $id;
240
-		} else {
241
-			return $this->insert($file, $data);
242
-		}
243
-	}
244
-
245
-	/**
246
-	 * insert meta data for a new file or folder
247
-	 *
248
-	 * @param string $file
249
-	 * @param array $data
250
-	 *
251
-	 * @return int file id
252
-	 * @throws \RuntimeException|Exception
253
-	 */
254
-	public function insert($file, array $data) {
255
-		// normalize file
256
-		$file = $this->normalize($file);
257
-
258
-		if (isset($this->partial[$file])) { //add any saved partial data
259
-			$data = array_merge($this->partial[$file]->getData(), $data);
260
-			unset($this->partial[$file]);
261
-		}
262
-
263
-		$requiredFields = ['size', 'mtime', 'mimetype'];
264
-		foreach ($requiredFields as $field) {
265
-			if (!isset($data[$field])) { //data not complete save as partial and return
266
-				$this->partial[$file] = new CacheEntry($data);
267
-				return -1;
268
-			}
269
-		}
270
-
271
-		$data['path'] = $file;
272
-		if (!isset($data['parent'])) {
273
-			$data['parent'] = $this->getParentId($file);
274
-		}
275
-		if ($data['parent'] === -1 && $file !== '') {
276
-			throw new \Exception('Parent folder not in filecache for ' . $file);
277
-		}
278
-		$data['name'] = basename($file);
279
-
280
-		[$values, $extensionValues] = $this->normalizeData($data);
281
-		$storageId = $this->getNumericStorageId();
282
-		$values['storage'] = $storageId;
283
-
284
-		try {
285
-			$builder = $this->connection->getQueryBuilder();
286
-			$builder->insert('filecache');
287
-
288
-			foreach ($values as $column => $value) {
289
-				$builder->setValue($column, $builder->createNamedParameter($value));
290
-			}
291
-
292
-			if ($builder->executeStatement()) {
293
-				$fileId = $builder->getLastInsertId();
294
-
295
-				if (count($extensionValues)) {
296
-					$query = $this->getQueryBuilder();
297
-					$query->insert('filecache_extended');
298
-					$query->hintShardKey('storage', $storageId);
299
-
300
-					$query->setValue('fileid', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT));
301
-					foreach ($extensionValues as $column => $value) {
302
-						$query->setValue($column, $query->createNamedParameter($value));
303
-					}
304
-					$query->executeStatement();
305
-				}
306
-
307
-				$event = new CacheEntryInsertedEvent($this->storage, $file, $fileId, $storageId);
308
-				$this->eventDispatcher->dispatch(CacheInsertEvent::class, $event);
309
-				$this->eventDispatcher->dispatchTyped($event);
310
-				return $fileId;
311
-			}
312
-		} catch (Exception $e) {
313
-			if ($e->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
314
-				// entry exists already
315
-				if ($this->connection->inTransaction()) {
316
-					$this->connection->commit();
317
-					$this->connection->beginTransaction();
318
-				}
319
-			} else {
320
-				throw $e;
321
-			}
322
-		}
323
-
324
-		// The file was created in the meantime
325
-		if (($id = $this->getId($file)) > -1) {
326
-			$this->update($id, $data);
327
-			return $id;
328
-		} else {
329
-			throw new \RuntimeException('File entry could not be inserted but could also not be selected with getId() in order to perform an update. Please try again.');
330
-		}
331
-	}
332
-
333
-	/**
334
-	 * update the metadata of an existing file or folder in the cache
335
-	 *
336
-	 * @param int $id the fileid of the existing file or folder
337
-	 * @param array $data [$key => $value] the metadata to update, only the fields provided in the array will be updated, non-provided values will remain unchanged
338
-	 */
339
-	public function update($id, array $data) {
340
-		if (isset($data['path'])) {
341
-			// normalize path
342
-			$data['path'] = $this->normalize($data['path']);
343
-		}
344
-
345
-		if (isset($data['name'])) {
346
-			// normalize path
347
-			$data['name'] = $this->normalize($data['name']);
348
-		}
349
-
350
-		[$values, $extensionValues] = $this->normalizeData($data);
351
-
352
-		if (count($values)) {
353
-			$query = $this->getQueryBuilder();
354
-
355
-			$query->update('filecache')
356
-				->whereFileId($id)
357
-				->whereStorageId($this->getNumericStorageId())
358
-				->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
359
-					return $query->expr()->orX(
360
-						$query->expr()->neq($key, $query->createNamedParameter($value)),
361
-						$query->expr()->isNull($key)
362
-					);
363
-				}, array_keys($values), array_values($values))));
364
-
365
-			foreach ($values as $key => $value) {
366
-				$query->set($key, $query->createNamedParameter($value));
367
-			}
368
-
369
-			$query->executeStatement();
370
-		}
371
-
372
-		if (count($extensionValues)) {
373
-			try {
374
-				$query = $this->getQueryBuilder();
375
-				$query->insert('filecache_extended');
376
-				$query->hintShardKey('storage', $this->getNumericStorageId());
377
-
378
-				$query->setValue('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT));
379
-				foreach ($extensionValues as $column => $value) {
380
-					$query->setValue($column, $query->createNamedParameter($value));
381
-				}
382
-
383
-				$query->executeStatement();
384
-			} catch (Exception $e) {
385
-				if ($e->getReason() !== Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
386
-					throw $e;
387
-				}
388
-				$query = $this->getQueryBuilder();
389
-				$query->update('filecache_extended')
390
-					->whereFileId($id)
391
-					->hintShardKey('storage', $this->getNumericStorageId())
392
-					->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
393
-						return $query->expr()->orX(
394
-							$query->expr()->neq($key, $query->createNamedParameter($value)),
395
-							$query->expr()->isNull($key)
396
-						);
397
-					}, array_keys($extensionValues), array_values($extensionValues))));
398
-
399
-				foreach ($extensionValues as $key => $value) {
400
-					$query->set($key, $query->createNamedParameter($value));
401
-				}
402
-
403
-				$query->executeStatement();
404
-			}
405
-		}
406
-
407
-		$path = $this->getPathById($id);
408
-		// path can still be null if the file doesn't exist
409
-		if ($path !== null) {
410
-			$event = new CacheEntryUpdatedEvent($this->storage, $path, $id, $this->getNumericStorageId());
411
-			$this->eventDispatcher->dispatch(CacheUpdateEvent::class, $event);
412
-			$this->eventDispatcher->dispatchTyped($event);
413
-		}
414
-	}
415
-
416
-	/**
417
-	 * extract query parts and params array from data array
418
-	 *
419
-	 * @param array $data
420
-	 * @return array
421
-	 */
422
-	protected function normalizeData(array $data): array {
423
-		$fields = [
424
-			'path', 'parent', 'name', 'mimetype', 'size', 'mtime', 'storage_mtime', 'encrypted',
425
-			'etag', 'permissions', 'checksum', 'storage', 'unencrypted_size'];
426
-		$extensionFields = ['metadata_etag', 'creation_time', 'upload_time'];
427
-
428
-		$doNotCopyStorageMTime = false;
429
-		if (array_key_exists('mtime', $data) && $data['mtime'] === null) {
430
-			// this horrific magic tells it to not copy storage_mtime to mtime
431
-			unset($data['mtime']);
432
-			$doNotCopyStorageMTime = true;
433
-		}
434
-
435
-		$params = [];
436
-		$extensionParams = [];
437
-		foreach ($data as $name => $value) {
438
-			if (in_array($name, $fields)) {
439
-				if ($name === 'path') {
440
-					$params['path_hash'] = md5($value);
441
-				} elseif ($name === 'mimetype') {
442
-					$params['mimepart'] = $this->mimetypeLoader->getId(substr($value, 0, strpos($value, '/')));
443
-					$value = $this->mimetypeLoader->getId($value);
444
-				} elseif ($name === 'storage_mtime') {
445
-					if (!$doNotCopyStorageMTime && !isset($data['mtime'])) {
446
-						$params['mtime'] = $value;
447
-					}
448
-				} elseif ($name === 'encrypted') {
449
-					if (isset($data['encryptedVersion'])) {
450
-						$value = $data['encryptedVersion'];
451
-					} else {
452
-						// Boolean to integer conversion
453
-						$value = $value ? 1 : 0;
454
-					}
455
-				}
456
-				$params[$name] = $value;
457
-			}
458
-			if (in_array($name, $extensionFields)) {
459
-				$extensionParams[$name] = $value;
460
-			}
461
-		}
462
-		return [$params, array_filter($extensionParams)];
463
-	}
464
-
465
-	/**
466
-	 * get the file id for a file
467
-	 *
468
-	 * A file id is a numeric id for a file or folder that's unique within an owncloud instance which stays the same for the lifetime of a file
469
-	 *
470
-	 * File ids are easiest way for apps to store references to a file since unlike paths they are not affected by renames or sharing
471
-	 *
472
-	 * @param string $file
473
-	 * @return int
474
-	 */
475
-	public function getId($file) {
476
-		// normalize file
477
-		$file = $this->normalize($file);
478
-
479
-		$query = $this->getQueryBuilder();
480
-		$query->select('fileid')
481
-			->from('filecache')
482
-			->whereStorageId($this->getNumericStorageId())
483
-			->wherePath($file);
484
-
485
-		$result = $query->executeQuery();
486
-		$id = $result->fetchOne();
487
-		$result->closeCursor();
488
-
489
-		return $id === false ? -1 : (int)$id;
490
-	}
491
-
492
-	/**
493
-	 * get the id of the parent folder of a file
494
-	 *
495
-	 * @param string $file
496
-	 * @return int
497
-	 */
498
-	public function getParentId($file) {
499
-		if ($file === '') {
500
-			return -1;
501
-		} else {
502
-			$parent = $this->getParentPath($file);
503
-			return (int)$this->getId($parent);
504
-		}
505
-	}
506
-
507
-	private function getParentPath($path) {
508
-		$parent = dirname($path);
509
-		if ($parent === '.') {
510
-			$parent = '';
511
-		}
512
-		return $parent;
513
-	}
514
-
515
-	/**
516
-	 * check if a file is available in the cache
517
-	 *
518
-	 * @param string $file
519
-	 * @return bool
520
-	 */
521
-	public function inCache($file) {
522
-		return $this->getId($file) != -1;
523
-	}
524
-
525
-	/**
526
-	 * remove a file or folder from the cache
527
-	 *
528
-	 * when removing a folder from the cache all files and folders inside the folder will be removed as well
529
-	 *
530
-	 * @param string $file
531
-	 */
532
-	public function remove($file) {
533
-		$entry = $this->get($file);
534
-
535
-		if ($entry instanceof ICacheEntry) {
536
-			$query = $this->getQueryBuilder();
537
-			$query->delete('filecache')
538
-				->whereStorageId($this->getNumericStorageId())
539
-				->whereFileId($entry->getId());
540
-			$query->executeStatement();
541
-
542
-			$query = $this->getQueryBuilder();
543
-			$query->delete('filecache_extended')
544
-				->whereFileId($entry->getId())
545
-				->hintShardKey('storage', $this->getNumericStorageId());
546
-			$query->executeStatement();
547
-
548
-			if ($entry->getMimeType() == FileInfo::MIMETYPE_FOLDER) {
549
-				$this->removeChildren($entry);
550
-			}
551
-
552
-			$this->eventDispatcher->dispatchTyped(new CacheEntryRemovedEvent($this->storage, $entry->getPath(), $entry->getId(), $this->getNumericStorageId()));
553
-		}
554
-	}
555
-
556
-	/**
557
-	 * Remove all children of a folder
558
-	 *
559
-	 * @param ICacheEntry $entry the cache entry of the folder to remove the children of
560
-	 * @throws \OC\DatabaseException
561
-	 */
562
-	private function removeChildren(ICacheEntry $entry) {
563
-		$parentIds = [$entry->getId()];
564
-		$queue = [$entry->getId()];
565
-		$deletedIds = [];
566
-		$deletedPaths = [];
567
-
568
-		// we walk depth first through the file tree, removing all filecache_extended attributes while we walk
569
-		// and collecting all folder ids to later use to delete the filecache entries
570
-		while ($entryId = array_pop($queue)) {
571
-			$children = $this->getFolderContentsById($entryId);
572
-			$childIds = array_map(function (ICacheEntry $cacheEntry) {
573
-				return $cacheEntry->getId();
574
-			}, $children);
575
-			$childPaths = array_map(function (ICacheEntry $cacheEntry) {
576
-				return $cacheEntry->getPath();
577
-			}, $children);
578
-
579
-			foreach ($childIds as $childId) {
580
-				$deletedIds[] = $childId;
581
-			}
582
-
583
-			foreach ($childPaths as $childPath) {
584
-				$deletedPaths[] = $childPath;
585
-			}
586
-
587
-			$query = $this->getQueryBuilder();
588
-			$query->delete('filecache_extended')
589
-				->where($query->expr()->in('fileid', $query->createParameter('childIds')))
590
-				->hintShardKey('storage', $this->getNumericStorageId());
591
-
592
-			foreach (array_chunk($childIds, 1000) as $childIdChunk) {
593
-				$query->setParameter('childIds', $childIdChunk, IQueryBuilder::PARAM_INT_ARRAY);
594
-				$query->executeStatement();
595
-			}
596
-
597
-			/** @var ICacheEntry[] $childFolders */
598
-			$childFolders = [];
599
-			foreach ($children as $child) {
600
-				if ($child->getMimeType() == FileInfo::MIMETYPE_FOLDER) {
601
-					$childFolders[] = $child;
602
-				}
603
-			}
604
-			foreach ($childFolders as $folder) {
605
-				$parentIds[] = $folder->getId();
606
-				$queue[] = $folder->getId();
607
-			}
608
-		}
609
-
610
-		$query = $this->getQueryBuilder();
611
-		$query->delete('filecache')
612
-			->whereStorageId($this->getNumericStorageId())
613
-			->whereParentInParameter('parentIds');
614
-
615
-		// Sorting before chunking allows the db to find the entries close to each
616
-		// other in the index
617
-		sort($parentIds, SORT_NUMERIC);
618
-		foreach (array_chunk($parentIds, 1000) as $parentIdChunk) {
619
-			$query->setParameter('parentIds', $parentIdChunk, IQueryBuilder::PARAM_INT_ARRAY);
620
-			$query->executeStatement();
621
-		}
622
-
623
-		foreach (array_combine($deletedIds, $deletedPaths) as $fileId => $filePath) {
624
-			$cacheEntryRemovedEvent = new CacheEntryRemovedEvent(
625
-				$this->storage,
626
-				$filePath,
627
-				$fileId,
628
-				$this->getNumericStorageId()
629
-			);
630
-			$this->eventDispatcher->dispatchTyped($cacheEntryRemovedEvent);
631
-		}
632
-	}
633
-
634
-	/**
635
-	 * Move a file or folder in the cache
636
-	 *
637
-	 * @param string $source
638
-	 * @param string $target
639
-	 */
640
-	public function move($source, $target) {
641
-		$this->moveFromCache($this, $source, $target);
642
-	}
643
-
644
-	/**
645
-	 * Get the storage id and path needed for a move
646
-	 *
647
-	 * @param string $path
648
-	 * @return array [$storageId, $internalPath]
649
-	 */
650
-	protected function getMoveInfo($path) {
651
-		return [$this->getNumericStorageId(), $path];
652
-	}
653
-
654
-	protected function hasEncryptionWrapper(): bool {
655
-		return $this->storage->instanceOfStorage(Encryption::class);
656
-	}
657
-
658
-	protected function shouldEncrypt(string $targetPath): bool {
659
-		if (!$this->storage->instanceOfStorage(Encryption::class)) {
660
-			return false;
661
-		}
662
-		return $this->storage->shouldEncrypt($targetPath);
663
-	}
664
-
665
-	/**
666
-	 * Move a file or folder in the cache
667
-	 *
668
-	 * @param ICache $sourceCache
669
-	 * @param string $sourcePath
670
-	 * @param string $targetPath
671
-	 * @throws \OC\DatabaseException
672
-	 * @throws \Exception if the given storages have an invalid id
673
-	 */
674
-	public function moveFromCache(ICache $sourceCache, $sourcePath, $targetPath) {
675
-		if ($sourceCache instanceof Cache) {
676
-			// normalize source and target
677
-			$sourcePath = $this->normalize($sourcePath);
678
-			$targetPath = $this->normalize($targetPath);
679
-
680
-			$sourceData = $sourceCache->get($sourcePath);
681
-			if (!$sourceData) {
682
-				throw new \Exception('Source path not found in cache: ' . $sourcePath);
683
-			}
684
-
685
-			$shardDefinition = $this->connection->getShardDefinition('filecache');
686
-			if (
687
-				$shardDefinition
688
-				&& $shardDefinition->getShardForKey($sourceCache->getNumericStorageId()) !== $shardDefinition->getShardForKey($this->getNumericStorageId())
689
-			) {
690
-				$this->moveFromStorageSharded($shardDefinition, $sourceCache, $sourceData, $targetPath);
691
-				return;
692
-			}
693
-
694
-			$sourceId = $sourceData['fileid'];
695
-			$newParentId = $this->getParentId($targetPath);
696
-
697
-			[$sourceStorageId, $sourcePath] = $sourceCache->getMoveInfo($sourcePath);
698
-			[$targetStorageId, $targetPath] = $this->getMoveInfo($targetPath);
699
-
700
-			if (is_null($sourceStorageId) || $sourceStorageId === false) {
701
-				throw new \Exception('Invalid source storage id: ' . $sourceStorageId);
702
-			}
703
-			if (is_null($targetStorageId) || $targetStorageId === false) {
704
-				throw new \Exception('Invalid target storage id: ' . $targetStorageId);
705
-			}
706
-
707
-			if ($sourceData['mimetype'] === 'httpd/unix-directory') {
708
-				//update all child entries
709
-				$sourceLength = mb_strlen($sourcePath);
710
-
711
-				$childIds = $this->getChildIds($sourceStorageId, $sourcePath);
712
-
713
-				$childChunks = array_chunk($childIds, 1000);
714
-
715
-				$query = $this->getQueryBuilder();
716
-
717
-				$fun = $query->func();
718
-				$newPathFunction = $fun->concat(
719
-					$query->createNamedParameter($targetPath),
720
-					$fun->substring('path', $query->createNamedParameter($sourceLength + 1, IQueryBuilder::PARAM_INT))// +1 for the leading slash
721
-				);
722
-				$query->update('filecache')
723
-					->set('path_hash', $fun->md5($newPathFunction))
724
-					->set('path', $newPathFunction)
725
-					->whereStorageId($sourceStorageId)
726
-					->andWhere($query->expr()->in('fileid', $query->createParameter('files')));
727
-
728
-				if ($sourceStorageId !== $targetStorageId) {
729
-					$query->set('storage', $query->createNamedParameter($targetStorageId), IQueryBuilder::PARAM_INT);
730
-				}
731
-
732
-				// when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
733
-				if ($sourceCache->hasEncryptionWrapper() && !$this->hasEncryptionWrapper()) {
734
-					$query->set('encrypted', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT));
735
-				}
736
-
737
-				// Retry transaction in case of RetryableException like deadlocks.
738
-				// Retry up to 4 times because we should receive up to 4 concurrent requests from the frontend
739
-				$retryLimit = 4;
740
-				for ($i = 1; $i <= $retryLimit; $i++) {
741
-					try {
742
-						$this->connection->beginTransaction();
743
-						foreach ($childChunks as $chunk) {
744
-							$query->setParameter('files', $chunk, IQueryBuilder::PARAM_INT_ARRAY);
745
-							$query->executeStatement();
746
-						}
747
-						break;
748
-					} catch (\OC\DatabaseException $e) {
749
-						$this->connection->rollBack();
750
-						throw $e;
751
-					} catch (DbalException $e) {
752
-						$this->connection->rollBack();
753
-
754
-						if (!$e->isRetryable()) {
755
-							throw $e;
756
-						}
757
-
758
-						// Simply throw if we already retried 4 times.
759
-						if ($i === $retryLimit) {
760
-							throw $e;
761
-						}
762
-
763
-						// Sleep a bit to give some time to the other transaction to finish.
764
-						usleep(100 * 1000 * $i);
765
-					}
766
-				}
767
-			} else {
768
-				$this->connection->beginTransaction();
769
-			}
770
-
771
-			$query = $this->getQueryBuilder();
772
-			$query->update('filecache')
773
-				->set('path', $query->createNamedParameter($targetPath))
774
-				->set('path_hash', $query->createNamedParameter(md5($targetPath)))
775
-				->set('name', $query->createNamedParameter(basename($targetPath)))
776
-				->set('parent', $query->createNamedParameter($newParentId, IQueryBuilder::PARAM_INT))
777
-				->whereStorageId($sourceStorageId)
778
-				->whereFileId($sourceId);
779
-
780
-			if ($sourceStorageId !== $targetStorageId) {
781
-				$query->set('storage', $query->createNamedParameter($targetStorageId), IQueryBuilder::PARAM_INT);
782
-			}
783
-
784
-			// when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
785
-			if ($sourceCache->hasEncryptionWrapper() && !$this->hasEncryptionWrapper()) {
786
-				$query->set('encrypted', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT));
787
-			}
788
-
789
-			$query->executeStatement();
790
-
791
-			$this->connection->commit();
792
-
793
-			if ($sourceCache->getNumericStorageId() !== $this->getNumericStorageId()) {
794
-				$this->eventDispatcher->dispatchTyped(new CacheEntryRemovedEvent($this->storage, $sourcePath, $sourceId, $sourceCache->getNumericStorageId()));
795
-				$event = new CacheEntryInsertedEvent($this->storage, $targetPath, $sourceId, $this->getNumericStorageId());
796
-				$this->eventDispatcher->dispatch(CacheInsertEvent::class, $event);
797
-				$this->eventDispatcher->dispatchTyped($event);
798
-			} else {
799
-				$event = new CacheEntryUpdatedEvent($this->storage, $targetPath, $sourceId, $this->getNumericStorageId());
800
-				$this->eventDispatcher->dispatch(CacheUpdateEvent::class, $event);
801
-				$this->eventDispatcher->dispatchTyped($event);
802
-			}
803
-		} else {
804
-			$this->moveFromCacheFallback($sourceCache, $sourcePath, $targetPath);
805
-		}
806
-	}
807
-
808
-	private function getChildIds(int $storageId, string $path): array {
809
-		$query = $this->connection->getQueryBuilder();
810
-		$query->select('fileid')
811
-			->from('filecache')
812
-			->where($query->expr()->eq('storage', $query->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
813
-			->andWhere($query->expr()->like('path', $query->createNamedParameter($this->connection->escapeLikeParameter($path) . '/%')));
814
-		return $query->executeQuery()->fetchAll(\PDO::FETCH_COLUMN);
815
-	}
816
-
817
-	/**
818
-	 * remove all entries for files that are stored on the storage from the cache
819
-	 */
820
-	public function clear() {
821
-		$query = $this->getQueryBuilder();
822
-		$query->delete('filecache')
823
-			->whereStorageId($this->getNumericStorageId());
824
-		$query->executeStatement();
825
-
826
-		$query = $this->connection->getQueryBuilder();
827
-		$query->delete('storages')
828
-			->where($query->expr()->eq('id', $query->createNamedParameter($this->storageId)));
829
-		$query->executeStatement();
830
-	}
831
-
832
-	/**
833
-	 * Get the scan status of a file
834
-	 *
835
-	 * - Cache::NOT_FOUND: File is not in the cache
836
-	 * - Cache::PARTIAL: File is not stored in the cache but some incomplete data is known
837
-	 * - Cache::SHALLOW: The folder and it's direct children are in the cache but not all sub folders are fully scanned
838
-	 * - Cache::COMPLETE: The file or folder, with all it's children) are fully scanned
839
-	 *
840
-	 * @param string $file
841
-	 *
842
-	 * @return int Cache::NOT_FOUND, Cache::PARTIAL, Cache::SHALLOW or Cache::COMPLETE
843
-	 */
844
-	public function getStatus($file) {
845
-		// normalize file
846
-		$file = $this->normalize($file);
847
-
848
-		$query = $this->getQueryBuilder();
849
-		$query->select('size')
850
-			->from('filecache')
851
-			->whereStorageId($this->getNumericStorageId())
852
-			->wherePath($file);
853
-
854
-		$result = $query->executeQuery();
855
-		$size = $result->fetchOne();
856
-		$result->closeCursor();
857
-
858
-		if ($size !== false) {
859
-			if ((int)$size === -1) {
860
-				return self::SHALLOW;
861
-			} else {
862
-				return self::COMPLETE;
863
-			}
864
-		} else {
865
-			if (isset($this->partial[$file])) {
866
-				return self::PARTIAL;
867
-			} else {
868
-				return self::NOT_FOUND;
869
-			}
870
-		}
871
-	}
872
-
873
-	/**
874
-	 * search for files matching $pattern
875
-	 *
876
-	 * @param string $pattern the search pattern using SQL search syntax (e.g. '%searchstring%')
877
-	 * @return ICacheEntry[] an array of cache entries where the name matches the search pattern
878
-	 */
879
-	public function search($pattern) {
880
-		$operator = new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', $pattern);
881
-		return $this->searchQuery(new SearchQuery($operator, 0, 0, [], null));
882
-	}
883
-
884
-	/**
885
-	 * search for files by mimetype
886
-	 *
887
-	 * @param string $mimetype either a full mimetype to search ('text/plain') or only the first part of a mimetype ('image')
888
-	 *                         where it will search for all mimetypes in the group ('image/*')
889
-	 * @return ICacheEntry[] an array of cache entries where the mimetype matches the search
890
-	 */
891
-	public function searchByMime($mimetype) {
892
-		if (!str_contains($mimetype, '/')) {
893
-			$operator = new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', $mimetype . '/%');
894
-		} else {
895
-			$operator = new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', $mimetype);
896
-		}
897
-		return $this->searchQuery(new SearchQuery($operator, 0, 0, [], null));
898
-	}
899
-
900
-	public function searchQuery(ISearchQuery $query) {
901
-		return current($this->querySearchHelper->searchInCaches($query, [$this]));
902
-	}
903
-
904
-	/**
905
-	 * Re-calculate the folder size and the size of all parent folders
906
-	 *
907
-	 * @param array|ICacheEntry|null $data (optional) meta data of the folder
908
-	 */
909
-	public function correctFolderSize(string $path, $data = null, bool $isBackgroundScan = false): void {
910
-		$this->calculateFolderSize($path, $data);
911
-
912
-		if ($path !== '') {
913
-			$parent = dirname($path);
914
-			if ($parent === '.' || $parent === '/') {
915
-				$parent = '';
916
-			}
917
-
918
-			if ($isBackgroundScan) {
919
-				$parentData = $this->get($parent);
920
-				if ($parentData !== false
921
-					&& $parentData['size'] !== -1
922
-					&& $this->getIncompleteChildrenCount($parentData['fileid']) === 0
923
-				) {
924
-					$this->correctFolderSize($parent, $parentData, $isBackgroundScan);
925
-				}
926
-			} else {
927
-				$this->correctFolderSize($parent);
928
-			}
929
-		}
930
-	}
931
-
932
-	/**
933
-	 * get the incomplete count that shares parent $folder
934
-	 *
935
-	 * @param int $fileId the file id of the folder
936
-	 * @return int
937
-	 */
938
-	public function getIncompleteChildrenCount($fileId) {
939
-		if ($fileId > -1) {
940
-			$query = $this->getQueryBuilder();
941
-			$query->select($query->func()->count())
942
-				->from('filecache')
943
-				->whereParent($fileId)
944
-				->whereStorageId($this->getNumericStorageId())
945
-				->andWhere($query->expr()->eq('size', $query->createNamedParameter(-1, IQueryBuilder::PARAM_INT)));
946
-
947
-			$result = $query->executeQuery();
948
-			$size = (int)$result->fetchOne();
949
-			$result->closeCursor();
950
-
951
-			return $size;
952
-		}
953
-		return -1;
954
-	}
955
-
956
-	/**
957
-	 * calculate the size of a folder and set it in the cache
958
-	 *
959
-	 * @param string $path
960
-	 * @param array|null|ICacheEntry $entry (optional) meta data of the folder
961
-	 * @return int|float
962
-	 */
963
-	public function calculateFolderSize($path, $entry = null) {
964
-		return $this->calculateFolderSizeInner($path, $entry);
965
-	}
966
-
967
-
968
-	/**
969
-	 * inner function because we can't add new params to the public function without breaking any child classes
970
-	 *
971
-	 * @param string $path
972
-	 * @param array|null|ICacheEntry $entry (optional) meta data of the folder
973
-	 * @param bool $ignoreUnknown don't mark the folder size as unknown if any of it's children are unknown
974
-	 * @return int|float
975
-	 */
976
-	protected function calculateFolderSizeInner(string $path, $entry = null, bool $ignoreUnknown = false) {
977
-		$totalSize = 0;
978
-		if (is_null($entry) || !isset($entry['fileid'])) {
979
-			$entry = $this->get($path);
980
-		}
981
-		if (isset($entry['mimetype']) && $entry['mimetype'] === FileInfo::MIMETYPE_FOLDER) {
982
-			$id = $entry['fileid'];
983
-
984
-			$query = $this->getQueryBuilder();
985
-			$query->select('size', 'unencrypted_size')
986
-				->from('filecache')
987
-				->whereStorageId($this->getNumericStorageId())
988
-				->whereParent($id);
989
-			if ($ignoreUnknown) {
990
-				$query->andWhere($query->expr()->gte('size', $query->createNamedParameter(0)));
991
-			}
992
-
993
-			$result = $query->executeQuery();
994
-			$rows = $result->fetchAll();
995
-			$result->closeCursor();
996
-
997
-			if ($rows) {
998
-				$sizes = array_map(function (array $row) {
999
-					return Util::numericToNumber($row['size']);
1000
-				}, $rows);
1001
-				$unencryptedOnlySizes = array_map(function (array $row) {
1002
-					return Util::numericToNumber($row['unencrypted_size']);
1003
-				}, $rows);
1004
-				$unencryptedSizes = array_map(function (array $row) {
1005
-					return Util::numericToNumber(($row['unencrypted_size'] > 0) ? $row['unencrypted_size'] : $row['size']);
1006
-				}, $rows);
1007
-
1008
-				$sum = array_sum($sizes);
1009
-				$min = min($sizes);
1010
-
1011
-				$unencryptedSum = array_sum($unencryptedSizes);
1012
-				$unencryptedMin = min($unencryptedSizes);
1013
-				$unencryptedMax = max($unencryptedOnlySizes);
1014
-
1015
-				$sum = 0 + $sum;
1016
-				$min = 0 + $min;
1017
-				if ($min === -1) {
1018
-					$totalSize = $min;
1019
-				} else {
1020
-					$totalSize = $sum;
1021
-				}
1022
-				if ($unencryptedMin === -1 || $min === -1) {
1023
-					$unencryptedTotal = $unencryptedMin;
1024
-				} else {
1025
-					$unencryptedTotal = $unencryptedSum;
1026
-				}
1027
-			} else {
1028
-				$totalSize = 0;
1029
-				$unencryptedTotal = 0;
1030
-				$unencryptedMax = 0;
1031
-			}
1032
-
1033
-			// only set unencrypted size for a folder if any child entries have it set, or the folder is empty
1034
-			$shouldWriteUnEncryptedSize = $unencryptedMax > 0 || $totalSize === 0 || ($entry['unencrypted_size'] ?? 0) > 0;
1035
-			if ($entry['size'] !== $totalSize || (($entry['unencrypted_size'] ?? 0) !== $unencryptedTotal && $shouldWriteUnEncryptedSize)) {
1036
-				if ($shouldWriteUnEncryptedSize) {
1037
-					// if all children have an unencrypted size of 0, just set the folder unencrypted size to 0 instead of summing the sizes
1038
-					if ($unencryptedMax === 0) {
1039
-						$unencryptedTotal = 0;
1040
-					}
1041
-
1042
-					$this->update($id, [
1043
-						'size' => $totalSize,
1044
-						'unencrypted_size' => $unencryptedTotal,
1045
-					]);
1046
-				} else {
1047
-					$this->update($id, [
1048
-						'size' => $totalSize,
1049
-					]);
1050
-				}
1051
-			}
1052
-		}
1053
-		return $totalSize;
1054
-	}
1055
-
1056
-	/**
1057
-	 * get all file ids on the files on the storage
1058
-	 *
1059
-	 * @return int[]
1060
-	 */
1061
-	public function getAll() {
1062
-		$query = $this->getQueryBuilder();
1063
-		$query->select('fileid')
1064
-			->from('filecache')
1065
-			->whereStorageId($this->getNumericStorageId());
1066
-
1067
-		$result = $query->executeQuery();
1068
-		$files = $result->fetchAll(\PDO::FETCH_COLUMN);
1069
-		$result->closeCursor();
1070
-
1071
-		return array_map(function ($id) {
1072
-			return (int)$id;
1073
-		}, $files);
1074
-	}
1075
-
1076
-	/**
1077
-	 * find a folder in the cache which has not been fully scanned
1078
-	 *
1079
-	 * If multiple incomplete folders are in the cache, the one with the highest id will be returned,
1080
-	 * use the one with the highest id gives the best result with the background scanner, since that is most
1081
-	 * likely the folder where we stopped scanning previously
1082
-	 *
1083
-	 * @return string|false the path of the folder or false when no folder matched
1084
-	 */
1085
-	public function getIncomplete() {
1086
-		$query = $this->getQueryBuilder();
1087
-		$query->select('path')
1088
-			->from('filecache')
1089
-			->whereStorageId($this->getNumericStorageId())
1090
-			->andWhere($query->expr()->eq('size', $query->createNamedParameter(-1, IQueryBuilder::PARAM_INT)))
1091
-			->orderBy('fileid', 'DESC')
1092
-			->setMaxResults(1);
1093
-
1094
-		$result = $query->executeQuery();
1095
-		$path = $result->fetchOne();
1096
-		$result->closeCursor();
1097
-
1098
-		return $path === false ? false : (string)$path;
1099
-	}
1100
-
1101
-	/**
1102
-	 * get the path of a file on this storage by it's file id
1103
-	 *
1104
-	 * @param int $id the file id of the file or folder to search
1105
-	 * @return string|null the path of the file (relative to the storage) or null if a file with the given id does not exists within this cache
1106
-	 */
1107
-	public function getPathById($id) {
1108
-		$query = $this->getQueryBuilder();
1109
-		$query->select('path')
1110
-			->from('filecache')
1111
-			->whereStorageId($this->getNumericStorageId())
1112
-			->whereFileId($id);
1113
-
1114
-		$result = $query->executeQuery();
1115
-		$path = $result->fetchOne();
1116
-		$result->closeCursor();
1117
-
1118
-		if ($path === false) {
1119
-			return null;
1120
-		}
1121
-
1122
-		return (string)$path;
1123
-	}
1124
-
1125
-	/**
1126
-	 * get the storage id of the storage for a file and the internal path of the file
1127
-	 * unlike getPathById this does not limit the search to files on this storage and
1128
-	 * instead does a global search in the cache table
1129
-	 *
1130
-	 * @param int $id
1131
-	 * @return array first element holding the storage id, second the path
1132
-	 * @deprecated 17.0.0 use getPathById() instead
1133
-	 */
1134
-	public static function getById($id) {
1135
-		$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
1136
-		$query->select('path', 'storage')
1137
-			->from('filecache')
1138
-			->where($query->expr()->eq('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
1139
-
1140
-		$result = $query->executeQuery();
1141
-		$row = $result->fetch();
1142
-		$result->closeCursor();
1143
-
1144
-		if ($row) {
1145
-			$numericId = $row['storage'];
1146
-			$path = $row['path'];
1147
-		} else {
1148
-			return null;
1149
-		}
1150
-
1151
-		if ($id = Storage::getStorageId($numericId)) {
1152
-			return [$id, $path];
1153
-		} else {
1154
-			return null;
1155
-		}
1156
-	}
1157
-
1158
-	/**
1159
-	 * normalize the given path
1160
-	 *
1161
-	 * @param string $path
1162
-	 * @return string
1163
-	 */
1164
-	public function normalize($path) {
1165
-		return trim(\OC_Util::normalizeUnicode($path), '/');
1166
-	}
1167
-
1168
-	/**
1169
-	 * Copy a file or folder in the cache
1170
-	 *
1171
-	 * @param ICache $sourceCache
1172
-	 * @param ICacheEntry $sourceEntry
1173
-	 * @param string $targetPath
1174
-	 * @return int fileId of copied entry
1175
-	 */
1176
-	public function copyFromCache(ICache $sourceCache, ICacheEntry $sourceEntry, string $targetPath): int {
1177
-		if ($sourceEntry->getId() < 0) {
1178
-			throw new \RuntimeException('Invalid source cache entry on copyFromCache');
1179
-		}
1180
-		$data = $this->cacheEntryToArray($sourceEntry);
1181
-
1182
-		// when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
1183
-		if ($sourceCache instanceof Cache
1184
-			&& $sourceCache->hasEncryptionWrapper()
1185
-			&& !$this->shouldEncrypt($targetPath)) {
1186
-			$data['encrypted'] = 0;
1187
-		}
1188
-
1189
-		$fileId = $this->put($targetPath, $data);
1190
-		if ($fileId <= 0) {
1191
-			throw new \RuntimeException('Failed to copy to ' . $targetPath . ' from cache with source data ' . json_encode($data) . ' ');
1192
-		}
1193
-		if ($sourceEntry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
1194
-			$folderContent = $sourceCache->getFolderContentsById($sourceEntry->getId());
1195
-			foreach ($folderContent as $subEntry) {
1196
-				$subTargetPath = $targetPath . '/' . $subEntry->getName();
1197
-				$this->copyFromCache($sourceCache, $subEntry, $subTargetPath);
1198
-			}
1199
-		}
1200
-		return $fileId;
1201
-	}
1202
-
1203
-	private function cacheEntryToArray(ICacheEntry $entry): array {
1204
-		$data = [
1205
-			'size' => $entry->getSize(),
1206
-			'mtime' => $entry->getMTime(),
1207
-			'storage_mtime' => $entry->getStorageMTime(),
1208
-			'mimetype' => $entry->getMimeType(),
1209
-			'mimepart' => $entry->getMimePart(),
1210
-			'etag' => $entry->getEtag(),
1211
-			'permissions' => $entry->getPermissions(),
1212
-			'encrypted' => $entry->isEncrypted(),
1213
-			'creation_time' => $entry->getCreationTime(),
1214
-			'upload_time' => $entry->getUploadTime(),
1215
-			'metadata_etag' => $entry->getMetadataEtag(),
1216
-		];
1217
-		if ($entry instanceof CacheEntry && isset($entry['scan_permissions'])) {
1218
-			$data['permissions'] = $entry['scan_permissions'];
1219
-		}
1220
-		return $data;
1221
-	}
1222
-
1223
-	public function getQueryFilterForStorage(): ISearchOperator {
1224
-		return new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', $this->getNumericStorageId());
1225
-	}
1226
-
1227
-	public function getCacheEntryFromSearchResult(ICacheEntry $rawEntry): ?ICacheEntry {
1228
-		if ($rawEntry->getStorageId() === $this->getNumericStorageId()) {
1229
-			return $rawEntry;
1230
-		} else {
1231
-			return null;
1232
-		}
1233
-	}
1234
-
1235
-	private function moveFromStorageSharded(ShardDefinition $shardDefinition, ICache $sourceCache, ICacheEntry $sourceEntry, $targetPath): void {
1236
-		$sourcePath = $sourceEntry->getPath();
1237
-		while ($sourceCache instanceof CacheWrapper) {
1238
-			if ($sourceCache instanceof CacheJail) {
1239
-				$sourcePath = $sourceCache->getSourcePath($sourcePath);
1240
-			}
1241
-			$sourceCache = $sourceCache->getCache();
1242
-		}
1243
-
1244
-		if ($sourceEntry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
1245
-			$fileIds = $this->getChildIds($sourceCache->getNumericStorageId(), $sourcePath);
1246
-		} else {
1247
-			$fileIds = [];
1248
-		}
1249
-		$fileIds[] = $sourceEntry->getId();
1250
-
1251
-		$helper = $this->connection->getCrossShardMoveHelper();
1252
-
1253
-		$sourceConnection = $helper->getConnection($shardDefinition, $sourceCache->getNumericStorageId());
1254
-		$targetConnection = $helper->getConnection($shardDefinition, $this->getNumericStorageId());
1255
-
1256
-		$cacheItems = $helper->loadItems($sourceConnection, 'filecache', 'fileid', $fileIds);
1257
-		$extendedItems = $helper->loadItems($sourceConnection, 'filecache_extended', 'fileid', $fileIds);
1258
-		$metadataItems = $helper->loadItems($sourceConnection, 'files_metadata', 'file_id', $fileIds);
1259
-
1260
-		// when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
1261
-		$removeEncryptedFlag = ($sourceCache instanceof Cache && $sourceCache->hasEncryptionWrapper()) && !$this->hasEncryptionWrapper();
1262
-
1263
-		$sourcePathLength = strlen($sourcePath);
1264
-		foreach ($cacheItems as &$cacheItem) {
1265
-			if ($cacheItem['path'] === $sourcePath) {
1266
-				$cacheItem['path'] = $targetPath;
1267
-				$cacheItem['parent'] = $this->getParentId($targetPath);
1268
-				$cacheItem['name'] = basename($cacheItem['path']);
1269
-			} else {
1270
-				$cacheItem['path'] = $targetPath . '/' . substr($cacheItem['path'], $sourcePathLength + 1); // +1 for the leading slash
1271
-			}
1272
-			$cacheItem['path_hash'] = md5($cacheItem['path']);
1273
-			$cacheItem['storage'] = $this->getNumericStorageId();
1274
-			if ($removeEncryptedFlag) {
1275
-				$cacheItem['encrypted'] = 0;
1276
-			}
1277
-		}
1278
-
1279
-		$targetConnection->beginTransaction();
1280
-
1281
-		try {
1282
-			$helper->saveItems($targetConnection, 'filecache', $cacheItems);
1283
-			$helper->saveItems($targetConnection, 'filecache_extended', $extendedItems);
1284
-			$helper->saveItems($targetConnection, 'files_metadata', $metadataItems);
1285
-		} catch (\Exception $e) {
1286
-			$targetConnection->rollback();
1287
-			throw $e;
1288
-		}
1289
-
1290
-		$sourceConnection->beginTransaction();
1291
-
1292
-		try {
1293
-			$helper->deleteItems($sourceConnection, 'filecache', 'fileid', $fileIds);
1294
-			$helper->deleteItems($sourceConnection, 'filecache_extended', 'fileid', $fileIds);
1295
-			$helper->deleteItems($sourceConnection, 'files_metadata', 'file_id', $fileIds);
1296
-		} catch (\Exception $e) {
1297
-			$targetConnection->rollback();
1298
-			$sourceConnection->rollBack();
1299
-			throw $e;
1300
-		}
1301
-
1302
-		try {
1303
-			$sourceConnection->commit();
1304
-		} catch (\Exception $e) {
1305
-			$targetConnection->rollback();
1306
-			throw $e;
1307
-		}
1308
-		$targetConnection->commit();
1309
-	}
50
+    use MoveFromCacheTrait {
51
+        MoveFromCacheTrait::moveFromCache as moveFromCacheFallback;
52
+    }
53
+
54
+    /**
55
+     * @var array partial data for the cache
56
+     */
57
+    protected array $partial = [];
58
+    protected string $storageId;
59
+    protected Storage $storageCache;
60
+    protected IMimeTypeLoader $mimetypeLoader;
61
+    protected IDBConnection $connection;
62
+    protected SystemConfig $systemConfig;
63
+    protected LoggerInterface $logger;
64
+    protected QuerySearchHelper $querySearchHelper;
65
+    protected IEventDispatcher $eventDispatcher;
66
+    protected IFilesMetadataManager $metadataManager;
67
+
68
+    public function __construct(
69
+        private IStorage $storage,
70
+        // this constructor is used in to many pleases to easily do proper di
71
+        // so instead we group it all together
72
+        ?CacheDependencies $dependencies = null,
73
+    ) {
74
+        $this->storageId = $storage->getId();
75
+        if (strlen($this->storageId) > 64) {
76
+            $this->storageId = md5($this->storageId);
77
+        }
78
+        if (!$dependencies) {
79
+            $dependencies = \OCP\Server::get(CacheDependencies::class);
80
+        }
81
+        $this->storageCache = new Storage($this->storage, true, $dependencies->getConnection());
82
+        $this->mimetypeLoader = $dependencies->getMimeTypeLoader();
83
+        $this->connection = $dependencies->getConnection();
84
+        $this->systemConfig = $dependencies->getSystemConfig();
85
+        $this->logger = $dependencies->getLogger();
86
+        $this->querySearchHelper = $dependencies->getQuerySearchHelper();
87
+        $this->eventDispatcher = $dependencies->getEventDispatcher();
88
+        $this->metadataManager = $dependencies->getMetadataManager();
89
+    }
90
+
91
+    protected function getQueryBuilder() {
92
+        return new CacheQueryBuilder(
93
+            $this->connection->getQueryBuilder(),
94
+            $this->metadataManager,
95
+        );
96
+    }
97
+
98
+    public function getStorageCache(): Storage {
99
+        return $this->storageCache;
100
+    }
101
+
102
+    /**
103
+     * Get the numeric storage id for this cache's storage
104
+     *
105
+     * @return int
106
+     */
107
+    public function getNumericStorageId() {
108
+        return $this->storageCache->getNumericId();
109
+    }
110
+
111
+    /**
112
+     * get the stored metadata of a file or folder
113
+     *
114
+     * @param string|int $file either the path of a file or folder or the file id for a file or folder
115
+     * @return ICacheEntry|false the cache entry as array or false if the file is not found in the cache
116
+     */
117
+    public function get($file) {
118
+        $query = $this->getQueryBuilder();
119
+        $query->selectFileCache();
120
+        $metadataQuery = $query->selectMetadata();
121
+
122
+        if (is_string($file) || $file == '') {
123
+            // normalize file
124
+            $file = $this->normalize($file);
125
+
126
+            $query->wherePath($file);
127
+        } else { //file id
128
+            $query->whereFileId($file);
129
+        }
130
+        $query->whereStorageId($this->getNumericStorageId());
131
+
132
+        $result = $query->executeQuery();
133
+        $data = $result->fetch();
134
+        $result->closeCursor();
135
+
136
+        if ($data !== false) {
137
+            $data['metadata'] = $metadataQuery->extractMetadata($data)->asArray();
138
+            return self::cacheEntryFromData($data, $this->mimetypeLoader);
139
+        } else {
140
+            //merge partial data
141
+            if (is_string($file) && isset($this->partial[$file])) {
142
+                return $this->partial[$file];
143
+            }
144
+        }
145
+
146
+        return false;
147
+    }
148
+
149
+    /**
150
+     * Create a CacheEntry from database row
151
+     *
152
+     * @param array $data
153
+     * @param IMimeTypeLoader $mimetypeLoader
154
+     * @return CacheEntry
155
+     */
156
+    public static function cacheEntryFromData($data, IMimeTypeLoader $mimetypeLoader) {
157
+        //fix types
158
+        $data['name'] = (string)$data['name'];
159
+        $data['path'] = (string)$data['path'];
160
+        $data['fileid'] = (int)$data['fileid'];
161
+        $data['parent'] = (int)$data['parent'];
162
+        $data['size'] = Util::numericToNumber($data['size']);
163
+        $data['unencrypted_size'] = Util::numericToNumber($data['unencrypted_size'] ?? 0);
164
+        $data['mtime'] = (int)$data['mtime'];
165
+        $data['storage_mtime'] = (int)$data['storage_mtime'];
166
+        $data['encryptedVersion'] = (int)$data['encrypted'];
167
+        $data['encrypted'] = (bool)$data['encrypted'];
168
+        $data['storage_id'] = $data['storage'];
169
+        $data['storage'] = (int)$data['storage'];
170
+        $data['mimetype'] = $mimetypeLoader->getMimetypeById($data['mimetype']);
171
+        $data['mimepart'] = $mimetypeLoader->getMimetypeById($data['mimepart']);
172
+        if ($data['storage_mtime'] == 0) {
173
+            $data['storage_mtime'] = $data['mtime'];
174
+        }
175
+        if (isset($data['f_permissions'])) {
176
+            $data['scan_permissions'] = $data['f_permissions'];
177
+        }
178
+        $data['permissions'] = (int)$data['permissions'];
179
+        if (isset($data['creation_time'])) {
180
+            $data['creation_time'] = (int)$data['creation_time'];
181
+        }
182
+        if (isset($data['upload_time'])) {
183
+            $data['upload_time'] = (int)$data['upload_time'];
184
+        }
185
+        return new CacheEntry($data);
186
+    }
187
+
188
+    /**
189
+     * get the metadata of all files stored in $folder
190
+     *
191
+     * @param string $folder
192
+     * @return ICacheEntry[]
193
+     */
194
+    public function getFolderContents($folder) {
195
+        $fileId = $this->getId($folder);
196
+        return $this->getFolderContentsById($fileId);
197
+    }
198
+
199
+    /**
200
+     * get the metadata of all files stored in $folder
201
+     *
202
+     * @param int $fileId the file id of the folder
203
+     * @return ICacheEntry[]
204
+     */
205
+    public function getFolderContentsById($fileId) {
206
+        if ($fileId > -1) {
207
+            $query = $this->getQueryBuilder();
208
+            $query->selectFileCache()
209
+                ->whereParent($fileId)
210
+                ->whereStorageId($this->getNumericStorageId())
211
+                ->orderBy('name', 'ASC');
212
+
213
+            $metadataQuery = $query->selectMetadata();
214
+
215
+            $result = $query->executeQuery();
216
+            $files = $result->fetchAll();
217
+            $result->closeCursor();
218
+
219
+            return array_map(function (array $data) use ($metadataQuery) {
220
+                $data['metadata'] = $metadataQuery->extractMetadata($data)->asArray();
221
+                return self::cacheEntryFromData($data, $this->mimetypeLoader);
222
+            }, $files);
223
+        }
224
+        return [];
225
+    }
226
+
227
+    /**
228
+     * insert or update meta data for a file or folder
229
+     *
230
+     * @param string $file
231
+     * @param array $data
232
+     *
233
+     * @return int file id
234
+     * @throws \RuntimeException
235
+     */
236
+    public function put($file, array $data) {
237
+        if (($id = $this->getId($file)) > -1) {
238
+            $this->update($id, $data);
239
+            return $id;
240
+        } else {
241
+            return $this->insert($file, $data);
242
+        }
243
+    }
244
+
245
+    /**
246
+     * insert meta data for a new file or folder
247
+     *
248
+     * @param string $file
249
+     * @param array $data
250
+     *
251
+     * @return int file id
252
+     * @throws \RuntimeException|Exception
253
+     */
254
+    public function insert($file, array $data) {
255
+        // normalize file
256
+        $file = $this->normalize($file);
257
+
258
+        if (isset($this->partial[$file])) { //add any saved partial data
259
+            $data = array_merge($this->partial[$file]->getData(), $data);
260
+            unset($this->partial[$file]);
261
+        }
262
+
263
+        $requiredFields = ['size', 'mtime', 'mimetype'];
264
+        foreach ($requiredFields as $field) {
265
+            if (!isset($data[$field])) { //data not complete save as partial and return
266
+                $this->partial[$file] = new CacheEntry($data);
267
+                return -1;
268
+            }
269
+        }
270
+
271
+        $data['path'] = $file;
272
+        if (!isset($data['parent'])) {
273
+            $data['parent'] = $this->getParentId($file);
274
+        }
275
+        if ($data['parent'] === -1 && $file !== '') {
276
+            throw new \Exception('Parent folder not in filecache for ' . $file);
277
+        }
278
+        $data['name'] = basename($file);
279
+
280
+        [$values, $extensionValues] = $this->normalizeData($data);
281
+        $storageId = $this->getNumericStorageId();
282
+        $values['storage'] = $storageId;
283
+
284
+        try {
285
+            $builder = $this->connection->getQueryBuilder();
286
+            $builder->insert('filecache');
287
+
288
+            foreach ($values as $column => $value) {
289
+                $builder->setValue($column, $builder->createNamedParameter($value));
290
+            }
291
+
292
+            if ($builder->executeStatement()) {
293
+                $fileId = $builder->getLastInsertId();
294
+
295
+                if (count($extensionValues)) {
296
+                    $query = $this->getQueryBuilder();
297
+                    $query->insert('filecache_extended');
298
+                    $query->hintShardKey('storage', $storageId);
299
+
300
+                    $query->setValue('fileid', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT));
301
+                    foreach ($extensionValues as $column => $value) {
302
+                        $query->setValue($column, $query->createNamedParameter($value));
303
+                    }
304
+                    $query->executeStatement();
305
+                }
306
+
307
+                $event = new CacheEntryInsertedEvent($this->storage, $file, $fileId, $storageId);
308
+                $this->eventDispatcher->dispatch(CacheInsertEvent::class, $event);
309
+                $this->eventDispatcher->dispatchTyped($event);
310
+                return $fileId;
311
+            }
312
+        } catch (Exception $e) {
313
+            if ($e->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
314
+                // entry exists already
315
+                if ($this->connection->inTransaction()) {
316
+                    $this->connection->commit();
317
+                    $this->connection->beginTransaction();
318
+                }
319
+            } else {
320
+                throw $e;
321
+            }
322
+        }
323
+
324
+        // The file was created in the meantime
325
+        if (($id = $this->getId($file)) > -1) {
326
+            $this->update($id, $data);
327
+            return $id;
328
+        } else {
329
+            throw new \RuntimeException('File entry could not be inserted but could also not be selected with getId() in order to perform an update. Please try again.');
330
+        }
331
+    }
332
+
333
+    /**
334
+     * update the metadata of an existing file or folder in the cache
335
+     *
336
+     * @param int $id the fileid of the existing file or folder
337
+     * @param array $data [$key => $value] the metadata to update, only the fields provided in the array will be updated, non-provided values will remain unchanged
338
+     */
339
+    public function update($id, array $data) {
340
+        if (isset($data['path'])) {
341
+            // normalize path
342
+            $data['path'] = $this->normalize($data['path']);
343
+        }
344
+
345
+        if (isset($data['name'])) {
346
+            // normalize path
347
+            $data['name'] = $this->normalize($data['name']);
348
+        }
349
+
350
+        [$values, $extensionValues] = $this->normalizeData($data);
351
+
352
+        if (count($values)) {
353
+            $query = $this->getQueryBuilder();
354
+
355
+            $query->update('filecache')
356
+                ->whereFileId($id)
357
+                ->whereStorageId($this->getNumericStorageId())
358
+                ->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
359
+                    return $query->expr()->orX(
360
+                        $query->expr()->neq($key, $query->createNamedParameter($value)),
361
+                        $query->expr()->isNull($key)
362
+                    );
363
+                }, array_keys($values), array_values($values))));
364
+
365
+            foreach ($values as $key => $value) {
366
+                $query->set($key, $query->createNamedParameter($value));
367
+            }
368
+
369
+            $query->executeStatement();
370
+        }
371
+
372
+        if (count($extensionValues)) {
373
+            try {
374
+                $query = $this->getQueryBuilder();
375
+                $query->insert('filecache_extended');
376
+                $query->hintShardKey('storage', $this->getNumericStorageId());
377
+
378
+                $query->setValue('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT));
379
+                foreach ($extensionValues as $column => $value) {
380
+                    $query->setValue($column, $query->createNamedParameter($value));
381
+                }
382
+
383
+                $query->executeStatement();
384
+            } catch (Exception $e) {
385
+                if ($e->getReason() !== Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
386
+                    throw $e;
387
+                }
388
+                $query = $this->getQueryBuilder();
389
+                $query->update('filecache_extended')
390
+                    ->whereFileId($id)
391
+                    ->hintShardKey('storage', $this->getNumericStorageId())
392
+                    ->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
393
+                        return $query->expr()->orX(
394
+                            $query->expr()->neq($key, $query->createNamedParameter($value)),
395
+                            $query->expr()->isNull($key)
396
+                        );
397
+                    }, array_keys($extensionValues), array_values($extensionValues))));
398
+
399
+                foreach ($extensionValues as $key => $value) {
400
+                    $query->set($key, $query->createNamedParameter($value));
401
+                }
402
+
403
+                $query->executeStatement();
404
+            }
405
+        }
406
+
407
+        $path = $this->getPathById($id);
408
+        // path can still be null if the file doesn't exist
409
+        if ($path !== null) {
410
+            $event = new CacheEntryUpdatedEvent($this->storage, $path, $id, $this->getNumericStorageId());
411
+            $this->eventDispatcher->dispatch(CacheUpdateEvent::class, $event);
412
+            $this->eventDispatcher->dispatchTyped($event);
413
+        }
414
+    }
415
+
416
+    /**
417
+     * extract query parts and params array from data array
418
+     *
419
+     * @param array $data
420
+     * @return array
421
+     */
422
+    protected function normalizeData(array $data): array {
423
+        $fields = [
424
+            'path', 'parent', 'name', 'mimetype', 'size', 'mtime', 'storage_mtime', 'encrypted',
425
+            'etag', 'permissions', 'checksum', 'storage', 'unencrypted_size'];
426
+        $extensionFields = ['metadata_etag', 'creation_time', 'upload_time'];
427
+
428
+        $doNotCopyStorageMTime = false;
429
+        if (array_key_exists('mtime', $data) && $data['mtime'] === null) {
430
+            // this horrific magic tells it to not copy storage_mtime to mtime
431
+            unset($data['mtime']);
432
+            $doNotCopyStorageMTime = true;
433
+        }
434
+
435
+        $params = [];
436
+        $extensionParams = [];
437
+        foreach ($data as $name => $value) {
438
+            if (in_array($name, $fields)) {
439
+                if ($name === 'path') {
440
+                    $params['path_hash'] = md5($value);
441
+                } elseif ($name === 'mimetype') {
442
+                    $params['mimepart'] = $this->mimetypeLoader->getId(substr($value, 0, strpos($value, '/')));
443
+                    $value = $this->mimetypeLoader->getId($value);
444
+                } elseif ($name === 'storage_mtime') {
445
+                    if (!$doNotCopyStorageMTime && !isset($data['mtime'])) {
446
+                        $params['mtime'] = $value;
447
+                    }
448
+                } elseif ($name === 'encrypted') {
449
+                    if (isset($data['encryptedVersion'])) {
450
+                        $value = $data['encryptedVersion'];
451
+                    } else {
452
+                        // Boolean to integer conversion
453
+                        $value = $value ? 1 : 0;
454
+                    }
455
+                }
456
+                $params[$name] = $value;
457
+            }
458
+            if (in_array($name, $extensionFields)) {
459
+                $extensionParams[$name] = $value;
460
+            }
461
+        }
462
+        return [$params, array_filter($extensionParams)];
463
+    }
464
+
465
+    /**
466
+     * get the file id for a file
467
+     *
468
+     * A file id is a numeric id for a file or folder that's unique within an owncloud instance which stays the same for the lifetime of a file
469
+     *
470
+     * File ids are easiest way for apps to store references to a file since unlike paths they are not affected by renames or sharing
471
+     *
472
+     * @param string $file
473
+     * @return int
474
+     */
475
+    public function getId($file) {
476
+        // normalize file
477
+        $file = $this->normalize($file);
478
+
479
+        $query = $this->getQueryBuilder();
480
+        $query->select('fileid')
481
+            ->from('filecache')
482
+            ->whereStorageId($this->getNumericStorageId())
483
+            ->wherePath($file);
484
+
485
+        $result = $query->executeQuery();
486
+        $id = $result->fetchOne();
487
+        $result->closeCursor();
488
+
489
+        return $id === false ? -1 : (int)$id;
490
+    }
491
+
492
+    /**
493
+     * get the id of the parent folder of a file
494
+     *
495
+     * @param string $file
496
+     * @return int
497
+     */
498
+    public function getParentId($file) {
499
+        if ($file === '') {
500
+            return -1;
501
+        } else {
502
+            $parent = $this->getParentPath($file);
503
+            return (int)$this->getId($parent);
504
+        }
505
+    }
506
+
507
+    private function getParentPath($path) {
508
+        $parent = dirname($path);
509
+        if ($parent === '.') {
510
+            $parent = '';
511
+        }
512
+        return $parent;
513
+    }
514
+
515
+    /**
516
+     * check if a file is available in the cache
517
+     *
518
+     * @param string $file
519
+     * @return bool
520
+     */
521
+    public function inCache($file) {
522
+        return $this->getId($file) != -1;
523
+    }
524
+
525
+    /**
526
+     * remove a file or folder from the cache
527
+     *
528
+     * when removing a folder from the cache all files and folders inside the folder will be removed as well
529
+     *
530
+     * @param string $file
531
+     */
532
+    public function remove($file) {
533
+        $entry = $this->get($file);
534
+
535
+        if ($entry instanceof ICacheEntry) {
536
+            $query = $this->getQueryBuilder();
537
+            $query->delete('filecache')
538
+                ->whereStorageId($this->getNumericStorageId())
539
+                ->whereFileId($entry->getId());
540
+            $query->executeStatement();
541
+
542
+            $query = $this->getQueryBuilder();
543
+            $query->delete('filecache_extended')
544
+                ->whereFileId($entry->getId())
545
+                ->hintShardKey('storage', $this->getNumericStorageId());
546
+            $query->executeStatement();
547
+
548
+            if ($entry->getMimeType() == FileInfo::MIMETYPE_FOLDER) {
549
+                $this->removeChildren($entry);
550
+            }
551
+
552
+            $this->eventDispatcher->dispatchTyped(new CacheEntryRemovedEvent($this->storage, $entry->getPath(), $entry->getId(), $this->getNumericStorageId()));
553
+        }
554
+    }
555
+
556
+    /**
557
+     * Remove all children of a folder
558
+     *
559
+     * @param ICacheEntry $entry the cache entry of the folder to remove the children of
560
+     * @throws \OC\DatabaseException
561
+     */
562
+    private function removeChildren(ICacheEntry $entry) {
563
+        $parentIds = [$entry->getId()];
564
+        $queue = [$entry->getId()];
565
+        $deletedIds = [];
566
+        $deletedPaths = [];
567
+
568
+        // we walk depth first through the file tree, removing all filecache_extended attributes while we walk
569
+        // and collecting all folder ids to later use to delete the filecache entries
570
+        while ($entryId = array_pop($queue)) {
571
+            $children = $this->getFolderContentsById($entryId);
572
+            $childIds = array_map(function (ICacheEntry $cacheEntry) {
573
+                return $cacheEntry->getId();
574
+            }, $children);
575
+            $childPaths = array_map(function (ICacheEntry $cacheEntry) {
576
+                return $cacheEntry->getPath();
577
+            }, $children);
578
+
579
+            foreach ($childIds as $childId) {
580
+                $deletedIds[] = $childId;
581
+            }
582
+
583
+            foreach ($childPaths as $childPath) {
584
+                $deletedPaths[] = $childPath;
585
+            }
586
+
587
+            $query = $this->getQueryBuilder();
588
+            $query->delete('filecache_extended')
589
+                ->where($query->expr()->in('fileid', $query->createParameter('childIds')))
590
+                ->hintShardKey('storage', $this->getNumericStorageId());
591
+
592
+            foreach (array_chunk($childIds, 1000) as $childIdChunk) {
593
+                $query->setParameter('childIds', $childIdChunk, IQueryBuilder::PARAM_INT_ARRAY);
594
+                $query->executeStatement();
595
+            }
596
+
597
+            /** @var ICacheEntry[] $childFolders */
598
+            $childFolders = [];
599
+            foreach ($children as $child) {
600
+                if ($child->getMimeType() == FileInfo::MIMETYPE_FOLDER) {
601
+                    $childFolders[] = $child;
602
+                }
603
+            }
604
+            foreach ($childFolders as $folder) {
605
+                $parentIds[] = $folder->getId();
606
+                $queue[] = $folder->getId();
607
+            }
608
+        }
609
+
610
+        $query = $this->getQueryBuilder();
611
+        $query->delete('filecache')
612
+            ->whereStorageId($this->getNumericStorageId())
613
+            ->whereParentInParameter('parentIds');
614
+
615
+        // Sorting before chunking allows the db to find the entries close to each
616
+        // other in the index
617
+        sort($parentIds, SORT_NUMERIC);
618
+        foreach (array_chunk($parentIds, 1000) as $parentIdChunk) {
619
+            $query->setParameter('parentIds', $parentIdChunk, IQueryBuilder::PARAM_INT_ARRAY);
620
+            $query->executeStatement();
621
+        }
622
+
623
+        foreach (array_combine($deletedIds, $deletedPaths) as $fileId => $filePath) {
624
+            $cacheEntryRemovedEvent = new CacheEntryRemovedEvent(
625
+                $this->storage,
626
+                $filePath,
627
+                $fileId,
628
+                $this->getNumericStorageId()
629
+            );
630
+            $this->eventDispatcher->dispatchTyped($cacheEntryRemovedEvent);
631
+        }
632
+    }
633
+
634
+    /**
635
+     * Move a file or folder in the cache
636
+     *
637
+     * @param string $source
638
+     * @param string $target
639
+     */
640
+    public function move($source, $target) {
641
+        $this->moveFromCache($this, $source, $target);
642
+    }
643
+
644
+    /**
645
+     * Get the storage id and path needed for a move
646
+     *
647
+     * @param string $path
648
+     * @return array [$storageId, $internalPath]
649
+     */
650
+    protected function getMoveInfo($path) {
651
+        return [$this->getNumericStorageId(), $path];
652
+    }
653
+
654
+    protected function hasEncryptionWrapper(): bool {
655
+        return $this->storage->instanceOfStorage(Encryption::class);
656
+    }
657
+
658
+    protected function shouldEncrypt(string $targetPath): bool {
659
+        if (!$this->storage->instanceOfStorage(Encryption::class)) {
660
+            return false;
661
+        }
662
+        return $this->storage->shouldEncrypt($targetPath);
663
+    }
664
+
665
+    /**
666
+     * Move a file or folder in the cache
667
+     *
668
+     * @param ICache $sourceCache
669
+     * @param string $sourcePath
670
+     * @param string $targetPath
671
+     * @throws \OC\DatabaseException
672
+     * @throws \Exception if the given storages have an invalid id
673
+     */
674
+    public function moveFromCache(ICache $sourceCache, $sourcePath, $targetPath) {
675
+        if ($sourceCache instanceof Cache) {
676
+            // normalize source and target
677
+            $sourcePath = $this->normalize($sourcePath);
678
+            $targetPath = $this->normalize($targetPath);
679
+
680
+            $sourceData = $sourceCache->get($sourcePath);
681
+            if (!$sourceData) {
682
+                throw new \Exception('Source path not found in cache: ' . $sourcePath);
683
+            }
684
+
685
+            $shardDefinition = $this->connection->getShardDefinition('filecache');
686
+            if (
687
+                $shardDefinition
688
+                && $shardDefinition->getShardForKey($sourceCache->getNumericStorageId()) !== $shardDefinition->getShardForKey($this->getNumericStorageId())
689
+            ) {
690
+                $this->moveFromStorageSharded($shardDefinition, $sourceCache, $sourceData, $targetPath);
691
+                return;
692
+            }
693
+
694
+            $sourceId = $sourceData['fileid'];
695
+            $newParentId = $this->getParentId($targetPath);
696
+
697
+            [$sourceStorageId, $sourcePath] = $sourceCache->getMoveInfo($sourcePath);
698
+            [$targetStorageId, $targetPath] = $this->getMoveInfo($targetPath);
699
+
700
+            if (is_null($sourceStorageId) || $sourceStorageId === false) {
701
+                throw new \Exception('Invalid source storage id: ' . $sourceStorageId);
702
+            }
703
+            if (is_null($targetStorageId) || $targetStorageId === false) {
704
+                throw new \Exception('Invalid target storage id: ' . $targetStorageId);
705
+            }
706
+
707
+            if ($sourceData['mimetype'] === 'httpd/unix-directory') {
708
+                //update all child entries
709
+                $sourceLength = mb_strlen($sourcePath);
710
+
711
+                $childIds = $this->getChildIds($sourceStorageId, $sourcePath);
712
+
713
+                $childChunks = array_chunk($childIds, 1000);
714
+
715
+                $query = $this->getQueryBuilder();
716
+
717
+                $fun = $query->func();
718
+                $newPathFunction = $fun->concat(
719
+                    $query->createNamedParameter($targetPath),
720
+                    $fun->substring('path', $query->createNamedParameter($sourceLength + 1, IQueryBuilder::PARAM_INT))// +1 for the leading slash
721
+                );
722
+                $query->update('filecache')
723
+                    ->set('path_hash', $fun->md5($newPathFunction))
724
+                    ->set('path', $newPathFunction)
725
+                    ->whereStorageId($sourceStorageId)
726
+                    ->andWhere($query->expr()->in('fileid', $query->createParameter('files')));
727
+
728
+                if ($sourceStorageId !== $targetStorageId) {
729
+                    $query->set('storage', $query->createNamedParameter($targetStorageId), IQueryBuilder::PARAM_INT);
730
+                }
731
+
732
+                // when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
733
+                if ($sourceCache->hasEncryptionWrapper() && !$this->hasEncryptionWrapper()) {
734
+                    $query->set('encrypted', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT));
735
+                }
736
+
737
+                // Retry transaction in case of RetryableException like deadlocks.
738
+                // Retry up to 4 times because we should receive up to 4 concurrent requests from the frontend
739
+                $retryLimit = 4;
740
+                for ($i = 1; $i <= $retryLimit; $i++) {
741
+                    try {
742
+                        $this->connection->beginTransaction();
743
+                        foreach ($childChunks as $chunk) {
744
+                            $query->setParameter('files', $chunk, IQueryBuilder::PARAM_INT_ARRAY);
745
+                            $query->executeStatement();
746
+                        }
747
+                        break;
748
+                    } catch (\OC\DatabaseException $e) {
749
+                        $this->connection->rollBack();
750
+                        throw $e;
751
+                    } catch (DbalException $e) {
752
+                        $this->connection->rollBack();
753
+
754
+                        if (!$e->isRetryable()) {
755
+                            throw $e;
756
+                        }
757
+
758
+                        // Simply throw if we already retried 4 times.
759
+                        if ($i === $retryLimit) {
760
+                            throw $e;
761
+                        }
762
+
763
+                        // Sleep a bit to give some time to the other transaction to finish.
764
+                        usleep(100 * 1000 * $i);
765
+                    }
766
+                }
767
+            } else {
768
+                $this->connection->beginTransaction();
769
+            }
770
+
771
+            $query = $this->getQueryBuilder();
772
+            $query->update('filecache')
773
+                ->set('path', $query->createNamedParameter($targetPath))
774
+                ->set('path_hash', $query->createNamedParameter(md5($targetPath)))
775
+                ->set('name', $query->createNamedParameter(basename($targetPath)))
776
+                ->set('parent', $query->createNamedParameter($newParentId, IQueryBuilder::PARAM_INT))
777
+                ->whereStorageId($sourceStorageId)
778
+                ->whereFileId($sourceId);
779
+
780
+            if ($sourceStorageId !== $targetStorageId) {
781
+                $query->set('storage', $query->createNamedParameter($targetStorageId), IQueryBuilder::PARAM_INT);
782
+            }
783
+
784
+            // when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
785
+            if ($sourceCache->hasEncryptionWrapper() && !$this->hasEncryptionWrapper()) {
786
+                $query->set('encrypted', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT));
787
+            }
788
+
789
+            $query->executeStatement();
790
+
791
+            $this->connection->commit();
792
+
793
+            if ($sourceCache->getNumericStorageId() !== $this->getNumericStorageId()) {
794
+                $this->eventDispatcher->dispatchTyped(new CacheEntryRemovedEvent($this->storage, $sourcePath, $sourceId, $sourceCache->getNumericStorageId()));
795
+                $event = new CacheEntryInsertedEvent($this->storage, $targetPath, $sourceId, $this->getNumericStorageId());
796
+                $this->eventDispatcher->dispatch(CacheInsertEvent::class, $event);
797
+                $this->eventDispatcher->dispatchTyped($event);
798
+            } else {
799
+                $event = new CacheEntryUpdatedEvent($this->storage, $targetPath, $sourceId, $this->getNumericStorageId());
800
+                $this->eventDispatcher->dispatch(CacheUpdateEvent::class, $event);
801
+                $this->eventDispatcher->dispatchTyped($event);
802
+            }
803
+        } else {
804
+            $this->moveFromCacheFallback($sourceCache, $sourcePath, $targetPath);
805
+        }
806
+    }
807
+
808
+    private function getChildIds(int $storageId, string $path): array {
809
+        $query = $this->connection->getQueryBuilder();
810
+        $query->select('fileid')
811
+            ->from('filecache')
812
+            ->where($query->expr()->eq('storage', $query->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
813
+            ->andWhere($query->expr()->like('path', $query->createNamedParameter($this->connection->escapeLikeParameter($path) . '/%')));
814
+        return $query->executeQuery()->fetchAll(\PDO::FETCH_COLUMN);
815
+    }
816
+
817
+    /**
818
+     * remove all entries for files that are stored on the storage from the cache
819
+     */
820
+    public function clear() {
821
+        $query = $this->getQueryBuilder();
822
+        $query->delete('filecache')
823
+            ->whereStorageId($this->getNumericStorageId());
824
+        $query->executeStatement();
825
+
826
+        $query = $this->connection->getQueryBuilder();
827
+        $query->delete('storages')
828
+            ->where($query->expr()->eq('id', $query->createNamedParameter($this->storageId)));
829
+        $query->executeStatement();
830
+    }
831
+
832
+    /**
833
+     * Get the scan status of a file
834
+     *
835
+     * - Cache::NOT_FOUND: File is not in the cache
836
+     * - Cache::PARTIAL: File is not stored in the cache but some incomplete data is known
837
+     * - Cache::SHALLOW: The folder and it's direct children are in the cache but not all sub folders are fully scanned
838
+     * - Cache::COMPLETE: The file or folder, with all it's children) are fully scanned
839
+     *
840
+     * @param string $file
841
+     *
842
+     * @return int Cache::NOT_FOUND, Cache::PARTIAL, Cache::SHALLOW or Cache::COMPLETE
843
+     */
844
+    public function getStatus($file) {
845
+        // normalize file
846
+        $file = $this->normalize($file);
847
+
848
+        $query = $this->getQueryBuilder();
849
+        $query->select('size')
850
+            ->from('filecache')
851
+            ->whereStorageId($this->getNumericStorageId())
852
+            ->wherePath($file);
853
+
854
+        $result = $query->executeQuery();
855
+        $size = $result->fetchOne();
856
+        $result->closeCursor();
857
+
858
+        if ($size !== false) {
859
+            if ((int)$size === -1) {
860
+                return self::SHALLOW;
861
+            } else {
862
+                return self::COMPLETE;
863
+            }
864
+        } else {
865
+            if (isset($this->partial[$file])) {
866
+                return self::PARTIAL;
867
+            } else {
868
+                return self::NOT_FOUND;
869
+            }
870
+        }
871
+    }
872
+
873
+    /**
874
+     * search for files matching $pattern
875
+     *
876
+     * @param string $pattern the search pattern using SQL search syntax (e.g. '%searchstring%')
877
+     * @return ICacheEntry[] an array of cache entries where the name matches the search pattern
878
+     */
879
+    public function search($pattern) {
880
+        $operator = new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', $pattern);
881
+        return $this->searchQuery(new SearchQuery($operator, 0, 0, [], null));
882
+    }
883
+
884
+    /**
885
+     * search for files by mimetype
886
+     *
887
+     * @param string $mimetype either a full mimetype to search ('text/plain') or only the first part of a mimetype ('image')
888
+     *                         where it will search for all mimetypes in the group ('image/*')
889
+     * @return ICacheEntry[] an array of cache entries where the mimetype matches the search
890
+     */
891
+    public function searchByMime($mimetype) {
892
+        if (!str_contains($mimetype, '/')) {
893
+            $operator = new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', $mimetype . '/%');
894
+        } else {
895
+            $operator = new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', $mimetype);
896
+        }
897
+        return $this->searchQuery(new SearchQuery($operator, 0, 0, [], null));
898
+    }
899
+
900
+    public function searchQuery(ISearchQuery $query) {
901
+        return current($this->querySearchHelper->searchInCaches($query, [$this]));
902
+    }
903
+
904
+    /**
905
+     * Re-calculate the folder size and the size of all parent folders
906
+     *
907
+     * @param array|ICacheEntry|null $data (optional) meta data of the folder
908
+     */
909
+    public function correctFolderSize(string $path, $data = null, bool $isBackgroundScan = false): void {
910
+        $this->calculateFolderSize($path, $data);
911
+
912
+        if ($path !== '') {
913
+            $parent = dirname($path);
914
+            if ($parent === '.' || $parent === '/') {
915
+                $parent = '';
916
+            }
917
+
918
+            if ($isBackgroundScan) {
919
+                $parentData = $this->get($parent);
920
+                if ($parentData !== false
921
+                    && $parentData['size'] !== -1
922
+                    && $this->getIncompleteChildrenCount($parentData['fileid']) === 0
923
+                ) {
924
+                    $this->correctFolderSize($parent, $parentData, $isBackgroundScan);
925
+                }
926
+            } else {
927
+                $this->correctFolderSize($parent);
928
+            }
929
+        }
930
+    }
931
+
932
+    /**
933
+     * get the incomplete count that shares parent $folder
934
+     *
935
+     * @param int $fileId the file id of the folder
936
+     * @return int
937
+     */
938
+    public function getIncompleteChildrenCount($fileId) {
939
+        if ($fileId > -1) {
940
+            $query = $this->getQueryBuilder();
941
+            $query->select($query->func()->count())
942
+                ->from('filecache')
943
+                ->whereParent($fileId)
944
+                ->whereStorageId($this->getNumericStorageId())
945
+                ->andWhere($query->expr()->eq('size', $query->createNamedParameter(-1, IQueryBuilder::PARAM_INT)));
946
+
947
+            $result = $query->executeQuery();
948
+            $size = (int)$result->fetchOne();
949
+            $result->closeCursor();
950
+
951
+            return $size;
952
+        }
953
+        return -1;
954
+    }
955
+
956
+    /**
957
+     * calculate the size of a folder and set it in the cache
958
+     *
959
+     * @param string $path
960
+     * @param array|null|ICacheEntry $entry (optional) meta data of the folder
961
+     * @return int|float
962
+     */
963
+    public function calculateFolderSize($path, $entry = null) {
964
+        return $this->calculateFolderSizeInner($path, $entry);
965
+    }
966
+
967
+
968
+    /**
969
+     * inner function because we can't add new params to the public function without breaking any child classes
970
+     *
971
+     * @param string $path
972
+     * @param array|null|ICacheEntry $entry (optional) meta data of the folder
973
+     * @param bool $ignoreUnknown don't mark the folder size as unknown if any of it's children are unknown
974
+     * @return int|float
975
+     */
976
+    protected function calculateFolderSizeInner(string $path, $entry = null, bool $ignoreUnknown = false) {
977
+        $totalSize = 0;
978
+        if (is_null($entry) || !isset($entry['fileid'])) {
979
+            $entry = $this->get($path);
980
+        }
981
+        if (isset($entry['mimetype']) && $entry['mimetype'] === FileInfo::MIMETYPE_FOLDER) {
982
+            $id = $entry['fileid'];
983
+
984
+            $query = $this->getQueryBuilder();
985
+            $query->select('size', 'unencrypted_size')
986
+                ->from('filecache')
987
+                ->whereStorageId($this->getNumericStorageId())
988
+                ->whereParent($id);
989
+            if ($ignoreUnknown) {
990
+                $query->andWhere($query->expr()->gte('size', $query->createNamedParameter(0)));
991
+            }
992
+
993
+            $result = $query->executeQuery();
994
+            $rows = $result->fetchAll();
995
+            $result->closeCursor();
996
+
997
+            if ($rows) {
998
+                $sizes = array_map(function (array $row) {
999
+                    return Util::numericToNumber($row['size']);
1000
+                }, $rows);
1001
+                $unencryptedOnlySizes = array_map(function (array $row) {
1002
+                    return Util::numericToNumber($row['unencrypted_size']);
1003
+                }, $rows);
1004
+                $unencryptedSizes = array_map(function (array $row) {
1005
+                    return Util::numericToNumber(($row['unencrypted_size'] > 0) ? $row['unencrypted_size'] : $row['size']);
1006
+                }, $rows);
1007
+
1008
+                $sum = array_sum($sizes);
1009
+                $min = min($sizes);
1010
+
1011
+                $unencryptedSum = array_sum($unencryptedSizes);
1012
+                $unencryptedMin = min($unencryptedSizes);
1013
+                $unencryptedMax = max($unencryptedOnlySizes);
1014
+
1015
+                $sum = 0 + $sum;
1016
+                $min = 0 + $min;
1017
+                if ($min === -1) {
1018
+                    $totalSize = $min;
1019
+                } else {
1020
+                    $totalSize = $sum;
1021
+                }
1022
+                if ($unencryptedMin === -1 || $min === -1) {
1023
+                    $unencryptedTotal = $unencryptedMin;
1024
+                } else {
1025
+                    $unencryptedTotal = $unencryptedSum;
1026
+                }
1027
+            } else {
1028
+                $totalSize = 0;
1029
+                $unencryptedTotal = 0;
1030
+                $unencryptedMax = 0;
1031
+            }
1032
+
1033
+            // only set unencrypted size for a folder if any child entries have it set, or the folder is empty
1034
+            $shouldWriteUnEncryptedSize = $unencryptedMax > 0 || $totalSize === 0 || ($entry['unencrypted_size'] ?? 0) > 0;
1035
+            if ($entry['size'] !== $totalSize || (($entry['unencrypted_size'] ?? 0) !== $unencryptedTotal && $shouldWriteUnEncryptedSize)) {
1036
+                if ($shouldWriteUnEncryptedSize) {
1037
+                    // if all children have an unencrypted size of 0, just set the folder unencrypted size to 0 instead of summing the sizes
1038
+                    if ($unencryptedMax === 0) {
1039
+                        $unencryptedTotal = 0;
1040
+                    }
1041
+
1042
+                    $this->update($id, [
1043
+                        'size' => $totalSize,
1044
+                        'unencrypted_size' => $unencryptedTotal,
1045
+                    ]);
1046
+                } else {
1047
+                    $this->update($id, [
1048
+                        'size' => $totalSize,
1049
+                    ]);
1050
+                }
1051
+            }
1052
+        }
1053
+        return $totalSize;
1054
+    }
1055
+
1056
+    /**
1057
+     * get all file ids on the files on the storage
1058
+     *
1059
+     * @return int[]
1060
+     */
1061
+    public function getAll() {
1062
+        $query = $this->getQueryBuilder();
1063
+        $query->select('fileid')
1064
+            ->from('filecache')
1065
+            ->whereStorageId($this->getNumericStorageId());
1066
+
1067
+        $result = $query->executeQuery();
1068
+        $files = $result->fetchAll(\PDO::FETCH_COLUMN);
1069
+        $result->closeCursor();
1070
+
1071
+        return array_map(function ($id) {
1072
+            return (int)$id;
1073
+        }, $files);
1074
+    }
1075
+
1076
+    /**
1077
+     * find a folder in the cache which has not been fully scanned
1078
+     *
1079
+     * If multiple incomplete folders are in the cache, the one with the highest id will be returned,
1080
+     * use the one with the highest id gives the best result with the background scanner, since that is most
1081
+     * likely the folder where we stopped scanning previously
1082
+     *
1083
+     * @return string|false the path of the folder or false when no folder matched
1084
+     */
1085
+    public function getIncomplete() {
1086
+        $query = $this->getQueryBuilder();
1087
+        $query->select('path')
1088
+            ->from('filecache')
1089
+            ->whereStorageId($this->getNumericStorageId())
1090
+            ->andWhere($query->expr()->eq('size', $query->createNamedParameter(-1, IQueryBuilder::PARAM_INT)))
1091
+            ->orderBy('fileid', 'DESC')
1092
+            ->setMaxResults(1);
1093
+
1094
+        $result = $query->executeQuery();
1095
+        $path = $result->fetchOne();
1096
+        $result->closeCursor();
1097
+
1098
+        return $path === false ? false : (string)$path;
1099
+    }
1100
+
1101
+    /**
1102
+     * get the path of a file on this storage by it's file id
1103
+     *
1104
+     * @param int $id the file id of the file or folder to search
1105
+     * @return string|null the path of the file (relative to the storage) or null if a file with the given id does not exists within this cache
1106
+     */
1107
+    public function getPathById($id) {
1108
+        $query = $this->getQueryBuilder();
1109
+        $query->select('path')
1110
+            ->from('filecache')
1111
+            ->whereStorageId($this->getNumericStorageId())
1112
+            ->whereFileId($id);
1113
+
1114
+        $result = $query->executeQuery();
1115
+        $path = $result->fetchOne();
1116
+        $result->closeCursor();
1117
+
1118
+        if ($path === false) {
1119
+            return null;
1120
+        }
1121
+
1122
+        return (string)$path;
1123
+    }
1124
+
1125
+    /**
1126
+     * get the storage id of the storage for a file and the internal path of the file
1127
+     * unlike getPathById this does not limit the search to files on this storage and
1128
+     * instead does a global search in the cache table
1129
+     *
1130
+     * @param int $id
1131
+     * @return array first element holding the storage id, second the path
1132
+     * @deprecated 17.0.0 use getPathById() instead
1133
+     */
1134
+    public static function getById($id) {
1135
+        $query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
1136
+        $query->select('path', 'storage')
1137
+            ->from('filecache')
1138
+            ->where($query->expr()->eq('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
1139
+
1140
+        $result = $query->executeQuery();
1141
+        $row = $result->fetch();
1142
+        $result->closeCursor();
1143
+
1144
+        if ($row) {
1145
+            $numericId = $row['storage'];
1146
+            $path = $row['path'];
1147
+        } else {
1148
+            return null;
1149
+        }
1150
+
1151
+        if ($id = Storage::getStorageId($numericId)) {
1152
+            return [$id, $path];
1153
+        } else {
1154
+            return null;
1155
+        }
1156
+    }
1157
+
1158
+    /**
1159
+     * normalize the given path
1160
+     *
1161
+     * @param string $path
1162
+     * @return string
1163
+     */
1164
+    public function normalize($path) {
1165
+        return trim(\OC_Util::normalizeUnicode($path), '/');
1166
+    }
1167
+
1168
+    /**
1169
+     * Copy a file or folder in the cache
1170
+     *
1171
+     * @param ICache $sourceCache
1172
+     * @param ICacheEntry $sourceEntry
1173
+     * @param string $targetPath
1174
+     * @return int fileId of copied entry
1175
+     */
1176
+    public function copyFromCache(ICache $sourceCache, ICacheEntry $sourceEntry, string $targetPath): int {
1177
+        if ($sourceEntry->getId() < 0) {
1178
+            throw new \RuntimeException('Invalid source cache entry on copyFromCache');
1179
+        }
1180
+        $data = $this->cacheEntryToArray($sourceEntry);
1181
+
1182
+        // when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
1183
+        if ($sourceCache instanceof Cache
1184
+            && $sourceCache->hasEncryptionWrapper()
1185
+            && !$this->shouldEncrypt($targetPath)) {
1186
+            $data['encrypted'] = 0;
1187
+        }
1188
+
1189
+        $fileId = $this->put($targetPath, $data);
1190
+        if ($fileId <= 0) {
1191
+            throw new \RuntimeException('Failed to copy to ' . $targetPath . ' from cache with source data ' . json_encode($data) . ' ');
1192
+        }
1193
+        if ($sourceEntry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
1194
+            $folderContent = $sourceCache->getFolderContentsById($sourceEntry->getId());
1195
+            foreach ($folderContent as $subEntry) {
1196
+                $subTargetPath = $targetPath . '/' . $subEntry->getName();
1197
+                $this->copyFromCache($sourceCache, $subEntry, $subTargetPath);
1198
+            }
1199
+        }
1200
+        return $fileId;
1201
+    }
1202
+
1203
+    private function cacheEntryToArray(ICacheEntry $entry): array {
1204
+        $data = [
1205
+            'size' => $entry->getSize(),
1206
+            'mtime' => $entry->getMTime(),
1207
+            'storage_mtime' => $entry->getStorageMTime(),
1208
+            'mimetype' => $entry->getMimeType(),
1209
+            'mimepart' => $entry->getMimePart(),
1210
+            'etag' => $entry->getEtag(),
1211
+            'permissions' => $entry->getPermissions(),
1212
+            'encrypted' => $entry->isEncrypted(),
1213
+            'creation_time' => $entry->getCreationTime(),
1214
+            'upload_time' => $entry->getUploadTime(),
1215
+            'metadata_etag' => $entry->getMetadataEtag(),
1216
+        ];
1217
+        if ($entry instanceof CacheEntry && isset($entry['scan_permissions'])) {
1218
+            $data['permissions'] = $entry['scan_permissions'];
1219
+        }
1220
+        return $data;
1221
+    }
1222
+
1223
+    public function getQueryFilterForStorage(): ISearchOperator {
1224
+        return new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', $this->getNumericStorageId());
1225
+    }
1226
+
1227
+    public function getCacheEntryFromSearchResult(ICacheEntry $rawEntry): ?ICacheEntry {
1228
+        if ($rawEntry->getStorageId() === $this->getNumericStorageId()) {
1229
+            return $rawEntry;
1230
+        } else {
1231
+            return null;
1232
+        }
1233
+    }
1234
+
1235
+    private function moveFromStorageSharded(ShardDefinition $shardDefinition, ICache $sourceCache, ICacheEntry $sourceEntry, $targetPath): void {
1236
+        $sourcePath = $sourceEntry->getPath();
1237
+        while ($sourceCache instanceof CacheWrapper) {
1238
+            if ($sourceCache instanceof CacheJail) {
1239
+                $sourcePath = $sourceCache->getSourcePath($sourcePath);
1240
+            }
1241
+            $sourceCache = $sourceCache->getCache();
1242
+        }
1243
+
1244
+        if ($sourceEntry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
1245
+            $fileIds = $this->getChildIds($sourceCache->getNumericStorageId(), $sourcePath);
1246
+        } else {
1247
+            $fileIds = [];
1248
+        }
1249
+        $fileIds[] = $sourceEntry->getId();
1250
+
1251
+        $helper = $this->connection->getCrossShardMoveHelper();
1252
+
1253
+        $sourceConnection = $helper->getConnection($shardDefinition, $sourceCache->getNumericStorageId());
1254
+        $targetConnection = $helper->getConnection($shardDefinition, $this->getNumericStorageId());
1255
+
1256
+        $cacheItems = $helper->loadItems($sourceConnection, 'filecache', 'fileid', $fileIds);
1257
+        $extendedItems = $helper->loadItems($sourceConnection, 'filecache_extended', 'fileid', $fileIds);
1258
+        $metadataItems = $helper->loadItems($sourceConnection, 'files_metadata', 'file_id', $fileIds);
1259
+
1260
+        // when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
1261
+        $removeEncryptedFlag = ($sourceCache instanceof Cache && $sourceCache->hasEncryptionWrapper()) && !$this->hasEncryptionWrapper();
1262
+
1263
+        $sourcePathLength = strlen($sourcePath);
1264
+        foreach ($cacheItems as &$cacheItem) {
1265
+            if ($cacheItem['path'] === $sourcePath) {
1266
+                $cacheItem['path'] = $targetPath;
1267
+                $cacheItem['parent'] = $this->getParentId($targetPath);
1268
+                $cacheItem['name'] = basename($cacheItem['path']);
1269
+            } else {
1270
+                $cacheItem['path'] = $targetPath . '/' . substr($cacheItem['path'], $sourcePathLength + 1); // +1 for the leading slash
1271
+            }
1272
+            $cacheItem['path_hash'] = md5($cacheItem['path']);
1273
+            $cacheItem['storage'] = $this->getNumericStorageId();
1274
+            if ($removeEncryptedFlag) {
1275
+                $cacheItem['encrypted'] = 0;
1276
+            }
1277
+        }
1278
+
1279
+        $targetConnection->beginTransaction();
1280
+
1281
+        try {
1282
+            $helper->saveItems($targetConnection, 'filecache', $cacheItems);
1283
+            $helper->saveItems($targetConnection, 'filecache_extended', $extendedItems);
1284
+            $helper->saveItems($targetConnection, 'files_metadata', $metadataItems);
1285
+        } catch (\Exception $e) {
1286
+            $targetConnection->rollback();
1287
+            throw $e;
1288
+        }
1289
+
1290
+        $sourceConnection->beginTransaction();
1291
+
1292
+        try {
1293
+            $helper->deleteItems($sourceConnection, 'filecache', 'fileid', $fileIds);
1294
+            $helper->deleteItems($sourceConnection, 'filecache_extended', 'fileid', $fileIds);
1295
+            $helper->deleteItems($sourceConnection, 'files_metadata', 'file_id', $fileIds);
1296
+        } catch (\Exception $e) {
1297
+            $targetConnection->rollback();
1298
+            $sourceConnection->rollBack();
1299
+            throw $e;
1300
+        }
1301
+
1302
+        try {
1303
+            $sourceConnection->commit();
1304
+        } catch (\Exception $e) {
1305
+            $targetConnection->rollback();
1306
+            throw $e;
1307
+        }
1308
+        $targetConnection->commit();
1309
+    }
1310 1310
 }
Please login to merge, or discard this patch.
apps/encryption/lib/KeyManager.php 2 patches
Indentation   +664 added lines, -664 removed lines patch added patch discarded remove patch
@@ -20,668 +20,668 @@
 block discarded – undo
20 20
 use Psr\Log\LoggerInterface;
21 21
 
22 22
 class KeyManager {
23
-	private string $recoveryKeyId;
24
-	private string $publicShareKeyId;
25
-	private string $masterKeyId;
26
-	private ?string $keyUid;
27
-	private string $publicKeyId = 'publicKey';
28
-	private string $privateKeyId = 'privateKey';
29
-	private string $shareKeyId = 'shareKey';
30
-	private string $fileKeyId = 'fileKey';
31
-
32
-	public function __construct(
33
-		private IStorage $keyStorage,
34
-		private Crypt $crypt,
35
-		private IConfig $config,
36
-		IUserSession $userSession,
37
-		private Session $session,
38
-		private LoggerInterface $logger,
39
-		private Util $util,
40
-		private ILockingProvider $lockingProvider,
41
-	) {
42
-		$this->recoveryKeyId = $this->config->getAppValue('encryption',
43
-			'recoveryKeyId');
44
-		if (empty($this->recoveryKeyId)) {
45
-			$this->recoveryKeyId = 'recoveryKey_' . substr(md5((string)time()), 0, 8);
46
-			$this->config->setAppValue('encryption',
47
-				'recoveryKeyId',
48
-				$this->recoveryKeyId);
49
-		}
50
-
51
-		$this->publicShareKeyId = $this->config->getAppValue('encryption',
52
-			'publicShareKeyId');
53
-		if (empty($this->publicShareKeyId)) {
54
-			$this->publicShareKeyId = 'pubShare_' . substr(md5((string)time()), 0, 8);
55
-			$this->config->setAppValue('encryption', 'publicShareKeyId', $this->publicShareKeyId);
56
-		}
57
-
58
-		$this->masterKeyId = $this->config->getAppValue('encryption',
59
-			'masterKeyId');
60
-		if (empty($this->masterKeyId)) {
61
-			$this->masterKeyId = 'master_' . substr(md5((string)time()), 0, 8);
62
-			$this->config->setAppValue('encryption', 'masterKeyId', $this->masterKeyId);
63
-		}
64
-
65
-		$this->keyUid = $userSession->isLoggedIn() ? $userSession->getUser()?->getUID() : null;
66
-	}
67
-
68
-	/**
69
-	 * check if key pair for public link shares exists, if not we create one
70
-	 */
71
-	public function validateShareKey() {
72
-		$shareKey = $this->getPublicShareKey();
73
-		if (empty($shareKey)) {
74
-			$this->lockingProvider->acquireLock('encryption-generateSharedKey', ILockingProvider::LOCK_EXCLUSIVE, 'Encryption: shared key generation');
75
-			try {
76
-				$keyPair = $this->crypt->createKeyPair();
77
-
78
-				// Save public key
79
-				$this->keyStorage->setSystemUserKey(
80
-					$this->publicShareKeyId . '.' . $this->publicKeyId, $keyPair['publicKey'],
81
-					Encryption::ID);
82
-
83
-				// Encrypt private key empty passphrase
84
-				$encryptedKey = $this->crypt->encryptPrivateKey($keyPair['privateKey'], '');
85
-				$header = $this->crypt->generateHeader();
86
-				$this->setSystemPrivateKey($this->publicShareKeyId, $header . $encryptedKey);
87
-			} catch (\Throwable $e) {
88
-				$this->lockingProvider->releaseLock('encryption-generateSharedKey', ILockingProvider::LOCK_EXCLUSIVE);
89
-				throw $e;
90
-			}
91
-			$this->lockingProvider->releaseLock('encryption-generateSharedKey', ILockingProvider::LOCK_EXCLUSIVE);
92
-		}
93
-	}
94
-
95
-	/**
96
-	 * check if a key pair for the master key exists, if not we create one
97
-	 */
98
-	public function validateMasterKey() {
99
-		if ($this->util->isMasterKeyEnabled() === false) {
100
-			return;
101
-		}
102
-
103
-		$publicMasterKey = $this->getPublicMasterKey();
104
-		$privateMasterKey = $this->getPrivateMasterKey();
105
-
106
-		if (empty($publicMasterKey) && empty($privateMasterKey)) {
107
-			// There could be a race condition here if two requests would trigger
108
-			// the generation the second one would enter the key generation as long
109
-			// as the first one didn't write the key to the keystorage yet
110
-			$this->lockingProvider->acquireLock('encryption-generateMasterKey', ILockingProvider::LOCK_EXCLUSIVE, 'Encryption: master key generation');
111
-			try {
112
-				$keyPair = $this->crypt->createKeyPair();
113
-
114
-				// Save public key
115
-				$this->keyStorage->setSystemUserKey(
116
-					$this->masterKeyId . '.' . $this->publicKeyId, $keyPair['publicKey'],
117
-					Encryption::ID);
118
-
119
-				// Encrypt private key with system password
120
-				$encryptedKey = $this->crypt->encryptPrivateKey($keyPair['privateKey'], $this->getMasterKeyPassword(), $this->masterKeyId);
121
-				$header = $this->crypt->generateHeader();
122
-				$this->setSystemPrivateKey($this->masterKeyId, $header . $encryptedKey);
123
-			} catch (\Throwable $e) {
124
-				$this->lockingProvider->releaseLock('encryption-generateMasterKey', ILockingProvider::LOCK_EXCLUSIVE);
125
-				throw $e;
126
-			}
127
-			$this->lockingProvider->releaseLock('encryption-generateMasterKey', ILockingProvider::LOCK_EXCLUSIVE);
128
-		} elseif (empty($publicMasterKey)) {
129
-			$this->logger->error('A private master key is available but the public key could not be found. This should never happen.');
130
-			return;
131
-		} elseif (empty($privateMasterKey)) {
132
-			$this->logger->error('A public master key is available but the private key could not be found. This should never happen.');
133
-			return;
134
-		}
135
-
136
-		if (!$this->session->isPrivateKeySet()) {
137
-			$masterKey = $this->getSystemPrivateKey($this->masterKeyId);
138
-			$decryptedMasterKey = $this->crypt->decryptPrivateKey($masterKey, $this->getMasterKeyPassword(), $this->masterKeyId);
139
-			if ($decryptedMasterKey === false) {
140
-				$this->logger->error('A public master key is available but decrypting it failed. This should never happen.');
141
-			} else {
142
-				$this->session->setPrivateKey($decryptedMasterKey);
143
-			}
144
-		}
145
-
146
-		// after the encryption key is available we are ready to go
147
-		$this->session->setStatus(Session::INIT_SUCCESSFUL);
148
-	}
149
-
150
-	/**
151
-	 * @return bool
152
-	 */
153
-	public function recoveryKeyExists() {
154
-		$key = $this->getRecoveryKey();
155
-		return !empty($key);
156
-	}
157
-
158
-	/**
159
-	 * get recovery key
160
-	 *
161
-	 * @return string
162
-	 */
163
-	public function getRecoveryKey() {
164
-		return $this->keyStorage->getSystemUserKey($this->recoveryKeyId . '.' . $this->publicKeyId, Encryption::ID);
165
-	}
166
-
167
-	/**
168
-	 * get recovery key ID
169
-	 *
170
-	 * @return string
171
-	 */
172
-	public function getRecoveryKeyId() {
173
-		return $this->recoveryKeyId;
174
-	}
175
-
176
-	/**
177
-	 * @param string $password
178
-	 * @return bool
179
-	 */
180
-	public function checkRecoveryPassword($password) {
181
-		$recoveryKey = $this->keyStorage->getSystemUserKey($this->recoveryKeyId . '.' . $this->privateKeyId, Encryption::ID);
182
-		$decryptedRecoveryKey = $this->crypt->decryptPrivateKey($recoveryKey, $password);
183
-
184
-		if ($decryptedRecoveryKey) {
185
-			return true;
186
-		}
187
-		return false;
188
-	}
189
-
190
-	/**
191
-	 * @param string $uid
192
-	 * @param string $password
193
-	 * @param array $keyPair
194
-	 * @return bool
195
-	 */
196
-	public function storeKeyPair($uid, $password, $keyPair) {
197
-		// Save Public Key
198
-		$this->setPublicKey($uid, $keyPair['publicKey']);
199
-
200
-		$encryptedKey = $this->crypt->encryptPrivateKey($keyPair['privateKey'], $password, $uid);
201
-
202
-		$header = $this->crypt->generateHeader();
203
-
204
-		if ($encryptedKey) {
205
-			$this->setPrivateKey($uid, $header . $encryptedKey);
206
-			return true;
207
-		}
208
-		return false;
209
-	}
210
-
211
-	/**
212
-	 * @param string $password
213
-	 * @param array $keyPair
214
-	 * @return bool
215
-	 */
216
-	public function setRecoveryKey($password, $keyPair) {
217
-		// Save Public Key
218
-		$this->keyStorage->setSystemUserKey($this->getRecoveryKeyId()
219
-			. '.' . $this->publicKeyId,
220
-			$keyPair['publicKey'],
221
-			Encryption::ID);
222
-
223
-		$encryptedKey = $this->crypt->encryptPrivateKey($keyPair['privateKey'], $password);
224
-		$header = $this->crypt->generateHeader();
225
-
226
-		if ($encryptedKey) {
227
-			$this->setSystemPrivateKey($this->getRecoveryKeyId(), $header . $encryptedKey);
228
-			return true;
229
-		}
230
-		return false;
231
-	}
232
-
233
-	/**
234
-	 * @param $userId
235
-	 * @param $key
236
-	 * @return bool
237
-	 */
238
-	public function setPublicKey($userId, $key) {
239
-		return $this->keyStorage->setUserKey($userId, $this->publicKeyId, $key, Encryption::ID);
240
-	}
241
-
242
-	/**
243
-	 * @param $userId
244
-	 * @param string $key
245
-	 * @return bool
246
-	 */
247
-	public function setPrivateKey($userId, $key) {
248
-		return $this->keyStorage->setUserKey($userId,
249
-			$this->privateKeyId,
250
-			$key,
251
-			Encryption::ID);
252
-	}
253
-
254
-	/**
255
-	 * write file key to key storage
256
-	 *
257
-	 * @param string $path
258
-	 * @param string $key
259
-	 * @return boolean
260
-	 */
261
-	public function setFileKey($path, $key) {
262
-		return $this->keyStorage->setFileKey($path, $this->fileKeyId, $key, Encryption::ID);
263
-	}
264
-
265
-	/**
266
-	 * set all file keys (the file key and the corresponding share keys)
267
-	 *
268
-	 * @param string $path
269
-	 * @param array $keys
270
-	 */
271
-	public function setAllFileKeys($path, $keys) {
272
-		$this->setFileKey($path, $keys['data']);
273
-		foreach ($keys['keys'] as $uid => $keyFile) {
274
-			$this->setShareKey($path, $uid, $keyFile);
275
-		}
276
-	}
277
-
278
-	/**
279
-	 * write share key to the key storage
280
-	 *
281
-	 * @param string $path
282
-	 * @param string $uid
283
-	 * @param string $key
284
-	 * @return boolean
285
-	 */
286
-	public function setShareKey($path, $uid, $key) {
287
-		$keyId = $uid . '.' . $this->shareKeyId;
288
-		return $this->keyStorage->setFileKey($path, $keyId, $key, Encryption::ID);
289
-	}
290
-
291
-	/**
292
-	 * Decrypt private key and store it
293
-	 *
294
-	 * @return boolean
295
-	 */
296
-	public function init(string $uid, ?string $passPhrase) {
297
-		$this->session->setStatus(Session::INIT_EXECUTED);
298
-
299
-		try {
300
-			if ($this->util->isMasterKeyEnabled()) {
301
-				$uid = $this->getMasterKeyId();
302
-				$passPhrase = $this->getMasterKeyPassword();
303
-				$privateKey = $this->getSystemPrivateKey($uid);
304
-			} else {
305
-				if ($passPhrase === null) {
306
-					$this->logger->warning('Master key is disabled but not passphrase provided.');
307
-					return false;
308
-				}
309
-				$privateKey = $this->getPrivateKey($uid);
310
-			}
311
-			$privateKey = $this->crypt->decryptPrivateKey($privateKey, $passPhrase, $uid);
312
-		} catch (PrivateKeyMissingException $e) {
313
-			return false;
314
-		} catch (DecryptionFailedException $e) {
315
-			return false;
316
-		} catch (\Exception $e) {
317
-			$this->logger->warning(
318
-				'Could not decrypt the private key from user "' . $uid . '"" during login. Assume password change on the user back-end.',
319
-				[
320
-					'app' => 'encryption',
321
-					'exception' => $e,
322
-				]
323
-			);
324
-			return false;
325
-		}
326
-
327
-		if ($privateKey) {
328
-			$this->session->setPrivateKey($privateKey);
329
-			$this->session->setStatus(Session::INIT_SUCCESSFUL);
330
-			return true;
331
-		}
332
-
333
-		return false;
334
-	}
335
-
336
-	/**
337
-	 * @param $userId
338
-	 * @return string
339
-	 * @throws PrivateKeyMissingException
340
-	 */
341
-	public function getPrivateKey($userId) {
342
-		$privateKey = $this->keyStorage->getUserKey($userId,
343
-			$this->privateKeyId, Encryption::ID);
344
-
345
-		if (strlen($privateKey) !== 0) {
346
-			return $privateKey;
347
-		}
348
-		throw new PrivateKeyMissingException($userId);
349
-	}
350
-
351
-	/**
352
-	 * @param ?bool $useLegacyFileKey null means try both
353
-	 */
354
-	public function getFileKey(string $path, ?bool $useLegacyFileKey, bool $useDecryptAll = false): string {
355
-		$publicAccess = ($this->keyUid === null);
356
-		$encryptedFileKey = '';
357
-		if ($useLegacyFileKey ?? true) {
358
-			$encryptedFileKey = $this->keyStorage->getFileKey($path, $this->fileKeyId, Encryption::ID);
359
-
360
-			if (empty($encryptedFileKey) && $useLegacyFileKey) {
361
-				return '';
362
-			}
363
-		}
364
-		if ($useDecryptAll) {
365
-			$shareKey = $this->getShareKey($path, $this->session->getDecryptAllUid());
366
-			$privateKey = $this->session->getDecryptAllKey();
367
-		} elseif ($this->util->isMasterKeyEnabled()) {
368
-			$uid = $this->getMasterKeyId();
369
-			$shareKey = $this->getShareKey($path, $uid);
370
-			if ($publicAccess) {
371
-				$privateKey = $this->getSystemPrivateKey($uid);
372
-				$privateKey = $this->crypt->decryptPrivateKey($privateKey, $this->getMasterKeyPassword(), $uid);
373
-			} else {
374
-				// when logged in, the master key is already decrypted in the session
375
-				$privateKey = $this->session->getPrivateKey();
376
-			}
377
-		} elseif ($publicAccess) {
378
-			// use public share key for public links
379
-			$uid = $this->getPublicShareKeyId();
380
-			$shareKey = $this->getShareKey($path, $uid);
381
-			$privateKey = $this->keyStorage->getSystemUserKey($this->publicShareKeyId . '.' . $this->privateKeyId, Encryption::ID);
382
-			$privateKey = $this->crypt->decryptPrivateKey($privateKey);
383
-		} else {
384
-			$uid = $this->keyUid;
385
-			$shareKey = $this->getShareKey($path, $uid);
386
-			$privateKey = $this->session->getPrivateKey();
387
-		}
388
-
389
-		if ($useLegacyFileKey ?? true) {
390
-			if ($encryptedFileKey && $shareKey && $privateKey) {
391
-				return $this->crypt->multiKeyDecryptLegacy($encryptedFileKey,
392
-					$shareKey,
393
-					$privateKey);
394
-			}
395
-		}
396
-		if (!($useLegacyFileKey ?? false)) {
397
-			if ($shareKey && $privateKey) {
398
-				return $this->crypt->multiKeyDecrypt($shareKey, $privateKey);
399
-			}
400
-		}
401
-
402
-		return '';
403
-	}
404
-
405
-	/**
406
-	 * Get the current version of a file
407
-	 *
408
-	 * @param string $path
409
-	 * @param View $view
410
-	 * @return int
411
-	 */
412
-	public function getVersion($path, View $view) {
413
-		$fileInfo = $view->getFileInfo($path);
414
-		if ($fileInfo === false) {
415
-			return 0;
416
-		}
417
-		return $fileInfo->getEncryptedVersion();
418
-	}
419
-
420
-	/**
421
-	 * Set the current version of a file
422
-	 *
423
-	 * @param string $path
424
-	 * @param int $version
425
-	 * @param View $view
426
-	 */
427
-	public function setVersion($path, $version, View $view) {
428
-		$fileInfo = $view->getFileInfo($path);
429
-
430
-		if ($fileInfo !== false) {
431
-			$cache = $fileInfo->getStorage()->getCache();
432
-			$cache->update($fileInfo->getId(), ['encrypted' => $version, 'encryptedVersion' => $version]);
433
-		}
434
-	}
435
-
436
-	/**
437
-	 * get the encrypted file key
438
-	 *
439
-	 * @param string $path
440
-	 * @return string
441
-	 */
442
-	public function getEncryptedFileKey($path) {
443
-		$encryptedFileKey = $this->keyStorage->getFileKey($path,
444
-			$this->fileKeyId, Encryption::ID);
445
-
446
-		return $encryptedFileKey;
447
-	}
448
-
449
-	/**
450
-	 * delete share key
451
-	 *
452
-	 * @param string $path
453
-	 * @param string $keyId
454
-	 * @return boolean
455
-	 */
456
-	public function deleteShareKey($path, $keyId) {
457
-		return $this->keyStorage->deleteFileKey(
458
-			$path,
459
-			$keyId . '.' . $this->shareKeyId,
460
-			Encryption::ID);
461
-	}
462
-
463
-
464
-	/**
465
-	 * @param $path
466
-	 * @param $uid
467
-	 * @return mixed
468
-	 */
469
-	public function getShareKey($path, $uid) {
470
-		$keyId = $uid . '.' . $this->shareKeyId;
471
-		return $this->keyStorage->getFileKey($path, $keyId, Encryption::ID);
472
-	}
473
-
474
-	/**
475
-	 * check if user has a private and a public key
476
-	 *
477
-	 * @param string $userId
478
-	 * @return bool
479
-	 * @throws PrivateKeyMissingException
480
-	 * @throws PublicKeyMissingException
481
-	 */
482
-	public function userHasKeys($userId) {
483
-		$privateKey = $publicKey = true;
484
-		$exception = null;
485
-
486
-		try {
487
-			$this->getPrivateKey($userId);
488
-		} catch (PrivateKeyMissingException $e) {
489
-			$privateKey = false;
490
-			$exception = $e;
491
-		}
492
-		try {
493
-			$this->getPublicKey($userId);
494
-		} catch (PublicKeyMissingException $e) {
495
-			$publicKey = false;
496
-			$exception = $e;
497
-		}
498
-
499
-		if ($privateKey && $publicKey) {
500
-			return true;
501
-		} elseif (!$privateKey && !$publicKey) {
502
-			return false;
503
-		} else {
504
-			throw $exception;
505
-		}
506
-	}
507
-
508
-	/**
509
-	 * @param $userId
510
-	 * @return mixed
511
-	 * @throws PublicKeyMissingException
512
-	 */
513
-	public function getPublicKey($userId) {
514
-		$publicKey = $this->keyStorage->getUserKey($userId, $this->publicKeyId, Encryption::ID);
515
-
516
-		if (strlen($publicKey) !== 0) {
517
-			return $publicKey;
518
-		}
519
-		throw new PublicKeyMissingException($userId);
520
-	}
521
-
522
-	public function getPublicShareKeyId() {
523
-		return $this->publicShareKeyId;
524
-	}
525
-
526
-	/**
527
-	 * get public key for public link shares
528
-	 *
529
-	 * @return string
530
-	 */
531
-	public function getPublicShareKey() {
532
-		return $this->keyStorage->getSystemUserKey($this->publicShareKeyId . '.' . $this->publicKeyId, Encryption::ID);
533
-	}
534
-
535
-	/**
536
-	 * @param string $purpose
537
-	 * @param string $uid
538
-	 */
539
-	public function backupUserKeys($purpose, $uid) {
540
-		$this->keyStorage->backupUserKeys(Encryption::ID, $purpose, $uid);
541
-	}
542
-
543
-	/**
544
-	 * create a backup of the users private and public key and then delete it
545
-	 *
546
-	 * @param string $uid
547
-	 */
548
-	public function deleteUserKeys($uid) {
549
-		$this->deletePublicKey($uid);
550
-		$this->deletePrivateKey($uid);
551
-	}
552
-
553
-	/**
554
-	 * @param $uid
555
-	 * @return bool
556
-	 */
557
-	public function deletePublicKey($uid) {
558
-		return $this->keyStorage->deleteUserKey($uid, $this->publicKeyId, Encryption::ID);
559
-	}
560
-
561
-	/**
562
-	 * @param string $uid
563
-	 * @return bool
564
-	 */
565
-	private function deletePrivateKey($uid) {
566
-		return $this->keyStorage->deleteUserKey($uid, $this->privateKeyId, Encryption::ID);
567
-	}
568
-
569
-	/**
570
-	 * @param string $path
571
-	 * @return bool
572
-	 */
573
-	public function deleteAllFileKeys($path) {
574
-		return $this->keyStorage->deleteAllFileKeys($path);
575
-	}
576
-
577
-	public function deleteLegacyFileKey(string $path): bool {
578
-		return $this->keyStorage->deleteFileKey($path, $this->fileKeyId, Encryption::ID);
579
-	}
580
-
581
-	/**
582
-	 * @param array $userIds
583
-	 * @return array
584
-	 * @throws PublicKeyMissingException
585
-	 */
586
-	public function getPublicKeys(array $userIds) {
587
-		$keys = [];
588
-
589
-		foreach ($userIds as $userId) {
590
-			try {
591
-				$keys[$userId] = $this->getPublicKey($userId);
592
-			} catch (PublicKeyMissingException $e) {
593
-				continue;
594
-			}
595
-		}
596
-
597
-		return $keys;
598
-	}
599
-
600
-	/**
601
-	 * @param string $keyId
602
-	 * @return string returns openssl key
603
-	 */
604
-	public function getSystemPrivateKey($keyId) {
605
-		return $this->keyStorage->getSystemUserKey($keyId . '.' . $this->privateKeyId, Encryption::ID);
606
-	}
607
-
608
-	/**
609
-	 * @param string $keyId
610
-	 * @param string $key
611
-	 * @return string returns openssl key
612
-	 */
613
-	public function setSystemPrivateKey($keyId, $key) {
614
-		return $this->keyStorage->setSystemUserKey(
615
-			$keyId . '.' . $this->privateKeyId,
616
-			$key,
617
-			Encryption::ID);
618
-	}
619
-
620
-	/**
621
-	 * add system keys such as the public share key and the recovery key
622
-	 *
623
-	 * @param array $accessList
624
-	 * @param array $publicKeys
625
-	 * @param string $uid
626
-	 * @return array
627
-	 * @throws PublicKeyMissingException
628
-	 */
629
-	public function addSystemKeys(array $accessList, array $publicKeys, $uid) {
630
-		if (!empty($accessList['public'])) {
631
-			$publicShareKey = $this->getPublicShareKey();
632
-			if (empty($publicShareKey)) {
633
-				throw new PublicKeyMissingException($this->getPublicShareKeyId());
634
-			}
635
-			$publicKeys[$this->getPublicShareKeyId()] = $publicShareKey;
636
-		}
637
-
638
-		if ($this->recoveryKeyExists()
639
-			&& $this->util->isRecoveryEnabledForUser($uid)) {
640
-			$publicKeys[$this->getRecoveryKeyId()] = $this->getRecoveryKey();
641
-		}
642
-
643
-		return $publicKeys;
644
-	}
645
-
646
-	/**
647
-	 * get master key password
648
-	 *
649
-	 * @return string
650
-	 * @throws \Exception
651
-	 */
652
-	public function getMasterKeyPassword() {
653
-		$password = $this->config->getSystemValue('secret');
654
-		if (empty($password)) {
655
-			throw new \Exception('Can not get secret from Nextcloud instance');
656
-		}
657
-
658
-		return $password;
659
-	}
660
-
661
-	/**
662
-	 * return master key id
663
-	 *
664
-	 * @return string
665
-	 */
666
-	public function getMasterKeyId() {
667
-		return $this->masterKeyId;
668
-	}
669
-
670
-	/**
671
-	 * get public master key
672
-	 *
673
-	 * @return string
674
-	 */
675
-	public function getPublicMasterKey() {
676
-		return $this->keyStorage->getSystemUserKey($this->masterKeyId . '.' . $this->publicKeyId, Encryption::ID);
677
-	}
678
-
679
-	/**
680
-	 * get public master key
681
-	 *
682
-	 * @return string
683
-	 */
684
-	public function getPrivateMasterKey() {
685
-		return $this->keyStorage->getSystemUserKey($this->masterKeyId . '.' . $this->privateKeyId, Encryption::ID);
686
-	}
23
+    private string $recoveryKeyId;
24
+    private string $publicShareKeyId;
25
+    private string $masterKeyId;
26
+    private ?string $keyUid;
27
+    private string $publicKeyId = 'publicKey';
28
+    private string $privateKeyId = 'privateKey';
29
+    private string $shareKeyId = 'shareKey';
30
+    private string $fileKeyId = 'fileKey';
31
+
32
+    public function __construct(
33
+        private IStorage $keyStorage,
34
+        private Crypt $crypt,
35
+        private IConfig $config,
36
+        IUserSession $userSession,
37
+        private Session $session,
38
+        private LoggerInterface $logger,
39
+        private Util $util,
40
+        private ILockingProvider $lockingProvider,
41
+    ) {
42
+        $this->recoveryKeyId = $this->config->getAppValue('encryption',
43
+            'recoveryKeyId');
44
+        if (empty($this->recoveryKeyId)) {
45
+            $this->recoveryKeyId = 'recoveryKey_' . substr(md5((string)time()), 0, 8);
46
+            $this->config->setAppValue('encryption',
47
+                'recoveryKeyId',
48
+                $this->recoveryKeyId);
49
+        }
50
+
51
+        $this->publicShareKeyId = $this->config->getAppValue('encryption',
52
+            'publicShareKeyId');
53
+        if (empty($this->publicShareKeyId)) {
54
+            $this->publicShareKeyId = 'pubShare_' . substr(md5((string)time()), 0, 8);
55
+            $this->config->setAppValue('encryption', 'publicShareKeyId', $this->publicShareKeyId);
56
+        }
57
+
58
+        $this->masterKeyId = $this->config->getAppValue('encryption',
59
+            'masterKeyId');
60
+        if (empty($this->masterKeyId)) {
61
+            $this->masterKeyId = 'master_' . substr(md5((string)time()), 0, 8);
62
+            $this->config->setAppValue('encryption', 'masterKeyId', $this->masterKeyId);
63
+        }
64
+
65
+        $this->keyUid = $userSession->isLoggedIn() ? $userSession->getUser()?->getUID() : null;
66
+    }
67
+
68
+    /**
69
+     * check if key pair for public link shares exists, if not we create one
70
+     */
71
+    public function validateShareKey() {
72
+        $shareKey = $this->getPublicShareKey();
73
+        if (empty($shareKey)) {
74
+            $this->lockingProvider->acquireLock('encryption-generateSharedKey', ILockingProvider::LOCK_EXCLUSIVE, 'Encryption: shared key generation');
75
+            try {
76
+                $keyPair = $this->crypt->createKeyPair();
77
+
78
+                // Save public key
79
+                $this->keyStorage->setSystemUserKey(
80
+                    $this->publicShareKeyId . '.' . $this->publicKeyId, $keyPair['publicKey'],
81
+                    Encryption::ID);
82
+
83
+                // Encrypt private key empty passphrase
84
+                $encryptedKey = $this->crypt->encryptPrivateKey($keyPair['privateKey'], '');
85
+                $header = $this->crypt->generateHeader();
86
+                $this->setSystemPrivateKey($this->publicShareKeyId, $header . $encryptedKey);
87
+            } catch (\Throwable $e) {
88
+                $this->lockingProvider->releaseLock('encryption-generateSharedKey', ILockingProvider::LOCK_EXCLUSIVE);
89
+                throw $e;
90
+            }
91
+            $this->lockingProvider->releaseLock('encryption-generateSharedKey', ILockingProvider::LOCK_EXCLUSIVE);
92
+        }
93
+    }
94
+
95
+    /**
96
+     * check if a key pair for the master key exists, if not we create one
97
+     */
98
+    public function validateMasterKey() {
99
+        if ($this->util->isMasterKeyEnabled() === false) {
100
+            return;
101
+        }
102
+
103
+        $publicMasterKey = $this->getPublicMasterKey();
104
+        $privateMasterKey = $this->getPrivateMasterKey();
105
+
106
+        if (empty($publicMasterKey) && empty($privateMasterKey)) {
107
+            // There could be a race condition here if two requests would trigger
108
+            // the generation the second one would enter the key generation as long
109
+            // as the first one didn't write the key to the keystorage yet
110
+            $this->lockingProvider->acquireLock('encryption-generateMasterKey', ILockingProvider::LOCK_EXCLUSIVE, 'Encryption: master key generation');
111
+            try {
112
+                $keyPair = $this->crypt->createKeyPair();
113
+
114
+                // Save public key
115
+                $this->keyStorage->setSystemUserKey(
116
+                    $this->masterKeyId . '.' . $this->publicKeyId, $keyPair['publicKey'],
117
+                    Encryption::ID);
118
+
119
+                // Encrypt private key with system password
120
+                $encryptedKey = $this->crypt->encryptPrivateKey($keyPair['privateKey'], $this->getMasterKeyPassword(), $this->masterKeyId);
121
+                $header = $this->crypt->generateHeader();
122
+                $this->setSystemPrivateKey($this->masterKeyId, $header . $encryptedKey);
123
+            } catch (\Throwable $e) {
124
+                $this->lockingProvider->releaseLock('encryption-generateMasterKey', ILockingProvider::LOCK_EXCLUSIVE);
125
+                throw $e;
126
+            }
127
+            $this->lockingProvider->releaseLock('encryption-generateMasterKey', ILockingProvider::LOCK_EXCLUSIVE);
128
+        } elseif (empty($publicMasterKey)) {
129
+            $this->logger->error('A private master key is available but the public key could not be found. This should never happen.');
130
+            return;
131
+        } elseif (empty($privateMasterKey)) {
132
+            $this->logger->error('A public master key is available but the private key could not be found. This should never happen.');
133
+            return;
134
+        }
135
+
136
+        if (!$this->session->isPrivateKeySet()) {
137
+            $masterKey = $this->getSystemPrivateKey($this->masterKeyId);
138
+            $decryptedMasterKey = $this->crypt->decryptPrivateKey($masterKey, $this->getMasterKeyPassword(), $this->masterKeyId);
139
+            if ($decryptedMasterKey === false) {
140
+                $this->logger->error('A public master key is available but decrypting it failed. This should never happen.');
141
+            } else {
142
+                $this->session->setPrivateKey($decryptedMasterKey);
143
+            }
144
+        }
145
+
146
+        // after the encryption key is available we are ready to go
147
+        $this->session->setStatus(Session::INIT_SUCCESSFUL);
148
+    }
149
+
150
+    /**
151
+     * @return bool
152
+     */
153
+    public function recoveryKeyExists() {
154
+        $key = $this->getRecoveryKey();
155
+        return !empty($key);
156
+    }
157
+
158
+    /**
159
+     * get recovery key
160
+     *
161
+     * @return string
162
+     */
163
+    public function getRecoveryKey() {
164
+        return $this->keyStorage->getSystemUserKey($this->recoveryKeyId . '.' . $this->publicKeyId, Encryption::ID);
165
+    }
166
+
167
+    /**
168
+     * get recovery key ID
169
+     *
170
+     * @return string
171
+     */
172
+    public function getRecoveryKeyId() {
173
+        return $this->recoveryKeyId;
174
+    }
175
+
176
+    /**
177
+     * @param string $password
178
+     * @return bool
179
+     */
180
+    public function checkRecoveryPassword($password) {
181
+        $recoveryKey = $this->keyStorage->getSystemUserKey($this->recoveryKeyId . '.' . $this->privateKeyId, Encryption::ID);
182
+        $decryptedRecoveryKey = $this->crypt->decryptPrivateKey($recoveryKey, $password);
183
+
184
+        if ($decryptedRecoveryKey) {
185
+            return true;
186
+        }
187
+        return false;
188
+    }
189
+
190
+    /**
191
+     * @param string $uid
192
+     * @param string $password
193
+     * @param array $keyPair
194
+     * @return bool
195
+     */
196
+    public function storeKeyPair($uid, $password, $keyPair) {
197
+        // Save Public Key
198
+        $this->setPublicKey($uid, $keyPair['publicKey']);
199
+
200
+        $encryptedKey = $this->crypt->encryptPrivateKey($keyPair['privateKey'], $password, $uid);
201
+
202
+        $header = $this->crypt->generateHeader();
203
+
204
+        if ($encryptedKey) {
205
+            $this->setPrivateKey($uid, $header . $encryptedKey);
206
+            return true;
207
+        }
208
+        return false;
209
+    }
210
+
211
+    /**
212
+     * @param string $password
213
+     * @param array $keyPair
214
+     * @return bool
215
+     */
216
+    public function setRecoveryKey($password, $keyPair) {
217
+        // Save Public Key
218
+        $this->keyStorage->setSystemUserKey($this->getRecoveryKeyId()
219
+            . '.' . $this->publicKeyId,
220
+            $keyPair['publicKey'],
221
+            Encryption::ID);
222
+
223
+        $encryptedKey = $this->crypt->encryptPrivateKey($keyPair['privateKey'], $password);
224
+        $header = $this->crypt->generateHeader();
225
+
226
+        if ($encryptedKey) {
227
+            $this->setSystemPrivateKey($this->getRecoveryKeyId(), $header . $encryptedKey);
228
+            return true;
229
+        }
230
+        return false;
231
+    }
232
+
233
+    /**
234
+     * @param $userId
235
+     * @param $key
236
+     * @return bool
237
+     */
238
+    public function setPublicKey($userId, $key) {
239
+        return $this->keyStorage->setUserKey($userId, $this->publicKeyId, $key, Encryption::ID);
240
+    }
241
+
242
+    /**
243
+     * @param $userId
244
+     * @param string $key
245
+     * @return bool
246
+     */
247
+    public function setPrivateKey($userId, $key) {
248
+        return $this->keyStorage->setUserKey($userId,
249
+            $this->privateKeyId,
250
+            $key,
251
+            Encryption::ID);
252
+    }
253
+
254
+    /**
255
+     * write file key to key storage
256
+     *
257
+     * @param string $path
258
+     * @param string $key
259
+     * @return boolean
260
+     */
261
+    public function setFileKey($path, $key) {
262
+        return $this->keyStorage->setFileKey($path, $this->fileKeyId, $key, Encryption::ID);
263
+    }
264
+
265
+    /**
266
+     * set all file keys (the file key and the corresponding share keys)
267
+     *
268
+     * @param string $path
269
+     * @param array $keys
270
+     */
271
+    public function setAllFileKeys($path, $keys) {
272
+        $this->setFileKey($path, $keys['data']);
273
+        foreach ($keys['keys'] as $uid => $keyFile) {
274
+            $this->setShareKey($path, $uid, $keyFile);
275
+        }
276
+    }
277
+
278
+    /**
279
+     * write share key to the key storage
280
+     *
281
+     * @param string $path
282
+     * @param string $uid
283
+     * @param string $key
284
+     * @return boolean
285
+     */
286
+    public function setShareKey($path, $uid, $key) {
287
+        $keyId = $uid . '.' . $this->shareKeyId;
288
+        return $this->keyStorage->setFileKey($path, $keyId, $key, Encryption::ID);
289
+    }
290
+
291
+    /**
292
+     * Decrypt private key and store it
293
+     *
294
+     * @return boolean
295
+     */
296
+    public function init(string $uid, ?string $passPhrase) {
297
+        $this->session->setStatus(Session::INIT_EXECUTED);
298
+
299
+        try {
300
+            if ($this->util->isMasterKeyEnabled()) {
301
+                $uid = $this->getMasterKeyId();
302
+                $passPhrase = $this->getMasterKeyPassword();
303
+                $privateKey = $this->getSystemPrivateKey($uid);
304
+            } else {
305
+                if ($passPhrase === null) {
306
+                    $this->logger->warning('Master key is disabled but not passphrase provided.');
307
+                    return false;
308
+                }
309
+                $privateKey = $this->getPrivateKey($uid);
310
+            }
311
+            $privateKey = $this->crypt->decryptPrivateKey($privateKey, $passPhrase, $uid);
312
+        } catch (PrivateKeyMissingException $e) {
313
+            return false;
314
+        } catch (DecryptionFailedException $e) {
315
+            return false;
316
+        } catch (\Exception $e) {
317
+            $this->logger->warning(
318
+                'Could not decrypt the private key from user "' . $uid . '"" during login. Assume password change on the user back-end.',
319
+                [
320
+                    'app' => 'encryption',
321
+                    'exception' => $e,
322
+                ]
323
+            );
324
+            return false;
325
+        }
326
+
327
+        if ($privateKey) {
328
+            $this->session->setPrivateKey($privateKey);
329
+            $this->session->setStatus(Session::INIT_SUCCESSFUL);
330
+            return true;
331
+        }
332
+
333
+        return false;
334
+    }
335
+
336
+    /**
337
+     * @param $userId
338
+     * @return string
339
+     * @throws PrivateKeyMissingException
340
+     */
341
+    public function getPrivateKey($userId) {
342
+        $privateKey = $this->keyStorage->getUserKey($userId,
343
+            $this->privateKeyId, Encryption::ID);
344
+
345
+        if (strlen($privateKey) !== 0) {
346
+            return $privateKey;
347
+        }
348
+        throw new PrivateKeyMissingException($userId);
349
+    }
350
+
351
+    /**
352
+     * @param ?bool $useLegacyFileKey null means try both
353
+     */
354
+    public function getFileKey(string $path, ?bool $useLegacyFileKey, bool $useDecryptAll = false): string {
355
+        $publicAccess = ($this->keyUid === null);
356
+        $encryptedFileKey = '';
357
+        if ($useLegacyFileKey ?? true) {
358
+            $encryptedFileKey = $this->keyStorage->getFileKey($path, $this->fileKeyId, Encryption::ID);
359
+
360
+            if (empty($encryptedFileKey) && $useLegacyFileKey) {
361
+                return '';
362
+            }
363
+        }
364
+        if ($useDecryptAll) {
365
+            $shareKey = $this->getShareKey($path, $this->session->getDecryptAllUid());
366
+            $privateKey = $this->session->getDecryptAllKey();
367
+        } elseif ($this->util->isMasterKeyEnabled()) {
368
+            $uid = $this->getMasterKeyId();
369
+            $shareKey = $this->getShareKey($path, $uid);
370
+            if ($publicAccess) {
371
+                $privateKey = $this->getSystemPrivateKey($uid);
372
+                $privateKey = $this->crypt->decryptPrivateKey($privateKey, $this->getMasterKeyPassword(), $uid);
373
+            } else {
374
+                // when logged in, the master key is already decrypted in the session
375
+                $privateKey = $this->session->getPrivateKey();
376
+            }
377
+        } elseif ($publicAccess) {
378
+            // use public share key for public links
379
+            $uid = $this->getPublicShareKeyId();
380
+            $shareKey = $this->getShareKey($path, $uid);
381
+            $privateKey = $this->keyStorage->getSystemUserKey($this->publicShareKeyId . '.' . $this->privateKeyId, Encryption::ID);
382
+            $privateKey = $this->crypt->decryptPrivateKey($privateKey);
383
+        } else {
384
+            $uid = $this->keyUid;
385
+            $shareKey = $this->getShareKey($path, $uid);
386
+            $privateKey = $this->session->getPrivateKey();
387
+        }
388
+
389
+        if ($useLegacyFileKey ?? true) {
390
+            if ($encryptedFileKey && $shareKey && $privateKey) {
391
+                return $this->crypt->multiKeyDecryptLegacy($encryptedFileKey,
392
+                    $shareKey,
393
+                    $privateKey);
394
+            }
395
+        }
396
+        if (!($useLegacyFileKey ?? false)) {
397
+            if ($shareKey && $privateKey) {
398
+                return $this->crypt->multiKeyDecrypt($shareKey, $privateKey);
399
+            }
400
+        }
401
+
402
+        return '';
403
+    }
404
+
405
+    /**
406
+     * Get the current version of a file
407
+     *
408
+     * @param string $path
409
+     * @param View $view
410
+     * @return int
411
+     */
412
+    public function getVersion($path, View $view) {
413
+        $fileInfo = $view->getFileInfo($path);
414
+        if ($fileInfo === false) {
415
+            return 0;
416
+        }
417
+        return $fileInfo->getEncryptedVersion();
418
+    }
419
+
420
+    /**
421
+     * Set the current version of a file
422
+     *
423
+     * @param string $path
424
+     * @param int $version
425
+     * @param View $view
426
+     */
427
+    public function setVersion($path, $version, View $view) {
428
+        $fileInfo = $view->getFileInfo($path);
429
+
430
+        if ($fileInfo !== false) {
431
+            $cache = $fileInfo->getStorage()->getCache();
432
+            $cache->update($fileInfo->getId(), ['encrypted' => $version, 'encryptedVersion' => $version]);
433
+        }
434
+    }
435
+
436
+    /**
437
+     * get the encrypted file key
438
+     *
439
+     * @param string $path
440
+     * @return string
441
+     */
442
+    public function getEncryptedFileKey($path) {
443
+        $encryptedFileKey = $this->keyStorage->getFileKey($path,
444
+            $this->fileKeyId, Encryption::ID);
445
+
446
+        return $encryptedFileKey;
447
+    }
448
+
449
+    /**
450
+     * delete share key
451
+     *
452
+     * @param string $path
453
+     * @param string $keyId
454
+     * @return boolean
455
+     */
456
+    public function deleteShareKey($path, $keyId) {
457
+        return $this->keyStorage->deleteFileKey(
458
+            $path,
459
+            $keyId . '.' . $this->shareKeyId,
460
+            Encryption::ID);
461
+    }
462
+
463
+
464
+    /**
465
+     * @param $path
466
+     * @param $uid
467
+     * @return mixed
468
+     */
469
+    public function getShareKey($path, $uid) {
470
+        $keyId = $uid . '.' . $this->shareKeyId;
471
+        return $this->keyStorage->getFileKey($path, $keyId, Encryption::ID);
472
+    }
473
+
474
+    /**
475
+     * check if user has a private and a public key
476
+     *
477
+     * @param string $userId
478
+     * @return bool
479
+     * @throws PrivateKeyMissingException
480
+     * @throws PublicKeyMissingException
481
+     */
482
+    public function userHasKeys($userId) {
483
+        $privateKey = $publicKey = true;
484
+        $exception = null;
485
+
486
+        try {
487
+            $this->getPrivateKey($userId);
488
+        } catch (PrivateKeyMissingException $e) {
489
+            $privateKey = false;
490
+            $exception = $e;
491
+        }
492
+        try {
493
+            $this->getPublicKey($userId);
494
+        } catch (PublicKeyMissingException $e) {
495
+            $publicKey = false;
496
+            $exception = $e;
497
+        }
498
+
499
+        if ($privateKey && $publicKey) {
500
+            return true;
501
+        } elseif (!$privateKey && !$publicKey) {
502
+            return false;
503
+        } else {
504
+            throw $exception;
505
+        }
506
+    }
507
+
508
+    /**
509
+     * @param $userId
510
+     * @return mixed
511
+     * @throws PublicKeyMissingException
512
+     */
513
+    public function getPublicKey($userId) {
514
+        $publicKey = $this->keyStorage->getUserKey($userId, $this->publicKeyId, Encryption::ID);
515
+
516
+        if (strlen($publicKey) !== 0) {
517
+            return $publicKey;
518
+        }
519
+        throw new PublicKeyMissingException($userId);
520
+    }
521
+
522
+    public function getPublicShareKeyId() {
523
+        return $this->publicShareKeyId;
524
+    }
525
+
526
+    /**
527
+     * get public key for public link shares
528
+     *
529
+     * @return string
530
+     */
531
+    public function getPublicShareKey() {
532
+        return $this->keyStorage->getSystemUserKey($this->publicShareKeyId . '.' . $this->publicKeyId, Encryption::ID);
533
+    }
534
+
535
+    /**
536
+     * @param string $purpose
537
+     * @param string $uid
538
+     */
539
+    public function backupUserKeys($purpose, $uid) {
540
+        $this->keyStorage->backupUserKeys(Encryption::ID, $purpose, $uid);
541
+    }
542
+
543
+    /**
544
+     * create a backup of the users private and public key and then delete it
545
+     *
546
+     * @param string $uid
547
+     */
548
+    public function deleteUserKeys($uid) {
549
+        $this->deletePublicKey($uid);
550
+        $this->deletePrivateKey($uid);
551
+    }
552
+
553
+    /**
554
+     * @param $uid
555
+     * @return bool
556
+     */
557
+    public function deletePublicKey($uid) {
558
+        return $this->keyStorage->deleteUserKey($uid, $this->publicKeyId, Encryption::ID);
559
+    }
560
+
561
+    /**
562
+     * @param string $uid
563
+     * @return bool
564
+     */
565
+    private function deletePrivateKey($uid) {
566
+        return $this->keyStorage->deleteUserKey($uid, $this->privateKeyId, Encryption::ID);
567
+    }
568
+
569
+    /**
570
+     * @param string $path
571
+     * @return bool
572
+     */
573
+    public function deleteAllFileKeys($path) {
574
+        return $this->keyStorage->deleteAllFileKeys($path);
575
+    }
576
+
577
+    public function deleteLegacyFileKey(string $path): bool {
578
+        return $this->keyStorage->deleteFileKey($path, $this->fileKeyId, Encryption::ID);
579
+    }
580
+
581
+    /**
582
+     * @param array $userIds
583
+     * @return array
584
+     * @throws PublicKeyMissingException
585
+     */
586
+    public function getPublicKeys(array $userIds) {
587
+        $keys = [];
588
+
589
+        foreach ($userIds as $userId) {
590
+            try {
591
+                $keys[$userId] = $this->getPublicKey($userId);
592
+            } catch (PublicKeyMissingException $e) {
593
+                continue;
594
+            }
595
+        }
596
+
597
+        return $keys;
598
+    }
599
+
600
+    /**
601
+     * @param string $keyId
602
+     * @return string returns openssl key
603
+     */
604
+    public function getSystemPrivateKey($keyId) {
605
+        return $this->keyStorage->getSystemUserKey($keyId . '.' . $this->privateKeyId, Encryption::ID);
606
+    }
607
+
608
+    /**
609
+     * @param string $keyId
610
+     * @param string $key
611
+     * @return string returns openssl key
612
+     */
613
+    public function setSystemPrivateKey($keyId, $key) {
614
+        return $this->keyStorage->setSystemUserKey(
615
+            $keyId . '.' . $this->privateKeyId,
616
+            $key,
617
+            Encryption::ID);
618
+    }
619
+
620
+    /**
621
+     * add system keys such as the public share key and the recovery key
622
+     *
623
+     * @param array $accessList
624
+     * @param array $publicKeys
625
+     * @param string $uid
626
+     * @return array
627
+     * @throws PublicKeyMissingException
628
+     */
629
+    public function addSystemKeys(array $accessList, array $publicKeys, $uid) {
630
+        if (!empty($accessList['public'])) {
631
+            $publicShareKey = $this->getPublicShareKey();
632
+            if (empty($publicShareKey)) {
633
+                throw new PublicKeyMissingException($this->getPublicShareKeyId());
634
+            }
635
+            $publicKeys[$this->getPublicShareKeyId()] = $publicShareKey;
636
+        }
637
+
638
+        if ($this->recoveryKeyExists()
639
+            && $this->util->isRecoveryEnabledForUser($uid)) {
640
+            $publicKeys[$this->getRecoveryKeyId()] = $this->getRecoveryKey();
641
+        }
642
+
643
+        return $publicKeys;
644
+    }
645
+
646
+    /**
647
+     * get master key password
648
+     *
649
+     * @return string
650
+     * @throws \Exception
651
+     */
652
+    public function getMasterKeyPassword() {
653
+        $password = $this->config->getSystemValue('secret');
654
+        if (empty($password)) {
655
+            throw new \Exception('Can not get secret from Nextcloud instance');
656
+        }
657
+
658
+        return $password;
659
+    }
660
+
661
+    /**
662
+     * return master key id
663
+     *
664
+     * @return string
665
+     */
666
+    public function getMasterKeyId() {
667
+        return $this->masterKeyId;
668
+    }
669
+
670
+    /**
671
+     * get public master key
672
+     *
673
+     * @return string
674
+     */
675
+    public function getPublicMasterKey() {
676
+        return $this->keyStorage->getSystemUserKey($this->masterKeyId . '.' . $this->publicKeyId, Encryption::ID);
677
+    }
678
+
679
+    /**
680
+     * get public master key
681
+     *
682
+     * @return string
683
+     */
684
+    public function getPrivateMasterKey() {
685
+        return $this->keyStorage->getSystemUserKey($this->masterKeyId . '.' . $this->privateKeyId, Encryption::ID);
686
+    }
687 687
 }
Please login to merge, or discard this patch.
Spacing   +22 added lines, -22 removed lines patch added patch discarded remove patch
@@ -42,7 +42,7 @@  discard block
 block discarded – undo
42 42
 		$this->recoveryKeyId = $this->config->getAppValue('encryption',
43 43
 			'recoveryKeyId');
44 44
 		if (empty($this->recoveryKeyId)) {
45
-			$this->recoveryKeyId = 'recoveryKey_' . substr(md5((string)time()), 0, 8);
45
+			$this->recoveryKeyId = 'recoveryKey_'.substr(md5((string) time()), 0, 8);
46 46
 			$this->config->setAppValue('encryption',
47 47
 				'recoveryKeyId',
48 48
 				$this->recoveryKeyId);
@@ -51,14 +51,14 @@  discard block
 block discarded – undo
51 51
 		$this->publicShareKeyId = $this->config->getAppValue('encryption',
52 52
 			'publicShareKeyId');
53 53
 		if (empty($this->publicShareKeyId)) {
54
-			$this->publicShareKeyId = 'pubShare_' . substr(md5((string)time()), 0, 8);
54
+			$this->publicShareKeyId = 'pubShare_'.substr(md5((string) time()), 0, 8);
55 55
 			$this->config->setAppValue('encryption', 'publicShareKeyId', $this->publicShareKeyId);
56 56
 		}
57 57
 
58 58
 		$this->masterKeyId = $this->config->getAppValue('encryption',
59 59
 			'masterKeyId');
60 60
 		if (empty($this->masterKeyId)) {
61
-			$this->masterKeyId = 'master_' . substr(md5((string)time()), 0, 8);
61
+			$this->masterKeyId = 'master_'.substr(md5((string) time()), 0, 8);
62 62
 			$this->config->setAppValue('encryption', 'masterKeyId', $this->masterKeyId);
63 63
 		}
64 64
 
@@ -77,13 +77,13 @@  discard block
 block discarded – undo
77 77
 
78 78
 				// Save public key
79 79
 				$this->keyStorage->setSystemUserKey(
80
-					$this->publicShareKeyId . '.' . $this->publicKeyId, $keyPair['publicKey'],
80
+					$this->publicShareKeyId.'.'.$this->publicKeyId, $keyPair['publicKey'],
81 81
 					Encryption::ID);
82 82
 
83 83
 				// Encrypt private key empty passphrase
84 84
 				$encryptedKey = $this->crypt->encryptPrivateKey($keyPair['privateKey'], '');
85 85
 				$header = $this->crypt->generateHeader();
86
-				$this->setSystemPrivateKey($this->publicShareKeyId, $header . $encryptedKey);
86
+				$this->setSystemPrivateKey($this->publicShareKeyId, $header.$encryptedKey);
87 87
 			} catch (\Throwable $e) {
88 88
 				$this->lockingProvider->releaseLock('encryption-generateSharedKey', ILockingProvider::LOCK_EXCLUSIVE);
89 89
 				throw $e;
@@ -113,13 +113,13 @@  discard block
 block discarded – undo
113 113
 
114 114
 				// Save public key
115 115
 				$this->keyStorage->setSystemUserKey(
116
-					$this->masterKeyId . '.' . $this->publicKeyId, $keyPair['publicKey'],
116
+					$this->masterKeyId.'.'.$this->publicKeyId, $keyPair['publicKey'],
117 117
 					Encryption::ID);
118 118
 
119 119
 				// Encrypt private key with system password
120 120
 				$encryptedKey = $this->crypt->encryptPrivateKey($keyPair['privateKey'], $this->getMasterKeyPassword(), $this->masterKeyId);
121 121
 				$header = $this->crypt->generateHeader();
122
-				$this->setSystemPrivateKey($this->masterKeyId, $header . $encryptedKey);
122
+				$this->setSystemPrivateKey($this->masterKeyId, $header.$encryptedKey);
123 123
 			} catch (\Throwable $e) {
124 124
 				$this->lockingProvider->releaseLock('encryption-generateMasterKey', ILockingProvider::LOCK_EXCLUSIVE);
125 125
 				throw $e;
@@ -161,7 +161,7 @@  discard block
 block discarded – undo
161 161
 	 * @return string
162 162
 	 */
163 163
 	public function getRecoveryKey() {
164
-		return $this->keyStorage->getSystemUserKey($this->recoveryKeyId . '.' . $this->publicKeyId, Encryption::ID);
164
+		return $this->keyStorage->getSystemUserKey($this->recoveryKeyId.'.'.$this->publicKeyId, Encryption::ID);
165 165
 	}
166 166
 
167 167
 	/**
@@ -178,7 +178,7 @@  discard block
 block discarded – undo
178 178
 	 * @return bool
179 179
 	 */
180 180
 	public function checkRecoveryPassword($password) {
181
-		$recoveryKey = $this->keyStorage->getSystemUserKey($this->recoveryKeyId . '.' . $this->privateKeyId, Encryption::ID);
181
+		$recoveryKey = $this->keyStorage->getSystemUserKey($this->recoveryKeyId.'.'.$this->privateKeyId, Encryption::ID);
182 182
 		$decryptedRecoveryKey = $this->crypt->decryptPrivateKey($recoveryKey, $password);
183 183
 
184 184
 		if ($decryptedRecoveryKey) {
@@ -202,7 +202,7 @@  discard block
 block discarded – undo
202 202
 		$header = $this->crypt->generateHeader();
203 203
 
204 204
 		if ($encryptedKey) {
205
-			$this->setPrivateKey($uid, $header . $encryptedKey);
205
+			$this->setPrivateKey($uid, $header.$encryptedKey);
206 206
 			return true;
207 207
 		}
208 208
 		return false;
@@ -216,7 +216,7 @@  discard block
 block discarded – undo
216 216
 	public function setRecoveryKey($password, $keyPair) {
217 217
 		// Save Public Key
218 218
 		$this->keyStorage->setSystemUserKey($this->getRecoveryKeyId()
219
-			. '.' . $this->publicKeyId,
219
+			. '.'.$this->publicKeyId,
220 220
 			$keyPair['publicKey'],
221 221
 			Encryption::ID);
222 222
 
@@ -224,7 +224,7 @@  discard block
 block discarded – undo
224 224
 		$header = $this->crypt->generateHeader();
225 225
 
226 226
 		if ($encryptedKey) {
227
-			$this->setSystemPrivateKey($this->getRecoveryKeyId(), $header . $encryptedKey);
227
+			$this->setSystemPrivateKey($this->getRecoveryKeyId(), $header.$encryptedKey);
228 228
 			return true;
229 229
 		}
230 230
 		return false;
@@ -284,7 +284,7 @@  discard block
 block discarded – undo
284 284
 	 * @return boolean
285 285
 	 */
286 286
 	public function setShareKey($path, $uid, $key) {
287
-		$keyId = $uid . '.' . $this->shareKeyId;
287
+		$keyId = $uid.'.'.$this->shareKeyId;
288 288
 		return $this->keyStorage->setFileKey($path, $keyId, $key, Encryption::ID);
289 289
 	}
290 290
 
@@ -315,7 +315,7 @@  discard block
 block discarded – undo
315 315
 			return false;
316 316
 		} catch (\Exception $e) {
317 317
 			$this->logger->warning(
318
-				'Could not decrypt the private key from user "' . $uid . '"" during login. Assume password change on the user back-end.',
318
+				'Could not decrypt the private key from user "'.$uid.'"" during login. Assume password change on the user back-end.',
319 319
 				[
320 320
 					'app' => 'encryption',
321 321
 					'exception' => $e,
@@ -378,7 +378,7 @@  discard block
 block discarded – undo
378 378
 			// use public share key for public links
379 379
 			$uid = $this->getPublicShareKeyId();
380 380
 			$shareKey = $this->getShareKey($path, $uid);
381
-			$privateKey = $this->keyStorage->getSystemUserKey($this->publicShareKeyId . '.' . $this->privateKeyId, Encryption::ID);
381
+			$privateKey = $this->keyStorage->getSystemUserKey($this->publicShareKeyId.'.'.$this->privateKeyId, Encryption::ID);
382 382
 			$privateKey = $this->crypt->decryptPrivateKey($privateKey);
383 383
 		} else {
384 384
 			$uid = $this->keyUid;
@@ -456,7 +456,7 @@  discard block
 block discarded – undo
456 456
 	public function deleteShareKey($path, $keyId) {
457 457
 		return $this->keyStorage->deleteFileKey(
458 458
 			$path,
459
-			$keyId . '.' . $this->shareKeyId,
459
+			$keyId.'.'.$this->shareKeyId,
460 460
 			Encryption::ID);
461 461
 	}
462 462
 
@@ -467,7 +467,7 @@  discard block
 block discarded – undo
467 467
 	 * @return mixed
468 468
 	 */
469 469
 	public function getShareKey($path, $uid) {
470
-		$keyId = $uid . '.' . $this->shareKeyId;
470
+		$keyId = $uid.'.'.$this->shareKeyId;
471 471
 		return $this->keyStorage->getFileKey($path, $keyId, Encryption::ID);
472 472
 	}
473 473
 
@@ -529,7 +529,7 @@  discard block
 block discarded – undo
529 529
 	 * @return string
530 530
 	 */
531 531
 	public function getPublicShareKey() {
532
-		return $this->keyStorage->getSystemUserKey($this->publicShareKeyId . '.' . $this->publicKeyId, Encryption::ID);
532
+		return $this->keyStorage->getSystemUserKey($this->publicShareKeyId.'.'.$this->publicKeyId, Encryption::ID);
533 533
 	}
534 534
 
535 535
 	/**
@@ -602,7 +602,7 @@  discard block
 block discarded – undo
602 602
 	 * @return string returns openssl key
603 603
 	 */
604 604
 	public function getSystemPrivateKey($keyId) {
605
-		return $this->keyStorage->getSystemUserKey($keyId . '.' . $this->privateKeyId, Encryption::ID);
605
+		return $this->keyStorage->getSystemUserKey($keyId.'.'.$this->privateKeyId, Encryption::ID);
606 606
 	}
607 607
 
608 608
 	/**
@@ -612,7 +612,7 @@  discard block
 block discarded – undo
612 612
 	 */
613 613
 	public function setSystemPrivateKey($keyId, $key) {
614 614
 		return $this->keyStorage->setSystemUserKey(
615
-			$keyId . '.' . $this->privateKeyId,
615
+			$keyId.'.'.$this->privateKeyId,
616 616
 			$key,
617 617
 			Encryption::ID);
618 618
 	}
@@ -673,7 +673,7 @@  discard block
 block discarded – undo
673 673
 	 * @return string
674 674
 	 */
675 675
 	public function getPublicMasterKey() {
676
-		return $this->keyStorage->getSystemUserKey($this->masterKeyId . '.' . $this->publicKeyId, Encryption::ID);
676
+		return $this->keyStorage->getSystemUserKey($this->masterKeyId.'.'.$this->publicKeyId, Encryption::ID);
677 677
 	}
678 678
 
679 679
 	/**
@@ -682,6 +682,6 @@  discard block
 block discarded – undo
682 682
 	 * @return string
683 683
 	 */
684 684
 	public function getPrivateMasterKey() {
685
-		return $this->keyStorage->getSystemUserKey($this->masterKeyId . '.' . $this->privateKeyId, Encryption::ID);
685
+		return $this->keyStorage->getSystemUserKey($this->masterKeyId.'.'.$this->privateKeyId, Encryption::ID);
686 686
 	}
687 687
 }
Please login to merge, or discard this patch.
apps/encryption/lib/Crypto/DecryptAll.php 2 patches
Indentation   +78 added lines, -78 removed lines patch added patch discarded remove patch
@@ -21,91 +21,91 @@
 block discarded – undo
21 21
 use Symfony\Component\Console\Question\Question;
22 22
 
23 23
 class DecryptAll {
24
-	public function __construct(
25
-		protected Util $util,
26
-		protected KeyManager $keyManager,
27
-		protected Crypt $crypt,
28
-		protected Session $session,
29
-		protected QuestionHelper $questionHelper,
30
-	) {
31
-	}
24
+    public function __construct(
25
+        protected Util $util,
26
+        protected KeyManager $keyManager,
27
+        protected Crypt $crypt,
28
+        protected Session $session,
29
+        protected QuestionHelper $questionHelper,
30
+    ) {
31
+    }
32 32
 
33
-	/**
34
-	 * prepare encryption module to decrypt all files
35
-	 */
36
-	public function prepare(InputInterface $input, OutputInterface $output, ?string $user): bool {
37
-		$question = new Question('Please enter the recovery key password: ');
33
+    /**
34
+     * prepare encryption module to decrypt all files
35
+     */
36
+    public function prepare(InputInterface $input, OutputInterface $output, ?string $user): bool {
37
+        $question = new Question('Please enter the recovery key password: ');
38 38
 
39
-		if ($this->util->isMasterKeyEnabled()) {
40
-			$output->writeln('Use master key to decrypt all files');
41
-			$user = $this->keyManager->getMasterKeyId();
42
-			$password = $this->keyManager->getMasterKeyPassword();
43
-		} else {
44
-			$recoveryKeyId = $this->keyManager->getRecoveryKeyId();
45
-			if ($user !== null && $user !== '') {
46
-				$output->writeln('You can only decrypt the users files if you know');
47
-				$output->writeln('the users password or if they activated the recovery key.');
48
-				$output->writeln('');
49
-				$questionUseLoginPassword = new ConfirmationQuestion(
50
-					'Do you want to use the users login password to decrypt all files? (y/n) ',
51
-					false
52
-				);
53
-				$useLoginPassword = $this->questionHelper->ask($input, $output, $questionUseLoginPassword);
54
-				if ($useLoginPassword) {
55
-					$question = new Question('Please enter the user\'s login password: ');
56
-				} elseif ($this->util->isRecoveryEnabledForUser($user) === false) {
57
-					$output->writeln('No recovery key available for user ' . $user);
58
-					return false;
59
-				} else {
60
-					$user = $recoveryKeyId;
61
-				}
62
-			} else {
63
-				$output->writeln('You can only decrypt the files of all users if the');
64
-				$output->writeln('recovery key is enabled by the admin and activated by the users.');
65
-				$output->writeln('');
66
-				$user = $recoveryKeyId;
67
-			}
39
+        if ($this->util->isMasterKeyEnabled()) {
40
+            $output->writeln('Use master key to decrypt all files');
41
+            $user = $this->keyManager->getMasterKeyId();
42
+            $password = $this->keyManager->getMasterKeyPassword();
43
+        } else {
44
+            $recoveryKeyId = $this->keyManager->getRecoveryKeyId();
45
+            if ($user !== null && $user !== '') {
46
+                $output->writeln('You can only decrypt the users files if you know');
47
+                $output->writeln('the users password or if they activated the recovery key.');
48
+                $output->writeln('');
49
+                $questionUseLoginPassword = new ConfirmationQuestion(
50
+                    'Do you want to use the users login password to decrypt all files? (y/n) ',
51
+                    false
52
+                );
53
+                $useLoginPassword = $this->questionHelper->ask($input, $output, $questionUseLoginPassword);
54
+                if ($useLoginPassword) {
55
+                    $question = new Question('Please enter the user\'s login password: ');
56
+                } elseif ($this->util->isRecoveryEnabledForUser($user) === false) {
57
+                    $output->writeln('No recovery key available for user ' . $user);
58
+                    return false;
59
+                } else {
60
+                    $user = $recoveryKeyId;
61
+                }
62
+            } else {
63
+                $output->writeln('You can only decrypt the files of all users if the');
64
+                $output->writeln('recovery key is enabled by the admin and activated by the users.');
65
+                $output->writeln('');
66
+                $user = $recoveryKeyId;
67
+            }
68 68
 
69
-			$question->setHidden(true);
70
-			$question->setHiddenFallback(false);
71
-			$password = $this->questionHelper->ask($input, $output, $question);
72
-		}
69
+            $question->setHidden(true);
70
+            $question->setHiddenFallback(false);
71
+            $password = $this->questionHelper->ask($input, $output, $question);
72
+        }
73 73
 
74
-		$privateKey = $this->getPrivateKey($user, $password);
75
-		if ($privateKey !== false) {
76
-			$this->updateSession($user, $privateKey);
77
-			return true;
78
-		} else {
79
-			$output->writeln('Could not decrypt private key, maybe you entered the wrong password?');
80
-		}
74
+        $privateKey = $this->getPrivateKey($user, $password);
75
+        if ($privateKey !== false) {
76
+            $this->updateSession($user, $privateKey);
77
+            return true;
78
+        } else {
79
+            $output->writeln('Could not decrypt private key, maybe you entered the wrong password?');
80
+        }
81 81
 
82 82
 
83
-		return false;
84
-	}
83
+        return false;
84
+    }
85 85
 
86
-	/**
87
-	 * get the private key which will be used to decrypt all files
88
-	 *
89
-	 * @throws PrivateKeyMissingException
90
-	 */
91
-	protected function getPrivateKey(string $user, string $password): string|false {
92
-		$recoveryKeyId = $this->keyManager->getRecoveryKeyId();
93
-		$masterKeyId = $this->keyManager->getMasterKeyId();
94
-		if ($user === $recoveryKeyId) {
95
-			$recoveryKey = $this->keyManager->getSystemPrivateKey($recoveryKeyId);
96
-			$privateKey = $this->crypt->decryptPrivateKey($recoveryKey, $password);
97
-		} elseif ($user === $masterKeyId) {
98
-			$masterKey = $this->keyManager->getSystemPrivateKey($masterKeyId);
99
-			$privateKey = $this->crypt->decryptPrivateKey($masterKey, $password, $masterKeyId);
100
-		} else {
101
-			$userKey = $this->keyManager->getPrivateKey($user);
102
-			$privateKey = $this->crypt->decryptPrivateKey($userKey, $password, $user);
103
-		}
86
+    /**
87
+     * get the private key which will be used to decrypt all files
88
+     *
89
+     * @throws PrivateKeyMissingException
90
+     */
91
+    protected function getPrivateKey(string $user, string $password): string|false {
92
+        $recoveryKeyId = $this->keyManager->getRecoveryKeyId();
93
+        $masterKeyId = $this->keyManager->getMasterKeyId();
94
+        if ($user === $recoveryKeyId) {
95
+            $recoveryKey = $this->keyManager->getSystemPrivateKey($recoveryKeyId);
96
+            $privateKey = $this->crypt->decryptPrivateKey($recoveryKey, $password);
97
+        } elseif ($user === $masterKeyId) {
98
+            $masterKey = $this->keyManager->getSystemPrivateKey($masterKeyId);
99
+            $privateKey = $this->crypt->decryptPrivateKey($masterKey, $password, $masterKeyId);
100
+        } else {
101
+            $userKey = $this->keyManager->getPrivateKey($user);
102
+            $privateKey = $this->crypt->decryptPrivateKey($userKey, $password, $user);
103
+        }
104 104
 
105
-		return $privateKey;
106
-	}
105
+        return $privateKey;
106
+    }
107 107
 
108
-	protected function updateSession(string $user, string $privateKey): void {
109
-		$this->session->prepareDecryptAll($user, $privateKey);
110
-	}
108
+    protected function updateSession(string $user, string $privateKey): void {
109
+        $this->session->prepareDecryptAll($user, $privateKey);
110
+    }
111 111
 }
Please login to merge, or discard this patch.
Spacing   +2 added lines, -2 removed lines patch added patch discarded remove patch
@@ -54,7 +54,7 @@  discard block
 block discarded – undo
54 54
 				if ($useLoginPassword) {
55 55
 					$question = new Question('Please enter the user\'s login password: ');
56 56
 				} elseif ($this->util->isRecoveryEnabledForUser($user) === false) {
57
-					$output->writeln('No recovery key available for user ' . $user);
57
+					$output->writeln('No recovery key available for user '.$user);
58 58
 					return false;
59 59
 				} else {
60 60
 					$user = $recoveryKeyId;
@@ -88,7 +88,7 @@  discard block
 block discarded – undo
88 88
 	 *
89 89
 	 * @throws PrivateKeyMissingException
90 90
 	 */
91
-	protected function getPrivateKey(string $user, string $password): string|false {
91
+	protected function getPrivateKey(string $user, string $password): string | false {
92 92
 		$recoveryKeyId = $this->keyManager->getRecoveryKeyId();
93 93
 		$masterKeyId = $this->keyManager->getMasterKeyId();
94 94
 		if ($user === $recoveryKeyId) {
Please login to merge, or discard this patch.
apps/encryption/lib/Crypto/EncryptAll.php 2 patches
Indentation   +381 added lines, -381 removed lines patch added patch discarded remove patch
@@ -34,385 +34,385 @@
 block discarded – undo
34 34
 
35 35
 class EncryptAll {
36 36
 
37
-	/** @var array */
38
-	protected $userPasswords;
39
-
40
-	/** @var OutputInterface */
41
-	protected $output;
42
-
43
-	/** @var InputInterface */
44
-	protected $input;
45
-
46
-	public function __construct(
47
-		protected Setup $userSetup,
48
-		protected IUserManager $userManager,
49
-		protected View $rootView,
50
-		protected KeyManager $keyManager,
51
-		protected Util $util,
52
-		protected IConfig $config,
53
-		protected IMailer $mailer,
54
-		protected IL10N $l,
55
-		protected IFactory $l10nFactory,
56
-		protected QuestionHelper $questionHelper,
57
-		protected ISecureRandom $secureRandom,
58
-		protected LoggerInterface $logger,
59
-	) {
60
-		// store one time passwords for the users
61
-		$this->userPasswords = [];
62
-	}
63
-
64
-	/**
65
-	 * start to encrypt all files
66
-	 */
67
-	public function encryptAll(InputInterface $input, OutputInterface $output): void {
68
-		$this->input = $input;
69
-		$this->output = $output;
70
-
71
-		$headline = 'Encrypt all files with the ' . Encryption::DISPLAY_NAME;
72
-		$this->output->writeln("\n");
73
-		$this->output->writeln($headline);
74
-		$this->output->writeln(str_pad('', strlen($headline), '='));
75
-		$this->output->writeln("\n");
76
-
77
-		if ($this->util->isMasterKeyEnabled()) {
78
-			$this->output->writeln('Use master key to encrypt all files.');
79
-			$this->keyManager->validateMasterKey();
80
-		} else {
81
-			//create private/public keys for each user and store the private key password
82
-			$this->output->writeln('Create key-pair for every user');
83
-			$this->output->writeln('------------------------------');
84
-			$this->output->writeln('');
85
-			$this->output->writeln('This module will encrypt all files in the users files folder initially.');
86
-			$this->output->writeln('Already existing versions and files in the trash bin will not be encrypted.');
87
-			$this->output->writeln('');
88
-			$this->createKeyPairs();
89
-		}
90
-
91
-
92
-		// output generated encryption key passwords
93
-		if ($this->util->isMasterKeyEnabled() === false) {
94
-			//send-out or display password list and write it to a file
95
-			$this->output->writeln("\n");
96
-			$this->output->writeln('Generated encryption key passwords');
97
-			$this->output->writeln('----------------------------------');
98
-			$this->output->writeln('');
99
-			$this->outputPasswords();
100
-		}
101
-
102
-		//setup users file system and encrypt all files one by one (take should encrypt setting of storage into account)
103
-		$this->output->writeln("\n");
104
-		$this->output->writeln('Start to encrypt users files');
105
-		$this->output->writeln('----------------------------');
106
-		$this->output->writeln('');
107
-		$this->encryptAllUsersFiles();
108
-		$this->output->writeln("\n");
109
-	}
110
-
111
-	/**
112
-	 * create key-pair for every user
113
-	 */
114
-	protected function createKeyPairs(): void {
115
-		$this->output->writeln("\n");
116
-		$progress = new ProgressBar($this->output);
117
-		$progress->setFormat(" %message% \n [%bar%]");
118
-		$progress->start();
119
-
120
-		foreach ($this->userManager->getBackends() as $backend) {
121
-			$limit = 500;
122
-			$offset = 0;
123
-			do {
124
-				$users = $backend->getUsers('', $limit, $offset);
125
-				foreach ($users as $user) {
126
-					if ($this->keyManager->userHasKeys($user) === false) {
127
-						$progress->setMessage('Create key-pair for ' . $user);
128
-						$progress->advance();
129
-						$this->setupUserFS($user);
130
-						$password = $this->generateOneTimePassword($user);
131
-						$this->userSetup->setupUser($user, $password);
132
-					} else {
133
-						// users which already have a key-pair will be stored with a
134
-						// empty password and filtered out later
135
-						$this->userPasswords[$user] = '';
136
-					}
137
-				}
138
-				$offset += $limit;
139
-			} while (count($users) >= $limit);
140
-		}
141
-
142
-		$progress->setMessage('Key-pair created for all users');
143
-		$progress->finish();
144
-	}
145
-
146
-	/**
147
-	 * iterate over all user and encrypt their files
148
-	 */
149
-	protected function encryptAllUsersFiles(): void {
150
-		$this->output->writeln("\n");
151
-		$progress = new ProgressBar($this->output);
152
-		$progress->setFormat(" %message% \n [%bar%]");
153
-		$progress->start();
154
-		$numberOfUsers = count($this->userPasswords);
155
-		$userNo = 1;
156
-		if ($this->util->isMasterKeyEnabled()) {
157
-			$this->encryptAllUserFilesWithMasterKey($progress);
158
-		} else {
159
-			foreach ($this->userPasswords as $uid => $password) {
160
-				$userCount = "$uid ($userNo of $numberOfUsers)";
161
-				$this->encryptUsersFiles($uid, $progress, $userCount);
162
-				$userNo++;
163
-			}
164
-		}
165
-		$progress->setMessage('all files encrypted');
166
-		$progress->finish();
167
-	}
168
-
169
-	/**
170
-	 * encrypt all user files with the master key
171
-	 */
172
-	protected function encryptAllUserFilesWithMasterKey(ProgressBar $progress): void {
173
-		$userNo = 1;
174
-		foreach ($this->userManager->getBackends() as $backend) {
175
-			$limit = 500;
176
-			$offset = 0;
177
-			do {
178
-				$users = $backend->getUsers('', $limit, $offset);
179
-				foreach ($users as $user) {
180
-					$userCount = "$user ($userNo)";
181
-					$this->encryptUsersFiles($user, $progress, $userCount);
182
-					$userNo++;
183
-				}
184
-				$offset += $limit;
185
-			} while (count($users) >= $limit);
186
-		}
187
-	}
188
-
189
-	/**
190
-	 * encrypt files from the given user
191
-	 */
192
-	protected function encryptUsersFiles(string $uid, ProgressBar $progress, string $userCount): void {
193
-		$this->setupUserFS($uid);
194
-		$directories = [];
195
-		$directories[] = '/' . $uid . '/files';
196
-
197
-		while ($root = array_pop($directories)) {
198
-			$content = $this->rootView->getDirectoryContent($root);
199
-			foreach ($content as $file) {
200
-				$path = $root . '/' . $file->getName();
201
-				if ($file->isShared()) {
202
-					$progress->setMessage("Skip shared file/folder $path");
203
-					$progress->advance();
204
-					continue;
205
-				} elseif ($file->getType() === FileInfo::TYPE_FOLDER) {
206
-					$directories[] = $path;
207
-					continue;
208
-				} else {
209
-					$progress->setMessage("encrypt files for user $userCount: $path");
210
-					$progress->advance();
211
-					try {
212
-						if ($this->encryptFile($file, $path) === false) {
213
-							$progress->setMessage("encrypt files for user $userCount: $path (already encrypted)");
214
-							$progress->advance();
215
-						}
216
-					} catch (\Exception $e) {
217
-						$progress->setMessage("Failed to encrypt path $path: " . $e->getMessage());
218
-						$progress->advance();
219
-						$this->logger->error(
220
-							'Failed to encrypt path {path}',
221
-							[
222
-								'user' => $uid,
223
-								'path' => $path,
224
-								'exception' => $e,
225
-							]
226
-						);
227
-					}
228
-				}
229
-			}
230
-		}
231
-	}
232
-
233
-	protected function encryptFile(FileInfo $fileInfo, string $path): bool {
234
-		// skip already encrypted files
235
-		if ($fileInfo->isEncrypted()) {
236
-			return true;
237
-		}
238
-
239
-		$source = $path;
240
-		$target = $path . '.encrypted.' . time();
241
-
242
-		try {
243
-			$copySuccess = $this->rootView->copy($source, $target);
244
-			if ($copySuccess === false) {
245
-				/* Copy failed, abort */
246
-				if ($this->rootView->file_exists($target)) {
247
-					$this->rootView->unlink($target);
248
-				}
249
-				throw new \Exception('Copy failed for ' . $source);
250
-			}
251
-			$this->rootView->rename($target, $source);
252
-		} catch (DecryptionFailedException $e) {
253
-			if ($this->rootView->file_exists($target)) {
254
-				$this->rootView->unlink($target);
255
-			}
256
-			return false;
257
-		}
258
-
259
-		return true;
260
-	}
261
-
262
-	/**
263
-	 * output one-time encryption passwords
264
-	 */
265
-	protected function outputPasswords(): void {
266
-		$table = new Table($this->output);
267
-		$table->setHeaders(['Username', 'Private key password']);
268
-
269
-		//create rows
270
-		$newPasswords = [];
271
-		$unchangedPasswords = [];
272
-		foreach ($this->userPasswords as $uid => $password) {
273
-			if (empty($password)) {
274
-				$unchangedPasswords[] = $uid;
275
-			} else {
276
-				$newPasswords[] = [$uid, $password];
277
-			}
278
-		}
279
-
280
-		if (empty($newPasswords)) {
281
-			$this->output->writeln("\nAll users already had a key-pair, no further action needed.\n");
282
-			return;
283
-		}
284
-
285
-		$table->setRows($newPasswords);
286
-		$table->render();
287
-
288
-		if (!empty($unchangedPasswords)) {
289
-			$this->output->writeln("\nThe following users already had a key-pair which was reused without setting a new password:\n");
290
-			foreach ($unchangedPasswords as $uid) {
291
-				$this->output->writeln("    $uid");
292
-			}
293
-		}
294
-
295
-		$this->writePasswordsToFile($newPasswords);
296
-
297
-		$this->output->writeln('');
298
-		$question = new ConfirmationQuestion('Do you want to send the passwords directly to the users by mail? (y/n) ', false);
299
-		if ($this->questionHelper->ask($this->input, $this->output, $question)) {
300
-			$this->sendPasswordsByMail();
301
-		}
302
-	}
303
-
304
-	/**
305
-	 * write one-time encryption passwords to a csv file
306
-	 */
307
-	protected function writePasswordsToFile(array $passwords): void {
308
-		$fp = $this->rootView->fopen('oneTimeEncryptionPasswords.csv', 'w');
309
-		foreach ($passwords as $pwd) {
310
-			fputcsv($fp, $pwd);
311
-		}
312
-		fclose($fp);
313
-		$this->output->writeln("\n");
314
-		$this->output->writeln('A list of all newly created passwords was written to data/oneTimeEncryptionPasswords.csv');
315
-		$this->output->writeln('');
316
-		$this->output->writeln('Each of these users need to login to the web interface, go to the');
317
-		$this->output->writeln('personal settings section "basic encryption module" and');
318
-		$this->output->writeln('update the private key password to match the login password again by');
319
-		$this->output->writeln('entering the one-time password into the "old log-in password" field');
320
-		$this->output->writeln('and their current login password');
321
-	}
322
-
323
-	/**
324
-	 * setup user file system
325
-	 */
326
-	protected function setupUserFS(string $uid): void {
327
-		\OC_Util::tearDownFS();
328
-		\OC_Util::setupFS($uid);
329
-	}
330
-
331
-	/**
332
-	 * generate one time password for the user and store it in a array
333
-	 *
334
-	 * @return string password
335
-	 */
336
-	protected function generateOneTimePassword(string $uid): string {
337
-		$password = $this->secureRandom->generate(16, ISecureRandom::CHAR_HUMAN_READABLE);
338
-		$this->userPasswords[$uid] = $password;
339
-		return $password;
340
-	}
341
-
342
-	/**
343
-	 * send encryption key passwords to the users by mail
344
-	 */
345
-	protected function sendPasswordsByMail(): void {
346
-		$noMail = [];
347
-
348
-		$this->output->writeln('');
349
-		$progress = new ProgressBar($this->output, count($this->userPasswords));
350
-		$progress->start();
351
-
352
-		foreach ($this->userPasswords as $uid => $password) {
353
-			$progress->advance();
354
-			if (!empty($password)) {
355
-				$recipient = $this->userManager->get($uid);
356
-				if (!$recipient instanceof IUser) {
357
-					continue;
358
-				}
359
-
360
-				$recipientDisplayName = $recipient->getDisplayName();
361
-				$to = $recipient->getEMailAddress();
362
-
363
-				if ($to === '' || $to === null) {
364
-					$noMail[] = $uid;
365
-					continue;
366
-				}
367
-
368
-				$l = $this->l10nFactory->get('encryption', $this->l10nFactory->getUserLanguage($recipient));
369
-
370
-				$template = $this->mailer->createEMailTemplate('encryption.encryptAllPassword', [
371
-					'user' => $recipient->getUID(),
372
-					'password' => $password,
373
-				]);
374
-
375
-				$template->setSubject($l->t('one-time password for server-side-encryption'));
376
-				// 'Hey there,<br><br>The administration enabled server-side-encryption. Your files were encrypted using the password <strong>%s</strong>.<br><br>
377
-				// Please login to the web interface, go to the section "Basic encryption module" of your personal settings and update your encryption password by entering this password into the "Old log-in password" field and your current login-password.<br><br>'
378
-				$template->addHeader();
379
-				$template->addHeading($l->t('Encryption password'));
380
-				$template->addBodyText(
381
-					$l->t('The administration enabled server-side-encryption. Your files were encrypted using the password <strong>%s</strong>.', [htmlspecialchars($password)]),
382
-					$l->t('The administration enabled server-side-encryption. Your files were encrypted using the password "%s".', $password)
383
-				);
384
-				$template->addBodyText(
385
-					$l->t('Please login to the web interface, go to the "Security" section of your personal settings and update your encryption password by entering this password into the "Old login password" field and your current login password.')
386
-				);
387
-				$template->addFooter();
388
-
389
-				// send it out now
390
-				try {
391
-					$message = $this->mailer->createMessage();
392
-					$message->setTo([$to => $recipientDisplayName]);
393
-					$message->useTemplate($template);
394
-					$message->setAutoSubmitted(AutoSubmitted::VALUE_AUTO_GENERATED);
395
-					$this->mailer->send($message);
396
-				} catch (\Exception $e) {
397
-					$noMail[] = $uid;
398
-				}
399
-			}
400
-		}
401
-
402
-		$progress->finish();
403
-
404
-		if (empty($noMail)) {
405
-			$this->output->writeln("\n\nPassword successfully send to all users");
406
-		} else {
407
-			$table = new Table($this->output);
408
-			$table->setHeaders(['Username', 'Private key password']);
409
-			$this->output->writeln("\n\nCould not send password to following users:\n");
410
-			$rows = [];
411
-			foreach ($noMail as $uid) {
412
-				$rows[] = [$uid, $this->userPasswords[$uid]];
413
-			}
414
-			$table->setRows($rows);
415
-			$table->render();
416
-		}
417
-	}
37
+    /** @var array */
38
+    protected $userPasswords;
39
+
40
+    /** @var OutputInterface */
41
+    protected $output;
42
+
43
+    /** @var InputInterface */
44
+    protected $input;
45
+
46
+    public function __construct(
47
+        protected Setup $userSetup,
48
+        protected IUserManager $userManager,
49
+        protected View $rootView,
50
+        protected KeyManager $keyManager,
51
+        protected Util $util,
52
+        protected IConfig $config,
53
+        protected IMailer $mailer,
54
+        protected IL10N $l,
55
+        protected IFactory $l10nFactory,
56
+        protected QuestionHelper $questionHelper,
57
+        protected ISecureRandom $secureRandom,
58
+        protected LoggerInterface $logger,
59
+    ) {
60
+        // store one time passwords for the users
61
+        $this->userPasswords = [];
62
+    }
63
+
64
+    /**
65
+     * start to encrypt all files
66
+     */
67
+    public function encryptAll(InputInterface $input, OutputInterface $output): void {
68
+        $this->input = $input;
69
+        $this->output = $output;
70
+
71
+        $headline = 'Encrypt all files with the ' . Encryption::DISPLAY_NAME;
72
+        $this->output->writeln("\n");
73
+        $this->output->writeln($headline);
74
+        $this->output->writeln(str_pad('', strlen($headline), '='));
75
+        $this->output->writeln("\n");
76
+
77
+        if ($this->util->isMasterKeyEnabled()) {
78
+            $this->output->writeln('Use master key to encrypt all files.');
79
+            $this->keyManager->validateMasterKey();
80
+        } else {
81
+            //create private/public keys for each user and store the private key password
82
+            $this->output->writeln('Create key-pair for every user');
83
+            $this->output->writeln('------------------------------');
84
+            $this->output->writeln('');
85
+            $this->output->writeln('This module will encrypt all files in the users files folder initially.');
86
+            $this->output->writeln('Already existing versions and files in the trash bin will not be encrypted.');
87
+            $this->output->writeln('');
88
+            $this->createKeyPairs();
89
+        }
90
+
91
+
92
+        // output generated encryption key passwords
93
+        if ($this->util->isMasterKeyEnabled() === false) {
94
+            //send-out or display password list and write it to a file
95
+            $this->output->writeln("\n");
96
+            $this->output->writeln('Generated encryption key passwords');
97
+            $this->output->writeln('----------------------------------');
98
+            $this->output->writeln('');
99
+            $this->outputPasswords();
100
+        }
101
+
102
+        //setup users file system and encrypt all files one by one (take should encrypt setting of storage into account)
103
+        $this->output->writeln("\n");
104
+        $this->output->writeln('Start to encrypt users files');
105
+        $this->output->writeln('----------------------------');
106
+        $this->output->writeln('');
107
+        $this->encryptAllUsersFiles();
108
+        $this->output->writeln("\n");
109
+    }
110
+
111
+    /**
112
+     * create key-pair for every user
113
+     */
114
+    protected function createKeyPairs(): void {
115
+        $this->output->writeln("\n");
116
+        $progress = new ProgressBar($this->output);
117
+        $progress->setFormat(" %message% \n [%bar%]");
118
+        $progress->start();
119
+
120
+        foreach ($this->userManager->getBackends() as $backend) {
121
+            $limit = 500;
122
+            $offset = 0;
123
+            do {
124
+                $users = $backend->getUsers('', $limit, $offset);
125
+                foreach ($users as $user) {
126
+                    if ($this->keyManager->userHasKeys($user) === false) {
127
+                        $progress->setMessage('Create key-pair for ' . $user);
128
+                        $progress->advance();
129
+                        $this->setupUserFS($user);
130
+                        $password = $this->generateOneTimePassword($user);
131
+                        $this->userSetup->setupUser($user, $password);
132
+                    } else {
133
+                        // users which already have a key-pair will be stored with a
134
+                        // empty password and filtered out later
135
+                        $this->userPasswords[$user] = '';
136
+                    }
137
+                }
138
+                $offset += $limit;
139
+            } while (count($users) >= $limit);
140
+        }
141
+
142
+        $progress->setMessage('Key-pair created for all users');
143
+        $progress->finish();
144
+    }
145
+
146
+    /**
147
+     * iterate over all user and encrypt their files
148
+     */
149
+    protected function encryptAllUsersFiles(): void {
150
+        $this->output->writeln("\n");
151
+        $progress = new ProgressBar($this->output);
152
+        $progress->setFormat(" %message% \n [%bar%]");
153
+        $progress->start();
154
+        $numberOfUsers = count($this->userPasswords);
155
+        $userNo = 1;
156
+        if ($this->util->isMasterKeyEnabled()) {
157
+            $this->encryptAllUserFilesWithMasterKey($progress);
158
+        } else {
159
+            foreach ($this->userPasswords as $uid => $password) {
160
+                $userCount = "$uid ($userNo of $numberOfUsers)";
161
+                $this->encryptUsersFiles($uid, $progress, $userCount);
162
+                $userNo++;
163
+            }
164
+        }
165
+        $progress->setMessage('all files encrypted');
166
+        $progress->finish();
167
+    }
168
+
169
+    /**
170
+     * encrypt all user files with the master key
171
+     */
172
+    protected function encryptAllUserFilesWithMasterKey(ProgressBar $progress): void {
173
+        $userNo = 1;
174
+        foreach ($this->userManager->getBackends() as $backend) {
175
+            $limit = 500;
176
+            $offset = 0;
177
+            do {
178
+                $users = $backend->getUsers('', $limit, $offset);
179
+                foreach ($users as $user) {
180
+                    $userCount = "$user ($userNo)";
181
+                    $this->encryptUsersFiles($user, $progress, $userCount);
182
+                    $userNo++;
183
+                }
184
+                $offset += $limit;
185
+            } while (count($users) >= $limit);
186
+        }
187
+    }
188
+
189
+    /**
190
+     * encrypt files from the given user
191
+     */
192
+    protected function encryptUsersFiles(string $uid, ProgressBar $progress, string $userCount): void {
193
+        $this->setupUserFS($uid);
194
+        $directories = [];
195
+        $directories[] = '/' . $uid . '/files';
196
+
197
+        while ($root = array_pop($directories)) {
198
+            $content = $this->rootView->getDirectoryContent($root);
199
+            foreach ($content as $file) {
200
+                $path = $root . '/' . $file->getName();
201
+                if ($file->isShared()) {
202
+                    $progress->setMessage("Skip shared file/folder $path");
203
+                    $progress->advance();
204
+                    continue;
205
+                } elseif ($file->getType() === FileInfo::TYPE_FOLDER) {
206
+                    $directories[] = $path;
207
+                    continue;
208
+                } else {
209
+                    $progress->setMessage("encrypt files for user $userCount: $path");
210
+                    $progress->advance();
211
+                    try {
212
+                        if ($this->encryptFile($file, $path) === false) {
213
+                            $progress->setMessage("encrypt files for user $userCount: $path (already encrypted)");
214
+                            $progress->advance();
215
+                        }
216
+                    } catch (\Exception $e) {
217
+                        $progress->setMessage("Failed to encrypt path $path: " . $e->getMessage());
218
+                        $progress->advance();
219
+                        $this->logger->error(
220
+                            'Failed to encrypt path {path}',
221
+                            [
222
+                                'user' => $uid,
223
+                                'path' => $path,
224
+                                'exception' => $e,
225
+                            ]
226
+                        );
227
+                    }
228
+                }
229
+            }
230
+        }
231
+    }
232
+
233
+    protected function encryptFile(FileInfo $fileInfo, string $path): bool {
234
+        // skip already encrypted files
235
+        if ($fileInfo->isEncrypted()) {
236
+            return true;
237
+        }
238
+
239
+        $source = $path;
240
+        $target = $path . '.encrypted.' . time();
241
+
242
+        try {
243
+            $copySuccess = $this->rootView->copy($source, $target);
244
+            if ($copySuccess === false) {
245
+                /* Copy failed, abort */
246
+                if ($this->rootView->file_exists($target)) {
247
+                    $this->rootView->unlink($target);
248
+                }
249
+                throw new \Exception('Copy failed for ' . $source);
250
+            }
251
+            $this->rootView->rename($target, $source);
252
+        } catch (DecryptionFailedException $e) {
253
+            if ($this->rootView->file_exists($target)) {
254
+                $this->rootView->unlink($target);
255
+            }
256
+            return false;
257
+        }
258
+
259
+        return true;
260
+    }
261
+
262
+    /**
263
+     * output one-time encryption passwords
264
+     */
265
+    protected function outputPasswords(): void {
266
+        $table = new Table($this->output);
267
+        $table->setHeaders(['Username', 'Private key password']);
268
+
269
+        //create rows
270
+        $newPasswords = [];
271
+        $unchangedPasswords = [];
272
+        foreach ($this->userPasswords as $uid => $password) {
273
+            if (empty($password)) {
274
+                $unchangedPasswords[] = $uid;
275
+            } else {
276
+                $newPasswords[] = [$uid, $password];
277
+            }
278
+        }
279
+
280
+        if (empty($newPasswords)) {
281
+            $this->output->writeln("\nAll users already had a key-pair, no further action needed.\n");
282
+            return;
283
+        }
284
+
285
+        $table->setRows($newPasswords);
286
+        $table->render();
287
+
288
+        if (!empty($unchangedPasswords)) {
289
+            $this->output->writeln("\nThe following users already had a key-pair which was reused without setting a new password:\n");
290
+            foreach ($unchangedPasswords as $uid) {
291
+                $this->output->writeln("    $uid");
292
+            }
293
+        }
294
+
295
+        $this->writePasswordsToFile($newPasswords);
296
+
297
+        $this->output->writeln('');
298
+        $question = new ConfirmationQuestion('Do you want to send the passwords directly to the users by mail? (y/n) ', false);
299
+        if ($this->questionHelper->ask($this->input, $this->output, $question)) {
300
+            $this->sendPasswordsByMail();
301
+        }
302
+    }
303
+
304
+    /**
305
+     * write one-time encryption passwords to a csv file
306
+     */
307
+    protected function writePasswordsToFile(array $passwords): void {
308
+        $fp = $this->rootView->fopen('oneTimeEncryptionPasswords.csv', 'w');
309
+        foreach ($passwords as $pwd) {
310
+            fputcsv($fp, $pwd);
311
+        }
312
+        fclose($fp);
313
+        $this->output->writeln("\n");
314
+        $this->output->writeln('A list of all newly created passwords was written to data/oneTimeEncryptionPasswords.csv');
315
+        $this->output->writeln('');
316
+        $this->output->writeln('Each of these users need to login to the web interface, go to the');
317
+        $this->output->writeln('personal settings section "basic encryption module" and');
318
+        $this->output->writeln('update the private key password to match the login password again by');
319
+        $this->output->writeln('entering the one-time password into the "old log-in password" field');
320
+        $this->output->writeln('and their current login password');
321
+    }
322
+
323
+    /**
324
+     * setup user file system
325
+     */
326
+    protected function setupUserFS(string $uid): void {
327
+        \OC_Util::tearDownFS();
328
+        \OC_Util::setupFS($uid);
329
+    }
330
+
331
+    /**
332
+     * generate one time password for the user and store it in a array
333
+     *
334
+     * @return string password
335
+     */
336
+    protected function generateOneTimePassword(string $uid): string {
337
+        $password = $this->secureRandom->generate(16, ISecureRandom::CHAR_HUMAN_READABLE);
338
+        $this->userPasswords[$uid] = $password;
339
+        return $password;
340
+    }
341
+
342
+    /**
343
+     * send encryption key passwords to the users by mail
344
+     */
345
+    protected function sendPasswordsByMail(): void {
346
+        $noMail = [];
347
+
348
+        $this->output->writeln('');
349
+        $progress = new ProgressBar($this->output, count($this->userPasswords));
350
+        $progress->start();
351
+
352
+        foreach ($this->userPasswords as $uid => $password) {
353
+            $progress->advance();
354
+            if (!empty($password)) {
355
+                $recipient = $this->userManager->get($uid);
356
+                if (!$recipient instanceof IUser) {
357
+                    continue;
358
+                }
359
+
360
+                $recipientDisplayName = $recipient->getDisplayName();
361
+                $to = $recipient->getEMailAddress();
362
+
363
+                if ($to === '' || $to === null) {
364
+                    $noMail[] = $uid;
365
+                    continue;
366
+                }
367
+
368
+                $l = $this->l10nFactory->get('encryption', $this->l10nFactory->getUserLanguage($recipient));
369
+
370
+                $template = $this->mailer->createEMailTemplate('encryption.encryptAllPassword', [
371
+                    'user' => $recipient->getUID(),
372
+                    'password' => $password,
373
+                ]);
374
+
375
+                $template->setSubject($l->t('one-time password for server-side-encryption'));
376
+                // 'Hey there,<br><br>The administration enabled server-side-encryption. Your files were encrypted using the password <strong>%s</strong>.<br><br>
377
+                // Please login to the web interface, go to the section "Basic encryption module" of your personal settings and update your encryption password by entering this password into the "Old log-in password" field and your current login-password.<br><br>'
378
+                $template->addHeader();
379
+                $template->addHeading($l->t('Encryption password'));
380
+                $template->addBodyText(
381
+                    $l->t('The administration enabled server-side-encryption. Your files were encrypted using the password <strong>%s</strong>.', [htmlspecialchars($password)]),
382
+                    $l->t('The administration enabled server-side-encryption. Your files were encrypted using the password "%s".', $password)
383
+                );
384
+                $template->addBodyText(
385
+                    $l->t('Please login to the web interface, go to the "Security" section of your personal settings and update your encryption password by entering this password into the "Old login password" field and your current login password.')
386
+                );
387
+                $template->addFooter();
388
+
389
+                // send it out now
390
+                try {
391
+                    $message = $this->mailer->createMessage();
392
+                    $message->setTo([$to => $recipientDisplayName]);
393
+                    $message->useTemplate($template);
394
+                    $message->setAutoSubmitted(AutoSubmitted::VALUE_AUTO_GENERATED);
395
+                    $this->mailer->send($message);
396
+                } catch (\Exception $e) {
397
+                    $noMail[] = $uid;
398
+                }
399
+            }
400
+        }
401
+
402
+        $progress->finish();
403
+
404
+        if (empty($noMail)) {
405
+            $this->output->writeln("\n\nPassword successfully send to all users");
406
+        } else {
407
+            $table = new Table($this->output);
408
+            $table->setHeaders(['Username', 'Private key password']);
409
+            $this->output->writeln("\n\nCould not send password to following users:\n");
410
+            $rows = [];
411
+            foreach ($noMail as $uid) {
412
+                $rows[] = [$uid, $this->userPasswords[$uid]];
413
+            }
414
+            $table->setRows($rows);
415
+            $table->render();
416
+        }
417
+    }
418 418
 }
Please login to merge, or discard this patch.
Spacing   +7 added lines, -7 removed lines patch added patch discarded remove patch
@@ -68,7 +68,7 @@  discard block
 block discarded – undo
68 68
 		$this->input = $input;
69 69
 		$this->output = $output;
70 70
 
71
-		$headline = 'Encrypt all files with the ' . Encryption::DISPLAY_NAME;
71
+		$headline = 'Encrypt all files with the '.Encryption::DISPLAY_NAME;
72 72
 		$this->output->writeln("\n");
73 73
 		$this->output->writeln($headline);
74 74
 		$this->output->writeln(str_pad('', strlen($headline), '='));
@@ -124,7 +124,7 @@  discard block
 block discarded – undo
124 124
 				$users = $backend->getUsers('', $limit, $offset);
125 125
 				foreach ($users as $user) {
126 126
 					if ($this->keyManager->userHasKeys($user) === false) {
127
-						$progress->setMessage('Create key-pair for ' . $user);
127
+						$progress->setMessage('Create key-pair for '.$user);
128 128
 						$progress->advance();
129 129
 						$this->setupUserFS($user);
130 130
 						$password = $this->generateOneTimePassword($user);
@@ -192,12 +192,12 @@  discard block
 block discarded – undo
192 192
 	protected function encryptUsersFiles(string $uid, ProgressBar $progress, string $userCount): void {
193 193
 		$this->setupUserFS($uid);
194 194
 		$directories = [];
195
-		$directories[] = '/' . $uid . '/files';
195
+		$directories[] = '/'.$uid.'/files';
196 196
 
197 197
 		while ($root = array_pop($directories)) {
198 198
 			$content = $this->rootView->getDirectoryContent($root);
199 199
 			foreach ($content as $file) {
200
-				$path = $root . '/' . $file->getName();
200
+				$path = $root.'/'.$file->getName();
201 201
 				if ($file->isShared()) {
202 202
 					$progress->setMessage("Skip shared file/folder $path");
203 203
 					$progress->advance();
@@ -214,7 +214,7 @@  discard block
 block discarded – undo
214 214
 							$progress->advance();
215 215
 						}
216 216
 					} catch (\Exception $e) {
217
-						$progress->setMessage("Failed to encrypt path $path: " . $e->getMessage());
217
+						$progress->setMessage("Failed to encrypt path $path: ".$e->getMessage());
218 218
 						$progress->advance();
219 219
 						$this->logger->error(
220 220
 							'Failed to encrypt path {path}',
@@ -237,7 +237,7 @@  discard block
 block discarded – undo
237 237
 		}
238 238
 
239 239
 		$source = $path;
240
-		$target = $path . '.encrypted.' . time();
240
+		$target = $path.'.encrypted.'.time();
241 241
 
242 242
 		try {
243 243
 			$copySuccess = $this->rootView->copy($source, $target);
@@ -246,7 +246,7 @@  discard block
 block discarded – undo
246 246
 				if ($this->rootView->file_exists($target)) {
247 247
 					$this->rootView->unlink($target);
248 248
 				}
249
-				throw new \Exception('Copy failed for ' . $source);
249
+				throw new \Exception('Copy failed for '.$source);
250 250
 			}
251 251
 			$this->rootView->rename($target, $source);
252 252
 		} catch (DecryptionFailedException $e) {
Please login to merge, or discard this patch.
apps/encryption/lib/Crypto/Crypt.php 2 patches
Indentation   +777 added lines, -777 removed lines patch added patch discarded remove patch
@@ -34,781 +34,781 @@
 block discarded – undo
34 34
  * @package OCA\Encryption\Crypto
35 35
  */
36 36
 class Crypt {
37
-	public const SUPPORTED_CIPHERS_AND_KEY_SIZE = [
38
-		'AES-256-CTR' => 32,
39
-		'AES-128-CTR' => 16,
40
-		'AES-256-CFB' => 32,
41
-		'AES-128-CFB' => 16,
42
-	];
43
-	// one out of SUPPORTED_CIPHERS_AND_KEY_SIZE
44
-	public const DEFAULT_CIPHER = 'AES-256-CTR';
45
-	// default cipher from old Nextcloud versions
46
-	public const LEGACY_CIPHER = 'AES-128-CFB';
47
-
48
-	public const SUPPORTED_KEY_FORMATS = ['hash2', 'hash', 'password'];
49
-	// one out of SUPPORTED_KEY_FORMATS
50
-	public const DEFAULT_KEY_FORMAT = 'hash2';
51
-	// default key format, old Nextcloud version encrypted the private key directly
52
-	// with the user password
53
-	public const LEGACY_KEY_FORMAT = 'password';
54
-
55
-	public const HEADER_START = 'HBEGIN';
56
-	public const HEADER_END = 'HEND';
57
-
58
-	// default encoding format, old Nextcloud versions used base64
59
-	public const BINARY_ENCODING_FORMAT = 'binary';
60
-
61
-	private string $user;
62
-
63
-	private ?string $currentCipher = null;
64
-
65
-	private bool $supportLegacy;
66
-
67
-	/**
68
-	 * Use the legacy base64 encoding instead of the more space-efficient binary encoding.
69
-	 */
70
-	private bool $useLegacyBase64Encoding;
71
-
72
-	public function __construct(
73
-		private LoggerInterface $logger,
74
-		IUserSession $userSession,
75
-		private IConfig $config,
76
-		private IL10N $l,
77
-	) {
78
-		$this->user = $userSession->isLoggedIn() ? $userSession->getUser()->getUID() : '"no user given"';
79
-		$this->supportLegacy = $this->config->getSystemValueBool('encryption.legacy_format_support', false);
80
-		$this->useLegacyBase64Encoding = $this->config->getSystemValueBool('encryption.use_legacy_base64_encoding', false);
81
-	}
82
-
83
-	/**
84
-	 * create new private/public key-pair for user
85
-	 *
86
-	 * @return array{publicKey: string, privateKey: string}|false
87
-	 */
88
-	public function createKeyPair() {
89
-		$res = $this->getOpenSSLPKey();
90
-
91
-		if (!$res) {
92
-			$this->logger->error("Encryption Library couldn't generate users key-pair for {$this->user}",
93
-				['app' => 'encryption']);
94
-
95
-			if (openssl_error_string()) {
96
-				$this->logger->error('Encryption library openssl_pkey_new() fails: ' . openssl_error_string(),
97
-					['app' => 'encryption']);
98
-			}
99
-		} elseif (openssl_pkey_export($res,
100
-			$privateKey,
101
-			null,
102
-			$this->getOpenSSLConfig())) {
103
-			$keyDetails = openssl_pkey_get_details($res);
104
-			$publicKey = $keyDetails['key'];
105
-
106
-			return [
107
-				'publicKey' => $publicKey,
108
-				'privateKey' => $privateKey
109
-			];
110
-		}
111
-		$this->logger->error('Encryption library couldn\'t export users private key, please check your servers OpenSSL configuration.' . $this->user,
112
-			['app' => 'encryption']);
113
-		if (openssl_error_string()) {
114
-			$this->logger->error('Encryption Library:' . openssl_error_string(),
115
-				['app' => 'encryption']);
116
-		}
117
-
118
-		return false;
119
-	}
120
-
121
-	/**
122
-	 * Generates a new private key
123
-	 *
124
-	 * @return \OpenSSLAsymmetricKey|false
125
-	 */
126
-	public function getOpenSSLPKey() {
127
-		$config = $this->getOpenSSLConfig();
128
-		return openssl_pkey_new($config);
129
-	}
130
-
131
-	private function getOpenSSLConfig(): array {
132
-		$config = ['private_key_bits' => 4096];
133
-		$config = array_merge(
134
-			$config,
135
-			$this->config->getSystemValue('openssl', [])
136
-		);
137
-		return $config;
138
-	}
139
-
140
-	/**
141
-	 * @throws EncryptionFailedException
142
-	 */
143
-	public function symmetricEncryptFileContent(string $plainContent, string $passPhrase, int $version, string $position): string|false {
144
-		if (!$plainContent) {
145
-			$this->logger->error('Encryption Library, symmetrical encryption failed no content given',
146
-				['app' => 'encryption']);
147
-			return false;
148
-		}
149
-
150
-		$iv = $this->generateIv();
151
-
152
-		$encryptedContent = $this->encrypt($plainContent,
153
-			$iv,
154
-			$passPhrase,
155
-			$this->getCipher());
156
-
157
-		// Create a signature based on the key as well as the current version
158
-		$sig = $this->createSignature($encryptedContent, $passPhrase . '_' . $version . '_' . $position);
159
-
160
-		// combine content to encrypt the IV identifier and actual IV
161
-		$catFile = $this->concatIV($encryptedContent, $iv);
162
-		$catFile = $this->concatSig($catFile, $sig);
163
-		return $this->addPadding($catFile);
164
-	}
165
-
166
-	/**
167
-	 * generate header for encrypted file
168
-	 *
169
-	 * @param string $keyFormat see SUPPORTED_KEY_FORMATS
170
-	 * @return string
171
-	 * @throws \InvalidArgumentException
172
-	 */
173
-	public function generateHeader($keyFormat = self::DEFAULT_KEY_FORMAT) {
174
-		if (in_array($keyFormat, self::SUPPORTED_KEY_FORMATS, true) === false) {
175
-			throw new \InvalidArgumentException('key format "' . $keyFormat . '" is not supported');
176
-		}
177
-
178
-		$header = self::HEADER_START
179
-			. ':cipher:' . $this->getCipher()
180
-			. ':keyFormat:' . $keyFormat;
181
-
182
-		if ($this->useLegacyBase64Encoding !== true) {
183
-			$header .= ':encoding:' . self::BINARY_ENCODING_FORMAT;
184
-		}
185
-
186
-		$header .= ':' . self::HEADER_END;
187
-
188
-		return $header;
189
-	}
190
-
191
-	/**
192
-	 * @throws EncryptionFailedException
193
-	 */
194
-	private function encrypt(string $plainContent, string $iv, string $passPhrase = '', string $cipher = self::DEFAULT_CIPHER): string {
195
-		$options = $this->useLegacyBase64Encoding ? 0 : OPENSSL_RAW_DATA;
196
-		$encryptedContent = openssl_encrypt($plainContent,
197
-			$cipher,
198
-			$passPhrase,
199
-			$options,
200
-			$iv);
201
-
202
-		if (!$encryptedContent) {
203
-			$error = 'Encryption (symmetric) of content failed';
204
-			$this->logger->error($error . openssl_error_string(),
205
-				['app' => 'encryption']);
206
-			throw new EncryptionFailedException($error);
207
-		}
208
-
209
-		return $encryptedContent;
210
-	}
211
-
212
-	/**
213
-	 * return cipher either from config.php or the default cipher defined in
214
-	 * this class
215
-	 */
216
-	private function getCachedCipher(): string {
217
-		if (isset($this->currentCipher)) {
218
-			return $this->currentCipher;
219
-		}
220
-
221
-		// Get cipher either from config.php or the default cipher defined in this class
222
-		$cipher = $this->config->getSystemValueString('cipher', self::DEFAULT_CIPHER);
223
-		if (!isset(self::SUPPORTED_CIPHERS_AND_KEY_SIZE[$cipher])) {
224
-			$this->logger->warning(
225
-				sprintf(
226
-					'Unsupported cipher (%s) defined in config.php supported. Falling back to %s',
227
-					$cipher,
228
-					self::DEFAULT_CIPHER
229
-				),
230
-				['app' => 'encryption']
231
-			);
232
-			$cipher = self::DEFAULT_CIPHER;
233
-		}
234
-
235
-		// Remember current cipher to avoid frequent lookups
236
-		$this->currentCipher = $cipher;
237
-		return $this->currentCipher;
238
-	}
239
-
240
-	/**
241
-	 * return current encryption cipher
242
-	 *
243
-	 * @return string
244
-	 */
245
-	public function getCipher() {
246
-		return $this->getCachedCipher();
247
-	}
248
-
249
-	/**
250
-	 * get key size depending on the cipher
251
-	 *
252
-	 * @param string $cipher
253
-	 * @return int
254
-	 * @throws \InvalidArgumentException
255
-	 */
256
-	protected function getKeySize($cipher) {
257
-		if (isset(self::SUPPORTED_CIPHERS_AND_KEY_SIZE[$cipher])) {
258
-			return self::SUPPORTED_CIPHERS_AND_KEY_SIZE[$cipher];
259
-		}
260
-
261
-		throw new \InvalidArgumentException(
262
-			sprintf(
263
-				'Unsupported cipher (%s) defined.',
264
-				$cipher
265
-			)
266
-		);
267
-	}
268
-
269
-	/**
270
-	 * get legacy cipher
271
-	 *
272
-	 * @return string
273
-	 */
274
-	public function getLegacyCipher() {
275
-		if (!$this->supportLegacy) {
276
-			throw new ServerNotAvailableException('Legacy cipher is no longer supported!');
277
-		}
278
-
279
-		return self::LEGACY_CIPHER;
280
-	}
281
-
282
-	private function concatIV(string $encryptedContent, string $iv): string {
283
-		return $encryptedContent . '00iv00' . $iv;
284
-	}
285
-
286
-	private function concatSig(string $encryptedContent, string $signature): string {
287
-		return $encryptedContent . '00sig00' . $signature;
288
-	}
289
-
290
-	/**
291
-	 * Note: This is _NOT_ a padding used for encryption purposes. It is solely
292
-	 * used to achieve the PHP stream size. It has _NOTHING_ to do with the
293
-	 * encrypted content and is not used in any crypto primitive.
294
-	 */
295
-	private function addPadding(string $data): string {
296
-		return $data . 'xxx';
297
-	}
298
-
299
-	/**
300
-	 * generate password hash used to encrypt the users private key
301
-	 *
302
-	 * @param string $uid only used for user keys
303
-	 */
304
-	protected function generatePasswordHash(string $password, string $cipher, string $uid = '', int $iterations = 600000): string {
305
-		$instanceId = $this->config->getSystemValue('instanceid');
306
-		$instanceSecret = $this->config->getSystemValue('secret');
307
-		$salt = hash('sha256', $uid . $instanceId . $instanceSecret, true);
308
-		$keySize = $this->getKeySize($cipher);
309
-
310
-		return hash_pbkdf2(
311
-			'sha256',
312
-			$password,
313
-			$salt,
314
-			$iterations,
315
-			$keySize,
316
-			true
317
-		);
318
-	}
319
-
320
-	/**
321
-	 * encrypt private key
322
-	 *
323
-	 * @param string $privateKey
324
-	 * @param string $password
325
-	 * @param string $uid for regular users, empty for system keys
326
-	 * @return false|string
327
-	 */
328
-	public function encryptPrivateKey($privateKey, $password, $uid = '') {
329
-		$cipher = $this->getCipher();
330
-		$hash = $this->generatePasswordHash($password, $cipher, $uid);
331
-		$encryptedKey = $this->symmetricEncryptFileContent(
332
-			$privateKey,
333
-			$hash,
334
-			0,
335
-			'0'
336
-		);
337
-
338
-		return $encryptedKey;
339
-	}
340
-
341
-	/**
342
-	 * @param string $privateKey
343
-	 * @param string $password
344
-	 * @param string $uid for regular users, empty for system keys
345
-	 */
346
-	public function decryptPrivateKey($privateKey, $password = '', $uid = '') : string|false {
347
-		$header = $this->parseHeader($privateKey);
348
-
349
-		if (isset($header['cipher'])) {
350
-			$cipher = $header['cipher'];
351
-		} else {
352
-			$cipher = $this->getLegacyCipher();
353
-		}
354
-
355
-		if (isset($header['keyFormat'])) {
356
-			$keyFormat = $header['keyFormat'];
357
-		} else {
358
-			$keyFormat = self::LEGACY_KEY_FORMAT;
359
-		}
360
-
361
-		if ($keyFormat === 'hash') {
362
-			$password = $this->generatePasswordHash($password, $cipher, $uid, 100000);
363
-		} elseif ($keyFormat === 'hash2') {
364
-			$password = $this->generatePasswordHash($password, $cipher, $uid, 600000);
365
-		}
366
-
367
-		$binaryEncoding = isset($header['encoding']) && $header['encoding'] === self::BINARY_ENCODING_FORMAT;
368
-
369
-		// If we found a header we need to remove it from the key we want to decrypt
370
-		if (!empty($header)) {
371
-			$privateKey = substr($privateKey,
372
-				strpos($privateKey,
373
-					self::HEADER_END) + strlen(self::HEADER_END));
374
-		}
375
-
376
-		$plainKey = $this->symmetricDecryptFileContent(
377
-			$privateKey,
378
-			$password,
379
-			$cipher,
380
-			0,
381
-			0,
382
-			$binaryEncoding
383
-		);
384
-
385
-		if ($this->isValidPrivateKey($plainKey) === false) {
386
-			return false;
387
-		}
388
-
389
-		return $plainKey;
390
-	}
391
-
392
-	/**
393
-	 * check if it is a valid private key
394
-	 *
395
-	 * @param string $plainKey
396
-	 * @return bool
397
-	 */
398
-	protected function isValidPrivateKey($plainKey) {
399
-		$res = openssl_get_privatekey($plainKey);
400
-		if (is_object($res) && get_class($res) === 'OpenSSLAsymmetricKey') {
401
-			$sslInfo = openssl_pkey_get_details($res);
402
-			if (isset($sslInfo['key'])) {
403
-				return true;
404
-			}
405
-		}
406
-
407
-		return false;
408
-	}
409
-
410
-	/**
411
-	 * @param string $keyFileContents
412
-	 * @param string $passPhrase
413
-	 * @param string $cipher
414
-	 * @param int $version
415
-	 * @param int|string $position
416
-	 * @param boolean $binaryEncoding
417
-	 * @return string
418
-	 * @throws DecryptionFailedException
419
-	 */
420
-	public function symmetricDecryptFileContent($keyFileContents, $passPhrase, $cipher = self::DEFAULT_CIPHER, $version = 0, $position = 0, bool $binaryEncoding = false) {
421
-		if ($keyFileContents == '') {
422
-			return '';
423
-		}
424
-
425
-		$catFile = $this->splitMetaData($keyFileContents, $cipher);
426
-
427
-		if ($catFile['signature'] !== false) {
428
-			try {
429
-				// First try the new format
430
-				$this->checkSignature($catFile['encrypted'], $passPhrase . '_' . $version . '_' . $position, $catFile['signature']);
431
-			} catch (GenericEncryptionException $e) {
432
-				// For compatibility with old files check the version without _
433
-				$this->checkSignature($catFile['encrypted'], $passPhrase . $version . $position, $catFile['signature']);
434
-			}
435
-		}
436
-
437
-		return $this->decrypt($catFile['encrypted'],
438
-			$catFile['iv'],
439
-			$passPhrase,
440
-			$cipher,
441
-			$binaryEncoding);
442
-	}
443
-
444
-	/**
445
-	 * check for valid signature
446
-	 *
447
-	 * @throws GenericEncryptionException
448
-	 */
449
-	private function checkSignature(string $data, string $passPhrase, string $expectedSignature): void {
450
-		$enforceSignature = !$this->config->getSystemValueBool('encryption_skip_signature_check', false);
451
-
452
-		$signature = $this->createSignature($data, $passPhrase);
453
-		$isCorrectHash = hash_equals($expectedSignature, $signature);
454
-
455
-		if (!$isCorrectHash) {
456
-			if ($enforceSignature) {
457
-				throw new GenericEncryptionException('Bad Signature', $this->l->t('Bad Signature'));
458
-			} else {
459
-				$this->logger->info('Signature check skipped', ['app' => 'encryption']);
460
-			}
461
-		}
462
-	}
463
-
464
-	/**
465
-	 * create signature
466
-	 */
467
-	private function createSignature(string $data, string $passPhrase): string {
468
-		$passPhrase = hash('sha512', $passPhrase . 'a', true);
469
-		return hash_hmac('sha256', $data, $passPhrase);
470
-	}
471
-
472
-
473
-	/**
474
-	 * @param bool $hasSignature did the block contain a signature, in this case we use a different padding
475
-	 */
476
-	private function removePadding(string $padded, bool $hasSignature = false): string|false {
477
-		if ($hasSignature === false && substr($padded, -2) === 'xx') {
478
-			return substr($padded, 0, -2);
479
-		} elseif ($hasSignature === true && substr($padded, -3) === 'xxx') {
480
-			return substr($padded, 0, -3);
481
-		}
482
-		return false;
483
-	}
484
-
485
-	/**
486
-	 * split meta data from encrypted file
487
-	 * Note: for now, we assume that the meta data always start with the iv
488
-	 *       followed by the signature, if available
489
-	 */
490
-	private function splitMetaData(string $catFile, string $cipher): array {
491
-		if ($this->hasSignature($catFile, $cipher)) {
492
-			$catFile = $this->removePadding($catFile, true);
493
-			$meta = substr($catFile, -93);
494
-			$iv = substr($meta, strlen('00iv00'), 16);
495
-			$sig = substr($meta, 22 + strlen('00sig00'));
496
-			$encrypted = substr($catFile, 0, -93);
497
-		} else {
498
-			$catFile = $this->removePadding($catFile);
499
-			$meta = substr($catFile, -22);
500
-			$iv = substr($meta, -16);
501
-			$sig = false;
502
-			$encrypted = substr($catFile, 0, -22);
503
-		}
504
-
505
-		return [
506
-			'encrypted' => $encrypted,
507
-			'iv' => $iv,
508
-			'signature' => $sig
509
-		];
510
-	}
511
-
512
-	/**
513
-	 * check if encrypted block is signed
514
-	 *
515
-	 * @throws GenericEncryptionException
516
-	 */
517
-	private function hasSignature(string $catFile, string $cipher): bool {
518
-		$skipSignatureCheck = $this->config->getSystemValueBool('encryption_skip_signature_check', false);
519
-
520
-		$meta = substr($catFile, -93);
521
-		$signaturePosition = strpos($meta, '00sig00');
522
-
523
-		// If we no longer support the legacy format then everything needs a signature
524
-		if (!$skipSignatureCheck && !$this->supportLegacy && $signaturePosition === false) {
525
-			throw new GenericEncryptionException('Missing Signature', $this->l->t('Missing Signature'));
526
-		}
527
-
528
-		// Enforce signature for the new 'CTR' ciphers
529
-		if (!$skipSignatureCheck && $signaturePosition === false && stripos($cipher, 'ctr') !== false) {
530
-			throw new GenericEncryptionException('Missing Signature', $this->l->t('Missing Signature'));
531
-		}
532
-
533
-		return ($signaturePosition !== false);
534
-	}
535
-
536
-
537
-	/**
538
-	 * @throws DecryptionFailedException
539
-	 */
540
-	private function decrypt(string $encryptedContent, string $iv, string $passPhrase = '', string $cipher = self::DEFAULT_CIPHER, bool $binaryEncoding = false): string {
541
-		$options = $binaryEncoding === true ? OPENSSL_RAW_DATA : 0;
542
-		$plainContent = openssl_decrypt($encryptedContent,
543
-			$cipher,
544
-			$passPhrase,
545
-			$options,
546
-			$iv);
547
-
548
-		if ($plainContent) {
549
-			return $plainContent;
550
-		} else {
551
-			throw new DecryptionFailedException('Encryption library: Decryption (symmetric) of content failed: ' . openssl_error_string());
552
-		}
553
-	}
554
-
555
-	/**
556
-	 * @param string $data
557
-	 * @return array
558
-	 */
559
-	protected function parseHeader($data) {
560
-		$result = [];
561
-
562
-		if (substr($data, 0, strlen(self::HEADER_START)) === self::HEADER_START) {
563
-			$endAt = strpos($data, self::HEADER_END);
564
-			$header = substr($data, 0, $endAt + strlen(self::HEADER_END));
565
-
566
-			// +1 not to start with an ':' which would result in empty element at the beginning
567
-			$exploded = explode(':',
568
-				substr($header, strlen(self::HEADER_START) + 1));
569
-
570
-			$element = array_shift($exploded);
571
-
572
-			while ($element !== self::HEADER_END) {
573
-				$result[$element] = array_shift($exploded);
574
-				$element = array_shift($exploded);
575
-			}
576
-		}
577
-
578
-		return $result;
579
-	}
580
-
581
-	/**
582
-	 * generate initialization vector
583
-	 *
584
-	 * @throws GenericEncryptionException
585
-	 */
586
-	private function generateIv(): string {
587
-		return random_bytes(16);
588
-	}
589
-
590
-	/**
591
-	 * Generate a cryptographically secure pseudo-random 256-bit ASCII key, used
592
-	 * as file key
593
-	 *
594
-	 * @return string
595
-	 * @throws \Exception
596
-	 */
597
-	public function generateFileKey() {
598
-		return random_bytes(32);
599
-	}
600
-
601
-	/**
602
-	 * @param \OpenSSLAsymmetricKey|\OpenSSLCertificate|array|string $privateKey
603
-	 * @throws MultiKeyDecryptException
604
-	 */
605
-	public function multiKeyDecrypt(string $shareKey, $privateKey): string {
606
-		$plainContent = '';
607
-
608
-		// decrypt the intermediate key with RSA
609
-		if (openssl_private_decrypt($shareKey, $intermediate, $privateKey, OPENSSL_PKCS1_OAEP_PADDING)) {
610
-			return $intermediate;
611
-		} else {
612
-			throw new MultiKeyDecryptException('multikeydecrypt with share key failed:' . openssl_error_string());
613
-		}
614
-	}
615
-
616
-	/**
617
-	 * @param \OpenSSLAsymmetricKey|\OpenSSLCertificate|array|string $privateKey
618
-	 * @throws MultiKeyDecryptException
619
-	 */
620
-	public function multiKeyDecryptLegacy(string $encKeyFile, string $shareKey, $privateKey): string {
621
-		if (!$encKeyFile) {
622
-			throw new MultiKeyDecryptException('Cannot multikey decrypt empty plain content');
623
-		}
624
-
625
-		$plainContent = '';
626
-		if ($this->opensslOpen($encKeyFile, $plainContent, $shareKey, $privateKey, 'RC4')) {
627
-			return $plainContent;
628
-		} else {
629
-			throw new MultiKeyDecryptException('multikeydecrypt with share key failed:' . openssl_error_string());
630
-		}
631
-	}
632
-
633
-	/**
634
-	 * @param array<string,\OpenSSLAsymmetricKey|\OpenSSLCertificate|array|string> $keyFiles
635
-	 * @throws MultiKeyEncryptException
636
-	 */
637
-	public function multiKeyEncrypt(string $plainContent, array $keyFiles): array {
638
-		if (empty($plainContent)) {
639
-			throw new MultiKeyEncryptException('Cannot multikeyencrypt empty plain content');
640
-		}
641
-
642
-		// Set empty vars to be set by openssl by reference
643
-		$shareKeys = [];
644
-		$mappedShareKeys = [];
645
-
646
-		// make sure that there is at least one public key to use
647
-		if (count($keyFiles) >= 1) {
648
-			// prepare the encrypted keys
649
-			$shareKeys = [];
650
-
651
-			// iterate over the public keys and encrypt the intermediate
652
-			// for each of them with RSA
653
-			foreach ($keyFiles as $tmp_key) {
654
-				if (openssl_public_encrypt($plainContent, $tmp_output, $tmp_key, OPENSSL_PKCS1_OAEP_PADDING)) {
655
-					$shareKeys[] = $tmp_output;
656
-				}
657
-			}
658
-
659
-			// set the result if everything worked fine
660
-			if (count($keyFiles) === count($shareKeys)) {
661
-				$i = 0;
662
-
663
-				// Ensure each shareKey is labelled with its corresponding key id
664
-				foreach ($keyFiles as $userId => $publicKey) {
665
-					$mappedShareKeys[$userId] = $shareKeys[$i];
666
-					$i++;
667
-				}
668
-
669
-				return $mappedShareKeys;
670
-			}
671
-		}
672
-		throw new MultiKeyEncryptException('multikeyencryption failed ' . openssl_error_string());
673
-	}
674
-
675
-	/**
676
-	 * @param string $plainContent
677
-	 * @param array $keyFiles
678
-	 * @return array
679
-	 * @throws MultiKeyEncryptException
680
-	 * @deprecated 27.0.0 use multiKeyEncrypt
681
-	 */
682
-	public function multiKeyEncryptLegacy($plainContent, array $keyFiles) {
683
-		// openssl_seal returns false without errors if plaincontent is empty
684
-		// so trigger our own error
685
-		if (empty($plainContent)) {
686
-			throw new MultiKeyEncryptException('Cannot multikeyencrypt empty plain content');
687
-		}
688
-
689
-		// Set empty vars to be set by openssl by reference
690
-		$sealed = '';
691
-		$shareKeys = [];
692
-		$mappedShareKeys = [];
693
-
694
-		if ($this->opensslSeal($plainContent, $sealed, $shareKeys, $keyFiles, 'RC4')) {
695
-			$i = 0;
696
-
697
-			// Ensure each shareKey is labelled with its corresponding key id
698
-			foreach ($keyFiles as $userId => $publicKey) {
699
-				$mappedShareKeys[$userId] = $shareKeys[$i];
700
-				$i++;
701
-			}
702
-
703
-			return [
704
-				'keys' => $mappedShareKeys,
705
-				'data' => $sealed
706
-			];
707
-		} else {
708
-			throw new MultiKeyEncryptException('multikeyencryption failed ' . openssl_error_string());
709
-		}
710
-	}
711
-
712
-	/**
713
-	 * returns the value of $useLegacyBase64Encoding
714
-	 *
715
-	 * @return bool
716
-	 */
717
-	public function useLegacyBase64Encoding(): bool {
718
-		return $this->useLegacyBase64Encoding;
719
-	}
720
-
721
-	/**
722
-	 * Uses phpseclib RC4 implementation
723
-	 */
724
-	private function rc4Decrypt(string $data, string $secret): string {
725
-		$rc4 = new RC4();
726
-		/** @psalm-suppress InternalMethod */
727
-		$rc4->setKey($secret);
728
-
729
-		return $rc4->decrypt($data);
730
-	}
731
-
732
-	/**
733
-	 * Uses phpseclib RC4 implementation
734
-	 */
735
-	private function rc4Encrypt(string $data, string $secret): string {
736
-		$rc4 = new RC4();
737
-		/** @psalm-suppress InternalMethod */
738
-		$rc4->setKey($secret);
739
-
740
-		return $rc4->encrypt($data);
741
-	}
742
-
743
-	/**
744
-	 * Custom implementation of openssl_open()
745
-	 *
746
-	 * @param \OpenSSLAsymmetricKey|\OpenSSLCertificate|array|string $private_key
747
-	 * @throws DecryptionFailedException
748
-	 */
749
-	private function opensslOpen(string $data, string &$output, string $encrypted_key, $private_key, string $cipher_algo): bool {
750
-		$result = false;
751
-
752
-		// check if RC4 is used
753
-		if (strcasecmp($cipher_algo, 'rc4') === 0) {
754
-			// decrypt the intermediate key with RSA
755
-			if (openssl_private_decrypt($encrypted_key, $intermediate, $private_key, OPENSSL_PKCS1_PADDING)) {
756
-				// decrypt the file key with the intermediate key
757
-				// using our own RC4 implementation
758
-				$output = $this->rc4Decrypt($data, $intermediate);
759
-				$result = (strlen($output) === strlen($data));
760
-			}
761
-		} else {
762
-			throw new DecryptionFailedException('Unsupported cipher ' . $cipher_algo);
763
-		}
764
-
765
-		return $result;
766
-	}
767
-
768
-	/**
769
-	 * Custom implementation of openssl_seal()
770
-	 *
771
-	 * @deprecated 27.0.0 use multiKeyEncrypt
772
-	 * @throws EncryptionFailedException
773
-	 */
774
-	private function opensslSeal(string $data, string &$sealed_data, array &$encrypted_keys, array $public_key, string $cipher_algo): int|false {
775
-		$result = false;
776
-
777
-		// check if RC4 is used
778
-		if (strcasecmp($cipher_algo, 'rc4') === 0) {
779
-			// make sure that there is at least one public key to use
780
-			if (count($public_key) >= 1) {
781
-				// generate the intermediate key
782
-				$intermediate = openssl_random_pseudo_bytes(16, $strong_result);
783
-
784
-				// check if we got strong random data
785
-				if ($strong_result) {
786
-					// encrypt the file key with the intermediate key
787
-					// using our own RC4 implementation
788
-					$sealed_data = $this->rc4Encrypt($data, $intermediate);
789
-					if (strlen($sealed_data) === strlen($data)) {
790
-						// prepare the encrypted keys
791
-						$encrypted_keys = [];
792
-
793
-						// iterate over the public keys and encrypt the intermediate
794
-						// for each of them with RSA
795
-						foreach ($public_key as $tmp_key) {
796
-							if (openssl_public_encrypt($intermediate, $tmp_output, $tmp_key, OPENSSL_PKCS1_PADDING)) {
797
-								$encrypted_keys[] = $tmp_output;
798
-							}
799
-						}
800
-
801
-						// set the result if everything worked fine
802
-						if (count($public_key) === count($encrypted_keys)) {
803
-							$result = strlen($sealed_data);
804
-						}
805
-					}
806
-				}
807
-			}
808
-		} else {
809
-			throw new EncryptionFailedException('Unsupported cipher ' . $cipher_algo);
810
-		}
811
-
812
-		return $result;
813
-	}
37
+    public const SUPPORTED_CIPHERS_AND_KEY_SIZE = [
38
+        'AES-256-CTR' => 32,
39
+        'AES-128-CTR' => 16,
40
+        'AES-256-CFB' => 32,
41
+        'AES-128-CFB' => 16,
42
+    ];
43
+    // one out of SUPPORTED_CIPHERS_AND_KEY_SIZE
44
+    public const DEFAULT_CIPHER = 'AES-256-CTR';
45
+    // default cipher from old Nextcloud versions
46
+    public const LEGACY_CIPHER = 'AES-128-CFB';
47
+
48
+    public const SUPPORTED_KEY_FORMATS = ['hash2', 'hash', 'password'];
49
+    // one out of SUPPORTED_KEY_FORMATS
50
+    public const DEFAULT_KEY_FORMAT = 'hash2';
51
+    // default key format, old Nextcloud version encrypted the private key directly
52
+    // with the user password
53
+    public const LEGACY_KEY_FORMAT = 'password';
54
+
55
+    public const HEADER_START = 'HBEGIN';
56
+    public const HEADER_END = 'HEND';
57
+
58
+    // default encoding format, old Nextcloud versions used base64
59
+    public const BINARY_ENCODING_FORMAT = 'binary';
60
+
61
+    private string $user;
62
+
63
+    private ?string $currentCipher = null;
64
+
65
+    private bool $supportLegacy;
66
+
67
+    /**
68
+     * Use the legacy base64 encoding instead of the more space-efficient binary encoding.
69
+     */
70
+    private bool $useLegacyBase64Encoding;
71
+
72
+    public function __construct(
73
+        private LoggerInterface $logger,
74
+        IUserSession $userSession,
75
+        private IConfig $config,
76
+        private IL10N $l,
77
+    ) {
78
+        $this->user = $userSession->isLoggedIn() ? $userSession->getUser()->getUID() : '"no user given"';
79
+        $this->supportLegacy = $this->config->getSystemValueBool('encryption.legacy_format_support', false);
80
+        $this->useLegacyBase64Encoding = $this->config->getSystemValueBool('encryption.use_legacy_base64_encoding', false);
81
+    }
82
+
83
+    /**
84
+     * create new private/public key-pair for user
85
+     *
86
+     * @return array{publicKey: string, privateKey: string}|false
87
+     */
88
+    public function createKeyPair() {
89
+        $res = $this->getOpenSSLPKey();
90
+
91
+        if (!$res) {
92
+            $this->logger->error("Encryption Library couldn't generate users key-pair for {$this->user}",
93
+                ['app' => 'encryption']);
94
+
95
+            if (openssl_error_string()) {
96
+                $this->logger->error('Encryption library openssl_pkey_new() fails: ' . openssl_error_string(),
97
+                    ['app' => 'encryption']);
98
+            }
99
+        } elseif (openssl_pkey_export($res,
100
+            $privateKey,
101
+            null,
102
+            $this->getOpenSSLConfig())) {
103
+            $keyDetails = openssl_pkey_get_details($res);
104
+            $publicKey = $keyDetails['key'];
105
+
106
+            return [
107
+                'publicKey' => $publicKey,
108
+                'privateKey' => $privateKey
109
+            ];
110
+        }
111
+        $this->logger->error('Encryption library couldn\'t export users private key, please check your servers OpenSSL configuration.' . $this->user,
112
+            ['app' => 'encryption']);
113
+        if (openssl_error_string()) {
114
+            $this->logger->error('Encryption Library:' . openssl_error_string(),
115
+                ['app' => 'encryption']);
116
+        }
117
+
118
+        return false;
119
+    }
120
+
121
+    /**
122
+     * Generates a new private key
123
+     *
124
+     * @return \OpenSSLAsymmetricKey|false
125
+     */
126
+    public function getOpenSSLPKey() {
127
+        $config = $this->getOpenSSLConfig();
128
+        return openssl_pkey_new($config);
129
+    }
130
+
131
+    private function getOpenSSLConfig(): array {
132
+        $config = ['private_key_bits' => 4096];
133
+        $config = array_merge(
134
+            $config,
135
+            $this->config->getSystemValue('openssl', [])
136
+        );
137
+        return $config;
138
+    }
139
+
140
+    /**
141
+     * @throws EncryptionFailedException
142
+     */
143
+    public function symmetricEncryptFileContent(string $plainContent, string $passPhrase, int $version, string $position): string|false {
144
+        if (!$plainContent) {
145
+            $this->logger->error('Encryption Library, symmetrical encryption failed no content given',
146
+                ['app' => 'encryption']);
147
+            return false;
148
+        }
149
+
150
+        $iv = $this->generateIv();
151
+
152
+        $encryptedContent = $this->encrypt($plainContent,
153
+            $iv,
154
+            $passPhrase,
155
+            $this->getCipher());
156
+
157
+        // Create a signature based on the key as well as the current version
158
+        $sig = $this->createSignature($encryptedContent, $passPhrase . '_' . $version . '_' . $position);
159
+
160
+        // combine content to encrypt the IV identifier and actual IV
161
+        $catFile = $this->concatIV($encryptedContent, $iv);
162
+        $catFile = $this->concatSig($catFile, $sig);
163
+        return $this->addPadding($catFile);
164
+    }
165
+
166
+    /**
167
+     * generate header for encrypted file
168
+     *
169
+     * @param string $keyFormat see SUPPORTED_KEY_FORMATS
170
+     * @return string
171
+     * @throws \InvalidArgumentException
172
+     */
173
+    public function generateHeader($keyFormat = self::DEFAULT_KEY_FORMAT) {
174
+        if (in_array($keyFormat, self::SUPPORTED_KEY_FORMATS, true) === false) {
175
+            throw new \InvalidArgumentException('key format "' . $keyFormat . '" is not supported');
176
+        }
177
+
178
+        $header = self::HEADER_START
179
+            . ':cipher:' . $this->getCipher()
180
+            . ':keyFormat:' . $keyFormat;
181
+
182
+        if ($this->useLegacyBase64Encoding !== true) {
183
+            $header .= ':encoding:' . self::BINARY_ENCODING_FORMAT;
184
+        }
185
+
186
+        $header .= ':' . self::HEADER_END;
187
+
188
+        return $header;
189
+    }
190
+
191
+    /**
192
+     * @throws EncryptionFailedException
193
+     */
194
+    private function encrypt(string $plainContent, string $iv, string $passPhrase = '', string $cipher = self::DEFAULT_CIPHER): string {
195
+        $options = $this->useLegacyBase64Encoding ? 0 : OPENSSL_RAW_DATA;
196
+        $encryptedContent = openssl_encrypt($plainContent,
197
+            $cipher,
198
+            $passPhrase,
199
+            $options,
200
+            $iv);
201
+
202
+        if (!$encryptedContent) {
203
+            $error = 'Encryption (symmetric) of content failed';
204
+            $this->logger->error($error . openssl_error_string(),
205
+                ['app' => 'encryption']);
206
+            throw new EncryptionFailedException($error);
207
+        }
208
+
209
+        return $encryptedContent;
210
+    }
211
+
212
+    /**
213
+     * return cipher either from config.php or the default cipher defined in
214
+     * this class
215
+     */
216
+    private function getCachedCipher(): string {
217
+        if (isset($this->currentCipher)) {
218
+            return $this->currentCipher;
219
+        }
220
+
221
+        // Get cipher either from config.php or the default cipher defined in this class
222
+        $cipher = $this->config->getSystemValueString('cipher', self::DEFAULT_CIPHER);
223
+        if (!isset(self::SUPPORTED_CIPHERS_AND_KEY_SIZE[$cipher])) {
224
+            $this->logger->warning(
225
+                sprintf(
226
+                    'Unsupported cipher (%s) defined in config.php supported. Falling back to %s',
227
+                    $cipher,
228
+                    self::DEFAULT_CIPHER
229
+                ),
230
+                ['app' => 'encryption']
231
+            );
232
+            $cipher = self::DEFAULT_CIPHER;
233
+        }
234
+
235
+        // Remember current cipher to avoid frequent lookups
236
+        $this->currentCipher = $cipher;
237
+        return $this->currentCipher;
238
+    }
239
+
240
+    /**
241
+     * return current encryption cipher
242
+     *
243
+     * @return string
244
+     */
245
+    public function getCipher() {
246
+        return $this->getCachedCipher();
247
+    }
248
+
249
+    /**
250
+     * get key size depending on the cipher
251
+     *
252
+     * @param string $cipher
253
+     * @return int
254
+     * @throws \InvalidArgumentException
255
+     */
256
+    protected function getKeySize($cipher) {
257
+        if (isset(self::SUPPORTED_CIPHERS_AND_KEY_SIZE[$cipher])) {
258
+            return self::SUPPORTED_CIPHERS_AND_KEY_SIZE[$cipher];
259
+        }
260
+
261
+        throw new \InvalidArgumentException(
262
+            sprintf(
263
+                'Unsupported cipher (%s) defined.',
264
+                $cipher
265
+            )
266
+        );
267
+    }
268
+
269
+    /**
270
+     * get legacy cipher
271
+     *
272
+     * @return string
273
+     */
274
+    public function getLegacyCipher() {
275
+        if (!$this->supportLegacy) {
276
+            throw new ServerNotAvailableException('Legacy cipher is no longer supported!');
277
+        }
278
+
279
+        return self::LEGACY_CIPHER;
280
+    }
281
+
282
+    private function concatIV(string $encryptedContent, string $iv): string {
283
+        return $encryptedContent . '00iv00' . $iv;
284
+    }
285
+
286
+    private function concatSig(string $encryptedContent, string $signature): string {
287
+        return $encryptedContent . '00sig00' . $signature;
288
+    }
289
+
290
+    /**
291
+     * Note: This is _NOT_ a padding used for encryption purposes. It is solely
292
+     * used to achieve the PHP stream size. It has _NOTHING_ to do with the
293
+     * encrypted content and is not used in any crypto primitive.
294
+     */
295
+    private function addPadding(string $data): string {
296
+        return $data . 'xxx';
297
+    }
298
+
299
+    /**
300
+     * generate password hash used to encrypt the users private key
301
+     *
302
+     * @param string $uid only used for user keys
303
+     */
304
+    protected function generatePasswordHash(string $password, string $cipher, string $uid = '', int $iterations = 600000): string {
305
+        $instanceId = $this->config->getSystemValue('instanceid');
306
+        $instanceSecret = $this->config->getSystemValue('secret');
307
+        $salt = hash('sha256', $uid . $instanceId . $instanceSecret, true);
308
+        $keySize = $this->getKeySize($cipher);
309
+
310
+        return hash_pbkdf2(
311
+            'sha256',
312
+            $password,
313
+            $salt,
314
+            $iterations,
315
+            $keySize,
316
+            true
317
+        );
318
+    }
319
+
320
+    /**
321
+     * encrypt private key
322
+     *
323
+     * @param string $privateKey
324
+     * @param string $password
325
+     * @param string $uid for regular users, empty for system keys
326
+     * @return false|string
327
+     */
328
+    public function encryptPrivateKey($privateKey, $password, $uid = '') {
329
+        $cipher = $this->getCipher();
330
+        $hash = $this->generatePasswordHash($password, $cipher, $uid);
331
+        $encryptedKey = $this->symmetricEncryptFileContent(
332
+            $privateKey,
333
+            $hash,
334
+            0,
335
+            '0'
336
+        );
337
+
338
+        return $encryptedKey;
339
+    }
340
+
341
+    /**
342
+     * @param string $privateKey
343
+     * @param string $password
344
+     * @param string $uid for regular users, empty for system keys
345
+     */
346
+    public function decryptPrivateKey($privateKey, $password = '', $uid = '') : string|false {
347
+        $header = $this->parseHeader($privateKey);
348
+
349
+        if (isset($header['cipher'])) {
350
+            $cipher = $header['cipher'];
351
+        } else {
352
+            $cipher = $this->getLegacyCipher();
353
+        }
354
+
355
+        if (isset($header['keyFormat'])) {
356
+            $keyFormat = $header['keyFormat'];
357
+        } else {
358
+            $keyFormat = self::LEGACY_KEY_FORMAT;
359
+        }
360
+
361
+        if ($keyFormat === 'hash') {
362
+            $password = $this->generatePasswordHash($password, $cipher, $uid, 100000);
363
+        } elseif ($keyFormat === 'hash2') {
364
+            $password = $this->generatePasswordHash($password, $cipher, $uid, 600000);
365
+        }
366
+
367
+        $binaryEncoding = isset($header['encoding']) && $header['encoding'] === self::BINARY_ENCODING_FORMAT;
368
+
369
+        // If we found a header we need to remove it from the key we want to decrypt
370
+        if (!empty($header)) {
371
+            $privateKey = substr($privateKey,
372
+                strpos($privateKey,
373
+                    self::HEADER_END) + strlen(self::HEADER_END));
374
+        }
375
+
376
+        $plainKey = $this->symmetricDecryptFileContent(
377
+            $privateKey,
378
+            $password,
379
+            $cipher,
380
+            0,
381
+            0,
382
+            $binaryEncoding
383
+        );
384
+
385
+        if ($this->isValidPrivateKey($plainKey) === false) {
386
+            return false;
387
+        }
388
+
389
+        return $plainKey;
390
+    }
391
+
392
+    /**
393
+     * check if it is a valid private key
394
+     *
395
+     * @param string $plainKey
396
+     * @return bool
397
+     */
398
+    protected function isValidPrivateKey($plainKey) {
399
+        $res = openssl_get_privatekey($plainKey);
400
+        if (is_object($res) && get_class($res) === 'OpenSSLAsymmetricKey') {
401
+            $sslInfo = openssl_pkey_get_details($res);
402
+            if (isset($sslInfo['key'])) {
403
+                return true;
404
+            }
405
+        }
406
+
407
+        return false;
408
+    }
409
+
410
+    /**
411
+     * @param string $keyFileContents
412
+     * @param string $passPhrase
413
+     * @param string $cipher
414
+     * @param int $version
415
+     * @param int|string $position
416
+     * @param boolean $binaryEncoding
417
+     * @return string
418
+     * @throws DecryptionFailedException
419
+     */
420
+    public function symmetricDecryptFileContent($keyFileContents, $passPhrase, $cipher = self::DEFAULT_CIPHER, $version = 0, $position = 0, bool $binaryEncoding = false) {
421
+        if ($keyFileContents == '') {
422
+            return '';
423
+        }
424
+
425
+        $catFile = $this->splitMetaData($keyFileContents, $cipher);
426
+
427
+        if ($catFile['signature'] !== false) {
428
+            try {
429
+                // First try the new format
430
+                $this->checkSignature($catFile['encrypted'], $passPhrase . '_' . $version . '_' . $position, $catFile['signature']);
431
+            } catch (GenericEncryptionException $e) {
432
+                // For compatibility with old files check the version without _
433
+                $this->checkSignature($catFile['encrypted'], $passPhrase . $version . $position, $catFile['signature']);
434
+            }
435
+        }
436
+
437
+        return $this->decrypt($catFile['encrypted'],
438
+            $catFile['iv'],
439
+            $passPhrase,
440
+            $cipher,
441
+            $binaryEncoding);
442
+    }
443
+
444
+    /**
445
+     * check for valid signature
446
+     *
447
+     * @throws GenericEncryptionException
448
+     */
449
+    private function checkSignature(string $data, string $passPhrase, string $expectedSignature): void {
450
+        $enforceSignature = !$this->config->getSystemValueBool('encryption_skip_signature_check', false);
451
+
452
+        $signature = $this->createSignature($data, $passPhrase);
453
+        $isCorrectHash = hash_equals($expectedSignature, $signature);
454
+
455
+        if (!$isCorrectHash) {
456
+            if ($enforceSignature) {
457
+                throw new GenericEncryptionException('Bad Signature', $this->l->t('Bad Signature'));
458
+            } else {
459
+                $this->logger->info('Signature check skipped', ['app' => 'encryption']);
460
+            }
461
+        }
462
+    }
463
+
464
+    /**
465
+     * create signature
466
+     */
467
+    private function createSignature(string $data, string $passPhrase): string {
468
+        $passPhrase = hash('sha512', $passPhrase . 'a', true);
469
+        return hash_hmac('sha256', $data, $passPhrase);
470
+    }
471
+
472
+
473
+    /**
474
+     * @param bool $hasSignature did the block contain a signature, in this case we use a different padding
475
+     */
476
+    private function removePadding(string $padded, bool $hasSignature = false): string|false {
477
+        if ($hasSignature === false && substr($padded, -2) === 'xx') {
478
+            return substr($padded, 0, -2);
479
+        } elseif ($hasSignature === true && substr($padded, -3) === 'xxx') {
480
+            return substr($padded, 0, -3);
481
+        }
482
+        return false;
483
+    }
484
+
485
+    /**
486
+     * split meta data from encrypted file
487
+     * Note: for now, we assume that the meta data always start with the iv
488
+     *       followed by the signature, if available
489
+     */
490
+    private function splitMetaData(string $catFile, string $cipher): array {
491
+        if ($this->hasSignature($catFile, $cipher)) {
492
+            $catFile = $this->removePadding($catFile, true);
493
+            $meta = substr($catFile, -93);
494
+            $iv = substr($meta, strlen('00iv00'), 16);
495
+            $sig = substr($meta, 22 + strlen('00sig00'));
496
+            $encrypted = substr($catFile, 0, -93);
497
+        } else {
498
+            $catFile = $this->removePadding($catFile);
499
+            $meta = substr($catFile, -22);
500
+            $iv = substr($meta, -16);
501
+            $sig = false;
502
+            $encrypted = substr($catFile, 0, -22);
503
+        }
504
+
505
+        return [
506
+            'encrypted' => $encrypted,
507
+            'iv' => $iv,
508
+            'signature' => $sig
509
+        ];
510
+    }
511
+
512
+    /**
513
+     * check if encrypted block is signed
514
+     *
515
+     * @throws GenericEncryptionException
516
+     */
517
+    private function hasSignature(string $catFile, string $cipher): bool {
518
+        $skipSignatureCheck = $this->config->getSystemValueBool('encryption_skip_signature_check', false);
519
+
520
+        $meta = substr($catFile, -93);
521
+        $signaturePosition = strpos($meta, '00sig00');
522
+
523
+        // If we no longer support the legacy format then everything needs a signature
524
+        if (!$skipSignatureCheck && !$this->supportLegacy && $signaturePosition === false) {
525
+            throw new GenericEncryptionException('Missing Signature', $this->l->t('Missing Signature'));
526
+        }
527
+
528
+        // Enforce signature for the new 'CTR' ciphers
529
+        if (!$skipSignatureCheck && $signaturePosition === false && stripos($cipher, 'ctr') !== false) {
530
+            throw new GenericEncryptionException('Missing Signature', $this->l->t('Missing Signature'));
531
+        }
532
+
533
+        return ($signaturePosition !== false);
534
+    }
535
+
536
+
537
+    /**
538
+     * @throws DecryptionFailedException
539
+     */
540
+    private function decrypt(string $encryptedContent, string $iv, string $passPhrase = '', string $cipher = self::DEFAULT_CIPHER, bool $binaryEncoding = false): string {
541
+        $options = $binaryEncoding === true ? OPENSSL_RAW_DATA : 0;
542
+        $plainContent = openssl_decrypt($encryptedContent,
543
+            $cipher,
544
+            $passPhrase,
545
+            $options,
546
+            $iv);
547
+
548
+        if ($plainContent) {
549
+            return $plainContent;
550
+        } else {
551
+            throw new DecryptionFailedException('Encryption library: Decryption (symmetric) of content failed: ' . openssl_error_string());
552
+        }
553
+    }
554
+
555
+    /**
556
+     * @param string $data
557
+     * @return array
558
+     */
559
+    protected function parseHeader($data) {
560
+        $result = [];
561
+
562
+        if (substr($data, 0, strlen(self::HEADER_START)) === self::HEADER_START) {
563
+            $endAt = strpos($data, self::HEADER_END);
564
+            $header = substr($data, 0, $endAt + strlen(self::HEADER_END));
565
+
566
+            // +1 not to start with an ':' which would result in empty element at the beginning
567
+            $exploded = explode(':',
568
+                substr($header, strlen(self::HEADER_START) + 1));
569
+
570
+            $element = array_shift($exploded);
571
+
572
+            while ($element !== self::HEADER_END) {
573
+                $result[$element] = array_shift($exploded);
574
+                $element = array_shift($exploded);
575
+            }
576
+        }
577
+
578
+        return $result;
579
+    }
580
+
581
+    /**
582
+     * generate initialization vector
583
+     *
584
+     * @throws GenericEncryptionException
585
+     */
586
+    private function generateIv(): string {
587
+        return random_bytes(16);
588
+    }
589
+
590
+    /**
591
+     * Generate a cryptographically secure pseudo-random 256-bit ASCII key, used
592
+     * as file key
593
+     *
594
+     * @return string
595
+     * @throws \Exception
596
+     */
597
+    public function generateFileKey() {
598
+        return random_bytes(32);
599
+    }
600
+
601
+    /**
602
+     * @param \OpenSSLAsymmetricKey|\OpenSSLCertificate|array|string $privateKey
603
+     * @throws MultiKeyDecryptException
604
+     */
605
+    public function multiKeyDecrypt(string $shareKey, $privateKey): string {
606
+        $plainContent = '';
607
+
608
+        // decrypt the intermediate key with RSA
609
+        if (openssl_private_decrypt($shareKey, $intermediate, $privateKey, OPENSSL_PKCS1_OAEP_PADDING)) {
610
+            return $intermediate;
611
+        } else {
612
+            throw new MultiKeyDecryptException('multikeydecrypt with share key failed:' . openssl_error_string());
613
+        }
614
+    }
615
+
616
+    /**
617
+     * @param \OpenSSLAsymmetricKey|\OpenSSLCertificate|array|string $privateKey
618
+     * @throws MultiKeyDecryptException
619
+     */
620
+    public function multiKeyDecryptLegacy(string $encKeyFile, string $shareKey, $privateKey): string {
621
+        if (!$encKeyFile) {
622
+            throw new MultiKeyDecryptException('Cannot multikey decrypt empty plain content');
623
+        }
624
+
625
+        $plainContent = '';
626
+        if ($this->opensslOpen($encKeyFile, $plainContent, $shareKey, $privateKey, 'RC4')) {
627
+            return $plainContent;
628
+        } else {
629
+            throw new MultiKeyDecryptException('multikeydecrypt with share key failed:' . openssl_error_string());
630
+        }
631
+    }
632
+
633
+    /**
634
+     * @param array<string,\OpenSSLAsymmetricKey|\OpenSSLCertificate|array|string> $keyFiles
635
+     * @throws MultiKeyEncryptException
636
+     */
637
+    public function multiKeyEncrypt(string $plainContent, array $keyFiles): array {
638
+        if (empty($plainContent)) {
639
+            throw new MultiKeyEncryptException('Cannot multikeyencrypt empty plain content');
640
+        }
641
+
642
+        // Set empty vars to be set by openssl by reference
643
+        $shareKeys = [];
644
+        $mappedShareKeys = [];
645
+
646
+        // make sure that there is at least one public key to use
647
+        if (count($keyFiles) >= 1) {
648
+            // prepare the encrypted keys
649
+            $shareKeys = [];
650
+
651
+            // iterate over the public keys and encrypt the intermediate
652
+            // for each of them with RSA
653
+            foreach ($keyFiles as $tmp_key) {
654
+                if (openssl_public_encrypt($plainContent, $tmp_output, $tmp_key, OPENSSL_PKCS1_OAEP_PADDING)) {
655
+                    $shareKeys[] = $tmp_output;
656
+                }
657
+            }
658
+
659
+            // set the result if everything worked fine
660
+            if (count($keyFiles) === count($shareKeys)) {
661
+                $i = 0;
662
+
663
+                // Ensure each shareKey is labelled with its corresponding key id
664
+                foreach ($keyFiles as $userId => $publicKey) {
665
+                    $mappedShareKeys[$userId] = $shareKeys[$i];
666
+                    $i++;
667
+                }
668
+
669
+                return $mappedShareKeys;
670
+            }
671
+        }
672
+        throw new MultiKeyEncryptException('multikeyencryption failed ' . openssl_error_string());
673
+    }
674
+
675
+    /**
676
+     * @param string $plainContent
677
+     * @param array $keyFiles
678
+     * @return array
679
+     * @throws MultiKeyEncryptException
680
+     * @deprecated 27.0.0 use multiKeyEncrypt
681
+     */
682
+    public function multiKeyEncryptLegacy($plainContent, array $keyFiles) {
683
+        // openssl_seal returns false without errors if plaincontent is empty
684
+        // so trigger our own error
685
+        if (empty($plainContent)) {
686
+            throw new MultiKeyEncryptException('Cannot multikeyencrypt empty plain content');
687
+        }
688
+
689
+        // Set empty vars to be set by openssl by reference
690
+        $sealed = '';
691
+        $shareKeys = [];
692
+        $mappedShareKeys = [];
693
+
694
+        if ($this->opensslSeal($plainContent, $sealed, $shareKeys, $keyFiles, 'RC4')) {
695
+            $i = 0;
696
+
697
+            // Ensure each shareKey is labelled with its corresponding key id
698
+            foreach ($keyFiles as $userId => $publicKey) {
699
+                $mappedShareKeys[$userId] = $shareKeys[$i];
700
+                $i++;
701
+            }
702
+
703
+            return [
704
+                'keys' => $mappedShareKeys,
705
+                'data' => $sealed
706
+            ];
707
+        } else {
708
+            throw new MultiKeyEncryptException('multikeyencryption failed ' . openssl_error_string());
709
+        }
710
+    }
711
+
712
+    /**
713
+     * returns the value of $useLegacyBase64Encoding
714
+     *
715
+     * @return bool
716
+     */
717
+    public function useLegacyBase64Encoding(): bool {
718
+        return $this->useLegacyBase64Encoding;
719
+    }
720
+
721
+    /**
722
+     * Uses phpseclib RC4 implementation
723
+     */
724
+    private function rc4Decrypt(string $data, string $secret): string {
725
+        $rc4 = new RC4();
726
+        /** @psalm-suppress InternalMethod */
727
+        $rc4->setKey($secret);
728
+
729
+        return $rc4->decrypt($data);
730
+    }
731
+
732
+    /**
733
+     * Uses phpseclib RC4 implementation
734
+     */
735
+    private function rc4Encrypt(string $data, string $secret): string {
736
+        $rc4 = new RC4();
737
+        /** @psalm-suppress InternalMethod */
738
+        $rc4->setKey($secret);
739
+
740
+        return $rc4->encrypt($data);
741
+    }
742
+
743
+    /**
744
+     * Custom implementation of openssl_open()
745
+     *
746
+     * @param \OpenSSLAsymmetricKey|\OpenSSLCertificate|array|string $private_key
747
+     * @throws DecryptionFailedException
748
+     */
749
+    private function opensslOpen(string $data, string &$output, string $encrypted_key, $private_key, string $cipher_algo): bool {
750
+        $result = false;
751
+
752
+        // check if RC4 is used
753
+        if (strcasecmp($cipher_algo, 'rc4') === 0) {
754
+            // decrypt the intermediate key with RSA
755
+            if (openssl_private_decrypt($encrypted_key, $intermediate, $private_key, OPENSSL_PKCS1_PADDING)) {
756
+                // decrypt the file key with the intermediate key
757
+                // using our own RC4 implementation
758
+                $output = $this->rc4Decrypt($data, $intermediate);
759
+                $result = (strlen($output) === strlen($data));
760
+            }
761
+        } else {
762
+            throw new DecryptionFailedException('Unsupported cipher ' . $cipher_algo);
763
+        }
764
+
765
+        return $result;
766
+    }
767
+
768
+    /**
769
+     * Custom implementation of openssl_seal()
770
+     *
771
+     * @deprecated 27.0.0 use multiKeyEncrypt
772
+     * @throws EncryptionFailedException
773
+     */
774
+    private function opensslSeal(string $data, string &$sealed_data, array &$encrypted_keys, array $public_key, string $cipher_algo): int|false {
775
+        $result = false;
776
+
777
+        // check if RC4 is used
778
+        if (strcasecmp($cipher_algo, 'rc4') === 0) {
779
+            // make sure that there is at least one public key to use
780
+            if (count($public_key) >= 1) {
781
+                // generate the intermediate key
782
+                $intermediate = openssl_random_pseudo_bytes(16, $strong_result);
783
+
784
+                // check if we got strong random data
785
+                if ($strong_result) {
786
+                    // encrypt the file key with the intermediate key
787
+                    // using our own RC4 implementation
788
+                    $sealed_data = $this->rc4Encrypt($data, $intermediate);
789
+                    if (strlen($sealed_data) === strlen($data)) {
790
+                        // prepare the encrypted keys
791
+                        $encrypted_keys = [];
792
+
793
+                        // iterate over the public keys and encrypt the intermediate
794
+                        // for each of them with RSA
795
+                        foreach ($public_key as $tmp_key) {
796
+                            if (openssl_public_encrypt($intermediate, $tmp_output, $tmp_key, OPENSSL_PKCS1_PADDING)) {
797
+                                $encrypted_keys[] = $tmp_output;
798
+                            }
799
+                        }
800
+
801
+                        // set the result if everything worked fine
802
+                        if (count($public_key) === count($encrypted_keys)) {
803
+                            $result = strlen($sealed_data);
804
+                        }
805
+                    }
806
+                }
807
+            }
808
+        } else {
809
+            throw new EncryptionFailedException('Unsupported cipher ' . $cipher_algo);
810
+        }
811
+
812
+        return $result;
813
+    }
814 814
 }
Please login to merge, or discard this patch.
Spacing   +28 added lines, -28 removed lines patch added patch discarded remove patch
@@ -93,7 +93,7 @@  discard block
 block discarded – undo
93 93
 				['app' => 'encryption']);
94 94
 
95 95
 			if (openssl_error_string()) {
96
-				$this->logger->error('Encryption library openssl_pkey_new() fails: ' . openssl_error_string(),
96
+				$this->logger->error('Encryption library openssl_pkey_new() fails: '.openssl_error_string(),
97 97
 					['app' => 'encryption']);
98 98
 			}
99 99
 		} elseif (openssl_pkey_export($res,
@@ -108,10 +108,10 @@  discard block
 block discarded – undo
108 108
 				'privateKey' => $privateKey
109 109
 			];
110 110
 		}
111
-		$this->logger->error('Encryption library couldn\'t export users private key, please check your servers OpenSSL configuration.' . $this->user,
111
+		$this->logger->error('Encryption library couldn\'t export users private key, please check your servers OpenSSL configuration.'.$this->user,
112 112
 			['app' => 'encryption']);
113 113
 		if (openssl_error_string()) {
114
-			$this->logger->error('Encryption Library:' . openssl_error_string(),
114
+			$this->logger->error('Encryption Library:'.openssl_error_string(),
115 115
 				['app' => 'encryption']);
116 116
 		}
117 117
 
@@ -140,7 +140,7 @@  discard block
 block discarded – undo
140 140
 	/**
141 141
 	 * @throws EncryptionFailedException
142 142
 	 */
143
-	public function symmetricEncryptFileContent(string $plainContent, string $passPhrase, int $version, string $position): string|false {
143
+	public function symmetricEncryptFileContent(string $plainContent, string $passPhrase, int $version, string $position): string | false {
144 144
 		if (!$plainContent) {
145 145
 			$this->logger->error('Encryption Library, symmetrical encryption failed no content given',
146 146
 				['app' => 'encryption']);
@@ -155,7 +155,7 @@  discard block
 block discarded – undo
155 155
 			$this->getCipher());
156 156
 
157 157
 		// Create a signature based on the key as well as the current version
158
-		$sig = $this->createSignature($encryptedContent, $passPhrase . '_' . $version . '_' . $position);
158
+		$sig = $this->createSignature($encryptedContent, $passPhrase.'_'.$version.'_'.$position);
159 159
 
160 160
 		// combine content to encrypt the IV identifier and actual IV
161 161
 		$catFile = $this->concatIV($encryptedContent, $iv);
@@ -172,18 +172,18 @@  discard block
 block discarded – undo
172 172
 	 */
173 173
 	public function generateHeader($keyFormat = self::DEFAULT_KEY_FORMAT) {
174 174
 		if (in_array($keyFormat, self::SUPPORTED_KEY_FORMATS, true) === false) {
175
-			throw new \InvalidArgumentException('key format "' . $keyFormat . '" is not supported');
175
+			throw new \InvalidArgumentException('key format "'.$keyFormat.'" is not supported');
176 176
 		}
177 177
 
178 178
 		$header = self::HEADER_START
179
-			. ':cipher:' . $this->getCipher()
180
-			. ':keyFormat:' . $keyFormat;
179
+			. ':cipher:'.$this->getCipher()
180
+			. ':keyFormat:'.$keyFormat;
181 181
 
182 182
 		if ($this->useLegacyBase64Encoding !== true) {
183
-			$header .= ':encoding:' . self::BINARY_ENCODING_FORMAT;
183
+			$header .= ':encoding:'.self::BINARY_ENCODING_FORMAT;
184 184
 		}
185 185
 
186
-		$header .= ':' . self::HEADER_END;
186
+		$header .= ':'.self::HEADER_END;
187 187
 
188 188
 		return $header;
189 189
 	}
@@ -201,7 +201,7 @@  discard block
 block discarded – undo
201 201
 
202 202
 		if (!$encryptedContent) {
203 203
 			$error = 'Encryption (symmetric) of content failed';
204
-			$this->logger->error($error . openssl_error_string(),
204
+			$this->logger->error($error.openssl_error_string(),
205 205
 				['app' => 'encryption']);
206 206
 			throw new EncryptionFailedException($error);
207 207
 		}
@@ -280,11 +280,11 @@  discard block
 block discarded – undo
280 280
 	}
281 281
 
282 282
 	private function concatIV(string $encryptedContent, string $iv): string {
283
-		return $encryptedContent . '00iv00' . $iv;
283
+		return $encryptedContent.'00iv00'.$iv;
284 284
 	}
285 285
 
286 286
 	private function concatSig(string $encryptedContent, string $signature): string {
287
-		return $encryptedContent . '00sig00' . $signature;
287
+		return $encryptedContent.'00sig00'.$signature;
288 288
 	}
289 289
 
290 290
 	/**
@@ -293,7 +293,7 @@  discard block
 block discarded – undo
293 293
 	 * encrypted content and is not used in any crypto primitive.
294 294
 	 */
295 295
 	private function addPadding(string $data): string {
296
-		return $data . 'xxx';
296
+		return $data.'xxx';
297 297
 	}
298 298
 
299 299
 	/**
@@ -304,7 +304,7 @@  discard block
 block discarded – undo
304 304
 	protected function generatePasswordHash(string $password, string $cipher, string $uid = '', int $iterations = 600000): string {
305 305
 		$instanceId = $this->config->getSystemValue('instanceid');
306 306
 		$instanceSecret = $this->config->getSystemValue('secret');
307
-		$salt = hash('sha256', $uid . $instanceId . $instanceSecret, true);
307
+		$salt = hash('sha256', $uid.$instanceId.$instanceSecret, true);
308 308
 		$keySize = $this->getKeySize($cipher);
309 309
 
310 310
 		return hash_pbkdf2(
@@ -343,7 +343,7 @@  discard block
 block discarded – undo
343 343
 	 * @param string $password
344 344
 	 * @param string $uid for regular users, empty for system keys
345 345
 	 */
346
-	public function decryptPrivateKey($privateKey, $password = '', $uid = '') : string|false {
346
+	public function decryptPrivateKey($privateKey, $password = '', $uid = '') : string | false {
347 347
 		$header = $this->parseHeader($privateKey);
348 348
 
349 349
 		if (isset($header['cipher'])) {
@@ -427,10 +427,10 @@  discard block
 block discarded – undo
427 427
 		if ($catFile['signature'] !== false) {
428 428
 			try {
429 429
 				// First try the new format
430
-				$this->checkSignature($catFile['encrypted'], $passPhrase . '_' . $version . '_' . $position, $catFile['signature']);
430
+				$this->checkSignature($catFile['encrypted'], $passPhrase.'_'.$version.'_'.$position, $catFile['signature']);
431 431
 			} catch (GenericEncryptionException $e) {
432 432
 				// For compatibility with old files check the version without _
433
-				$this->checkSignature($catFile['encrypted'], $passPhrase . $version . $position, $catFile['signature']);
433
+				$this->checkSignature($catFile['encrypted'], $passPhrase.$version.$position, $catFile['signature']);
434 434
 			}
435 435
 		}
436 436
 
@@ -465,7 +465,7 @@  discard block
 block discarded – undo
465 465
 	 * create signature
466 466
 	 */
467 467
 	private function createSignature(string $data, string $passPhrase): string {
468
-		$passPhrase = hash('sha512', $passPhrase . 'a', true);
468
+		$passPhrase = hash('sha512', $passPhrase.'a', true);
469 469
 		return hash_hmac('sha256', $data, $passPhrase);
470 470
 	}
471 471
 
@@ -473,7 +473,7 @@  discard block
 block discarded – undo
473 473
 	/**
474 474
 	 * @param bool $hasSignature did the block contain a signature, in this case we use a different padding
475 475
 	 */
476
-	private function removePadding(string $padded, bool $hasSignature = false): string|false {
476
+	private function removePadding(string $padded, bool $hasSignature = false): string | false {
477 477
 		if ($hasSignature === false && substr($padded, -2) === 'xx') {
478 478
 			return substr($padded, 0, -2);
479 479
 		} elseif ($hasSignature === true && substr($padded, -3) === 'xxx') {
@@ -548,7 +548,7 @@  discard block
 block discarded – undo
548 548
 		if ($plainContent) {
549 549
 			return $plainContent;
550 550
 		} else {
551
-			throw new DecryptionFailedException('Encryption library: Decryption (symmetric) of content failed: ' . openssl_error_string());
551
+			throw new DecryptionFailedException('Encryption library: Decryption (symmetric) of content failed: '.openssl_error_string());
552 552
 		}
553 553
 	}
554 554
 
@@ -609,7 +609,7 @@  discard block
 block discarded – undo
609 609
 		if (openssl_private_decrypt($shareKey, $intermediate, $privateKey, OPENSSL_PKCS1_OAEP_PADDING)) {
610 610
 			return $intermediate;
611 611
 		} else {
612
-			throw new MultiKeyDecryptException('multikeydecrypt with share key failed:' . openssl_error_string());
612
+			throw new MultiKeyDecryptException('multikeydecrypt with share key failed:'.openssl_error_string());
613 613
 		}
614 614
 	}
615 615
 
@@ -626,7 +626,7 @@  discard block
 block discarded – undo
626 626
 		if ($this->opensslOpen($encKeyFile, $plainContent, $shareKey, $privateKey, 'RC4')) {
627 627
 			return $plainContent;
628 628
 		} else {
629
-			throw new MultiKeyDecryptException('multikeydecrypt with share key failed:' . openssl_error_string());
629
+			throw new MultiKeyDecryptException('multikeydecrypt with share key failed:'.openssl_error_string());
630 630
 		}
631 631
 	}
632 632
 
@@ -669,7 +669,7 @@  discard block
 block discarded – undo
669 669
 				return $mappedShareKeys;
670 670
 			}
671 671
 		}
672
-		throw new MultiKeyEncryptException('multikeyencryption failed ' . openssl_error_string());
672
+		throw new MultiKeyEncryptException('multikeyencryption failed '.openssl_error_string());
673 673
 	}
674 674
 
675 675
 	/**
@@ -705,7 +705,7 @@  discard block
 block discarded – undo
705 705
 				'data' => $sealed
706 706
 			];
707 707
 		} else {
708
-			throw new MultiKeyEncryptException('multikeyencryption failed ' . openssl_error_string());
708
+			throw new MultiKeyEncryptException('multikeyencryption failed '.openssl_error_string());
709 709
 		}
710 710
 	}
711 711
 
@@ -759,7 +759,7 @@  discard block
 block discarded – undo
759 759
 				$result = (strlen($output) === strlen($data));
760 760
 			}
761 761
 		} else {
762
-			throw new DecryptionFailedException('Unsupported cipher ' . $cipher_algo);
762
+			throw new DecryptionFailedException('Unsupported cipher '.$cipher_algo);
763 763
 		}
764 764
 
765 765
 		return $result;
@@ -771,7 +771,7 @@  discard block
 block discarded – undo
771 771
 	 * @deprecated 27.0.0 use multiKeyEncrypt
772 772
 	 * @throws EncryptionFailedException
773 773
 	 */
774
-	private function opensslSeal(string $data, string &$sealed_data, array &$encrypted_keys, array $public_key, string $cipher_algo): int|false {
774
+	private function opensslSeal(string $data, string &$sealed_data, array &$encrypted_keys, array $public_key, string $cipher_algo): int | false {
775 775
 		$result = false;
776 776
 
777 777
 		// check if RC4 is used
@@ -806,7 +806,7 @@  discard block
 block discarded – undo
806 806
 				}
807 807
 			}
808 808
 		} else {
809
-			throw new EncryptionFailedException('Unsupported cipher ' . $cipher_algo);
809
+			throw new EncryptionFailedException('Unsupported cipher '.$cipher_algo);
810 810
 		}
811 811
 
812 812
 		return $result;
Please login to merge, or discard this patch.