@@ -20,215 +20,215 @@ |
||
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 | } |
@@ -38,7 +38,7 @@ discard block |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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); |
@@ -22,96 +22,96 @@ |
||
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 | } |
@@ -33,14 +33,14 @@ discard block |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 | } |
@@ -27,68 +27,68 @@ |
||
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 | } |
@@ -29,923 +29,923 @@ |
||
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 | } |
@@ -47,1264 +47,1264 @@ |
||
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 | } |
@@ -20,668 +20,668 @@ |
||
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 | } |
@@ -42,7 +42,7 @@ discard block |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 | } |
@@ -21,91 +21,91 @@ |
||
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 | } |
@@ -54,7 +54,7 @@ discard block |
||
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 |
||
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) { |
@@ -34,385 +34,385 @@ |
||
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 | } |
@@ -68,7 +68,7 @@ discard block |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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) { |
@@ -34,781 +34,781 @@ |
||
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 | } |
@@ -93,7 +93,7 @@ discard block |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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; |